rs-zero 0.2.7

Rust-first microservice framework inspired by go-zero engineering practices
Documentation
//! Logging configuration and helpers.

pub mod aggregation;
pub mod config;
pub mod fields;
pub(crate) mod format;
pub mod redaction;
pub mod sampling;
pub mod writer;

use tracing_subscriber::{
    EnvFilter,
    fmt::{
        self,
        format::{DefaultFields, FmtSpan, Format},
        writer::BoxMakeWriter,
    },
    layer::SubscriberExt,
    util::SubscriberInitExt,
};

use crate::core::{CoreError, CoreResult};

pub use aggregation::{ErrorAggregationSnapshot, ErrorAggregator, ErrorGroup};
pub use config::{LogConfig, LogFormat, LogSpanEvents};
pub use fields::LogFields;
use format::{RedactingJsonFields, RedactingJsonFormat, RedactingTextFormat};
pub use redaction::{RedactionConfig, redact_text};
pub use sampling::{LogSampler, SamplingConfig};
pub use writer::{
    LogWriterConfig, PreparedLogWriter, RollingFileConfig, RuntimeRollingFileWriter,
    validate_writer,
};

/// Initializes a global tracing subscriber.
///
/// Calling this more than once returns `CoreError::SubscriberInit`; tests can
/// use `try_init` semantics by ignoring that specific error.
pub fn init_tracing(config: LogConfig) -> CoreResult<()> {
    build_subscriber(config)?
        .try_init()
        .map_err(|_| CoreError::SubscriberInit)
}

/// Runs a closure with a scoped tracing subscriber.
///
/// This is primarily useful for tests and embedded runtimes that need isolated
/// logging without installing a process-global subscriber.
pub fn with_scoped_tracing<T>(config: LogConfig, run: impl FnOnce() -> T) -> CoreResult<T> {
    let subscriber = build_subscriber(config)?;
    let guard = subscriber.set_default();
    let result = run();
    drop(guard);
    Ok(result)
}

fn build_subscriber(config: LogConfig) -> CoreResult<tracing::Dispatch> {
    let filter =
        EnvFilter::try_new(config.filter.clone()).unwrap_or_else(|_| EnvFilter::new("info"));
    let span_events = span_events(config.span_events);
    let writer = build_writer(&config.writer)?;

    match config.format {
        LogFormat::Text => build_text_subscriber(config, filter, span_events, writer),
        LogFormat::Json => build_json_subscriber(config, filter, span_events, writer),
    }
}

fn build_text_subscriber(
    config: LogConfig,
    filter: EnvFilter,
    span_events: FmtSpan,
    writer: BoxMakeWriter,
) -> CoreResult<tracing::Dispatch> {
    let formatter = Format::default()
        .with_ansi(config.ansi)
        .with_target(config.with_target);
    let layer = fmt::layer()
        .event_format(RedactingTextFormat::new(
            formatter,
            config.redaction.clone(),
        ))
        .fmt_fields(DefaultFields::new())
        .with_writer(writer);
    let mut layer = layer;
    layer.set_span_events(span_events);
    Ok(tracing::Dispatch::new(
        tracing_subscriber::registry().with(layer).with(filter),
    ))
}

fn build_json_subscriber(
    config: LogConfig,
    filter: EnvFilter,
    span_events: FmtSpan,
    writer: BoxMakeWriter,
) -> CoreResult<tracing::Dispatch> {
    let formatter = Format::default()
        .json()
        .with_ansi(false)
        .with_target(config.with_target)
        .with_current_span(config.include_current_span)
        .with_span_list(config.include_span_list);

    let layer = fmt::layer()
        .fmt_fields(RedactingJsonFields::new(config.redaction.clone()))
        .event_format(RedactingJsonFormat::new(
            formatter,
            config.redaction.clone(),
        ))
        .with_writer(writer);
    let mut layer = layer;
    layer.set_span_events(span_events);
    Ok(tracing::Dispatch::new(
        tracing_subscriber::registry().with(layer).with(filter),
    ))
}

fn build_writer(config: &LogWriterConfig) -> CoreResult<BoxMakeWriter> {
    validate_writer(config)?;
    match config {
        LogWriterConfig::Stdout => Ok(BoxMakeWriter::new(std::io::stdout)),
        LogWriterConfig::Stderr => Ok(BoxMakeWriter::new(std::io::stderr)),
        LogWriterConfig::File(path) => {
            let file = writer::open_append_file(path)?;
            Ok(BoxMakeWriter::new(move || {
                file.try_clone().expect("clone log file")
            }))
        }
        LogWriterConfig::RollingFile(rolling) => {
            let writer = RuntimeRollingFileWriter::new(rolling.clone())?;
            Ok(BoxMakeWriter::new(move || writer.clone()))
        }
    }
}

fn span_events(events: LogSpanEvents) -> FmtSpan {
    match events {
        LogSpanEvents::None => FmtSpan::NONE,
        LogSpanEvents::Close => FmtSpan::CLOSE,
        LogSpanEvents::NewAndClose => FmtSpan::NEW | FmtSpan::CLOSE,
        LogSpanEvents::Full => FmtSpan::FULL,
    }
}

#[cfg(test)]
mod tests {
    use super::{LogConfig, init_tracing};

    #[test]
    fn init_tracing_is_callable() {
        let _ = init_tracing(LogConfig {
            filter: "debug".to_string(),
            ansi: false,
            ..LogConfig::default()
        });
    }
}