Skip to main content

log_io/
logger.rs

1//! The top-level [`Logger`] and its [`LoggerBuilder`].
2//!
3//! A `Logger` bundles a [`Filter`] gate, one or more [`Sink`]s, and
4//! (when std is available) timestamping and thread-local context
5//! propagation. Cloning a `Logger` clones a pair of `Arc`s; the
6//! underlying sinks are shared.
7
8use std::io::Write;
9use std::sync::Arc;
10
11use crate::context;
12use crate::error::Result;
13use crate::filter::Filter;
14use crate::format::Format;
15use crate::level::Level;
16use crate::record::{Field, Metadata, Record};
17use crate::sink::{Sink, StderrSink, StdoutSink, WriterSink};
18use crate::time;
19use crate::value::Value;
20
21/// Owned counterpart of [`Value`], used for default fields stored in
22/// the logger.
23#[derive(Debug, Clone)]
24enum OwnedValue {
25    Null,
26    Bool(bool),
27    I64(i64),
28    U64(u64),
29    F64(f64),
30    Str(String),
31    Char(char),
32}
33
34impl OwnedValue {
35    fn as_value(&self) -> Value<'_> {
36        match self {
37            Self::Null => Value::Null,
38            Self::Bool(b) => Value::Bool(*b),
39            Self::I64(n) => Value::I64(*n),
40            Self::U64(n) => Value::U64(*n),
41            Self::F64(n) => Value::F64(*n),
42            Self::Str(s) => Value::Str(s.as_str()),
43            Self::Char(c) => Value::Char(*c),
44        }
45    }
46
47    fn from_value(v: Value<'_>) -> Self {
48        match v {
49            Value::Null => Self::Null,
50            Value::Bool(b) => Self::Bool(b),
51            Value::I64(n) => Self::I64(n),
52            Value::U64(n) => Self::U64(n),
53            Value::F64(n) => Self::F64(n),
54            Value::Str(s) => Self::Str(s.to_owned()),
55            Value::Char(c) => Self::Char(c),
56        }
57    }
58}
59
60#[derive(Debug, Clone)]
61struct OwnedField {
62    key: String,
63    value: OwnedValue,
64}
65
66/// Optional handler invoked when a sink returns an error.
67///
68/// The default behavior of [`Logger::log`] is to silently swallow sink
69/// errors. Install an error handler to surface them (typically to
70/// stderr) without taking down the calling thread.
71pub type ErrorHandler = Arc<dyn Fn(&crate::error::Error) + Send + Sync>;
72
73/// The configured log pipeline.
74#[derive(Clone)]
75pub struct Logger {
76    inner: Arc<LoggerInner>,
77}
78
79struct LoggerInner {
80    filter: Filter,
81    sinks: Box<[Arc<dyn Sink>]>,
82    default_fields: Box<[OwnedField]>,
83    capture_timestamps: bool,
84    capture_context: bool,
85    on_error: Option<ErrorHandler>,
86}
87
88impl Logger {
89    /// Start configuring a logger.
90    #[must_use]
91    pub fn builder() -> LoggerBuilder {
92        LoggerBuilder {
93            filter: Filter::new(Level::Info),
94            sinks: Vec::new(),
95            default_fields: Vec::new(),
96            capture_timestamps: true,
97            capture_context: true,
98            on_error: None,
99        }
100    }
101
102    /// The configured filter threshold for `target`.
103    #[must_use]
104    pub fn threshold_for(&self, target: &str) -> Level {
105        self.inner.filter.threshold_for(target)
106    }
107
108    /// Returns `true` if a record with this severity and target would
109    /// pass the filter. Use in hot paths to skip field construction
110    /// before [`Self::log`].
111    #[must_use]
112    pub fn enabled(&self, target: &str, level: Level) -> bool {
113        self.inner.filter.is_enabled(target, level)
114    }
115
116    /// The lowest severity the filter admits anywhere. Useful for
117    /// short-circuiting macros at the call site.
118    #[must_use]
119    pub fn min_level(&self) -> Level {
120        let mut min = self.inner.filter.default_level();
121        for rule in self.inner.filter.rules() {
122            if rule.level < min {
123                min = rule.level;
124            }
125        }
126        min
127    }
128
129    /// Emit a record using the empty string as the target. The
130    /// simplest entry point; macros prefer [`Self::try_emit`] so they
131    /// can attach source location.
132    pub fn log(&self, level: Level, message: &str, fields: &[Field<'_>]) {
133        let _ = self.try_log(level, "", message, fields);
134    }
135
136    /// Emit a record at `level` with explicit `target`.
137    ///
138    /// Sink errors are passed to the configured error handler (if any)
139    /// and then returned. Use [`Self::log`] for fire-and-forget calls.
140    ///
141    /// # Errors
142    ///
143    /// Returns the first sink error; remaining sinks still receive the
144    /// record so a transient failure on one destination does not
145    /// starve the others.
146    pub fn try_log(
147        &self,
148        level: Level,
149        target: &str,
150        message: &str,
151        fields: &[Field<'_>],
152    ) -> Result<()> {
153        let metadata = Metadata::new(level, target);
154        self.try_emit(metadata, message, fields)
155    }
156
157    /// Lowest-level emission entry point. Accepts a fully-constructed
158    /// [`Metadata`] so the caller (typically a macro) can attach
159    /// source location.
160    ///
161    /// The logger augments the metadata with a timestamp (if enabled)
162    /// before dispatch. Filtering uses `metadata.target` and
163    /// `metadata.level`.
164    ///
165    /// # Errors
166    ///
167    /// Returns the first sink error encountered. Other sinks are still
168    /// attempted; see the type-level docs on dispatch semantics.
169    pub fn try_emit(
170        &self,
171        mut metadata: Metadata<'_>,
172        message: &str,
173        fields: &[Field<'_>],
174    ) -> Result<()> {
175        if !self
176            .inner
177            .filter
178            .is_enabled(metadata.target, metadata.level)
179        {
180            return Ok(());
181        }
182        if self.inner.capture_timestamps && metadata.timestamp_unix_nanos.is_none() {
183            metadata = metadata.with_timestamp(time::now_unix_nanos());
184        }
185
186        let result = if self.inner.capture_context {
187            context::with_snapshot(|ctx_fields| {
188                self.dispatch_with_context(metadata, message, fields, ctx_fields)
189            })
190        } else {
191            self.dispatch_with_context(metadata, message, fields, &[])
192        };
193
194        if let Err(ref e) = result {
195            if let Some(handler) = &self.inner.on_error {
196                handler(e);
197            }
198        }
199        result
200    }
201
202    /// Flush every configured sink. Use during graceful shutdown to
203    /// drain buffered output.
204    ///
205    /// # Errors
206    ///
207    /// Returns the first flush error. Other sinks are still flushed.
208    pub fn flush(&self) -> Result<()> {
209        let mut first_err: Option<crate::error::Error> = None;
210        for sink in self.inner.sinks.iter() {
211            if let Err(e) = sink.flush() {
212                if first_err.is_none() {
213                    first_err = Some(e);
214                }
215            }
216        }
217        match first_err {
218            Some(e) => Err(e),
219            None => Ok(()),
220        }
221    }
222
223    /// Number of installed sinks. Useful for assertions in tests and
224    /// for logging the logger's own configuration.
225    #[must_use]
226    pub fn sink_count(&self) -> usize {
227        self.inner.sinks.len()
228    }
229
230    fn dispatch_with_context(
231        &self,
232        metadata: Metadata<'_>,
233        message: &str,
234        fields: &[Field<'_>],
235        ctx_fields: &[Field<'_>],
236    ) -> Result<()> {
237        // Merge logger-default fields, thread-local context, and call
238        // fields into the record's `context` slot. Per-call fields go
239        // in `fields` so formatters can still distinguish them.
240        //
241        // Materialize default fields onto the stack via a small inline
242        // buffer; on overflow fall back to a Vec.
243        if self.inner.default_fields.is_empty() {
244            let record = Record::with_context(metadata, message, fields, ctx_fields);
245            return self.dispatch(&record);
246        }
247
248        // Stack buffer large enough for typical cases.
249        const STACK: usize = 16;
250        let total = self.inner.default_fields.len() + ctx_fields.len();
251        if total <= STACK {
252            let mut buf: [Field<'_>; STACK] = [Field {
253                key: "",
254                value: Value::Null,
255            }; STACK];
256            let mut n = 0;
257            for f in self.inner.default_fields.iter() {
258                buf[n] = Field::new(f.key.as_str(), f.value.as_value());
259                n += 1;
260            }
261            for f in ctx_fields {
262                buf[n] = *f;
263                n += 1;
264            }
265            let record = Record::with_context(metadata, message, fields, &buf[..n]);
266            self.dispatch(&record)
267        } else {
268            let mut merged: Vec<Field<'_>> = Vec::with_capacity(total);
269            for f in self.inner.default_fields.iter() {
270                merged.push(Field::new(f.key.as_str(), f.value.as_value()));
271            }
272            merged.extend_from_slice(ctx_fields);
273            let record = Record::with_context(metadata, message, fields, &merged);
274            self.dispatch(&record)
275        }
276    }
277
278    fn dispatch(&self, record: &Record<'_>) -> Result<()> {
279        let mut first_err: Option<crate::error::Error> = None;
280        for sink in self.inner.sinks.iter() {
281            if let Err(e) = sink.write_record(record) {
282                if first_err.is_none() {
283                    first_err = Some(e);
284                }
285            }
286        }
287        match first_err {
288            Some(e) => Err(e),
289            None => Ok(()),
290        }
291    }
292}
293
294impl core::fmt::Debug for Logger {
295    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
296        f.debug_struct("Logger")
297            .field("filter", &self.inner.filter)
298            .field("sinks", &self.inner.sinks.len())
299            .field("default_fields", &self.inner.default_fields.len())
300            .field("capture_timestamps", &self.inner.capture_timestamps)
301            .field("capture_context", &self.inner.capture_context)
302            .finish()
303    }
304}
305
306/// Builder for [`Logger`]. Construct with [`Logger::builder`].
307///
308/// The builder is not generic over a format. Each call that installs
309/// a sink ([`Self::with_sink`], [`Self::stdout`], [`Self::stderr`],
310/// [`Self::writer`], or one of the format-specific shortcuts) carries
311/// its own format.
312pub struct LoggerBuilder {
313    filter: Filter,
314    sinks: Vec<Arc<dyn Sink>>,
315    default_fields: Vec<OwnedField>,
316    capture_timestamps: bool,
317    capture_context: bool,
318    on_error: Option<ErrorHandler>,
319}
320
321impl LoggerBuilder {
322    /// Set the default minimum severity.
323    #[must_use]
324    pub fn level(mut self, level: Level) -> Self {
325        let prior_rules = self.filter.rules().to_vec();
326        self.filter = Filter::new(level);
327        for rule in prior_rules {
328            self.filter = self.filter.with_rule(rule.target, rule.level);
329        }
330        self
331    }
332
333    /// Replace the filter wholesale.
334    #[must_use]
335    pub fn filter(mut self, filter: Filter) -> Self {
336        self.filter = filter;
337        self
338    }
339
340    /// Add a per-target filter override. See [`Filter::with_rule`].
341    #[must_use]
342    pub fn target_level(mut self, target: impl Into<String>, level: Level) -> Self {
343        self.filter = self.filter.with_rule(target, level);
344        self
345    }
346
347    /// Parse `directive` and replace the filter. Silently retains the
348    /// previous filter on parse error; use [`Filter::parse`] up front
349    /// if you need to surface the error.
350    #[must_use]
351    pub fn filter_directive(mut self, directive: &str) -> Self {
352        if let Ok(f) = Filter::parse(directive) {
353            self.filter = f;
354        }
355        self
356    }
357
358    /// Disable timestamp capture. By default the logger records a
359    /// timestamp on every record using [`std::time::SystemTime`].
360    #[must_use]
361    pub fn no_timestamps(mut self) -> Self {
362        self.capture_timestamps = false;
363        self
364    }
365
366    /// Disable thread-local context capture.
367    #[must_use]
368    pub fn no_context(mut self) -> Self {
369        self.capture_context = false;
370        self
371    }
372
373    /// Attach a field that the logger emits on every record. Useful
374    /// for service-level identifiers like `service_name` or `version`.
375    #[must_use]
376    pub fn with_default_field<'a, V: Into<Value<'a>>>(
377        mut self,
378        key: impl Into<String>,
379        value: V,
380    ) -> Self {
381        self.default_fields.push(OwnedField {
382            key: key.into(),
383            value: OwnedValue::from_value(value.into()),
384        });
385        self
386    }
387
388    /// Install a callback invoked when a sink returns an error.
389    #[must_use]
390    pub fn on_error(mut self, handler: ErrorHandler) -> Self {
391        self.on_error = Some(handler);
392        self
393    }
394
395    /// Install a sink directly. Use this when you have a pre-built
396    /// sink (typical for [`crate::sink::FileSink`] which is fallible
397    /// to open).
398    #[must_use]
399    pub fn with_sink(mut self, sink: impl Sink + 'static) -> Self {
400        self.sinks.push(Arc::new(sink));
401        self
402    }
403
404    /// Install a stdout sink with the given format.
405    #[must_use]
406    pub fn stdout<F>(mut self, format: F) -> Self
407    where
408        F: Format + Send + Sync + 'static,
409    {
410        self.sinks.push(Arc::new(StdoutSink::new(format)));
411        self
412    }
413
414    /// Install a stderr sink with the given format.
415    #[must_use]
416    pub fn stderr<F>(mut self, format: F) -> Self
417    where
418        F: Format + Send + Sync + 'static,
419    {
420        self.sinks.push(Arc::new(StderrSink::new(format)));
421        self
422    }
423
424    /// Install a sink that writes to `writer` with `format`.
425    #[must_use]
426    pub fn writer<W, F>(mut self, writer: W, format: F) -> Self
427    where
428        W: Write + Send + 'static,
429        F: Format + Send + Sync + 'static,
430    {
431        self.sinks.push(Arc::new(WriterSink::new(writer, format)));
432        self
433    }
434
435    /// Finish configuration. Panics if no sink has been installed.
436    ///
437    /// # Panics
438    ///
439    /// Panics if [`Self::with_sink`], [`Self::stdout`], [`Self::stderr`],
440    /// [`Self::writer`], or one of the format shortcuts has not been
441    /// called. Building a logger without a destination is almost
442    /// certainly a programmer error.
443    #[must_use]
444    pub fn build(self) -> Logger {
445        assert!(
446            !self.sinks.is_empty(),
447            "log_io: builder must install at least one sink before build()"
448        );
449        Logger {
450            inner: Arc::new(LoggerInner {
451                filter: self.filter,
452                sinks: self.sinks.into_boxed_slice(),
453                default_fields: self.default_fields.into_boxed_slice(),
454                capture_timestamps: self.capture_timestamps,
455                capture_context: self.capture_context,
456                on_error: self.on_error,
457            }),
458        }
459    }
460}
461
462// Format-shortcut methods. These are feature-gated and produce the
463// most common (stdout, stderr) sinks pre-wired to a built-in format.
464
465#[cfg(feature = "json")]
466impl LoggerBuilder {
467    /// Install a stdout sink with the default JSON format.
468    #[must_use]
469    pub fn stdout_json(self) -> Self {
470        self.stdout(crate::format::JsonFormat::new())
471    }
472
473    /// Install a stderr sink with the default JSON format.
474    #[must_use]
475    pub fn stderr_json(self) -> Self {
476        self.stderr(crate::format::JsonFormat::new())
477    }
478}
479
480#[cfg(feature = "logfmt")]
481impl LoggerBuilder {
482    /// Install a stdout sink with the logfmt format.
483    #[must_use]
484    pub fn stdout_logfmt(self) -> Self {
485        self.stdout(crate::format::LogfmtFormat::new())
486    }
487
488    /// Install a stderr sink with the logfmt format.
489    #[must_use]
490    pub fn stderr_logfmt(self) -> Self {
491        self.stderr(crate::format::LogfmtFormat::new())
492    }
493}
494
495#[cfg(feature = "human")]
496impl LoggerBuilder {
497    /// Install a stdout sink with the human-readable format.
498    #[must_use]
499    pub fn stdout_human(self) -> Self {
500        self.stdout(crate::format::HumanFormat::new())
501    }
502
503    /// Install a stderr sink with the human-readable format.
504    #[must_use]
505    pub fn stderr_human(self) -> Self {
506        self.stderr(crate::format::HumanFormat::new())
507    }
508}
509
510#[cfg(all(test, feature = "std", feature = "logfmt", feature = "json"))]
511mod tests {
512    use super::*;
513    use std::io::Write;
514    use std::sync::{Arc, Mutex};
515
516    #[derive(Default, Clone)]
517    struct MemBuf(Arc<Mutex<Vec<u8>>>);
518
519    impl Write for MemBuf {
520        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
521            self.0.lock().unwrap().extend_from_slice(buf);
522            Ok(buf.len())
523        }
524        fn flush(&mut self) -> std::io::Result<()> {
525            Ok(())
526        }
527    }
528
529    fn logfmt_logger() -> (Logger, MemBuf) {
530        let buf = MemBuf::default();
531        let logger = Logger::builder()
532            .level(Level::Trace)
533            .no_timestamps()
534            .no_context()
535            .writer(buf.clone(), crate::format::LogfmtFormat::new())
536            .build();
537        (logger, buf)
538    }
539
540    #[test]
541    fn enabled_respects_filter() {
542        let (logger, _) = logfmt_logger();
543        assert!(logger.enabled("x", Level::Trace));
544        assert!(logger.enabled("x", Level::Error));
545    }
546
547    #[test]
548    fn min_level_is_lowest_configured() {
549        let logger = Logger::builder()
550            .level(Level::Warn)
551            .target_level("loud", Level::Trace)
552            .no_timestamps()
553            .no_context()
554            .with_sink(crate::sink::NullSink::new())
555            .build();
556        assert_eq!(logger.min_level(), Level::Trace);
557    }
558
559    #[test]
560    fn log_writes_through_logfmt() {
561        let (logger, buf) = logfmt_logger();
562        logger.log(Level::Info, "hi", &[Field::new("k", 1_u64)]);
563        let out = String::from_utf8(buf.0.lock().unwrap().clone()).unwrap();
564        assert!(out.contains("level=info"));
565        assert!(out.contains("message=hi"));
566        assert!(out.contains("k=1"));
567    }
568
569    #[test]
570    fn filter_blocks_records_below_threshold() {
571        let buf = MemBuf::default();
572        let logger = Logger::builder()
573            .level(Level::Warn)
574            .no_timestamps()
575            .no_context()
576            .writer(buf.clone(), crate::format::LogfmtFormat::new())
577            .build();
578        logger.log(Level::Info, "skip", &[]);
579        logger.log(Level::Error, "keep", &[]);
580        let out = String::from_utf8(buf.0.lock().unwrap().clone()).unwrap();
581        assert!(!out.contains("skip"));
582        assert!(out.contains("keep"));
583    }
584
585    #[test]
586    fn context_is_spliced_in() {
587        let buf = MemBuf::default();
588        let logger = Logger::builder()
589            .level(Level::Trace)
590            .no_timestamps()
591            .writer(buf.clone(), crate::format::LogfmtFormat::new())
592            .build();
593        crate::context::clear();
594        let guard = crate::context::with_trace_id("tx-42");
595        logger.log(Level::Info, "hello", &[]);
596        drop(guard);
597        let out = String::from_utf8(buf.0.lock().unwrap().clone()).unwrap();
598        assert!(out.contains("trace_id=tx-42"), "{out}");
599    }
600
601    #[test]
602    fn default_fields_appear_on_every_record() {
603        let buf = MemBuf::default();
604        let logger = Logger::builder()
605            .level(Level::Trace)
606            .no_timestamps()
607            .no_context()
608            .with_default_field("service", "api")
609            .with_default_field("region", "us-east-1")
610            .writer(buf.clone(), crate::format::LogfmtFormat::new())
611            .build();
612        logger.log(Level::Info, "x", &[]);
613        logger.log(Level::Warn, "y", &[Field::new("port", 80_u32)]);
614        let out = String::from_utf8(buf.0.lock().unwrap().clone()).unwrap();
615        assert_eq!(out.matches("service=api").count(), 2);
616        assert_eq!(out.matches("region=us-east-1").count(), 2);
617        assert!(out.contains("port=80"));
618    }
619
620    #[test]
621    fn dispatch_continues_after_sink_error() {
622        use std::sync::atomic::{AtomicUsize, Ordering};
623        struct FailingSink(AtomicUsize);
624        impl Sink for FailingSink {
625            fn write_record(&self, _record: &Record<'_>) -> Result<()> {
626                self.0.fetch_add(1, Ordering::Relaxed);
627                Err(crate::error::Error::Configuration("boom"))
628            }
629            fn flush(&self) -> Result<()> {
630                Ok(())
631            }
632        }
633        let failing: Arc<FailingSink> = Arc::new(FailingSink(AtomicUsize::new(0)));
634        let buf = MemBuf::default();
635        let logger = Logger::builder()
636            .level(Level::Trace)
637            .no_timestamps()
638            .no_context()
639            .with_sink(failing.clone())
640            .writer(buf.clone(), crate::format::JsonFormat::new())
641            .build();
642        let _ = logger.try_log(Level::Info, "t", "m", &[]);
643        // Second sink still wrote even though first failed.
644        let out = String::from_utf8(buf.0.lock().unwrap().clone()).unwrap();
645        assert!(out.contains("\"message\":\"m\""), "{out}");
646        assert_eq!(failing.0.load(Ordering::Relaxed), 1);
647    }
648
649    #[test]
650    fn on_error_callback_fires() {
651        use std::sync::atomic::{AtomicUsize, Ordering};
652        struct Bad;
653        impl Sink for Bad {
654            fn write_record(&self, _r: &Record<'_>) -> Result<()> {
655                Err(crate::error::Error::Configuration("nope"))
656            }
657            fn flush(&self) -> Result<()> {
658                Ok(())
659            }
660        }
661        let count = Arc::new(AtomicUsize::new(0));
662        let count_clone = count.clone();
663        let logger = Logger::builder()
664            .level(Level::Trace)
665            .no_timestamps()
666            .no_context()
667            .with_sink(Bad)
668            .on_error(Arc::new(move |_| {
669                count_clone.fetch_add(1, Ordering::Relaxed);
670            }))
671            .build();
672        let _ = logger.try_log(Level::Info, "t", "m", &[]);
673        assert_eq!(count.load(Ordering::Relaxed), 1);
674    }
675}