Skip to main content

log_io/
logger.rs

1//! The [`Logger`]: filter, assemble, dispatch.
2//!
3//! A `Logger` bundles a [`Filter`], one or more [`Sink`]s, optional
4//! timestamping, thread-local context capture, default fields,
5//! redaction, and the flush policy. Cloning a `Logger` clones an `Arc`;
6//! all clones share the same sinks.
7
8use core::cell::{Cell, RefCell};
9use core::fmt;
10use std::sync::Arc;
11
12use crate::context;
13use crate::error::{Error, Result};
14use crate::filter::Filter;
15use crate::level::Level;
16use crate::record::{Field, Metadata, Record, same_output_key};
17use crate::sink::{Sink, trim_buffer};
18use crate::time;
19use crate::value::{OwnedField, Value};
20
21use crate::builder::LoggerBuilder;
22
23/// Callback invoked for every sink error, flush error, and message
24/// formatting error the logger encounters.
25///
26/// The handler runs on the thread that logged the record. If the
27/// handler itself logs through a logger whose sink keeps failing, the
28/// nested failures are not reported again (a per-thread guard prevents
29/// unbounded recursion), but the nested records are still dispatched.
30///
31/// # Examples
32///
33#[cfg_attr(all(feature = "logfmt"), doc = "```")]
34#[cfg_attr(not(all(feature = "logfmt")), doc = "```ignore")]
35/// use std::sync::Arc;
36/// use log_io::{ErrorHandler, Logger};
37///
38/// let handler: ErrorHandler = Arc::new(|err| eprintln!("logging failed: {err}"));
39/// let logger = Logger::builder().stderr_logfmt().on_error(handler).build();
40/// # let _ = logger;
41/// ```
42pub type ErrorHandler = Arc<dyn Fn(&Error) + Send + Sync>;
43
44/// Value written in place of a redacted field.
45pub const REDACTED: &str = "[REDACTED]";
46
47/// The configured logging pipeline.
48///
49/// Build one with [`Logger::builder`]. A `Logger` is cheap to clone and
50/// `Send + Sync`; share it by cloning or install it process-wide with
51/// [`crate::init`].
52///
53/// # Record assembly
54///
55/// For each admitted record the logger:
56///
57/// 1. stamps the wall-clock time (unless disabled);
58/// 2. collects ambient fields: default fields, then the thread's
59///    context ([`crate::context`]), then any extra context supplied by
60///    an integration (for example `tracing` span fields);
61/// 3. removes duplicate keys so every key appears once in the output.
62///    Precedence is call-site fields > extra context > thread context >
63///    defaults, and within call-site fields the last one wins (as in
64///    most JSON parsers). Keys count as duplicates when the JSON and
65///    logfmt formats would write them identically, so a `level` field
66///    (written as `fields.level`) and a `fields.level` field collide.
67///    Records written straight to a [`crate::format::Format`] without a
68///    logger are not de-duplicated;
69/// 4. replaces the values of redacted keys with [`REDACTED`];
70/// 5. hands the record to every sink in installation order. A failing
71///    sink does not stop the others; each failure goes to the error
72///    handler;
73/// 6. flushes every sink if the record is at or above the flush level
74///    (default [`Level::Error`]).
75///
76/// Steps 2-4 use stack buffers for up to 16 fields per group and do not
77/// allocate in that case.
78///
79/// # Shutdown
80///
81/// When the last clone is dropped, every sink is shut down (flushed,
82/// background threads drained and joined) and failures are reported
83/// to the error handler. A logger stored in a `static` is never
84/// dropped: keep the [`crate::FlushGuard`] from [`crate::init`] or
85/// [`Logger::flush_guard`] alive, or call [`Logger::shutdown`].
86///
87/// # Examples
88///
89#[cfg_attr(all(feature = "json"), doc = "```")]
90#[cfg_attr(not(all(feature = "json")), doc = "```ignore")]
91/// use log_io::{Field, Level, Logger};
92///
93/// let logger = Logger::builder()
94///     .level(Level::Info)
95///     .with_default_field("service", "billing")
96///     .stdout_json()
97///     .build();
98///
99/// logger.log(Level::Info, "ready", &[Field::new("port", 8443_u16)]);
100/// log_io::warn!(logger, "slow query took {} ms", 1280, table = "invoices");
101/// ```
102#[derive(Clone)]
103pub struct Logger {
104    inner: Arc<LoggerInner>,
105}
106
107// `Logger` keeps working after a panic anywhere in the pipeline: sink
108// locks recover from poisoning and no logger state is left half-updated
109// by an unwind, so observing it after `catch_unwind` is sound.
110impl std::panic::UnwindSafe for Logger {}
111impl std::panic::RefUnwindSafe for Logger {}
112
113pub(crate) struct LoggerInner {
114    pub(crate) filter: Filter,
115    pub(crate) sinks: Box<[Box<dyn Sink>]>,
116    pub(crate) defaults: Box<[OwnedField]>,
117    pub(crate) default_target: Box<str>,
118    pub(crate) capture_timestamps: bool,
119    pub(crate) capture_context: bool,
120    pub(crate) flush_level: Level,
121    pub(crate) on_error: Option<ErrorHandler>,
122    /// Lower-cased keys whose values are replaced by [`REDACTED`].
123    pub(crate) redact: Box<[Box<str>]>,
124}
125
126impl Drop for LoggerInner {
127    fn drop(&mut self) {
128        for sink in &self.sinks {
129            if let Err(e) = sink.shutdown() {
130                report_with(self.on_error.as_ref(), &e);
131            }
132        }
133    }
134}
135
136thread_local! {
137    /// Depth of `dispatch` calls on this thread. Read by the panic
138    /// hook so it never re-enters a sink that the panicking frame may
139    /// still hold locked.
140    static DISPATCH_DEPTH: Cell<u32> = const { Cell::new(0) };
141    /// Set while an error handler runs on this thread.
142    static IN_HANDLER: Cell<bool> = const { Cell::new(false) };
143    /// Reusable buffer for rendering `format_args!` messages.
144    static MESSAGE_BUF: RefCell<String> = const { RefCell::new(String::new()) };
145}
146
147/// `true` while this thread is inside a logger dispatch (a sink or
148/// format is running).
149pub(crate) fn in_dispatch() -> bool {
150    DISPATCH_DEPTH.try_with(Cell::get).unwrap_or(0) > 0
151}
152
153struct DepthGuard;
154
155impl DepthGuard {
156    fn enter() -> Self {
157        let _tls_may_be_gone = DISPATCH_DEPTH.try_with(|d| d.set(d.get().saturating_add(1)));
158        Self
159    }
160}
161
162impl Drop for DepthGuard {
163    fn drop(&mut self) {
164        let _tls_may_be_gone = DISPATCH_DEPTH.try_with(|d| d.set(d.get().saturating_sub(1)));
165    }
166}
167
168fn report_with(handler: Option<&ErrorHandler>, error: &Error) {
169    let Some(handler) = handler else {
170        return;
171    };
172    let _tls_may_be_gone = IN_HANDLER.try_with(|busy| {
173        if busy.get() {
174            return;
175        }
176        busy.set(true);
177        struct Reset<'a>(&'a Cell<bool>);
178        impl Drop for Reset<'_> {
179            fn drop(&mut self) {
180                self.0.set(false);
181            }
182        }
183        let _reset = Reset(busy);
184        handler(error);
185    });
186}
187
188impl Logger {
189    /// Start configuring a logger. See [`LoggerBuilder`] for defaults.
190    pub fn builder() -> LoggerBuilder {
191        LoggerBuilder::new()
192    }
193
194    pub(crate) fn from_inner(inner: LoggerInner) -> Self {
195        Self {
196            inner: Arc::new(inner),
197        }
198    }
199
200    /// The filter threshold that applies to `target`.
201    #[must_use]
202    pub fn threshold_for(&self, target: &str) -> Level {
203        self.inner.filter.threshold_for(target)
204    }
205
206    /// Returns `true` if a record with this target and level would pass
207    /// the filter. The macros call this before evaluating any field or
208    /// formatting the message.
209    #[inline]
210    #[must_use]
211    pub fn enabled(&self, target: &str, level: Level) -> bool {
212        self.inner.filter.is_enabled(target, level)
213    }
214
215    /// The lowest severity the filter admits for any target. A record
216    /// below it is rejected without looking at its target.
217    #[must_use]
218    pub fn min_level(&self) -> Level {
219        self.inner.filter.min_level()
220    }
221
222    /// The filter in use.
223    #[must_use]
224    pub fn filter(&self) -> &Filter {
225        &self.inner.filter
226    }
227
228    /// Number of installed sinks.
229    #[must_use]
230    pub fn sink_count(&self) -> usize {
231        self.inner.sinks.len()
232    }
233
234    /// The target used by [`Logger::log`] (empty unless set with
235    /// [`LoggerBuilder::default_target`]).
236    #[must_use]
237    pub fn default_target(&self) -> &str {
238        &self.inner.default_target
239    }
240
241    /// Emit a record with the logger's default target.
242    ///
243    /// Errors are passed to the error handler, not returned. Note that
244    /// per-target filter rules only apply to this record if they match
245    /// the default target; the macros use the calling module's path
246    /// instead.
247    #[inline]
248    pub fn log(&self, level: Level, message: &str, fields: &[Field<'_>]) {
249        let _reported_to_handler = self.try_log(level, &self.inner.default_target, message, fields);
250    }
251
252    /// Emit a record with an explicit target.
253    ///
254    /// # Errors
255    ///
256    /// Returns the first sink or flush error. Every sink still receives
257    /// the record, and every error is also passed to the error handler.
258    #[inline]
259    pub fn try_log(
260        &self,
261        level: Level,
262        target: &str,
263        message: &str,
264        fields: &[Field<'_>],
265    ) -> Result<()> {
266        self.try_emit(Metadata::new(level, target), message, fields)
267    }
268
269    /// Lowest-level entry point: emit a record with caller-supplied
270    /// metadata (for example a source location). The filter uses
271    /// `metadata.target` and `metadata.level`; a timestamp is added if
272    /// timestamps are enabled and `metadata` has none.
273    ///
274    /// # Errors
275    ///
276    /// Same as [`Logger::try_log`].
277    #[inline]
278    pub fn try_emit(
279        &self,
280        metadata: Metadata<'_>,
281        message: &str,
282        fields: &[Field<'_>],
283    ) -> Result<()> {
284        if !self.enabled(metadata.target, metadata.level) {
285            return Ok(());
286        }
287        self.emit_unchecked(metadata, message, fields, &[])
288    }
289
290    /// Flush every sink. Records at or above the flush level are already
291    /// flushed as they are written; call this before a planned exit.
292    ///
293    /// # Errors
294    ///
295    /// Returns the first error. Every sink is still flushed, and every
296    /// error is also passed to the error handler.
297    pub fn flush(&self) -> Result<()> {
298        self.for_each_sink(|sink| sink.flush())
299    }
300
301    /// Flush every sink and stop background workers, draining their
302    /// queues first. The logger stays usable afterwards: sinks with a
303    /// background thread write later records synchronously.
304    ///
305    /// # Errors
306    ///
307    /// Same as [`Logger::flush`].
308    pub fn shutdown(&self) -> Result<()> {
309        self.for_each_sink(|sink| sink.shutdown())
310    }
311
312    /// A guard that calls [`Logger::shutdown`] when dropped. Keep it
313    /// alive in `main` when the logger outlives `main` (for example in
314    /// a `static`).
315    pub fn flush_guard(&self) -> crate::FlushGuard {
316        crate::FlushGuard::new(self.clone())
317    }
318
319    fn for_each_sink(&self, op: impl Fn(&dyn Sink) -> Result<()>) -> Result<()> {
320        let _depth = DepthGuard::enter();
321        let mut first = None;
322        for sink in &self.inner.sinks {
323            if let Err(e) = op(&**sink) {
324                self.report(&e);
325                if first.is_none() {
326                    first = Some(e);
327                }
328            }
329        }
330        first.map_or(Ok(()), Err)
331    }
332
333    pub(crate) fn report(&self, error: &Error) {
334        report_with(self.inner.on_error.as_ref(), error);
335    }
336
337    /// Emit a message given as format arguments, without re-checking
338    /// the filter. Renders into a reused thread-local buffer, so it
339    /// does not allocate in steady state; a literal with no arguments
340    /// is used as-is.
341    pub(crate) fn emit_fmt(
342        &self,
343        metadata: Metadata<'_>,
344        args: fmt::Arguments<'_>,
345        fields: &[Field<'_>],
346        extra: &[Field<'_>],
347    ) {
348        if let Some(message) = args.as_str() {
349            let _reported = self.emit_unchecked(metadata, message, fields, extra);
350            return;
351        }
352        let pooled = MESSAGE_BUF.try_with(|cell| {
353            let mut buf = cell.try_borrow_mut().ok()?;
354            self.emit_rendered(&mut buf, metadata, args, fields, extra);
355            trim_buffer(&mut buf);
356            Some(())
357        });
358        if !matches!(pooled, Ok(Some(()))) {
359            // Reentrant call from inside a sink, or thread teardown.
360            self.emit_rendered(&mut String::new(), metadata, args, fields, extra);
361        }
362    }
363
364    fn emit_rendered(
365        &self,
366        buf: &mut String,
367        metadata: Metadata<'_>,
368        args: fmt::Arguments<'_>,
369        fields: &[Field<'_>],
370        extra: &[Field<'_>],
371    ) {
372        buf.clear();
373        let rendered = fmt::write(buf, args);
374        let _reported = self.emit_unchecked(metadata, buf, fields, extra);
375        if rendered.is_err() {
376            // A `Display` impl in the arguments failed; the record was
377            // emitted with whatever text was produced before the error.
378            self.report(&Error::Format(fmt::Error));
379        }
380    }
381
382    /// Assemble and dispatch without consulting the filter. `extra` is
383    /// ambient context supplied by an integration; it shadows thread
384    /// context and defaults.
385    pub(crate) fn emit_unchecked(
386        &self,
387        mut metadata: Metadata<'_>,
388        message: &str,
389        fields: &[Field<'_>],
390        extra: &[Field<'_>],
391    ) -> Result<()> {
392        if self.inner.capture_timestamps && metadata.timestamp_unix_nanos.is_none() {
393            metadata.timestamp_unix_nanos = Some(time::now_unix_nanos());
394        }
395        if self.inner.capture_context {
396            context::with_snapshot(|ctx| self.assemble(metadata, message, fields, ctx, extra))
397        } else {
398            self.assemble(metadata, message, fields, &[], extra)
399        }
400    }
401
402    fn assemble(
403        &self,
404        metadata: Metadata<'_>,
405        message: &str,
406        fields: &[Field<'_>],
407        ctx: &[Field<'_>],
408        extra: &[Field<'_>],
409    ) -> Result<()> {
410        // Call-site keys are unique in the output too: the last one wins,
411        // keeping its position. Copied only when a duplicate exists.
412        let unique;
413        let fields = if has_duplicate_keys(fields) {
414            unique = last_occurrences(fields);
415            unique.as_slice()
416        } else {
417            fields
418        };
419        let redacting = !self.inner.redact.is_empty();
420        if self.inner.defaults.is_empty()
421            && extra.is_empty()
422            && !redacting
423            && (ctx.is_empty() || !any_key_in(ctx, fields))
424        {
425            return self.dispatch(&Record::with_context(metadata, message, fields, ctx));
426        }
427
428        // Common case: default fields only (no thread or extra context,
429        // no redaction). Defaults have unique keys by construction, so
430        // only collisions with call fields need checking.
431        let defaults = &self.inner.defaults;
432        if ctx.is_empty() && extra.is_empty() && !redacting && defaults.len() <= INLINE_FIELDS {
433            let mut buf = [EMPTY_FIELD; INLINE_FIELDS];
434            let mut len = 0;
435            for owned in defaults {
436                if !fields.iter().any(|f| same_output_key(f.key, &owned.key)) {
437                    buf[len] = owned.as_field();
438                    len += 1;
439                }
440            }
441            return self.dispatch(&Record::with_context(
442                metadata,
443                message,
444                fields,
445                &buf[..len],
446            ));
447        }
448
449        if redacting && fields.iter().any(|f| self.is_redacted(f.key)) {
450            let mut call = FieldBuf::new();
451            for f in fields {
452                call.push(self.redacted(*f));
453            }
454            self.merge_ambient(metadata, message, call.as_slice(), ctx, extra)
455        } else {
456            self.merge_ambient(metadata, message, fields, ctx, extra)
457        }
458    }
459
460    /// Merge defaults, thread context and extra context into one
461    /// de-duplicated, redacted slice, then dispatch.
462    fn merge_ambient(
463        &self,
464        metadata: Metadata<'_>,
465        message: &str,
466        fields: &[Field<'_>],
467        ctx: &[Field<'_>],
468        extra: &[Field<'_>],
469    ) -> Result<()> {
470        // Highest precedence first; each layer newest first. Reversed
471        // at the end so the output keeps defaults, context, extra order.
472        let mut ambient = FieldBuf::new();
473        for layer in [extra, ctx] {
474            for f in layer.iter().rev() {
475                self.push_ambient(&mut ambient, *f, fields);
476            }
477        }
478        for owned in self.inner.defaults.iter().rev() {
479            self.push_ambient(&mut ambient, owned.as_field(), fields);
480        }
481        ambient.reverse();
482        self.dispatch(&Record::with_context(
483            metadata,
484            message,
485            fields,
486            ambient.as_slice(),
487        ))
488    }
489
490    #[inline]
491    fn push_ambient<'a>(&self, ambient: &mut FieldBuf<'a>, field: Field<'a>, call: &[Field<'_>]) {
492        let shadowed = call.iter().any(|c| same_output_key(c.key, field.key))
493            || ambient
494                .as_slice()
495                .iter()
496                .any(|a| same_output_key(a.key, field.key));
497        if !shadowed {
498            ambient.push(self.redacted(field));
499        }
500    }
501
502    #[inline]
503    fn redacted<'a>(&self, field: Field<'a>) -> Field<'a> {
504        if self.is_redacted(field.key) {
505            Field {
506                key: field.key,
507                value: Value::Str(REDACTED),
508            }
509        } else {
510            field
511        }
512    }
513
514    /// A key is redacted when it, or its last dot-separated segment,
515    /// matches a configured key (ASCII case-insensitive, `-` == `_`).
516    fn is_redacted(&self, key: &str) -> bool {
517        if self.inner.redact.is_empty() {
518            return false;
519        }
520        let last = key.rsplit('.').next().unwrap_or(key);
521        self.inner
522            .redact
523            .iter()
524            .any(|r| redact_key_matches(r, key) || redact_key_matches(r, last))
525    }
526
527    fn dispatch(&self, record: &Record<'_>) -> Result<()> {
528        let _depth = DepthGuard::enter();
529        let mut first = None;
530        for sink in &self.inner.sinks {
531            if let Err(e) = sink.write_record(record) {
532                self.report(&e);
533                if first.is_none() {
534                    first = Some(e);
535                }
536            }
537        }
538        if record.metadata.level >= self.inner.flush_level {
539            for sink in &self.inner.sinks {
540                if let Err(e) = sink.request_flush() {
541                    self.report(&e);
542                    if first.is_none() {
543                        first = Some(e);
544                    }
545                }
546            }
547        }
548        first.map_or(Ok(()), Err)
549    }
550}
551
552/// Canonical form of a redaction key: ASCII lower case, `-` as `_`.
553pub(crate) fn normalize_redact_key(key: &str) -> String {
554    key.chars()
555        .map(|c| match c {
556            '-' => '_',
557            c => c.to_ascii_lowercase(),
558        })
559        .collect()
560}
561
562/// `true` if `key` matches the normalized redaction key `normalized`.
563/// Allocation-free; compares byte by byte.
564#[inline]
565fn redact_key_matches(normalized: &str, key: &str) -> bool {
566    normalized.len() == key.len()
567        && normalized.bytes().zip(key.bytes()).all(|(r, k)| {
568            let k = if k == b'-' {
569                b'_'
570            } else {
571                k.to_ascii_lowercase()
572            };
573            r == k
574        })
575}
576
577fn any_key_in(a: &[Field<'_>], b: &[Field<'_>]) -> bool {
578    !b.is_empty()
579        && a.iter()
580            .any(|x| b.iter().any(|y| same_output_key(x.key, y.key)))
581}
582
583/// `true` if two call-site fields would be written under one key.
584///
585/// Runs on every record with two or more fields, so it avoids the
586/// pairwise comparison of every key: one pass sets a bit per key class
587/// (see [`key_class`]), and only a key whose class is already taken is
588/// compared, with the earlier keys of that class.
589#[inline]
590fn has_duplicate_keys(fields: &[Field<'_>]) -> bool {
591    if fields.len() < 2 {
592        return false;
593    }
594    let mut seen = 0_u64;
595    for (i, field) in fields.iter().enumerate() {
596        let class = key_class(field.key);
597        let bit = 1_u64 << class;
598        if seen & bit != 0
599            && fields[..i].iter().any(|earlier| {
600                key_class(earlier.key) == class && same_output_key(earlier.key, field.key)
601            })
602        {
603            return true;
604        }
605        seen |= bit;
606    }
607    false
608}
609
610/// A 6-bit class that is equal for keys the formats write identically:
611/// a hash of the last two bytes, which a key shares with its
612/// `fields.`-prefixed alias.
613#[inline]
614fn key_class(key: &str) -> u32 {
615    let tail = match key.as_bytes() {
616        [.., a, b] => u16::from_le_bytes([*a, *b]),
617        [b] => u16::from(*b),
618        [] => 0,
619    };
620    let hash = u64::from(tail).wrapping_mul(0x9E37_79B9_7F4A_7C15) >> 58;
621    #[allow(clippy::cast_possible_truncation)] // `hash < 64`
622    let class = hash as u32;
623    class
624}
625
626/// `fields` without the earlier occurrences of duplicated keys.
627#[cold]
628fn last_occurrences<'a>(fields: &[Field<'a>]) -> FieldBuf<'a> {
629    let mut unique = FieldBuf::new();
630    for (i, f) in fields.iter().enumerate() {
631        if !fields[i + 1..]
632            .iter()
633            .any(|later| same_output_key(later.key, f.key))
634        {
635            unique.push(*f);
636        }
637    }
638    unique
639}
640
641impl fmt::Debug for Logger {
642    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
643        f.debug_struct("Logger")
644            .field("filter", &self.inner.filter)
645            .field("sinks", &self.inner.sinks.len())
646            .field("default_fields", &self.inner.defaults.len())
647            .field("default_target", &self.inner.default_target)
648            .field("capture_timestamps", &self.inner.capture_timestamps)
649            .field("capture_context", &self.inner.capture_context)
650            .field("flush_level", &self.inner.flush_level)
651            .field("redacted_keys", &self.inner.redact.len())
652            .finish_non_exhaustive()
653    }
654}
655
656/// Fields kept on the stack before spilling to the heap.
657const INLINE_FIELDS: usize = 16;
658
659const EMPTY_FIELD: Field<'static> = Field {
660    key: "",
661    value: Value::Null,
662};
663
664/// Up to [`INLINE_FIELDS`] fields on the stack, spilling to the heap
665/// beyond that.
666struct FieldBuf<'a> {
667    stack: [Field<'a>; INLINE_FIELDS],
668    len: usize,
669    heap: Vec<Field<'a>>,
670}
671
672impl<'a> FieldBuf<'a> {
673    fn new() -> Self {
674        Self {
675            stack: [EMPTY_FIELD; INLINE_FIELDS],
676            len: 0,
677            heap: Vec::new(),
678        }
679    }
680
681    #[inline]
682    fn push(&mut self, field: Field<'a>) {
683        if !self.heap.is_empty() {
684            self.heap.push(field);
685        } else if let Some(slot) = self.stack.get_mut(self.len) {
686            *slot = field;
687            self.len += 1;
688        } else {
689            self.heap.reserve(self.len * 2);
690            self.heap.extend_from_slice(&self.stack[..self.len]);
691            self.heap.push(field);
692        }
693    }
694
695    #[inline]
696    fn as_slice(&self) -> &[Field<'a>] {
697        if self.heap.is_empty() {
698            &self.stack[..self.len]
699        } else {
700            &self.heap
701        }
702    }
703
704    fn reverse(&mut self) {
705        if self.heap.is_empty() {
706            self.stack[..self.len].reverse();
707        } else {
708            self.heap.reverse();
709        }
710    }
711}
712
713#[cfg(all(test, feature = "logfmt", feature = "json"))]
714mod tests {
715    use super::*;
716    use crate::format::{JsonFormat, LogfmtFormat};
717    use std::io::Write;
718    use std::sync::Mutex;
719    use std::sync::atomic::{AtomicUsize, Ordering};
720
721    #[derive(Default, Clone)]
722    struct MemBuf(Arc<Mutex<Vec<u8>>>);
723
724    impl Write for MemBuf {
725        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
726            self.0.lock().unwrap().extend_from_slice(buf);
727            Ok(buf.len())
728        }
729        fn flush(&mut self) -> std::io::Result<()> {
730            Ok(())
731        }
732    }
733
734    impl MemBuf {
735        fn text(&self) -> String {
736            String::from_utf8(self.0.lock().unwrap().clone()).unwrap()
737        }
738    }
739
740    fn quiet() -> LoggerBuilder {
741        Logger::builder()
742            .level(Level::Trace)
743            .no_timestamps()
744            .no_context()
745    }
746
747    struct Failing(AtomicUsize);
748    impl Sink for Failing {
749        fn write_record(&self, _record: &Record<'_>) -> Result<()> {
750            let _n = self.0.fetch_add(1, Ordering::Relaxed);
751            Err(Error::Configuration("boom"))
752        }
753        fn flush(&self) -> Result<()> {
754            Err(Error::Configuration("flush boom"))
755        }
756    }
757
758    #[test]
759    fn test_logger_enabled_and_min_level() {
760        let logger = Logger::builder()
761            .level(Level::Warn)
762            .target_level("loud", Level::Trace)
763            .with_sink(crate::sink::NullSink::new())
764            .build();
765        assert!(logger.enabled("loud", Level::Trace));
766        assert!(!logger.enabled("other", Level::Info));
767        assert_eq!(logger.min_level(), Level::Trace);
768        assert_eq!(logger.threshold_for("loud::x"), Level::Trace);
769        assert_eq!(logger.filter().default_level(), Level::Warn);
770        assert_eq!(logger.sink_count(), 1);
771    }
772
773    #[test]
774    fn test_logger_log_writes_through_logfmt() {
775        let buf = MemBuf::default();
776        let logger = quiet().writer(buf.clone(), LogfmtFormat::new()).build();
777        logger.log(Level::Info, "hi", &[Field::new("k", 1_u64)]);
778        assert_eq!(buf.text(), "level=info target=\"\" message=hi k=1\n");
779    }
780
781    #[test]
782    fn test_logger_default_target_is_used_by_log() {
783        // The audit's R11: `log` with a per-target filter.
784        let buf = MemBuf::default();
785        let logger = Logger::builder()
786            .filter_directive("myapp=debug")
787            .default_target("myapp")
788            .no_timestamps()
789            .writer(buf.clone(), JsonFormat::new())
790            .build();
791        assert_eq!(logger.default_target(), "myapp");
792        logger.log(Level::Error, "kept", &[]);
793        assert!(
794            buf.text().contains("\"target\":\"myapp\""),
795            "{}",
796            buf.text()
797        );
798    }
799
800    #[test]
801    fn test_logger_context_is_spliced_in() {
802        let buf = MemBuf::default();
803        let logger = Logger::builder()
804            .level(Level::Trace)
805            .no_timestamps()
806            .writer(buf.clone(), LogfmtFormat::new())
807            .build();
808        context::clear();
809        let guard = context::with_trace_id("tx-42");
810        logger.log(Level::Info, "hello", &[]);
811        drop(guard);
812        assert!(buf.text().contains("trace_id=tx-42"), "{}", buf.text());
813    }
814
815    #[test]
816    fn test_logger_duplicate_keys_follow_precedence() {
817        // The audit's R06: defaults < context < call fields, one key each.
818        context::clear();
819        let buf = MemBuf::default();
820        let logger = Logger::builder()
821            .level(Level::Trace)
822            .no_timestamps()
823            .with_default_field("user", "default")
824            .with_default_field("service", "api")
825            .with_default_field("region", "eu")
826            .writer(buf.clone(), JsonFormat::new())
827            .build();
828        let _a = context::with_field("user", "outer");
829        let _b = context::with_field("user", "inner");
830        let _c = context::with_field("region", "us");
831        logger.log(Level::Error, "m", &[Field::new("region", "call")]);
832        let out = buf.text();
833        assert_eq!(
834            out,
835            "{\"level\":\"error\",\"target\":\"\",\"message\":\"m\",\"service\":\"api\",\
836             \"user\":\"inner\",\"region\":\"call\"}\n"
837        );
838        context::clear();
839    }
840
841    #[test]
842    fn test_logger_context_vs_call_dedup_without_defaults() {
843        context::clear();
844        let buf = MemBuf::default();
845        let logger = Logger::builder()
846            .level(Level::Trace)
847            .no_timestamps()
848            .writer(buf.clone(), LogfmtFormat::new())
849            .build();
850        let _t = context::with_field("tenant", "ctx");
851        logger.log(Level::Info, "m", &[Field::new("tenant", "call")]);
852        assert_eq!(buf.text(), "level=info target=\"\" message=m tenant=call\n");
853        context::clear();
854    }
855
856    #[test]
857    fn test_logger_extra_context_shadows_thread_context() {
858        context::clear();
859        let buf = MemBuf::default();
860        let logger = Logger::builder()
861            .level(Level::Trace)
862            .no_timestamps()
863            .writer(buf.clone(), LogfmtFormat::new())
864            .build();
865        let _t = context::with_field("span", "thread");
866        let extra = [Field::new("span", "extra"), Field::new("depth", 2_u8)];
867        logger.emit_fmt(
868            Metadata::new(Level::Info, "t"),
869            format_args!("m"),
870            &[],
871            &extra,
872        );
873        assert_eq!(
874            buf.text(),
875            "level=info target=t message=m span=extra depth=2\n"
876        );
877        context::clear();
878    }
879
880    #[test]
881    fn test_logger_redacts_keys_everywhere() {
882        context::clear();
883        let buf = MemBuf::default();
884        let logger = Logger::builder()
885            .level(Level::Trace)
886            .no_timestamps()
887            .redact_common_secrets()
888            .redact_key("Session_ID")
889            .with_default_field("api_key", "k-123")
890            .writer(buf.clone(), LogfmtFormat::new())
891            .build();
892        let _c = context::with_field("authorization", "Bearer x");
893        logger.log(
894            Level::Info,
895            "login",
896            &[
897                Field::new("user", "alice"),
898                Field::new("db.PASSWORD", "hunter2"),
899                Field::new("session_id", "s1"),
900            ],
901        );
902        let out = buf.text();
903        for secret in ["k-123", "Bearer", "hunter2", "s1"] {
904            assert!(!out.contains(secret), "{secret} leaked: {out}");
905        }
906        assert!(out.contains("user=alice"), "{out}");
907        assert_eq!(out.matches(REDACTED).count(), 4, "{out}");
908        context::clear();
909    }
910
911    #[test]
912    fn test_logger_redaction_never_evaluates_lazy_values() {
913        struct Explodes;
914        impl fmt::Display for Explodes {
915            fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
916                Err(fmt::Error)
917            }
918        }
919        let buf = MemBuf::default();
920        let logger = quiet()
921            .redact_key("token")
922            .writer(buf.clone(), JsonFormat::new())
923            .build();
924        let result = logger.try_log(
925            Level::Info,
926            "t",
927            "m",
928            &[Field::new("token", Value::Display(&Explodes))],
929        );
930        assert!(result.is_ok());
931        assert!(buf.text().contains("\"token\":\"[REDACTED]\""));
932    }
933
934    #[test]
935    fn test_logger_more_than_sixteen_ambient_fields_spill_correctly() {
936        context::clear();
937        let buf = MemBuf::default();
938        let mut builder = Logger::builder().level(Level::Trace).no_timestamps();
939        let keys: Vec<String> = (0..20).map(|i| format!("d{i}")).collect();
940        for (i, k) in keys.iter().enumerate() {
941            builder = builder.with_default_field(k.clone(), i as u32);
942        }
943        let logger = builder.writer(buf.clone(), JsonFormat::new()).build();
944        let _c = context::with_field("c", 1_u8);
945        logger.log(Level::Info, "ok", &[Field::new("d0", "call")]);
946        let out = buf.text();
947        assert!(out.contains("\"d0\":\"call\""), "{out}");
948        assert_eq!(out.matches("\"d0\"").count(), 1, "{out}");
949        for (i, k) in keys.iter().enumerate().skip(1) {
950            assert!(out.contains(&format!("\"{k}\":{i}")), "missing {k}: {out}");
951        }
952        assert!(out.contains("\"c\":1"));
953        context::clear();
954    }
955
956    #[test]
957    fn test_logger_dispatch_continues_after_sink_error_and_reports_each() {
958        // The audit's R09: the handler sees every failing sink.
959        let failing = Arc::new(Failing(AtomicUsize::new(0)));
960        let calls = Arc::new(AtomicUsize::new(0));
961        let calls2 = Arc::clone(&calls);
962        let buf = MemBuf::default();
963        let logger = quiet()
964            .flush_on(Level::Off)
965            .with_sink(Arc::clone(&failing))
966            .with_sink(Failing(AtomicUsize::new(0)))
967            .writer(buf.clone(), JsonFormat::new())
968            .on_error(Arc::new(move |_| {
969                let _n = calls2.fetch_add(1, Ordering::Relaxed);
970            }))
971            .build();
972        let result = logger.try_log(Level::Info, "t", "m", &[]);
973        assert!(matches!(result, Err(Error::Configuration("boom"))));
974        assert!(buf.text().contains("\"message\":\"m\""));
975        assert_eq!(failing.0.load(Ordering::Relaxed), 1);
976        assert_eq!(
977            calls.load(Ordering::Relaxed),
978            2,
979            "one call per failing sink"
980        );
981    }
982
983    #[test]
984    fn test_logger_flush_on_level_flushes_and_reports() {
985        let calls = Arc::new(AtomicUsize::new(0));
986        let calls2 = Arc::clone(&calls);
987        let logger = quiet()
988            .flush_on(Level::Warn)
989            .with_sink(Failing(AtomicUsize::new(0)))
990            .on_error(Arc::new(move |_| {
991                let _n = calls2.fetch_add(1, Ordering::Relaxed);
992            }))
993            .build();
994        logger.log(Level::Info, "below", &[]);
995        assert_eq!(calls.load(Ordering::Relaxed), 1, "write error only");
996        logger.log(Level::Warn, "at", &[]);
997        assert_eq!(calls.load(Ordering::Relaxed), 3, "write and flush errors");
998        assert!(logger.flush().is_err());
999        assert!(logger.shutdown().is_err());
1000    }
1001
1002    #[test]
1003    fn test_logger_on_error_recursion_is_bounded() {
1004        // The audit's LOG-015: a handler that logs through the same
1005        // failing logger used to recurse until the stack overflowed.
1006        use std::sync::OnceLock;
1007        static SELF: OnceLock<Logger> = OnceLock::new();
1008        static CALLS: AtomicUsize = AtomicUsize::new(0);
1009        let logger = quiet()
1010            .with_sink(Failing(AtomicUsize::new(0)))
1011            .on_error(Arc::new(|_| {
1012                let _n = CALLS.fetch_add(1, Ordering::Relaxed);
1013                if let Some(l) = SELF.get() {
1014                    l.log(Level::Error, "sink failed", &[]);
1015                }
1016            }))
1017            .build();
1018        assert!(SELF.set(logger).is_ok());
1019        if let Some(l) = SELF.get() {
1020            l.log(Level::Error, "first", &[]);
1021        }
1022        // write error of the outer record (1) + flush error (1); the
1023        // nested record's failures are not re-reported.
1024        assert_eq!(CALLS.load(Ordering::Relaxed), 2);
1025    }
1026
1027    #[test]
1028    fn test_logger_drop_shuts_sinks_down() {
1029        struct CountShutdown(Arc<AtomicUsize>);
1030        impl Sink for CountShutdown {
1031            fn write_record(&self, _: &Record<'_>) -> Result<()> {
1032                Ok(())
1033            }
1034            fn flush(&self) -> Result<()> {
1035                Ok(())
1036            }
1037            fn shutdown(&self) -> Result<()> {
1038                let _n = self.0.fetch_add(1, Ordering::Relaxed);
1039                Ok(())
1040            }
1041        }
1042        let count = Arc::new(AtomicUsize::new(0));
1043        let logger = quiet().with_sink(CountShutdown(Arc::clone(&count))).build();
1044        let clone = logger.clone();
1045        drop(logger);
1046        assert_eq!(count.load(Ordering::Relaxed), 0, "a clone is still alive");
1047        drop(clone);
1048        assert_eq!(count.load(Ordering::Relaxed), 1);
1049    }
1050
1051    #[test]
1052    fn test_logger_emit_fmt_renders_arguments_and_reports_format_errors() {
1053        struct Broken;
1054        impl fmt::Display for Broken {
1055            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1056                f.write_str("partial")?;
1057                Err(fmt::Error)
1058            }
1059        }
1060        let calls = Arc::new(AtomicUsize::new(0));
1061        let calls2 = Arc::clone(&calls);
1062        let buf = MemBuf::default();
1063        let logger = quiet()
1064            .writer(buf.clone(), LogfmtFormat::new())
1065            .on_error(Arc::new(move |e| {
1066                assert!(matches!(e, Error::Format(_)));
1067                let _n = calls2.fetch_add(1, Ordering::Relaxed);
1068            }))
1069            .build();
1070        let n = 7;
1071        logger.emit_fmt(
1072            Metadata::new(Level::Info, "t"),
1073            format_args!("n={n}"),
1074            &[],
1075            &[],
1076        );
1077        logger.emit_fmt(
1078            Metadata::new(Level::Info, "t"),
1079            format_args!("{}", Broken),
1080            &[],
1081            &[],
1082        );
1083        let out = buf.text();
1084        assert!(out.contains("message=\"n=7\""), "{out}");
1085        assert!(out.contains("message=partial"), "{out}");
1086        assert_eq!(calls.load(Ordering::Relaxed), 1);
1087    }
1088
1089    #[test]
1090    fn test_logger_timestamps_are_added_when_enabled() {
1091        let buf = MemBuf::default();
1092        let logger = Logger::builder()
1093            .no_context()
1094            .writer(buf.clone(), LogfmtFormat::new())
1095            .build();
1096        logger.log(Level::Info, "t", &[]);
1097        assert!(buf.text().starts_with("timestamp="), "{}", buf.text());
1098        let explicit = Metadata::new(Level::Info, "x").with_timestamp(5);
1099        logger.try_emit(explicit, "m", &[]).unwrap();
1100        assert!(buf.text().contains("timestamp=5 "), "{}", buf.text());
1101    }
1102
1103    #[test]
1104    fn test_logger_debug_lists_configuration() {
1105        let logger = quiet().with_sink(crate::sink::NullSink::new()).build();
1106        let text = format!("{logger:?}");
1107        assert!(text.contains("flush_level: Error"), "{text}");
1108        assert!(text.contains("sinks: 1"), "{text}");
1109    }
1110
1111    #[test]
1112    fn test_logger_is_unwind_safe_and_thread_safe() {
1113        fn assert_traits<T: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe>() {}
1114        assert_traits::<Logger>();
1115    }
1116
1117    #[test]
1118    fn test_field_buf_spills_and_reverses() {
1119        let mut buf = FieldBuf::new();
1120        for i in 0..20_u64 {
1121            buf.push(Field::new("k", i));
1122        }
1123        assert_eq!(buf.as_slice().len(), 20);
1124        buf.reverse();
1125        assert_eq!(buf.as_slice()[0].value, Value::U64(19));
1126        let mut small = FieldBuf::new();
1127        small.push(Field::new("a", 1_u8));
1128        small.push(Field::new("b", 2_u8));
1129        small.reverse();
1130        assert_eq!(small.as_slice()[0].key, "b");
1131    }
1132}