feat: reconfig log dynamically (#5724)

This commit is contained in:
oomeow
2026-01-04 16:56:16 +08:00
committed by GitHub
parent 4adf678480
commit 421bbd090e
11 changed files with 279 additions and 169 deletions

View File

@@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
verge-dev = ["clash_verge_logger/color"]
tauri-dev = ["clash-verge-logging/tauri-dev"]
tauri-dev = []
tokio-trace = ["console-subscriber"]
clippy = ["tauri/test"]
tracing = []
@@ -65,7 +65,12 @@ boa_engine = "0.21.0"
once_cell = { version = "1.21.3", features = ["parking_lot"] }
delay_timer = "0.11.6"
percent-encoding = "2.3.2"
reqwest = { version = "0.13.1", features = ["json", "cookies", "rustls", "form"] }
reqwest = { version = "0.13.1", features = [
"json",
"cookies",
"rustls",
"form",
] }
regex = "1.12.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", features = [
"guard",
@@ -94,7 +99,7 @@ tauri-plugin-devtools = { version = "2.0.1" }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
async-trait = "0.1.89"
clash_verge_service_ipc = { version = "2.0.27", features = [
clash_verge_service_ipc = { version = "2.0.28", features = [
"client",
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
arc-swap = "1.8.0"

View File

@@ -1 +1,223 @@
// TODO: global logger to record verge log message
use std::{
str::FromStr as _,
sync::{
Arc,
atomic::{AtomicU64, AtomicUsize, Ordering},
},
};
use anyhow::{Result, bail};
use clash_verge_logging::{Type, logging};
use clash_verge_service_ipc::WriterConfig;
use compact_str::CompactString;
use flexi_logger::{
Cleanup, Criterion, DeferredNow, FileSpec, LogSpecBuilder, LogSpecification, LoggerHandle,
writers::{FileLogWriter, FileLogWriterBuilder, LogWriter as _},
};
use log::{Level, LevelFilter, Record};
use parking_lot::{Mutex, RwLock};
use crate::{
core::service,
singleton,
utils::dirs::{self, service_log_dir, sidecar_log_dir},
};
pub struct Logger {
handle: Arc<Mutex<Option<LoggerHandle>>>,
sidecar_file_writer: Arc<RwLock<Option<FileLogWriter>>>,
log_level: Arc<RwLock<LevelFilter>>,
log_max_size: AtomicU64,
log_max_count: AtomicUsize,
}
impl Default for Logger {
fn default() -> Self {
Self {
handle: Arc::new(Mutex::new(None)),
sidecar_file_writer: Arc::new(RwLock::new(None)),
log_level: Arc::new(RwLock::new(LevelFilter::Info)),
log_max_size: AtomicU64::new(128),
log_max_count: AtomicUsize::new(8),
}
}
}
singleton!(Logger, LOGGER);
impl Logger {
fn new() -> Self {
Self::default()
}
pub async fn init(&self) -> Result<()> {
let (log_level, log_max_size, log_max_count) = {
let verge_guard = crate::config::Config::verge().await;
let verge = verge_guard.latest_arc();
(
verge.get_log_level(),
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
)
};
let log_level = std::env::var("RUST_LOG")
.ok()
.and_then(|v| log::LevelFilter::from_str(&v).ok())
.unwrap_or(log_level);
*self.log_level.write() = log_level;
self.log_max_size.store(log_max_size, Ordering::SeqCst);
self.log_max_count.store(log_max_count, Ordering::SeqCst);
#[cfg(not(feature = "tauri-dev"))]
{
let log_spec = Self::generate_log_spec(log_level);
let log_dir = dirs::app_logs_dir()?;
let logger = flexi_logger::Logger::with(log_spec)
.log_to_file(FileSpec::default().directory(log_dir).basename(""))
.duplicate_to_stdout(log_level.into())
.format(clash_verge_logger::console_format)
.format_for_files(clash_verge_logger::file_format_with_level)
.rotate(
Criterion::Size(log_max_size * 1024),
flexi_logger::Naming::TimestampsCustomFormat {
current_infix: Some("latest"),
format: "%Y-%m-%d_%H-%M-%S",
},
Cleanup::KeepLogFiles(log_max_count),
);
let mut filter_modules = vec!["wry", "tokio_tungstenite", "tungstenite"];
#[cfg(not(feature = "tracing"))]
filter_modules.push("tauri");
#[cfg(feature = "tracing")]
filter_modules.extend(["tauri_plugin_mihomo", "kode_bridge"]);
let logger = logger.filter(Box::new(clash_verge_logging::NoModuleFilter(filter_modules)));
let handle = logger.start()?;
*self.handle.lock() = Some(handle);
}
let sidecar_file_writer = self.generate_sidecar_writer()?;
*self.sidecar_file_writer.write() = Some(sidecar_file_writer);
Ok(())
}
fn generate_log_spec(log_level: LevelFilter) -> LogSpecification {
let mut spec = LogSpecBuilder::new();
let log_level = std::env::var("RUST_LOG")
.ok()
.and_then(|v| log::LevelFilter::from_str(&v).ok())
.unwrap_or(log_level);
spec.default(log_level);
#[cfg(feature = "tracing")]
spec.module("tauri", log::LevelFilter::Debug)
.module("wry", log::LevelFilter::Off)
.module("tauri_plugin_mihomo", log::LevelFilter::Off);
spec.build()
}
fn generate_file_log_writer(&self) -> Result<FileLogWriterBuilder> {
let log_dir = dirs::app_logs_dir()?;
let log_max_size = self.log_max_size.load(Ordering::SeqCst);
let log_max_count = self.log_max_count.load(Ordering::SeqCst);
let flwb = FileLogWriter::builder(FileSpec::default().directory(log_dir).basename("")).rotate(
Criterion::Size(log_max_size * 1024),
flexi_logger::Naming::TimestampsCustomFormat {
current_infix: Some("latest"),
format: "%Y-%m-%d_%H-%M-%S",
},
Cleanup::KeepLogFiles(log_max_count),
);
Ok(flwb)
}
/// only update app log level
pub fn update_log_level(&self, level: LevelFilter) -> Result<()> {
println!("refresh log level");
*self.log_level.write() = level;
let log_level = self.log_level.read().to_owned();
if let Some(handle) = self.handle.lock().as_mut() {
let log_spec = Self::generate_log_spec(log_level);
handle.set_new_spec(log_spec);
handle.adapt_duplication_to_stdout(log_level.into())?;
} else {
bail!("failed to get logger handle, make sure it init");
};
Ok(())
}
/// update app and mihomo core log config
pub async fn update_log_config(&self, log_max_size: u64, log_max_count: usize) -> Result<()> {
println!("refresh log file");
self.log_max_size.store(log_max_size, Ordering::SeqCst);
self.log_max_count.store(log_max_count, Ordering::SeqCst);
if let Some(handle) = self.handle.lock().as_ref() {
let log_file_writer = self.generate_file_log_writer()?;
handle.reset_flw(&log_file_writer)?;
} else {
bail!("failed to get logger handle, make sure it init");
};
let sidecar_writer = self.generate_sidecar_writer()?;
*self.sidecar_file_writer.write() = Some(sidecar_writer);
// update service writer config
if service::is_service_ipc_path_exists() && service::is_service_available().await.is_ok() {
let service_log_dir = dirs::path_to_str(&service_log_dir()?)?.into();
clash_verge_service_ipc::update_writer(&WriterConfig {
directory: service_log_dir,
max_log_size: log_max_size * 1024,
max_log_files: log_max_count,
})
.await?;
}
Ok(())
}
fn generate_sidecar_writer(&self) -> Result<FileLogWriter> {
let sidecar_log_dir = sidecar_log_dir()?;
let log_max_size = self.log_max_size.load(Ordering::SeqCst);
let log_max_count = self.log_max_count.load(Ordering::SeqCst);
Ok(FileLogWriter::builder(
FileSpec::default()
.directory(sidecar_log_dir)
.basename("sidecar")
.suppress_timestamp(),
)
.format(clash_verge_logger::file_format_without_level)
.rotate(
Criterion::Size(log_max_size * 1024),
flexi_logger::Naming::TimestampsCustomFormat {
current_infix: Some("latest"),
format: "%Y-%m-%d_%H-%M-%S",
},
Cleanup::KeepLogFiles(log_max_count),
)
.try_build()?)
}
pub fn writer_sidecar_log(&self, level: Level, message: &CompactString) {
if let Some(writer) = self.sidecar_file_writer.read().as_ref() {
let mut now = DeferredNow::default();
let args = format_args!("{}", message);
let record = Record::builder().args(args).level(level).target("sidecar").build();
let _ = writer.write(&mut now, &record);
} else {
logging!(error, Type::System, "failed to get sidecar file log writer");
}
}
pub fn service_writer_config(&self) -> Result<WriterConfig> {
let service_log_dir = dirs::path_to_str(&service_log_dir()?)?.into();
let log_max_size = self.log_max_size.load(Ordering::SeqCst);
let log_max_count = self.log_max_count.load(Ordering::SeqCst);
let writer_config = WriterConfig {
directory: service_log_dir,
max_log_size: log_max_size * 1024,
max_log_files: log_max_count,
};
Ok(writer_config)
}
}

View File

@@ -2,14 +2,13 @@ use super::{CoreManager, RunningMode};
use crate::{
AsyncHandler,
config::{Config, IClashTemp},
core::{handle, manager::CLASH_LOGGER, service},
core::{handle, logger::Logger, manager::CLASH_LOGGER, service},
logging,
utils::{dirs, init::sidecar_writer},
utils::dirs,
};
use anyhow::Result;
use clash_verge_logging::{SharedWriter, Type, write_sidecar_log};
use clash_verge_logging::Type;
use compact_str::CompactString;
use flexi_logger::DeferredNow;
use log::Level;
use scopeguard::defer;
use tauri_plugin_shell::ShellExt as _;
@@ -60,20 +59,16 @@ impl CoreManager {
self.set_running_child_sidecar(child);
self.set_running_mode(RunningMode::Sidecar);
let shared_writer: SharedWriter = std::sync::Arc::new(tokio::sync::Mutex::new(sidecar_writer().await?));
AsyncHandler::spawn(|| async move {
while let Some(event) = rx.recv().await {
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(line)
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
let mut now = DeferredNow::default();
let message = CompactString::from(String::from_utf8_lossy(&line).as_ref());
write_sidecar_log(shared_writer.lock().await, &mut now, Level::Error, &message);
Logger::global().writer_sidecar_log(Level::Error, &message);
CLASH_LOGGER.append_log(message).await;
}
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
let mut now = DeferredNow::default();
let message = if let Some(code) = term.code {
CompactString::from(format!("Process terminated with code: {}", code))
} else if let Some(signal) = term.signal {
@@ -81,7 +76,7 @@ impl CoreManager {
} else {
CompactString::from("Process terminated")
};
write_sidecar_log(shared_writer.lock().await, &mut now, Level::Info, &message);
Logger::global().writer_sidecar_log(Level::Info, &message);
CLASH_LOGGER.clear_logs().await;
break;
}

View File

@@ -1,7 +1,7 @@
use crate::{
config::{Config, IClashTemp},
core::tray::Tray,
utils::{dirs, init::service_writer_config},
core::{logger::Logger, tray::Tray},
utils::dirs,
};
use anyhow::{Context as _, Result, bail};
use clash_verge_logging::{Type, logging, logging_error};
@@ -313,7 +313,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
core_ipc_path: IClashTemp::guard_external_controller_ipc(),
config_dir: dirs::path_to_str(&dirs::app_home_dir()?)?.into(),
},
log_config: service_writer_config().await?,
log_config: Logger::global().service_writer_config()?,
};
let response = clash_verge_service_ipc::start_clash(&payload)

View File

@@ -1,6 +1,6 @@
use crate::{
config::{Config, IVerge},
core::{CoreManager, handle, hotkey, sysopt, tray},
core::{CoreManager, handle, hotkey, logger::Logger, sysopt, tray},
module::{auto_backup::AutoBackupManager, lightweight},
};
use anyhow::Result;
@@ -65,6 +65,8 @@ bitflags! {
const SYSTRAY_CLICK_BEHAVIOR = 1 << 9;
const LIGHT_WEIGHT = 1 << 10;
const LANGUAGE = 1 << 11;
const LOG_LEVEL = 1 << 12;
const LOG_FILE = 1 << 13;
const GROUP_SYS_TRAY = Self::SYSTRAY_MENU.bits()
| Self::SYSTRAY_TOOLTIP.bits()
@@ -111,6 +113,9 @@ fn determine_update_flags(patch: &IVerge) -> UpdateFlags {
let tray_inline_outbound_modes = patch.tray_inline_outbound_modes;
let enable_proxy_guard = patch.enable_proxy_guard;
let proxy_guard_duration = patch.proxy_guard_duration;
let log_level = &patch.app_log_level;
let log_max_size = patch.app_log_max_size;
let log_max_count = patch.app_log_max_count;
#[cfg(target_os = "windows")]
let restart_core_needed = socks_enabled.is_some()
@@ -182,6 +187,12 @@ fn determine_update_flags(patch: &IVerge) -> UpdateFlags {
if tray_proxy_groups_display_mode.is_some() {
update_flags.insert(UpdateFlags::SYSTRAY_MENU);
}
if log_level.is_some() {
update_flags.insert(UpdateFlags::LOG_LEVEL);
}
if log_max_size.is_some() || log_max_count.is_some() {
update_flags.insert(UpdateFlags::LOG_FILE);
}
if tray_inline_outbound_modes.is_some() {
update_flags.insert(UpdateFlags::SYSTRAY_MENU);
}
@@ -244,6 +255,14 @@ async fn process_terminated_flags(update_flags: UpdateFlags, patch: &IVerge) ->
lightweight::disable_auto_light_weight_mode();
}
}
if update_flags.contains(UpdateFlags::LOG_LEVEL) {
Logger::global().update_log_level(patch.get_log_level())?;
}
if update_flags.contains(UpdateFlags::LOG_FILE) {
let log_max_size = patch.app_log_max_size.unwrap_or(128);
let log_max_count = patch.app_log_max_count.unwrap_or(8);
Logger::global().update_log_config(log_max_size, log_max_count).await?;
}
Ok(())
}

View File

@@ -238,7 +238,7 @@ pub fn run() {
.set(app.app_handle().clone())
.expect("failed to set global app handle");
let _handle = resolve::init_work_dir_and_logger();
resolve::init_work_dir_and_logger()?;
logging!(info, Type::Setup, "开始应用初始化...");
if let Err(e) = app_init::setup_autostart(app) {

View File

@@ -6,137 +6,18 @@ use crate::{
logging,
process::AsyncHandler,
utils::{
dirs::{self, PathBufExec as _, service_log_dir, sidecar_log_dir},
dirs::{self, PathBufExec as _},
help,
},
};
use anyhow::Result;
use chrono::{Local, TimeZone as _};
#[cfg(all(not(feature = "tauri-dev"), not(feature = "tracing-full")))]
use clash_verge_logging::NoModuleFilter;
use clash_verge_logging::Type;
use clash_verge_service_ipc::WriterConfig;
use flexi_logger::writers::FileLogWriter;
use flexi_logger::{Cleanup, Criterion, FileSpec};
#[cfg(not(feature = "tauri-dev"))]
use flexi_logger::{Duplicate, LogSpecBuilder, Logger, LoggerHandle};
use std::{path::PathBuf, str::FromStr as _};
use tauri_plugin_shell::ShellExt as _;
use tokio::fs;
use tokio::fs::DirEntry;
/// initialize this instance's log file
#[cfg(not(feature = "tauri-dev"))]
pub async fn init_logger() -> Result<LoggerHandle> {
// TODO 提供 runtime 级别实时修改
let (log_level, log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.data_arc();
(
verge.get_log_level(),
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
)
};
let log_dir = dirs::app_logs_dir()?;
let mut spec = LogSpecBuilder::new();
let level = std::env::var("RUST_LOG")
.ok()
.and_then(|v| log::LevelFilter::from_str(&v).ok())
.unwrap_or(log_level);
spec.default(level);
#[cfg(feature = "tracing")]
spec.module("tauri", log::LevelFilter::Debug)
.module("wry", log::LevelFilter::Off)
.module("tauri_plugin_mihomo", log::LevelFilter::Off);
let spec = spec.build();
let logger = Logger::with(spec)
.log_to_file(FileSpec::default().directory(log_dir).basename(""))
.duplicate_to_stdout(Duplicate::Debug)
.format(clash_verge_logger::console_format)
.format_for_files(clash_verge_logger::file_format_with_level)
.rotate(
Criterion::Size(log_max_size * 1024),
flexi_logger::Naming::TimestampsCustomFormat {
current_infix: Some("latest"),
format: "%Y-%m-%d_%H-%M-%S",
},
Cleanup::KeepLogFiles(log_max_count),
);
#[cfg(all(not(feature = "tracing"), not(feature = "tracing-full")))]
let logger = logger.filter(Box::new(NoModuleFilter(&[
"wry",
"tauri",
"tokio_tungstenite",
"tungstenite",
])));
#[cfg(feature = "tracing")]
let logger = logger.filter(Box::new(NoModuleFilter(&[
"wry",
"tauri_plugin_mihomo",
"tokio_tungstenite",
"tungstenite",
"kode_bridge",
])));
let handle = logger.start()?;
// TODO 全局 logger handle 控制
// GlobalLoggerProxy::global().set_inner(handle);
// TODO 提供前端设置等级,热更新等级
// logger.parse_new_spec(spec)
Ok(handle)
}
pub async fn sidecar_writer() -> Result<FileLogWriter> {
let (log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.data_arc();
(
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
)
};
let sidecar_log_dir = sidecar_log_dir()?;
Ok(FileLogWriter::builder(
FileSpec::default()
.directory(sidecar_log_dir)
.basename("sidecar")
.suppress_timestamp(),
)
.format(clash_verge_logger::file_format_without_level)
.rotate(
Criterion::Size(log_max_size * 1024),
flexi_logger::Naming::TimestampsCustomFormat {
current_infix: Some("latest"),
format: "%Y-%m-%d_%H-%M-%S",
},
Cleanup::KeepLogFiles(log_max_count),
)
.try_build()?)
}
pub async fn service_writer_config() -> Result<WriterConfig> {
let (log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.data_arc();
(
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
)
};
let service_log_dir = dirs::path_to_str(&service_log_dir()?)?.into();
Ok(WriterConfig {
directory: service_log_dir,
max_log_size: log_max_size * 1024,
max_log_files: log_max_count,
})
}
// TODO flexi_logger 提供了最大保留天数或许我们应该用内置删除log文件
/// 删除log文件
pub async fn delete_log() -> Result<()> {

View File

@@ -1,13 +1,13 @@
use std::sync::atomic::{AtomicBool, Ordering};
use anyhow::Result;
use flexi_logger::LoggerHandle;
use crate::{
config::Config,
core::{
CoreManager, Timer, handle,
hotkey::Hotkey,
logger::Logger,
service::{SERVICE_MANAGER, ServiceManager, is_service_ipc_path_exists},
sysopt,
tray::Tray,
@@ -28,20 +28,13 @@ pub mod window_script;
static RESOLVE_DONE: AtomicBool = AtomicBool::new(false);
pub fn init_work_dir_and_logger() -> Option<LoggerHandle> {
pub fn init_work_dir_and_logger() -> anyhow::Result<()> {
AsyncHandler::block_on(async {
init_work_config().await;
init_resources().await;
#[cfg(not(feature = "tauri-dev"))]
{
logging!(info, Type::Setup, "Initializing logger");
init::init_logger().await.ok()
}
#[cfg(feature = "tauri-dev")]
{
None
}
logging!(info, Type::Setup, "Initializing logger");
Logger::global().init().await?;
Ok(())
})
}