Json structured logs (#5784)

Co-authored-by: Alexey Shekhirin <a.shekhirin@gmail.com>
This commit is contained in:
Luca Provini
2024-01-09 12:19:41 +01:00
committed by GitHub
parent e12277303f
commit 8078c515c7
7 changed files with 592 additions and 211 deletions

28
Cargo.lock generated
View File

@ -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]]

View File

@ -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()
}
}
}

View File

@ -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;

View File

@ -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"] }

View 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"),
}
}
}

View 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()?))
})
}

View File

@ -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.
///
/// 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()
}
/// 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<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");
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;
impl Default for RethTracer {
fn default() -> Self {
Self::new()
}
}
/// Builds a new tracing layer that writes events to journald.
/// 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`.
///
/// # 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.
///
/// 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())
/// 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.