diff --git a/Cargo.lock b/Cargo.lock index c3fae470f..75812e226 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6733,10 +6733,13 @@ dependencies = [ name = "reth-tracing" version = "0.1.0-alpha.13" dependencies = [ + "clap", + "eyre", "rolling-file", "tracing", "tracing-appender", "tracing-journald", + "tracing-logfmt", "tracing-subscriber", ] @@ -8390,6 +8393,28 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-logfmt" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bab42e40ace4e4ff19c92023ee1dbc1510db60976828fbbdc6994852c7d065" +dependencies = [ + "time", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -8400,12 +8425,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] diff --git a/bin/reth/src/args/log_args.rs b/bin/reth/src/args/log_args.rs index 6c4b736b4..6584acc07 100644 --- a/bin/reth/src/args/log_args.rs +++ b/bin/reth/src/args/log_args.rs @@ -1,19 +1,13 @@ //! clap [Args](clap::Args) for logging configuration. use crate::dirs::{LogsDir, PlatformPath}; -use clap::{Args, ValueEnum}; +use clap::{ArgAction, Args, ValueEnum}; use reth_tracing::{ - tracing_subscriber::{registry::LookupSpan, EnvFilter}, - BoxedLayer, FileWorkerGuard, + tracing_subscriber::filter::Directive, FileInfo, FileWorkerGuard, LayerInfo, LogFormat, + RethTracer, Tracer, }; use std::{fmt, fmt::Display}; -use tracing::Subscriber; - -/// Default [directives](reth_tracing::tracing_subscriber::filter::Directive) for [EnvFilter] which -/// disables high-frequency debug logs from `hyper` and `trust-dns` -const DEFAULT_ENV_FILTER_DIRECTIVES: [&str; 3] = - ["hyper::proto::h1=off", "trust_dns_proto=off", "atrust_dns_resolver=off"]; - +use tracing::{level_filters::LevelFilter, Level}; /// Constant to convert megabytes to bytes const MB_TO_BYTES: u64 = 1024 * 1024; @@ -21,6 +15,27 @@ const MB_TO_BYTES: u64 = 1024 * 1024; #[derive(Debug, Args)] #[command(next_help_heading = "Logging")] pub struct LogArgs { + /// The format to use for logs written to stdout. + #[arg(long = "log.stdout.format", value_name = "FORMAT", global = true, default_value_t = LogFormat::Terminal)] + pub log_stdout_format: LogFormat, + + /// The filter to use for logs written to stdout. + #[arg( + long = "log.stdout.filter", + value_name = "FILTER", + global = true, + default_value = "info" + )] + pub log_stdout_filter: String, + + /// The format to use for logs written to the log file. + #[arg(long = "log.file.format", value_name = "FORMAT", global = true, default_value_t = LogFormat::Terminal)] + pub log_file_format: LogFormat, + + /// The filter to use for logs written to the log file. + #[arg(long = "log.file.filter", value_name = "FILTER", global = true, default_value = "debug")] + pub log_file_filter: String, + /// The path to put log files in. #[arg(long = "log.file.directory", value_name = "PATH", global = true, default_value_t)] pub log_file_directory: PlatformPath, @@ -34,10 +49,6 @@ pub struct LogArgs { #[arg(long = "log.file.max-files", value_name = "COUNT", global = true, default_value_t = 5)] pub log_file_max_files: usize, - /// The filter to use for logs written to the log file. - #[arg(long = "log.file.filter", value_name = "FILTER", global = true, default_value = "debug")] - pub log_file_filter: String, - /// Write logs to journald. #[arg(long = "log.journald", global = true)] pub journald: bool, @@ -60,55 +71,50 @@ pub struct LogArgs { default_value_t = ColorMode::Always )] pub color: ColorMode, + /// The verbosity settings for the tracer. + #[clap(flatten)] + pub verbosity: Verbosity, } impl LogArgs { - /// Builds tracing layers from the current log options. - pub fn layers(&self) -> eyre::Result<(Vec>, Option)> - where - S: Subscriber, - for<'a> S: LookupSpan<'a>, - { - let mut layers = Vec::new(); + /// Creates a [LayerInfo] instance. + fn layer(&self, format: LogFormat, filter: String, use_color: bool) -> LayerInfo { + LayerInfo::new( + format, + filter, + self.verbosity.directive(), + if use_color { Some(self.color.to_string()) } else { None }, + ) + } - // Function to create a new EnvFilter with environment (from `RUST_LOG` env var), default - // (from `DEFAULT_DIRECTIVES`) and additional directives. - let create_env_filter = |additional_directives: &str| -> eyre::Result { - let env_filter = EnvFilter::builder().from_env_lossy(); + /// File info from the current log options. + fn file_info(&self) -> FileInfo { + FileInfo::new( + self.log_file_directory.clone().into(), + self.log_file_max_size * MB_TO_BYTES, + self.log_file_max_files, + ) + } - DEFAULT_ENV_FILTER_DIRECTIVES - .into_iter() - .chain(additional_directives.split(',')) - .try_fold(env_filter, |env_filter, directive| { - Ok(env_filter.add_directive(directive.parse()?)) - }) - }; + /// Initializes tracing with the configured options from cli args. + pub fn init_tracing(&self) -> eyre::Result> { + let mut tracer = RethTracer::new(); + + let stdout = self.layer(self.log_stdout_format, self.log_stdout_filter.clone(), true); + tracer = tracer.with_stdout(stdout); - // Create and add the journald layer if enabled if self.journald { - let journald_filter = create_env_filter(&self.journald_filter)?; - layers.push( - reth_tracing::journald(journald_filter).expect("Could not connect to journald"), - ); + tracer = tracer.with_journald(self.journald_filter.clone()); } - // Create and add the file logging layer if enabled - let file_guard = if self.log_file_max_files > 0 { - let file_filter = create_env_filter(&self.log_file_filter)?; - let (layer, guard) = reth_tracing::file( - file_filter, - &self.log_file_directory, - "reth.log", - self.log_file_max_size * MB_TO_BYTES, - self.log_file_max_files, - ); - layers.push(layer); - Some(guard) - } else { - None - }; + if self.log_file_max_files > 0 { + let info = self.file_info(); + let file = self.layer(self.log_file_format, self.log_file_filter.clone(), false); + tracer = tracer.with_file(file, info); + } - Ok((layers, file_guard)) + let guard = tracer.init()?; + Ok(guard) } } @@ -132,3 +138,42 @@ impl Display for ColorMode { } } } + +/// The verbosity settings for the cli. +#[derive(Debug, Copy, Clone, Args)] +#[command(next_help_heading = "Display")] +pub struct Verbosity { + /// Set the minimum log level. + /// + /// -v Errors + /// -vv Warnings + /// -vvv Info + /// -vvvv Debug + /// -vvvvv Traces (warning: very verbose!) + #[clap(short, long, action = ArgAction::Count, global = true, default_value_t = 3, verbatim_doc_comment, help_heading = "Display")] + verbosity: u8, + + /// Silence all log output. + #[clap(long, alias = "silent", short = 'q', global = true, help_heading = "Display")] + quiet: bool, +} + +impl Verbosity { + /// Get the corresponding [Directive] for the given verbosity, or none if the verbosity + /// corresponds to silent. + pub fn directive(&self) -> Directive { + if self.quiet { + LevelFilter::OFF.into() + } else { + let level = match self.verbosity - 1 { + 0 => Level::ERROR, + 1 => Level::WARN, + 2 => Level::INFO, + 3 => Level::DEBUG, + _ => Level::TRACE, + }; + + level.into() + } + } +} diff --git a/bin/reth/src/cli/mod.rs b/bin/reth/src/cli/mod.rs index cb631394b..72cdef47a 100644 --- a/bin/reth/src/cli/mod.rs +++ b/bin/reth/src/cli/mod.rs @@ -12,13 +12,9 @@ use crate::{ runner::CliRunner, version::{LONG_VERSION, SHORT_VERSION}, }; -use clap::{value_parser, ArgAction, Args, Parser, Subcommand}; +use clap::{value_parser, Parser, Subcommand}; use reth_primitives::ChainSpec; -use reth_tracing::{ - tracing::{metadata::LevelFilter, Level}, - tracing_subscriber::filter::Directive, - FileWorkerGuard, -}; +use reth_tracing::FileWorkerGuard; use std::sync::Arc; pub mod components; @@ -67,9 +63,6 @@ pub struct Cli { #[clap(flatten)] logs: LogArgs, - - #[clap(flatten)] - verbosity: Verbosity, } impl Cli { @@ -101,13 +94,7 @@ impl Cli { /// If file logging is enabled, this function returns a guard that must be kept alive to ensure /// that all logs are flushed to disk. pub fn init_tracing(&self) -> eyre::Result> { - let mut layers = - vec![reth_tracing::stdout(self.verbosity.directive(), &self.logs.color.to_string())]; - - let (additional_layers, guard) = self.logs.layers()?; - layers.extend(additional_layers); - - reth_tracing::init(layers); + let guard = self.logs.init_tracing()?; Ok(guard) } @@ -173,45 +160,6 @@ impl Commands { } } -/// The verbosity settings for the cli. -#[derive(Debug, Copy, Clone, Args)] -#[command(next_help_heading = "Display")] -pub struct Verbosity { - /// Set the minimum log level. - /// - /// -v Errors - /// -vv Warnings - /// -vvv Info - /// -vvvv Debug - /// -vvvvv Traces (warning: very verbose!) - #[clap(short, long, action = ArgAction::Count, global = true, default_value_t = 3, verbatim_doc_comment, help_heading = "Display")] - verbosity: u8, - - /// Silence all log output. - #[clap(long, alias = "silent", short = 'q', global = true, help_heading = "Display")] - quiet: bool, -} - -impl Verbosity { - /// Get the corresponding [Directive] for the given verbosity, or none if the verbosity - /// corresponds to silent. - pub fn directive(&self) -> Directive { - if self.quiet { - LevelFilter::OFF.into() - } else { - let level = match self.verbosity - 1 { - 0 => Level::ERROR, - 1 => Level::WARN, - 2 => Level::INFO, - 3 => Level::DEBUG, - _ => Level::TRACE, - }; - - format!("{level}").parse().unwrap() - } - } -} - #[cfg(test)] mod tests { use clap::CommandFactory; diff --git a/crates/tracing/Cargo.toml b/crates/tracing/Cargo.toml index 4ed18c307..59631365d 100644 --- a/crates/tracing/Cargo.toml +++ b/crates/tracing/Cargo.toml @@ -13,7 +13,10 @@ workspace = true [dependencies] tracing.workspace = true -tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt"] } +tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt", "json"] } tracing-appender.workspace = true tracing-journald = "0.3" +tracing-logfmt = "0.3.3" rolling-file = "0.2.0" +eyre.workspace = true +clap = { workspace = true, features = ["derive"] } \ No newline at end of file diff --git a/crates/tracing/src/formatter.rs b/crates/tracing/src/formatter.rs new file mode 100644 index 000000000..ffd89903d --- /dev/null +++ b/crates/tracing/src/formatter.rs @@ -0,0 +1,86 @@ +use crate::layers::BoxedLayer; +use clap::ValueEnum; +use std::{fmt, fmt::Display}; +use tracing_appender::non_blocking::NonBlocking; +use tracing_subscriber::{EnvFilter, Layer, Registry}; + +/// Represents the logging format. +/// +/// This enum defines the supported formats for logging output. +/// It is used to configure the format layer of a tracing subscriber. +#[derive(Debug, Copy, Clone, ValueEnum, Eq, PartialEq)] +pub enum LogFormat { + /// Represents JSON formatting for logs. + /// This format outputs log records as JSON objects, + /// making it suitable for structured logging. + Json, + + /// Represents logfmt (key=value) formatting for logs. + /// This format is concise and human-readable, + /// typically used in command-line applications. + LogFmt, + + /// Represents terminal-friendly formatting for logs. + Terminal, +} + +impl LogFormat { + /// Applies the specified logging format to create a new layer. + /// + /// This method constructs a tracing layer with the selected format, + /// along with additional configurations for filtering and output. + /// + /// # Arguments + /// * `filter` - An `EnvFilter` used to determine which log records to output. + /// * `color` - An optional string that enables or disables ANSI color codes in the logs. + /// * `file_writer` - An optional `NonBlocking` writer for directing logs to a file. + /// + /// # Returns + /// A `BoxedLayer` that can be added to a tracing subscriber. + pub fn apply( + &self, + filter: EnvFilter, + color: Option, + file_writer: Option, + ) -> BoxedLayer { + let ansi = if let Some(color) = color { + std::env::var("RUST_LOG_STYLE").map(|val| val != "never").unwrap_or(color != "never") + } else { + false + }; + let target = std::env::var("RUST_LOG_TARGET").map(|val| val != "0").unwrap_or(true); + + match self { + LogFormat::Json => { + let layer = + tracing_subscriber::fmt::layer().json().with_ansi(ansi).with_target(target); + + if let Some(writer) = file_writer { + layer.with_writer(writer).with_filter(filter).boxed() + } else { + layer.with_filter(filter).boxed() + } + } + LogFormat::LogFmt => tracing_logfmt::layer().with_filter(filter).boxed(), + LogFormat::Terminal => { + let layer = tracing_subscriber::fmt::layer().with_ansi(ansi).with_target(target); + + if let Some(writer) = file_writer { + layer.with_writer(writer).with_filter(filter).boxed() + } else { + layer.with_filter(filter).boxed() + } + } + } + } +} + +impl Display for LogFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LogFormat::Json => write!(f, "json"), + LogFormat::LogFmt => write!(f, "logfmt"), + LogFormat::Terminal => write!(f, "terminal"), + } + } +} diff --git a/crates/tracing/src/layers.rs b/crates/tracing/src/layers.rs new file mode 100644 index 000000000..bcaef6408 --- /dev/null +++ b/crates/tracing/src/layers.rs @@ -0,0 +1,180 @@ +use std::path::{Path, PathBuf}; + +use rolling_file::{RollingConditionBasic, RollingFileAppender}; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::{filter::Directive, EnvFilter, Layer, Registry}; + +use crate::formatter::LogFormat; + +/// A worker guard returned by the file layer. +/// +/// When a guard is dropped, all events currently in-memory are flushed to the log file this guard +/// belongs to. +pub type FileWorkerGuard = tracing_appender::non_blocking::WorkerGuard; + +/// A boxed tracing [Layer]. +pub(crate) type BoxedLayer = Box + Send + Sync>; + +const RETH_LOG_FILE_NAME: &str = "reth.log"; + +/// Default [directives](Directive) for [EnvFilter] which disables high-frequency debug logs from +/// `hyper` and `trust-dns` +const DEFAULT_ENV_FILTER_DIRECTIVES: [&str; 3] = + ["hyper::proto::h1=off", "trust_dns_proto=off", "trust_dns_resolver=off"]; + +/// Manages the collection of layers for a tracing subscriber. +/// +/// `Layers` acts as a container for different logging layers such as stdout, file, or journald. +/// Each layer can be configured separately and then combined into a tracing subscriber. +pub(crate) struct Layers { + inner: Vec>, +} + +impl Layers { + /// Creates a new `Layers` instance. + pub(crate) fn new() -> Self { + Self { inner: vec![] } + } + + /// Consumes the `Layers` instance, returning the inner vector of layers. + pub(crate) fn into_inner(self) -> Vec> { + self.inner + } + + /// Adds a journald layer to the layers collection. + /// + /// # Arguments + /// * `filter` - A string containing additional filter directives for this layer. + /// + /// # Returns + /// An `eyre::Result<()>` indicating the success or failure of the operation. + pub(crate) fn journald(&mut self, filter: &str) -> eyre::Result<()> { + let journald_filter = build_env_filter(None, filter)?; + let layer = tracing_journald::layer()?.with_filter(journald_filter).boxed(); + self.inner.push(layer); + Ok(()) + } + + /// Adds a stdout layer with specified formatting and filtering. + /// + /// # Type Parameters + /// * `S` - The type of subscriber that will use these layers. + /// + /// # Arguments + /// * `format` - The log message format. + /// * `directive` - Directive for the default logging level. + /// * `filter` - Additional filter directives as a string. + /// * `color` - Optional color configuration for the log messages. + /// + /// # Returns + /// An `eyre::Result<()>` indicating the success or failure of the operation. + pub(crate) fn stdout( + &mut self, + format: LogFormat, + directive: Directive, + filter: &str, + color: Option, + ) -> eyre::Result<()> { + let filter = build_env_filter(Some(directive), filter)?; + let layer = format.apply(filter, color, None); + self.inner.push(layer.boxed()); + Ok(()) + } + + /// Adds a file logging layer to the layers collection. + /// + /// # Arguments + /// * `format` - The format for log messages. + /// * `filter` - Additional filter directives as a string. + /// * `file_info` - Information about the log file including path and rotation strategy. + /// + /// # Returns + /// An `eyre::Result` representing the file logging worker. + pub(crate) fn file( + &mut self, + format: LogFormat, + filter: &str, + file_info: FileInfo, + ) -> eyre::Result { + let (writer, guard) = file_info.create_log_writer(); + let file_filter = build_env_filter(None, filter)?; + let layer = format.apply(file_filter, None, Some(writer)); + self.inner.push(layer); + Ok(guard) + } +} + +/// Holds configuration information for file logging. +/// +/// Contains details about the log file's path, name, size, and rotation strategy. +#[derive(Debug, Clone)] +pub struct FileInfo { + dir: PathBuf, + file_name: String, + max_size_bytes: u64, + max_files: usize, +} + +impl FileInfo { + /// Creates a new `FileInfo` instance. + pub fn new(dir: PathBuf, max_size_bytes: u64, max_files: usize) -> Self { + Self { dir, file_name: RETH_LOG_FILE_NAME.to_string(), max_size_bytes, max_files } + } + + /// Creates the log directory if it doesn't exist. + /// + /// # Returns + /// A reference to the path of the log directory. + fn create_log_dir(&self) -> &Path { + let log_dir: &Path = self.dir.as_ref(); + if !log_dir.exists() { + std::fs::create_dir_all(log_dir).expect("Could not create log directory"); + } + log_dir + } + + /// Creates a non-blocking writer for the log file. + /// + /// # Returns + /// A tuple containing the non-blocking writer and its associated worker guard. + fn create_log_writer(&self) -> (tracing_appender::non_blocking::NonBlocking, WorkerGuard) { + let log_dir = self.create_log_dir(); + let (writer, guard) = tracing_appender::non_blocking( + RollingFileAppender::new( + log_dir.join(&self.file_name), + RollingConditionBasic::new().max_size(self.max_size_bytes), + self.max_files, + ) + .expect("Could not initialize file logging"), + ); + (writer, guard) + } +} + +/// Builds an environment filter for logging. +/// +/// The events are filtered by `default_directive`, unless overridden by `RUST_LOG`. +/// +/// # Arguments +/// * `default_directive` - An optional `Directive` that sets the default directive. +/// * `directives` - Additional directives as a comma-separated string. +/// +/// # Returns +/// An `eyre::Result` that can be used to configure a tracing subscriber. +fn build_env_filter( + default_directive: Option, + directives: &str, +) -> eyre::Result { + let env_filter = if let Some(default_directive) = default_directive { + EnvFilter::builder().with_default_directive(default_directive).from_env_lossy() + } else { + EnvFilter::builder().from_env_lossy() + }; + + DEFAULT_ENV_FILTER_DIRECTIVES + .into_iter() + .chain(directives.split(',')) + .try_fold(env_filter, |env_filter, directive| { + Ok(env_filter.add_directive(directive.parse()?)) + }) +} diff --git a/crates/tracing/src/lib.rs b/crates/tracing/src/lib.rs index 561991dad..b49680f9a 100644 --- a/crates/tracing/src/lib.rs +++ b/crates/tracing/src/lib.rs @@ -1,12 +1,39 @@ -//! Reth tracing subscribers and utilities. +//! The `tracing` module provides functionalities for setting up and configuring logging. //! -//! Contains a standardized set of layers: +//! It includes structures and functions to create and manage various logging layers: stdout, +//! file, or journald. The module's primary entry point is the `Tracer` struct, which can be +//! configured to use different logging formats and destinations. If no layer is specified, it will +//! default to stdout. //! -//! - [`stdout()`] -//! - [`file()`] -//! - [`journald()`] +//! # Examples //! -//! As well as a simple way to initialize a subscriber: [`init`]. +//! Basic usage: +//! +//! ``` +//! use reth_tracing::{ +//! LayerInfo, RethTracer, Tracer, +//! tracing::level_filters::LevelFilter, +//! LogFormat, +//! }; +//! +//! fn main() -> eyre::Result<()> { +//! let tracer = RethTracer::new().with_stdout(LayerInfo::new( +//! LogFormat::Json, +//! "debug".to_string(), +//! LevelFilter::INFO.into(), +//! None, +//! )); +//! +//! tracer.init()?; +//! +//! // Your application logic here +//! +//! Ok(()) +//! } +//! ``` +//! +//! This example sets up a tracer with JSON format logging for journald and terminal-friendly +//! format for file logging. #![doc( html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png", @@ -15,120 +42,184 @@ )] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -use rolling_file::{RollingConditionBasic, RollingFileAppender}; -use std::path::Path; -use tracing::Subscriber; -use tracing_subscriber::{ - filter::Directive, prelude::*, registry::LookupSpan, EnvFilter, Layer, Registry, -}; +use tracing_subscriber::{filter::Directive, EnvFilter}; // Re-export tracing crates pub use tracing; pub use tracing_subscriber; -/// A boxed tracing [Layer]. -pub type BoxedLayer = Box + Send + Sync>; +// Re-export LogFormat +pub use formatter::LogFormat; +pub use layers::{FileInfo, FileWorkerGuard}; -/// Initializes a new [Subscriber] based on the given layers. -pub fn init(layers: Vec>) { - // To avoid panicking in tests, we silently fail if we cannot initialize the subscriber. - let _ = tracing_subscriber::registry().with(layers).try_init(); +mod formatter; +mod layers; + +use crate::layers::Layers; +use tracing::level_filters::LevelFilter; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +/// Tracer for application logging. +/// +/// Manages the configuration and initialization of logging layers, +/// including standard output, optional journald, and optional file logging. +#[derive(Debug, Clone)] +pub struct RethTracer { + stdout: LayerInfo, + journald: Option, + file: Option<(LayerInfo, FileInfo)>, } -/// Builds a new tracing layer that writes to stdout. -/// -/// The events are filtered by `default_directive`, unless overridden by `RUST_LOG`. -/// -/// Colors can be disabled with `RUST_LOG_STYLE=never`, and event targets can be displayed with -/// `RUST_LOG_TARGET=1`. -pub fn stdout(default_directive: impl Into, color: &str) -> BoxedLayer -where - S: Subscriber, - for<'a> S: LookupSpan<'a>, -{ - // TODO: Auto-detect - let with_ansi = - std::env::var("RUST_LOG_STYLE").map(|val| val != "never").unwrap_or(color != "never"); - let with_target = std::env::var("RUST_LOG_TARGET").map(|val| val != "0").unwrap_or(true); - - let filter = - EnvFilter::builder().with_default_directive(default_directive.into()).from_env_lossy(); - - tracing_subscriber::fmt::layer() - .with_ansi(with_ansi) - .with_target(with_target) - .with_filter(filter) - .boxed() -} - -/// Builds a new tracing layer that appends to a log file. -/// -/// The events are filtered by `filter`. -/// -/// The boxed layer and a guard is returned. When the guard is dropped the buffer for the log -/// file is immediately flushed to disk. Any events after the guard is dropped may be missed. -#[must_use = "tracing guard must be kept alive to flush events to disk"] -pub fn file( - filter: EnvFilter, - dir: impl AsRef, - file_name: impl AsRef, - max_size_bytes: u64, - max_files: usize, -) -> (BoxedLayer, tracing_appender::non_blocking::WorkerGuard) -where - S: Subscriber, - for<'a> S: LookupSpan<'a>, -{ - // Create log dir if it doesn't exist (RFA doesn't do this for us) - let log_dir = dir.as_ref(); - if !log_dir.exists() { - std::fs::create_dir_all(log_dir).expect("Could not create log directory"); +impl RethTracer { + /// Constructs a new `Tracer` with default settings. + /// + /// Initializes with default stdout layer configuration. + /// Journald and file layers are not set by default. + pub fn new() -> Self { + Self { stdout: LayerInfo::default(), journald: None, file: None } } - // Create layer - let (writer, guard) = tracing_appender::non_blocking( - RollingFileAppender::new( - log_dir.join(file_name.as_ref()), - RollingConditionBasic::new().max_size(max_size_bytes), - max_files, - ) - .expect("Could not initialize file logging"), - ); - let layer = tracing_subscriber::fmt::layer() - .with_ansi(false) - .with_writer(writer) - .with_filter(filter) - .boxed(); + /// Sets a custom configuration for the stdout layer. + /// + /// # Arguments + /// * `config` - The `LayerInfo` to use for the stdout layer. + pub fn with_stdout(mut self, config: LayerInfo) -> Self { + self.stdout = config; + self + } - (layer, guard) + /// Sets the journald layer filter. + /// + /// # Arguments + /// * `filter` - The `filter` to use for the journald layer. + pub fn with_journald(mut self, filter: String) -> Self { + self.journald = Some(filter); + self + } + + /// Sets the file layer configuration and associated file info. + /// + /// # Arguments + /// * `config` - The `LayerInfo` to use for the file layer. + /// * `file_info` - The `FileInfo` containing details about the log file. + pub fn with_file(mut self, config: LayerInfo, file_info: FileInfo) -> Self { + self.file = Some((config, file_info)); + self + } } -/// A worker guard returned by [`file()`]. -/// -/// When a guard is dropped, all events currently in-memory are flushed to the log file this guard -/// belongs to. -pub type FileWorkerGuard = tracing_appender::non_blocking::WorkerGuard; - -/// Builds a new tracing layer that writes events to journald. -/// -/// The events are filtered by `filter`. -/// -/// If the layer cannot connect to journald for any reason this function will return an error. -pub fn journald(filter: EnvFilter) -> std::io::Result> -where - S: Subscriber, - for<'a> S: LookupSpan<'a>, -{ - Ok(tracing_journald::layer()?.with_filter(filter).boxed()) +impl Default for RethTracer { + fn default() -> Self { + Self::new() + } } -/// Initializes a tracing subscriber for tests. +/// Configuration for a logging layer. /// -/// The filter is configurable via `RUST_LOG`. +/// This struct holds configuration parameters for a tracing layer, including +/// the format, filtering directives, optional coloring, and directive. +#[derive(Debug, Clone)] +pub struct LayerInfo { + format: LogFormat, + filters: String, + directive: Directive, + color: Option, +} + +impl LayerInfo { + /// Constructs a new `LayerInfo`. + /// + /// # Arguments + /// * `format` - Specifies the format for log messages. Possible values are: + /// - `LogFormat::Json` for JSON formatting. + /// - `LogFormat::LogFmt` for logfmt (key=value) formatting. + /// - `LogFormat::Terminal` for human-readable, terminal-friendly formatting. + /// * `filters` - Additional filtering parameters as a string. + /// * `directive` - Directive for filtering log messages. + /// * `color` - Optional color configuration for the log messages. + pub fn new( + format: LogFormat, + filters: String, + directive: Directive, + color: Option, + ) -> Self { + Self { format, directive, filters, color } + } +} + +impl Default for LayerInfo { + /// Provides default values for `LayerInfo`. + /// + /// By default, it uses terminal format, INFO level filter, + /// no additional filters, and no color configuration. + fn default() -> Self { + Self { + format: LogFormat::Terminal, + directive: LevelFilter::INFO.into(), + filters: "debug".to_string(), + color: Some("always".to_string()), + } + } +} + +/// Trait defining a general interface for logging configuration. /// -/// # Note +/// The `Tracer` trait provides a standardized way to initialize logging configurations +/// in an application. Implementations of this trait can specify different logging setups, +/// such as standard output logging, file logging, journald logging, or custom logging +/// configurations tailored for specific environments (like testing). +pub trait Tracer { + /// Initialize the logging configuration. + /// # Returns + /// An `eyre::Result` which is `Ok` with an optional `WorkerGuard` if a file layer is used, + /// or an `Err` in case of an error during initialization. + fn init(self) -> eyre::Result>; +} + +impl Tracer for RethTracer { + /// Initializes the logging system based on the configured layers. + /// + /// This method sets up the global tracing subscriber with the specified + /// stdout, journald, and file layers. + /// + /// The default layer is stdout. + /// + /// # Returns + /// An `eyre::Result` which is `Ok` with an optional `WorkerGuard` if a file layer is used, + /// or an `Err` in case of an error during initialization. + fn init(self) -> eyre::Result> { + let mut layers = Layers::new(); + + layers.stdout( + self.stdout.format, + self.stdout.directive, + &self.stdout.filters, + self.stdout.color, + )?; + + if let Some(config) = self.journald { + layers.journald(&config)?; + } + + let file_guard = if let Some((config, file_info)) = self.file { + Some(layers.file(config.format, &config.filters, file_info)?) + } else { + None + }; + + tracing_subscriber::registry().with(layers.into_inner()).init(); + Ok(file_guard) + } +} + +/// Initializes a tracing subscriber for tests. /// -/// The subscriber will silently fail if it could not be installed. +/// The filter is configurable via `RUST_LOG`. +/// +/// # Note +/// +/// The subscriber will silently fail if it could not be installed. pub fn init_test_tracing() { let _ = tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env())