From dd916cfe5a0ff012f6977a039dd7bd9a432df193 Mon Sep 17 00:00:00 2001 From: shadowsocks69420 <193321218+shadowsocks69420@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:12:01 +0800 Subject: [PATCH 1/2] feat: support file logging with tracing --- Cargo.lock | 13 ++++++++ Cargo.toml | 3 +- README.md | 20 +++++++++++-- src/config.rs | 67 ++++++++++++++++++++++++++++++++++++++++++ src/logging/mod.rs | 3 +- src/logging/tracing.rs | 60 ++++++++++++++++++++++++++++++------- src/service/local.rs | 2 ++ src/service/manager.rs | 4 ++- src/service/server.rs | 4 ++- 9 files changed, 157 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 73815e867bba..a6d96694071d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3432,6 +3432,7 @@ dependencies = [ "time", "tokio", "tracing", + "tracing-appender", "tracing-subscriber", "windows-service", "xdg", @@ -4012,6 +4013,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.30" diff --git a/Cargo.toml b/Cargo.toml index 7ccc0bbac520..37aeccb55596 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,7 @@ dns-over-https = ["shadowsocks-service/dns-over-https"] dns-over-h3 = ["shadowsocks-service/dns-over-h3"] # Enable logging output -logging = ["log4rs", "tracing", "tracing-subscriber", "time"] +logging = ["log4rs", "tracing", "tracing-subscriber", "time", "tracing-appender"] # Enable DNS-relay local-dns = ["local", "shadowsocks-service/local-dns"] @@ -208,6 +208,7 @@ tracing-subscriber = { version = "0.3", optional = true, features = [ "time", "local-time", ] } +tracing-appender = { version = "0.2.3", optional = true, default-features = false } time = { version = "0.3", optional = true } serde = { version = "1.0", features = ["derive"] } diff --git a/README.md b/README.md index 2a43d3705a33..d5d6257a8807 100644 --- a/README.md +++ b/README.md @@ -877,9 +877,23 @@ Example configuration: // Euiqvalent to `--log-without-time` "without_time": false, }, - // Equivalent to `--log-config` - // More detail could be found in https://crates.io/crates/log4rs - "config_path": "/path/to/log4rs/config.yaml" + // File logging configuration (will disable stdout logging) + // This is particularly useful for running as a Windows Service + "file": { + // Directory to store log files. If not set, it will not log to file + "directory": "/var/log/shadowsocks-rust", + // Log rotation frequency, must be one of the following: + // - never (default): This will result in log file located at `directory/prefix.suffix` + // - daily: A new log file in the format of `directory/prefix.yyyy-MM-dd.suffix` will be created daily + // - hourly: A new log file in the format of `directory/prefix.yyyy-MM-dd-HH.suffix` will be created hourly + "rotation": "never", + // Prefix of log file, default is one of `sslocal`, `ssserver`, `ssmanager` depending on the service being run. + "prefix": "shadowsocks-rust", + // Suffix of log file, default is `log` + "suffix": "log", + // Keeps the last N log files. If not set, no limit will be applied. + "max_files": 5 + } }, // Runtime configuration "runtime": { diff --git a/src/config.rs b/src/config.rs index 1fef16551065..cacef84253f4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -142,6 +142,31 @@ impl Config { nlog.format = nformat; } + if let Some(file_config) = log.file + // directory must be configured for file logging + && let Some(directory) = file_config.directory + { + let mut nfile = LogFileConfig::new(directory); + if let Some(rotation) = file_config.rotation { + nfile.rotation = match rotation.as_str() { + "never" => tracing_appender::rolling::Rotation::NEVER, + "hourly" => tracing_appender::rolling::Rotation::HOURLY, + "daily" => tracing_appender::rolling::Rotation::DAILY, + _ => return Err(ConfigError::InvalidValue(rotation)), + }; + } + if let Some(prefix) = file_config.prefix { + nfile.prefix = Some(prefix); + } + if let Some(suffix) = file_config.suffix { + nfile.suffix = Some(suffix); + } + if let Some(max_files) = file_config.max_files { + nfile.max_files = Some(max_files); + } + nlog.file = Some(nfile); + } + if let Some(config_path) = log.config_path { nlog.config_path = Some(PathBuf::from(config_path)); } @@ -210,6 +235,8 @@ pub struct LogConfig { pub level: u32, /// Default logger format configuration pub format: LogFormatConfig, + /// File appender configuration + pub file: Option, /// Logging configuration file path pub config_path: Option, } @@ -221,6 +248,35 @@ pub struct LogFormatConfig { pub without_time: bool, } +/// File appender configuration for logging +#[cfg(feature = "logging")] +#[derive(Debug, Clone)] +pub struct LogFileConfig { + /// Directory to store log files + pub directory: PathBuf, + /// Rotation strategy for log files. Default is `Rotation::NEVER`. + pub rotation: tracing_appender::rolling::Rotation, + /// Prefix for log file names. Default is the binary name. + pub prefix: Option, + /// Suffix for log file names. Default is "log". + pub suffix: Option, + /// Maximum number of log files to keep. Default is `None`, meaning no limit. + pub max_files: Option, +} + +#[cfg(feature = "logging")] +impl LogFileConfig { + fn new(directory: impl Into) -> Self { + Self { + directory: directory.into(), + rotation: tracing_appender::rolling::Rotation::NEVER, + prefix: None, + suffix: None, + max_files: None, + } + } +} + /// Runtime mode (Tokio) #[derive(Debug, Clone, Copy, Default)] pub enum RuntimeMode { @@ -272,6 +328,7 @@ struct SSConfig { struct SSLogConfig { level: Option, format: Option, + file: Option, config_path: Option, } @@ -281,6 +338,16 @@ struct SSLogFormat { without_time: Option, } +#[cfg(feature = "logging")] +#[derive(Deserialize)] +struct SSLogFileConfig { + directory: Option, + rotation: Option, + prefix: Option, + suffix: Option, + max_files: Option, +} + #[derive(Deserialize)] struct SSRuntimeConfig { #[cfg(feature = "multi-threaded")] diff --git a/src/logging/mod.rs b/src/logging/mod.rs index 7f5098ccd61b..4a55e74aaaf4 100644 --- a/src/logging/mod.rs +++ b/src/logging/mod.rs @@ -9,7 +9,7 @@ use crate::config::LogConfig; mod log4rs; mod tracing; -/// Initialize logger ([log4rs](https://crates.io/crates/log4rs), [trace4rs](https://crates.io/crates/trace4rs)) from yaml configuration file +/// Initialize [log4rs](https://crates.io/crates/log4rs) from yaml configuration file pub fn init_with_file

(path: P) where P: AsRef, @@ -25,7 +25,6 @@ where /// Initialize logger with provided configuration pub fn init_with_config(bin_name: &str, config: &LogConfig) { - // log4rs::init_with_config(bin_name, config); tracing::init_with_config(bin_name, config); } diff --git a/src/logging/tracing.rs b/src/logging/tracing.rs index b045946d715e..cc4fe3ccbfb8 100644 --- a/src/logging/tracing.rs +++ b/src/logging/tracing.rs @@ -2,26 +2,27 @@ use std::io::IsTerminal; +use time::format_description::well_known::Rfc3339; use time::UtcOffset; use tracing::level_filters::LevelFilter; -use tracing_subscriber::{EnvFilter, FmtSubscriber, fmt::time::OffsetTime}; +use tracing_appender::rolling::{InitError, RollingFileAppender}; +use tracing_subscriber::fmt::format::{DefaultFields, Format, Full}; +use tracing_subscriber::fmt::time::OffsetTime; +use tracing_subscriber::fmt::{MakeWriter, SubscriberBuilder}; +use tracing_subscriber::{EnvFilter, FmtSubscriber}; -use crate::config::LogConfig; +use crate::config::{LogConfig, LogFileConfig}; /// Initialize logger with provided configuration pub fn init_with_config(bin_name: &str, config: &LogConfig) { let debug_level = config.level; let without_time = config.format.without_time; - let mut builder = FmtSubscriber::builder() - .with_level(true) - .with_timer(match OffsetTime::local_rfc_3339() { - Ok(t) => t, - Err(..) => { - // Reinit with UTC time - OffsetTime::new(UtcOffset::UTC, time::format_description::well_known::Rfc3339) - } - }); + let mut builder = FmtSubscriber::builder().with_level(true).with_timer( + OffsetTime::local_rfc_3339() + // Fallback to UTC. Eagerly evaluate because it is cheap to create. + .unwrap_or(OffsetTime::new(UtcOffset::UTC, Rfc3339)), + ); // NOTE: ansi is enabled by default. // Could be disabled by `NO_COLOR` environment variable. @@ -74,6 +75,43 @@ pub fn init_with_config(bin_name: &str, config: &LogConfig) { }; let builder = builder.with_env_filter(filter); + if let Some(ref file_config) = config.file { + let file_writer = make_file_writer(bin_name, file_config) + // don't have the room for a more graceful error handling here + .expect("Failed to create file writer for logging"); + init(builder.with_ansi(false).with_writer(file_writer), without_time); + } else { + init(builder, without_time); + } +} + +fn make_file_writer(bin_name: &str, config: &LogFileConfig) -> Result { + let rotation = config.rotation.clone(); + // We provide default values here because we don't have access to the + // `bin_name` elsewhere. + let prefix = config.prefix.as_deref().unwrap_or(bin_name); + let suffix = config.suffix.as_deref().unwrap_or("log"); + + let mut builder = RollingFileAppender::builder() + .rotation(rotation) + .filename_prefix(prefix) + .filename_suffix(suffix); + + if let Some(max_files) = config.max_files { + builder = builder.max_log_files(max_files); + } + + builder.build(&config.directory) +} + +/// Initialize the logger with the provided builder and options. +/// +/// This handles the `without_time` option generically for builders that +/// are configured with different `MakeWriter` concrete types. +fn init MakeWriter<'writer> + Send + Sync + 'static>( + builder: SubscriberBuilder>, EnvFilter, W>, + without_time: bool, +) { if without_time { builder.without_time().init(); } else { diff --git a/src/service/local.rs b/src/service/local.rs index cf01f37a3a84..f142e3c47d0f 100644 --- a/src/service/local.rs +++ b/src/service/local.rs @@ -261,6 +261,8 @@ pub fn define_command_line_options(mut app: Command) -> Command { .arg( Arg::new("LOG_CONFIG") .long("log-config") + // deprecated for removal + .hide(true) .num_args(1) .action(ArgAction::Set) .value_parser(clap::value_parser!(PathBuf)) diff --git a/src/service/manager.rs b/src/service/manager.rs index ae7f1d5ba002..0eadd30fd417 100644 --- a/src/service/manager.rs +++ b/src/service/manager.rs @@ -148,6 +148,8 @@ pub fn define_command_line_options(mut app: Command) -> Command { .arg( Arg::new("LOG_CONFIG") .long("log-config") + // deprecated for removal + .hide(true) .num_args(1) .action(ArgAction::Set) .value_parser(clap::value_parser!(PathBuf)) @@ -297,7 +299,7 @@ pub fn create(matches: &ArgMatches) -> ShadowsocksResult<(Runtime, impl Future { - logging::init_with_config("sslocal", &service_config.log); + logging::init_with_config("ssmanager", &service_config.log); } } diff --git a/src/service/server.rs b/src/service/server.rs index 045243aa3a2e..eef03d8167ab 100644 --- a/src/service/server.rs +++ b/src/service/server.rs @@ -178,6 +178,8 @@ pub fn define_command_line_options(mut app: Command) -> Command { .arg( Arg::new("LOG_CONFIG") .long("log-config") + // deprecated for removal + .hide(true) .num_args(1) .action(ArgAction::Set) .value_parser(clap::value_parser!(PathBuf)) @@ -309,7 +311,7 @@ pub fn create(matches: &ArgMatches) -> ShadowsocksResult<(Runtime, impl Future { - logging::init_with_config("sslocal", &service_config.log); + logging::init_with_config("ssserver", &service_config.log); } } From af27d864e91175d648cb10e527360b0d6202a100 Mon Sep 17 00:00:00 2001 From: shadowsocks69420 <193321218+shadowsocks69420@users.noreply.github.com> Date: Sun, 27 Jul 2025 02:13:07 +0800 Subject: [PATCH 2/2] fix: compilation error on rustc 1.85 --- src/config.rs | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/config.rs b/src/config.rs index cacef84253f4..bf34cf760e34 100644 --- a/src/config.rs +++ b/src/config.rs @@ -142,29 +142,29 @@ impl Config { nlog.format = nformat; } - if let Some(file_config) = log.file + if let Some(file_config) = log.file { // directory must be configured for file logging - && let Some(directory) = file_config.directory - { - let mut nfile = LogFileConfig::new(directory); - if let Some(rotation) = file_config.rotation { - nfile.rotation = match rotation.as_str() { - "never" => tracing_appender::rolling::Rotation::NEVER, - "hourly" => tracing_appender::rolling::Rotation::HOURLY, - "daily" => tracing_appender::rolling::Rotation::DAILY, - _ => return Err(ConfigError::InvalidValue(rotation)), - }; + if let Some(directory) = file_config.directory { + let mut nfile = LogFileConfig::new(directory); + if let Some(rotation) = file_config.rotation { + nfile.rotation = match rotation.as_str() { + "never" => tracing_appender::rolling::Rotation::NEVER, + "hourly" => tracing_appender::rolling::Rotation::HOURLY, + "daily" => tracing_appender::rolling::Rotation::DAILY, + _ => return Err(ConfigError::InvalidValue(rotation)), + }; + } + if let Some(prefix) = file_config.prefix { + nfile.prefix = Some(prefix); + } + if let Some(suffix) = file_config.suffix { + nfile.suffix = Some(suffix); + } + if let Some(max_files) = file_config.max_files { + nfile.max_files = Some(max_files); + } + nlog.file = Some(nfile); } - if let Some(prefix) = file_config.prefix { - nfile.prefix = Some(prefix); - } - if let Some(suffix) = file_config.suffix { - nfile.suffix = Some(suffix); - } - if let Some(max_files) = file_config.max_files { - nfile.max_files = Some(max_files); - } - nlog.file = Some(nfile); } if let Some(config_path) = log.config_path {