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/// One byte of a key in canonical form (see [`normalize_redact_key`]).
563#[inline]
564const fn fold_key_byte(b: u8) -> u8 {
565    if b == b'-' {
566        b'_'
567    } else {
568        b.to_ascii_lowercase()
569    }
570}
571
572/// `true` if `key` matches the normalized redaction key `normalized`,
573/// in which each `*` matches any run of bytes. Allocation-free.
574#[inline]
575fn redact_key_matches(normalized: &str, key: &str) -> bool {
576    let pattern = normalized.as_bytes();
577    let key = key.as_bytes();
578    if !pattern.contains(&b'*') {
579        return pattern.len() == key.len()
580            && pattern
581                .iter()
582                .zip(key)
583                .all(|(r, k)| *r == fold_key_byte(*k));
584    }
585    // Wildcard match with single-star backtracking: linear in practice,
586    // `O(pattern * key)` at worst, and keys are short.
587    let (mut p, mut k) = (0, 0);
588    let mut star: Option<(usize, usize)> = None;
589    while k < key.len() {
590        match pattern.get(p) {
591            Some(b'*') => {
592                star = Some((p, k));
593                p += 1;
594            }
595            Some(r) if *r == fold_key_byte(key[k]) => {
596                p += 1;
597                k += 1;
598            }
599            _ => match star {
600                Some((sp, sk)) => {
601                    p = sp + 1;
602                    k = sk + 1;
603                    star = Some((sp, sk + 1));
604                }
605                None => return false,
606            },
607        }
608    }
609    pattern[p..].iter().all(|b| *b == b'*')
610}
611
612fn any_key_in(a: &[Field<'_>], b: &[Field<'_>]) -> bool {
613    !b.is_empty()
614        && a.iter()
615            .any(|x| b.iter().any(|y| same_output_key(x.key, y.key)))
616}
617
618/// `true` if two call-site fields would be written under one key.
619///
620/// Runs on every record with two or more fields, so it avoids the
621/// pairwise comparison of every key: one pass sets a bit per key class
622/// (see [`key_class`]), and only a key whose class is already taken is
623/// compared, with the earlier keys of that class.
624#[inline]
625fn has_duplicate_keys(fields: &[Field<'_>]) -> bool {
626    if fields.len() < 2 {
627        return false;
628    }
629    let mut seen = 0_u64;
630    for (i, field) in fields.iter().enumerate() {
631        let class = key_class(field.key);
632        let bit = 1_u64 << class;
633        if seen & bit != 0
634            && fields[..i].iter().any(|earlier| {
635                key_class(earlier.key) == class && same_output_key(earlier.key, field.key)
636            })
637        {
638            return true;
639        }
640        seen |= bit;
641    }
642    false
643}
644
645/// A 6-bit class that is equal for keys the formats write identically:
646/// a hash of the last two bytes, which a key shares with its
647/// `fields.`-prefixed alias.
648#[inline]
649fn key_class(key: &str) -> u32 {
650    let tail = match key.as_bytes() {
651        [.., a, b] => u16::from_le_bytes([*a, *b]),
652        [b] => u16::from(*b),
653        [] => 0,
654    };
655    let hash = u64::from(tail).wrapping_mul(0x9E37_79B9_7F4A_7C15) >> 58;
656    #[allow(clippy::cast_possible_truncation)] // `hash < 64`
657    let class = hash as u32;
658    class
659}
660
661/// `fields` without the earlier occurrences of duplicated keys.
662#[cold]
663fn last_occurrences<'a>(fields: &[Field<'a>]) -> FieldBuf<'a> {
664    let mut unique = FieldBuf::new();
665    for (i, f) in fields.iter().enumerate() {
666        if !fields[i + 1..]
667            .iter()
668            .any(|later| same_output_key(later.key, f.key))
669        {
670            unique.push(*f);
671        }
672    }
673    unique
674}
675
676impl fmt::Debug for Logger {
677    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
678        f.debug_struct("Logger")
679            .field("filter", &self.inner.filter)
680            .field("sinks", &self.inner.sinks.len())
681            .field("default_fields", &self.inner.defaults.len())
682            .field("default_target", &self.inner.default_target)
683            .field("capture_timestamps", &self.inner.capture_timestamps)
684            .field("capture_context", &self.inner.capture_context)
685            .field("flush_level", &self.inner.flush_level)
686            .field("redacted_keys", &self.inner.redact.len())
687            .finish_non_exhaustive()
688    }
689}
690
691/// Fields kept on the stack before spilling to the heap.
692const INLINE_FIELDS: usize = 16;
693
694const EMPTY_FIELD: Field<'static> = Field {
695    key: "",
696    value: Value::Null,
697};
698
699/// Up to [`INLINE_FIELDS`] fields on the stack, spilling to the heap
700/// beyond that.
701struct FieldBuf<'a> {
702    stack: [Field<'a>; INLINE_FIELDS],
703    len: usize,
704    heap: Vec<Field<'a>>,
705}
706
707impl<'a> FieldBuf<'a> {
708    fn new() -> Self {
709        Self {
710            stack: [EMPTY_FIELD; INLINE_FIELDS],
711            len: 0,
712            heap: Vec::new(),
713        }
714    }
715
716    #[inline]
717    fn push(&mut self, field: Field<'a>) {
718        if !self.heap.is_empty() {
719            self.heap.push(field);
720        } else if let Some(slot) = self.stack.get_mut(self.len) {
721            *slot = field;
722            self.len += 1;
723        } else {
724            self.heap.reserve(self.len * 2);
725            self.heap.extend_from_slice(&self.stack[..self.len]);
726            self.heap.push(field);
727        }
728    }
729
730    #[inline]
731    fn as_slice(&self) -> &[Field<'a>] {
732        if self.heap.is_empty() {
733            &self.stack[..self.len]
734        } else {
735            &self.heap
736        }
737    }
738
739    fn reverse(&mut self) {
740        if self.heap.is_empty() {
741            self.stack[..self.len].reverse();
742        } else {
743            self.heap.reverse();
744        }
745    }
746}
747
748#[cfg(all(test, feature = "logfmt", feature = "json"))]
749mod tests {
750    use super::*;
751    use crate::format::{JsonFormat, LogfmtFormat};
752    use std::io::Write;
753    use std::sync::Mutex;
754    use std::sync::atomic::{AtomicUsize, Ordering};
755
756    #[derive(Default, Clone)]
757    struct MemBuf(Arc<Mutex<Vec<u8>>>);
758
759    impl Write for MemBuf {
760        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
761            self.0.lock().unwrap().extend_from_slice(buf);
762            Ok(buf.len())
763        }
764        fn flush(&mut self) -> std::io::Result<()> {
765            Ok(())
766        }
767    }
768
769    impl MemBuf {
770        fn text(&self) -> String {
771            String::from_utf8(self.0.lock().unwrap().clone()).unwrap()
772        }
773    }
774
775    fn quiet() -> LoggerBuilder {
776        Logger::builder()
777            .level(Level::Trace)
778            .no_timestamps()
779            .no_context()
780    }
781
782    struct Failing(AtomicUsize);
783    impl Sink for Failing {
784        fn write_record(&self, _record: &Record<'_>) -> Result<()> {
785            let _n = self.0.fetch_add(1, Ordering::Relaxed);
786            Err(Error::Configuration("boom"))
787        }
788        fn flush(&self) -> Result<()> {
789            Err(Error::Configuration("flush boom"))
790        }
791    }
792
793    #[test]
794    fn test_logger_enabled_and_min_level() {
795        let logger = Logger::builder()
796            .level(Level::Warn)
797            .target_level("loud", Level::Trace)
798            .with_sink(crate::sink::NullSink::new())
799            .build();
800        assert!(logger.enabled("loud", Level::Trace));
801        assert!(!logger.enabled("other", Level::Info));
802        assert_eq!(logger.min_level(), Level::Trace);
803        assert_eq!(logger.threshold_for("loud::x"), Level::Trace);
804        assert_eq!(logger.filter().default_level(), Level::Warn);
805        assert_eq!(logger.sink_count(), 1);
806    }
807
808    #[test]
809    fn test_logger_log_writes_through_logfmt() {
810        let buf = MemBuf::default();
811        let logger = quiet().writer(buf.clone(), LogfmtFormat::new()).build();
812        logger.log(Level::Info, "hi", &[Field::new("k", 1_u64)]);
813        assert_eq!(buf.text(), "level=info target=\"\" message=hi k=1\n");
814    }
815
816    #[test]
817    fn test_logger_default_target_is_used_by_log() {
818        // The audit's R11: `log` with a per-target filter.
819        let buf = MemBuf::default();
820        let logger = Logger::builder()
821            .filter_directive("myapp=debug")
822            .default_target("myapp")
823            .no_timestamps()
824            .writer(buf.clone(), JsonFormat::new())
825            .build();
826        assert_eq!(logger.default_target(), "myapp");
827        logger.log(Level::Error, "kept", &[]);
828        assert!(
829            buf.text().contains("\"target\":\"myapp\""),
830            "{}",
831            buf.text()
832        );
833    }
834
835    #[test]
836    fn test_logger_context_is_spliced_in() {
837        let buf = MemBuf::default();
838        let logger = Logger::builder()
839            .level(Level::Trace)
840            .no_timestamps()
841            .writer(buf.clone(), LogfmtFormat::new())
842            .build();
843        context::clear();
844        let guard = context::with_trace_id("tx-42");
845        logger.log(Level::Info, "hello", &[]);
846        drop(guard);
847        assert!(buf.text().contains("trace_id=tx-42"), "{}", buf.text());
848    }
849
850    #[test]
851    fn test_logger_duplicate_keys_follow_precedence() {
852        // The audit's R06: defaults < context < call fields, one key each.
853        context::clear();
854        let buf = MemBuf::default();
855        let logger = Logger::builder()
856            .level(Level::Trace)
857            .no_timestamps()
858            .with_default_field("user", "default")
859            .with_default_field("service", "api")
860            .with_default_field("region", "eu")
861            .writer(buf.clone(), JsonFormat::new())
862            .build();
863        let _a = context::with_field("user", "outer");
864        let _b = context::with_field("user", "inner");
865        let _c = context::with_field("region", "us");
866        logger.log(Level::Error, "m", &[Field::new("region", "call")]);
867        let out = buf.text();
868        assert_eq!(
869            out,
870            "{\"level\":\"error\",\"target\":\"\",\"message\":\"m\",\"service\":\"api\",\
871             \"user\":\"inner\",\"region\":\"call\"}\n"
872        );
873        context::clear();
874    }
875
876    #[test]
877    fn test_logger_context_vs_call_dedup_without_defaults() {
878        context::clear();
879        let buf = MemBuf::default();
880        let logger = Logger::builder()
881            .level(Level::Trace)
882            .no_timestamps()
883            .writer(buf.clone(), LogfmtFormat::new())
884            .build();
885        let _t = context::with_field("tenant", "ctx");
886        logger.log(Level::Info, "m", &[Field::new("tenant", "call")]);
887        assert_eq!(buf.text(), "level=info target=\"\" message=m tenant=call\n");
888        context::clear();
889    }
890
891    #[test]
892    fn test_logger_extra_context_shadows_thread_context() {
893        context::clear();
894        let buf = MemBuf::default();
895        let logger = Logger::builder()
896            .level(Level::Trace)
897            .no_timestamps()
898            .writer(buf.clone(), LogfmtFormat::new())
899            .build();
900        let _t = context::with_field("span", "thread");
901        let extra = [Field::new("span", "extra"), Field::new("depth", 2_u8)];
902        logger.emit_fmt(
903            Metadata::new(Level::Info, "t"),
904            format_args!("m"),
905            &[],
906            &extra,
907        );
908        assert_eq!(
909            buf.text(),
910            "level=info target=t message=m span=extra depth=2\n"
911        );
912        context::clear();
913    }
914
915    #[test]
916    fn test_logger_redacts_keys_everywhere() {
917        context::clear();
918        let buf = MemBuf::default();
919        let logger = Logger::builder()
920            .level(Level::Trace)
921            .no_timestamps()
922            .redact_common_secrets()
923            .redact_key("Session_ID")
924            .with_default_field("api_key", "k-123")
925            .writer(buf.clone(), LogfmtFormat::new())
926            .build();
927        let _c = context::with_field("authorization", "Bearer x");
928        logger.log(
929            Level::Info,
930            "login",
931            &[
932                Field::new("user", "alice"),
933                Field::new("db.PASSWORD", "hunter2"),
934                Field::new("session_id", "s1"),
935            ],
936        );
937        let out = buf.text();
938        for secret in ["k-123", "Bearer", "hunter2", "s1"] {
939            assert!(!out.contains(secret), "{secret} leaked: {out}");
940        }
941        assert!(out.contains("user=alice"), "{out}");
942        assert_eq!(out.matches(REDACTED).count(), 4, "{out}");
943        context::clear();
944    }
945
946    // Review F-5 (2.1.0): the common set matched exact names only, so
947    // compound names such as `db_password` and `x-auth-token` passed
948    // through.
949    #[test]
950    fn test_common_secrets_catch_compound_key_names() {
951        let buf = MemBuf::default();
952        let logger = Logger::builder()
953            .level(Level::Trace)
954            .no_timestamps()
955            .redact_common_secrets()
956            .writer(buf.clone(), LogfmtFormat::new())
957            .build();
958        logger.log(
959            Level::Info,
960            "login",
961            &[
962                Field::new("db_password", "S3CRET-1"),
963                Field::new("user_password", "S3CRET-2"),
964                Field::new("X-Auth-Token", "S3CRET-3"),
965                Field::new("client_secret_v2", "S3CRET-4"),
966                Field::new("session_id", "S3CRET-5"),
967                Field::new("passphrase", "S3CRET-6"),
968                Field::new("upstream.Api-Key", "S3CRET-7"),
969                Field::new("user", "alice"),
970                Field::new("status", "ok"),
971            ],
972        );
973        let out = buf.text();
974        for n in 1..=7 {
975            assert!(
976                !out.contains(&format!("S3CRET-{n}")),
977                "S3CRET-{n} leaked: {out}"
978            );
979        }
980        assert!(out.contains("user=alice"), "{out}");
981        assert!(out.contains("status=ok"), "{out}");
982    }
983
984    #[test]
985    fn test_redact_key_wildcards() {
986        let m = |p: &str, k: &str| redact_key_matches(&normalize_redact_key(p), k);
987        assert!(m("db_*", "DB-host"));
988        assert!(m("*_secret", "client_secret"));
989        assert!(!m("*_secret", "client_secret_v2"));
990        assert!(m("*token*", "token"));
991        assert!(m("a*b*c", "aXXbYYc"));
992        assert!(!m("a*b*c", "aXXbYY"));
993        assert!(m("*", ""));
994        assert!(m("password", "PassWord"));
995        assert!(!m("password", "db_password"));
996        assert!(!m("", "x"));
997    }
998
999    #[test]
1000    fn test_logger_redaction_never_evaluates_lazy_values() {
1001        struct Explodes;
1002        impl fmt::Display for Explodes {
1003            fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
1004                Err(fmt::Error)
1005            }
1006        }
1007        let buf = MemBuf::default();
1008        let logger = quiet()
1009            .redact_key("token")
1010            .writer(buf.clone(), JsonFormat::new())
1011            .build();
1012        let result = logger.try_log(
1013            Level::Info,
1014            "t",
1015            "m",
1016            &[Field::new("token", Value::Display(&Explodes))],
1017        );
1018        assert!(result.is_ok());
1019        assert!(buf.text().contains("\"token\":\"[REDACTED]\""));
1020    }
1021
1022    #[test]
1023    fn test_logger_more_than_sixteen_ambient_fields_spill_correctly() {
1024        context::clear();
1025        let buf = MemBuf::default();
1026        let mut builder = Logger::builder().level(Level::Trace).no_timestamps();
1027        let keys: Vec<String> = (0..20).map(|i| format!("d{i}")).collect();
1028        for (i, k) in keys.iter().enumerate() {
1029            builder = builder.with_default_field(k.clone(), i as u32);
1030        }
1031        let logger = builder.writer(buf.clone(), JsonFormat::new()).build();
1032        let _c = context::with_field("c", 1_u8);
1033        logger.log(Level::Info, "ok", &[Field::new("d0", "call")]);
1034        let out = buf.text();
1035        assert!(out.contains("\"d0\":\"call\""), "{out}");
1036        assert_eq!(out.matches("\"d0\"").count(), 1, "{out}");
1037        for (i, k) in keys.iter().enumerate().skip(1) {
1038            assert!(out.contains(&format!("\"{k}\":{i}")), "missing {k}: {out}");
1039        }
1040        assert!(out.contains("\"c\":1"));
1041        context::clear();
1042    }
1043
1044    #[test]
1045    fn test_logger_dispatch_continues_after_sink_error_and_reports_each() {
1046        // The audit's R09: the handler sees every failing sink.
1047        let failing = Arc::new(Failing(AtomicUsize::new(0)));
1048        let calls = Arc::new(AtomicUsize::new(0));
1049        let calls2 = Arc::clone(&calls);
1050        let buf = MemBuf::default();
1051        let logger = quiet()
1052            .flush_on(Level::Off)
1053            .with_sink(Arc::clone(&failing))
1054            .with_sink(Failing(AtomicUsize::new(0)))
1055            .writer(buf.clone(), JsonFormat::new())
1056            .on_error(Arc::new(move |_| {
1057                let _n = calls2.fetch_add(1, Ordering::Relaxed);
1058            }))
1059            .build();
1060        let result = logger.try_log(Level::Info, "t", "m", &[]);
1061        assert!(matches!(result, Err(Error::Configuration("boom"))));
1062        assert!(buf.text().contains("\"message\":\"m\""));
1063        assert_eq!(failing.0.load(Ordering::Relaxed), 1);
1064        assert_eq!(
1065            calls.load(Ordering::Relaxed),
1066            2,
1067            "one call per failing sink"
1068        );
1069    }
1070
1071    #[test]
1072    fn test_logger_flush_on_level_flushes_and_reports() {
1073        let calls = Arc::new(AtomicUsize::new(0));
1074        let calls2 = Arc::clone(&calls);
1075        let logger = quiet()
1076            .flush_on(Level::Warn)
1077            .with_sink(Failing(AtomicUsize::new(0)))
1078            .on_error(Arc::new(move |_| {
1079                let _n = calls2.fetch_add(1, Ordering::Relaxed);
1080            }))
1081            .build();
1082        logger.log(Level::Info, "below", &[]);
1083        assert_eq!(calls.load(Ordering::Relaxed), 1, "write error only");
1084        logger.log(Level::Warn, "at", &[]);
1085        assert_eq!(calls.load(Ordering::Relaxed), 3, "write and flush errors");
1086        assert!(logger.flush().is_err());
1087        assert!(logger.shutdown().is_err());
1088    }
1089
1090    #[test]
1091    fn test_logger_on_error_recursion_is_bounded() {
1092        // The audit's LOG-015: a handler that logs through the same
1093        // failing logger used to recurse until the stack overflowed.
1094        use std::sync::OnceLock;
1095        static SELF: OnceLock<Logger> = OnceLock::new();
1096        static CALLS: AtomicUsize = AtomicUsize::new(0);
1097        let logger = quiet()
1098            .with_sink(Failing(AtomicUsize::new(0)))
1099            .on_error(Arc::new(|_| {
1100                let _n = CALLS.fetch_add(1, Ordering::Relaxed);
1101                if let Some(l) = SELF.get() {
1102                    l.log(Level::Error, "sink failed", &[]);
1103                }
1104            }))
1105            .build();
1106        assert!(SELF.set(logger).is_ok());
1107        if let Some(l) = SELF.get() {
1108            l.log(Level::Error, "first", &[]);
1109        }
1110        // write error of the outer record (1) + flush error (1); the
1111        // nested record's failures are not re-reported.
1112        assert_eq!(CALLS.load(Ordering::Relaxed), 2);
1113    }
1114
1115    #[test]
1116    fn test_logger_drop_shuts_sinks_down() {
1117        struct CountShutdown(Arc<AtomicUsize>);
1118        impl Sink for CountShutdown {
1119            fn write_record(&self, _: &Record<'_>) -> Result<()> {
1120                Ok(())
1121            }
1122            fn flush(&self) -> Result<()> {
1123                Ok(())
1124            }
1125            fn shutdown(&self) -> Result<()> {
1126                let _n = self.0.fetch_add(1, Ordering::Relaxed);
1127                Ok(())
1128            }
1129        }
1130        let count = Arc::new(AtomicUsize::new(0));
1131        let logger = quiet().with_sink(CountShutdown(Arc::clone(&count))).build();
1132        let clone = logger.clone();
1133        drop(logger);
1134        assert_eq!(count.load(Ordering::Relaxed), 0, "a clone is still alive");
1135        drop(clone);
1136        assert_eq!(count.load(Ordering::Relaxed), 1);
1137    }
1138
1139    #[test]
1140    fn test_logger_emit_fmt_renders_arguments_and_reports_format_errors() {
1141        struct Broken;
1142        impl fmt::Display for Broken {
1143            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1144                f.write_str("partial")?;
1145                Err(fmt::Error)
1146            }
1147        }
1148        let calls = Arc::new(AtomicUsize::new(0));
1149        let calls2 = Arc::clone(&calls);
1150        let buf = MemBuf::default();
1151        let logger = quiet()
1152            .writer(buf.clone(), LogfmtFormat::new())
1153            .on_error(Arc::new(move |e| {
1154                assert!(matches!(e, Error::Format(_)));
1155                let _n = calls2.fetch_add(1, Ordering::Relaxed);
1156            }))
1157            .build();
1158        let n = 7;
1159        logger.emit_fmt(
1160            Metadata::new(Level::Info, "t"),
1161            format_args!("n={n}"),
1162            &[],
1163            &[],
1164        );
1165        logger.emit_fmt(
1166            Metadata::new(Level::Info, "t"),
1167            format_args!("{}", Broken),
1168            &[],
1169            &[],
1170        );
1171        let out = buf.text();
1172        assert!(out.contains("message=\"n=7\""), "{out}");
1173        assert!(out.contains("message=partial"), "{out}");
1174        assert_eq!(calls.load(Ordering::Relaxed), 1);
1175    }
1176
1177    #[test]
1178    fn test_logger_timestamps_are_added_when_enabled() {
1179        let buf = MemBuf::default();
1180        let logger = Logger::builder()
1181            .no_context()
1182            .writer(buf.clone(), LogfmtFormat::new())
1183            .build();
1184        logger.log(Level::Info, "t", &[]);
1185        assert!(buf.text().starts_with("timestamp="), "{}", buf.text());
1186        let explicit = Metadata::new(Level::Info, "x").with_timestamp(5);
1187        logger.try_emit(explicit, "m", &[]).unwrap();
1188        assert!(buf.text().contains("timestamp=5 "), "{}", buf.text());
1189    }
1190
1191    #[test]
1192    fn test_logger_debug_lists_configuration() {
1193        let logger = quiet().with_sink(crate::sink::NullSink::new()).build();
1194        let text = format!("{logger:?}");
1195        assert!(text.contains("flush_level: Error"), "{text}");
1196        assert!(text.contains("sinks: 1"), "{text}");
1197    }
1198
1199    #[test]
1200    fn test_logger_is_unwind_safe_and_thread_safe() {
1201        fn assert_traits<T: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe>() {}
1202        assert_traits::<Logger>();
1203    }
1204
1205    #[test]
1206    fn test_field_buf_spills_and_reverses() {
1207        let mut buf = FieldBuf::new();
1208        for i in 0..20_u64 {
1209            buf.push(Field::new("k", i));
1210        }
1211        assert_eq!(buf.as_slice().len(), 20);
1212        buf.reverse();
1213        assert_eq!(buf.as_slice()[0].value, Value::U64(19));
1214        let mut small = FieldBuf::new();
1215        small.push(Field::new("a", 1_u8));
1216        small.push(Field::new("b", 2_u8));
1217        small.reverse();
1218        assert_eq!(small.as_slice()[0].key, "b");
1219    }
1220}