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