mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
Json structured logs (#5784)
Co-authored-by: Alexey Shekhirin <a.shekhirin@gmail.com>
This commit is contained in:
28
Cargo.lock
generated
28
Cargo.lock
generated
@ -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]]
|
||||
|
||||
@ -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<LogsDir>,
|
||||
@ -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<S>(&self) -> eyre::Result<(Vec<BoxedLayer<S>>, Option<FileWorkerGuard>)>
|
||||
where
|
||||
S: Subscriber,
|
||||
for<'a> S: LookupSpan<'a>,
|
||||
{
|
||||
let mut layers = Vec::new();
|
||||
|
||||
// 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<EnvFilter> {
|
||||
let env_filter = EnvFilter::builder().from_env_lossy();
|
||||
|
||||
DEFAULT_ENV_FILTER_DIRECTIVES
|
||||
.into_iter()
|
||||
.chain(additional_directives.split(','))
|
||||
.try_fold(env_filter, |env_filter, directive| {
|
||||
Ok(env_filter.add_directive(directive.parse()?))
|
||||
})
|
||||
};
|
||||
|
||||
// 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"),
|
||||
);
|
||||
/// 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 },
|
||||
)
|
||||
}
|
||||
|
||||
// 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",
|
||||
/// 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,
|
||||
);
|
||||
layers.push(layer);
|
||||
Some(guard)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
Ok((layers, file_guard))
|
||||
/// Initializes tracing with the configured options from cli args.
|
||||
pub fn init_tracing(&self) -> eyre::Result<Option<FileWorkerGuard>> {
|
||||
let mut tracer = RethTracer::new();
|
||||
|
||||
let stdout = self.layer(self.log_stdout_format, self.log_stdout_filter.clone(), true);
|
||||
tracer = tracer.with_stdout(stdout);
|
||||
|
||||
if self.journald {
|
||||
tracer = tracer.with_journald(self.journald_filter.clone());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Ext: RethCliExt = ()> {
|
||||
|
||||
#[clap(flatten)]
|
||||
logs: LogArgs,
|
||||
|
||||
#[clap(flatten)]
|
||||
verbosity: Verbosity,
|
||||
}
|
||||
|
||||
impl<Ext: RethCliExt> Cli<Ext> {
|
||||
@ -101,13 +94,7 @@ impl<Ext: RethCliExt> Cli<Ext> {
|
||||
/// 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<Option<FileWorkerGuard>> {
|
||||
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<Ext: RethCliExt> Commands<Ext> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
@ -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"] }
|
||||
86
crates/tracing/src/formatter.rs
Normal file
86
crates/tracing/src/formatter.rs
Normal file
@ -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<Registry>` that can be added to a tracing subscriber.
|
||||
pub fn apply(
|
||||
&self,
|
||||
filter: EnvFilter,
|
||||
color: Option<String>,
|
||||
file_writer: Option<NonBlocking>,
|
||||
) -> BoxedLayer<Registry> {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
180
crates/tracing/src/layers.rs
Normal file
180
crates/tracing/src/layers.rs
Normal file
@ -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<S> = Box<dyn Layer<S> + 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<BoxedLayer<Registry>>,
|
||||
}
|
||||
|
||||
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<BoxedLayer<Registry>> {
|
||||
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<String>,
|
||||
) -> 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<FileWorkerGuard>` representing the file logging worker.
|
||||
pub(crate) fn file(
|
||||
&mut self,
|
||||
format: LogFormat,
|
||||
filter: &str,
|
||||
file_info: FileInfo,
|
||||
) -> eyre::Result<FileWorkerGuard> {
|
||||
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<EnvFilter>` that can be used to configure a tracing subscriber.
|
||||
fn build_env_filter(
|
||||
default_directive: Option<Directive>,
|
||||
directives: &str,
|
||||
) -> eyre::Result<EnvFilter> {
|
||||
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()?))
|
||||
})
|
||||
}
|
||||
@ -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,111 +42,175 @@
|
||||
)]
|
||||
#![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<S> = Box<dyn Layer<S> + 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<BoxedLayer<Registry>>) {
|
||||
// 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<String>,
|
||||
file: Option<(LayerInfo, FileInfo)>,
|
||||
}
|
||||
|
||||
/// Builds a new tracing layer that writes to stdout.
|
||||
impl RethTracer {
|
||||
/// Constructs a new `Tracer` with default settings.
|
||||
///
|
||||
/// 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<S>(default_directive: impl Into<Directive>, color: &str) -> BoxedLayer<S>
|
||||
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()
|
||||
/// 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 }
|
||||
}
|
||||
|
||||
/// Builds a new tracing layer that appends to a log file.
|
||||
/// Sets a custom configuration for the stdout layer.
|
||||
///
|
||||
/// 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<S>(
|
||||
filter: EnvFilter,
|
||||
dir: impl AsRef<Path>,
|
||||
file_name: impl AsRef<Path>,
|
||||
max_size_bytes: u64,
|
||||
max_files: usize,
|
||||
) -> (BoxedLayer<S>, 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");
|
||||
/// # Arguments
|
||||
/// * `config` - The `LayerInfo` to use for the stdout layer.
|
||||
pub fn with_stdout(mut self, config: LayerInfo) -> Self {
|
||||
self.stdout = config;
|
||||
self
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
(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
|
||||
}
|
||||
|
||||
/// A worker guard returned by [`file()`].
|
||||
/// Sets the file layer configuration and associated file info.
|
||||
///
|
||||
/// 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;
|
||||
/// # 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
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a new tracing layer that writes events to journald.
|
||||
impl Default for RethTracer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for a logging layer.
|
||||
///
|
||||
/// The events are filtered by `filter`.
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
impl LayerInfo {
|
||||
/// Constructs a new `LayerInfo`.
|
||||
///
|
||||
/// If the layer cannot connect to journald for any reason this function will return an error.
|
||||
pub fn journald<S>(filter: EnvFilter) -> std::io::Result<BoxedLayer<S>>
|
||||
where
|
||||
S: Subscriber,
|
||||
for<'a> S: LookupSpan<'a>,
|
||||
{
|
||||
Ok(tracing_journald::layer()?.with_filter(filter).boxed())
|
||||
/// # 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<String>,
|
||||
) -> 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.
|
||||
///
|
||||
/// 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<Option<WorkerGuard>>;
|
||||
}
|
||||
|
||||
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<Option<WorkerGuard>> {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user