Skip to main content

rs_zero/core/
logging.rs

1//! Logging configuration and helpers.
2
3pub mod aggregation;
4pub mod config;
5pub mod fields;
6pub(crate) mod format;
7pub mod redaction;
8pub(crate) mod rotation;
9pub mod sampling;
10pub mod writer;
11
12use tracing_subscriber::{
13    EnvFilter,
14    fmt::{
15        self,
16        format::{DefaultFields, FmtSpan, Format},
17        writer::BoxMakeWriter,
18    },
19    layer::SubscriberExt,
20    util::SubscriberInitExt,
21};
22
23use crate::core::{CoreError, CoreResult};
24
25pub use aggregation::{ErrorAggregationSnapshot, ErrorAggregator, ErrorGroup};
26pub use config::{LogConfig, LogFormat, LogSpanEvents};
27pub use fields::LogFields;
28use format::{RedactingJsonFields, RedactingJsonFormat, RedactingTextFormat};
29pub use redaction::{RedactionConfig, redact_text};
30pub use sampling::{LogSampler, SamplingConfig};
31pub use writer::{
32    LogWriterConfig, PreparedLogWriter, RollingFileConfig, RotationPolicy,
33    RuntimeRollingFileWriter, validate_writer,
34};
35
36/// Initializes a global tracing subscriber.
37///
38/// Calling this more than once returns `CoreError::SubscriberInit`; tests can
39/// use `try_init` semantics by ignoring that specific error.
40pub fn init_tracing(config: LogConfig) -> CoreResult<()> {
41    build_subscriber(config)?
42        .try_init()
43        .map_err(|_| CoreError::SubscriberInit)
44}
45
46/// Runs a closure with a scoped tracing subscriber.
47///
48/// This is primarily useful for tests and embedded runtimes that need isolated
49/// logging without installing a process-global subscriber.
50pub fn with_scoped_tracing<T>(config: LogConfig, run: impl FnOnce() -> T) -> CoreResult<T> {
51    let subscriber = build_subscriber(config)?;
52    let guard = subscriber.set_default();
53    let result = run();
54    drop(guard);
55    Ok(result)
56}
57
58fn build_subscriber(config: LogConfig) -> CoreResult<tracing::Dispatch> {
59    let filter =
60        EnvFilter::try_new(config.filter.clone()).unwrap_or_else(|_| EnvFilter::new("info"));
61    let span_events = span_events(config.span_events);
62    let writer = build_writer(&config.writer)?;
63
64    match config.format {
65        LogFormat::Text => build_text_subscriber(config, filter, span_events, writer),
66        LogFormat::Json => build_json_subscriber(config, filter, span_events, writer),
67    }
68}
69
70fn build_text_subscriber(
71    config: LogConfig,
72    filter: EnvFilter,
73    span_events: FmtSpan,
74    writer: BoxMakeWriter,
75) -> CoreResult<tracing::Dispatch> {
76    let formatter = Format::default()
77        .with_ansi(config.ansi)
78        .with_target(config.with_target);
79    let layer = fmt::layer()
80        .event_format(RedactingTextFormat::new(
81            formatter,
82            config.redaction.clone(),
83        ))
84        .fmt_fields(DefaultFields::new())
85        .with_writer(writer);
86    let mut layer = layer;
87    layer.set_span_events(span_events);
88    Ok(tracing::Dispatch::new(
89        tracing_subscriber::registry().with(layer).with(filter),
90    ))
91}
92
93fn build_json_subscriber(
94    config: LogConfig,
95    filter: EnvFilter,
96    span_events: FmtSpan,
97    writer: BoxMakeWriter,
98) -> CoreResult<tracing::Dispatch> {
99    let formatter = Format::default()
100        .json()
101        .with_ansi(false)
102        .with_target(config.with_target)
103        .with_current_span(config.include_current_span)
104        .with_span_list(config.include_span_list);
105
106    let layer = fmt::layer()
107        .fmt_fields(RedactingJsonFields::new(config.redaction.clone()))
108        .event_format(RedactingJsonFormat::new(
109            formatter,
110            config.redaction.clone(),
111        ))
112        .with_writer(writer);
113    let mut layer = layer;
114    layer.set_span_events(span_events);
115    Ok(tracing::Dispatch::new(
116        tracing_subscriber::registry().with(layer).with(filter),
117    ))
118}
119
120fn build_writer(config: &LogWriterConfig) -> CoreResult<BoxMakeWriter> {
121    validate_writer(config)?;
122    match config {
123        LogWriterConfig::Stdout => Ok(BoxMakeWriter::new(std::io::stdout)),
124        LogWriterConfig::Stderr => Ok(BoxMakeWriter::new(std::io::stderr)),
125        LogWriterConfig::File(path) => {
126            let file = writer::open_append_file(path)?;
127            Ok(BoxMakeWriter::new(move || {
128                file.try_clone().expect("clone log file")
129            }))
130        }
131        LogWriterConfig::RollingFile(rolling) => {
132            let writer = RuntimeRollingFileWriter::new(rolling.clone())?;
133            Ok(BoxMakeWriter::new(move || writer.clone()))
134        }
135    }
136}
137
138fn span_events(events: LogSpanEvents) -> FmtSpan {
139    match events {
140        LogSpanEvents::None => FmtSpan::NONE,
141        LogSpanEvents::Close => FmtSpan::CLOSE,
142        LogSpanEvents::NewAndClose => FmtSpan::NEW | FmtSpan::CLOSE,
143        LogSpanEvents::Full => FmtSpan::FULL,
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::{LogConfig, init_tracing};
150
151    #[test]
152    fn init_tracing_is_callable() {
153        let _ = init_tracing(LogConfig {
154            filter: "debug".to_string(),
155            ansi: false,
156            ..LogConfig::default()
157        });
158    }
159}