Skip to main content

log_io/
builder.rs

1//! [`LoggerBuilder`] and [`BuildError`].
2
3use std::ffi::OsString;
4use std::io::Write;
5
6use crate::filter::{Filter, ParseFilterError};
7use crate::format::Format;
8use crate::level::Level;
9use crate::logger::{ErrorHandler, Logger, LoggerInner};
10use crate::record::same_output_key;
11use crate::sink::{Sink, StderrSink, StdoutSink, WriterSink};
12use crate::value::{OwnedField, Value};
13
14/// Error returned by [`LoggerBuilder::try_build`].
15///
16/// # Examples
17///
18/// ```
19/// use log_io::{BuildError, Logger};
20///
21/// let err = Logger::builder().try_build().unwrap_err();
22/// assert!(matches!(err, BuildError::NoSink));
23/// ```
24#[derive(Debug, Clone, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum BuildError {
27    /// No sink was installed. Add one with [`LoggerBuilder::with_sink`],
28    /// [`LoggerBuilder::writer`], or a shortcut such as
29    /// [`LoggerBuilder::stdout_json`].
30    NoSink,
31    /// A directive passed to [`LoggerBuilder::filter_directive`] or read
32    /// by [`LoggerBuilder::env_filter`] did not parse. Fix the directive
33    /// (the error names the offending segment).
34    InvalidFilter(ParseFilterError),
35}
36
37impl core::fmt::Display for BuildError {
38    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
39        match self {
40            Self::NoSink => f.write_str("logger has no sink; install at least one before building"),
41            Self::InvalidFilter(e) => write!(f, "logger filter is invalid: {e}"),
42        }
43    }
44}
45
46impl std::error::Error for BuildError {
47    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
48        match self {
49            Self::NoSink => None,
50            Self::InvalidFilter(e) => Some(e),
51        }
52    }
53}
54
55/// Builder for [`Logger`]. Create with [`Logger::builder`].
56///
57/// Defaults: filter `info` for every target, timestamps on, thread-local
58/// context capture on, flush after every record at [`Level::Error`] or
59/// above, no default fields, no redaction, empty default target, no
60/// error handler, no sinks.
61///
62/// # Examples
63///
64#[cfg_attr(all(feature = "human"), doc = "```")]
65#[cfg_attr(not(all(feature = "human")), doc = "```ignore")]
66/// use log_io::{Level, Logger};
67///
68/// let logger = Logger::builder()
69///     .level(Level::Info)
70///     .env_filter("LOG_IO")              // e.g. LOG_IO="info,app::db=debug"
71///     .with_default_field("service", "api")
72///     .redact_common_secrets()
73///     .stderr_human()
74///     .try_build()?;
75/// # let _ = logger;
76/// # Ok::<(), log_io::BuildError>(())
77/// ```
78#[must_use = "a builder does nothing until `build()` or `try_build()` is called"]
79pub struct LoggerBuilder {
80    filter: Filter,
81    filter_error: Option<ParseFilterError>,
82    sinks: Vec<Box<dyn Sink>>,
83    defaults: Vec<OwnedField>,
84    default_target: String,
85    capture_timestamps: bool,
86    capture_context: bool,
87    flush_level: Level,
88    on_error: Option<ErrorHandler>,
89    redact: Vec<Box<str>>,
90}
91
92impl LoggerBuilder {
93    /// Key patterns redacted by [`LoggerBuilder::redact_common_secrets`].
94    ///
95    /// Each `*` matches any run of characters (see
96    /// [`LoggerBuilder::redact_key`]), so `*password*` covers `password`,
97    /// `db_password` and `user_password_hash`, and `*token*` covers
98    /// `x-auth-token` and `refresh_token`. Matching treats `-` and `_`
99    /// alike and ignores ASCII case, so `*api_key*` also covers the HTTP
100    /// header names `Api-Key` and `X-Api-Key`.
101    ///
102    /// The patterns favour catching secrets over keeping every value:
103    /// `*token*` also redacts a count such as `token_count`. Rename such
104    /// fields, or list your own keys with [`LoggerBuilder::redact_key`]
105    /// instead of this set. (Before 2.2 the set held exact names only.)
106    pub const COMMON_SECRET_KEYS: &'static [&'static str] = &[
107        "*password*",
108        "*passwd*",
109        "*passphrase*",
110        "*secret*",
111        "*token*",
112        "*jwt*",
113        "*bearer*",
114        "*api_key*",
115        "*apikey*",
116        "*authorization*",
117        "*cookie*",
118        "*private_key*",
119        "*privatekey*",
120        "*credential*",
121        "*session_id*",
122        "*sessionid*",
123    ];
124
125    pub(crate) fn new() -> Self {
126        Self {
127            filter: Filter::new(Level::Info),
128            filter_error: None,
129            sinks: Vec::new(),
130            defaults: Vec::new(),
131            default_target: String::new(),
132            capture_timestamps: true,
133            capture_context: true,
134            flush_level: Level::Error,
135            on_error: None,
136            redact: Vec::new(),
137        }
138    }
139
140    /// Set the default minimum severity, keeping any per-target rules.
141    pub fn level(mut self, level: Level) -> Self {
142        self.filter = self.filter.with_default_level(level);
143        self
144    }
145
146    /// Replace the filter wholesale.
147    pub fn filter(mut self, filter: Filter) -> Self {
148        self.filter = filter;
149        self
150    }
151
152    /// Add (or replace) a per-target rule. See [`Filter::with_rule`].
153    pub fn target_level(mut self, target: impl Into<String>, level: Level) -> Self {
154        self.filter = self.filter.with_rule(target, level);
155        self
156    }
157
158    /// Parse `directive` (see [`Filter::parse`]) and replace the filter.
159    ///
160    /// A blank directive (empty or whitespace only) changes nothing. An
161    /// invalid directive, including one with only empty segments such
162    /// as `","`, keeps the previous filter and is reported by [`Self::try_build`] as
163    /// [`BuildError::InvalidFilter`]; [`Self::build`] ignores it. Use
164    /// [`Self::try_filter_directive`] to handle the error immediately.
165    pub fn filter_directive(mut self, directive: &str) -> Self {
166        if directive.trim().is_empty() {
167            return self;
168        }
169        match Filter::parse(directive) {
170            Ok(filter) => self.filter = filter,
171            Err(e) => self.filter_error = Some(e),
172        }
173        self
174    }
175
176    /// Parse `directive` and replace the filter, failing immediately on
177    /// an invalid directive.
178    ///
179    /// # Errors
180    ///
181    /// Returns the [`ParseFilterError`] from [`Filter::parse`].
182    pub fn try_filter_directive(mut self, directive: &str) -> Result<Self, ParseFilterError> {
183        self.filter = Filter::parse(directive)?;
184        Ok(self)
185    }
186
187    /// Configure the filter from the environment variable `var`, using
188    /// the [`Filter::parse`] directive syntax (`RUST_LOG`-style), for
189    /// example `.env_filter("LOG_IO")` or `.env_filter("RUST_LOG")`.
190    ///
191    /// If the variable is unset, empty, or blank, the filter configured
192    /// so far (for example by an earlier [`Self::level`]) is kept. A set
193    /// value replaces the whole filter, default level included, as
194    /// `RUST_LOG` does: `LOG_IO=app::db=debug` logs only `app::db`, so
195    /// write `LOG_IO=info,app::db=debug` to keep `info` elsewhere. An
196    /// invalid value (including `","` and a value that is not valid
197    /// Unicode) keeps the previous filter and is reported by
198    /// [`Self::try_build`].
199    pub fn env_filter(self, var: &str) -> Self {
200        self.apply_env_value(std::env::var_os(var))
201    }
202
203    fn apply_env_value(mut self, value: Option<OsString>) -> Self {
204        match value {
205            None => self,
206            Some(raw) => match raw.to_str() {
207                Some(text) => self.filter_directive(text),
208                None => {
209                    self.filter_error = Some(ParseFilterError::not_unicode(&raw));
210                    self
211                }
212            },
213        }
214    }
215
216    /// Target used by [`Logger::log`], which has no target argument.
217    /// Defaults to the empty string. Per-target filter rules apply to
218    /// `Logger::log` records only through this target.
219    pub fn default_target(mut self, target: impl Into<String>) -> Self {
220        self.default_target = target.into();
221        self
222    }
223
224    /// Do not timestamp records. By default every record gets the
225    /// current wall-clock time from [`std::time::SystemTime`].
226    pub fn no_timestamps(mut self) -> Self {
227        self.capture_timestamps = false;
228        self
229    }
230
231    /// Do not attach thread-local context ([`crate::context`]).
232    pub fn no_context(mut self) -> Self {
233        self.capture_context = false;
234        self
235    }
236
237    /// Attach a field to every record, for identifiers such as
238    /// `service` or `version`. Setting the same key again replaces the
239    /// value. Context and call-site fields with the same key take
240    /// precedence over defaults.
241    pub fn with_default_field<'a, V: Into<Value<'a>>>(
242        mut self,
243        key: impl Into<String>,
244        value: V,
245    ) -> Self {
246        let key = key.into();
247        let field = OwnedField::new(&key, value.into());
248        if let Some(existing) = self
249            .defaults
250            .iter_mut()
251            .find(|f| same_output_key(&f.key, &key))
252        {
253            *existing = field;
254        } else {
255            self.defaults.push(field);
256        }
257        self
258    }
259
260    /// Flush every sink after each record at or above `level`. Default
261    /// [`Level::Error`], so errors reach the OS before the logging call
262    /// returns. [`Level::Off`] disables flush-on-level; [`Level::Trace`]
263    /// flushes after every record.
264    ///
265    /// The flush goes through [`Sink::request_flush`]. The built-in
266    /// synchronous sinks flush before the call returns. A
267    /// [`crate::sink::NonBlocking`] sink with
268    /// [`crate::sink::Overflow::Block`] also waits, until its queue has
269    /// been written and flushed; with
270    /// [`crate::sink::Overflow::DropNewest`] it only queues the request
271    /// and returns at once, so the record is not yet durable when the
272    /// call returns. Call [`Logger::flush`] when you need that.
273    pub fn flush_on(mut self, level: Level) -> Self {
274        self.flush_level = level;
275        self
276    }
277
278    /// Replace the value of every field named `key` with
279    /// `"[REDACTED]"`. Applies to call-site fields, context, and default
280    /// fields. The message text is never inspected.
281    ///
282    /// Matching is ASCII case-insensitive, treats `-` and `_` as the
283    /// same character (so `api_key` covers the header name `Api-Key`),
284    /// and also tests the last dot-separated segment of a key (so
285    /// `password` covers `db.password`).
286    ///
287    /// A `*` in `key` matches any run of characters, including none
288    /// (since 2.2): `*_secret` covers `client_secret`, `db_*` covers
289    /// every key starting with `db_`, and `*token*` any key containing
290    /// `token`. Without a `*` the key must match exactly.
291    ///
292    /// Only keys are matched. A secret inside a `Debug` or `Display`
293    /// value (`req = ?login_form`) or in the message text is not seen.
294    pub fn redact_key(mut self, key: impl Into<String>) -> Self {
295        let key = crate::logger::normalize_redact_key(&key.into()).into_boxed_str();
296        if !self.redact.contains(&key) {
297            self.redact.push(key);
298        }
299        self
300    }
301
302    /// Redact every key in [`Self::COMMON_SECRET_KEYS`].
303    pub fn redact_common_secrets(self) -> Self {
304        Self::COMMON_SECRET_KEYS
305            .iter()
306            .fold(self, |builder, key| builder.redact_key(*key))
307    }
308
309    /// Install a callback for sink, flush, and formatting errors. See
310    /// [`ErrorHandler`].
311    pub fn on_error(mut self, handler: ErrorHandler) -> Self {
312        self.on_error = Some(handler);
313        self
314    }
315
316    /// Install a sink.
317    pub fn with_sink(mut self, sink: impl Sink + 'static) -> Self {
318        self.sinks.push(Box::new(sink));
319        self
320    }
321
322    /// Install a standard-output sink with `format`.
323    pub fn stdout<F: Format + 'static>(self, format: F) -> Self {
324        self.with_sink(StdoutSink::new(format))
325    }
326
327    /// Install a standard-error sink with `format`.
328    pub fn stderr<F: Format + 'static>(self, format: F) -> Self {
329        self.with_sink(StderrSink::new(format))
330    }
331
332    /// Install a sink that writes to `writer` with `format`.
333    pub fn writer<W, F>(self, writer: W, format: F) -> Self
334    where
335        W: Write + Send + 'static,
336        F: Format + 'static,
337    {
338        self.with_sink(WriterSink::new(writer, format))
339    }
340
341    /// Finish configuration.
342    ///
343    /// An invalid filter directive recorded by
344    /// [`Self::filter_directive`] or [`Self::env_filter`] is ignored
345    /// (the previous filter stays in effect); use [`Self::try_build`]
346    /// to surface it.
347    ///
348    /// # Panics
349    ///
350    /// Panics if no sink has been installed, which is a programming
351    /// error rather than a runtime condition. Use [`Self::try_build`]
352    /// when the sinks depend on configuration.
353    pub fn build(self) -> Logger {
354        match self.into_logger(false) {
355            Ok(logger) => logger,
356            Err(e) => panic!("log_io: {e}"),
357        }
358    }
359
360    /// Finish configuration, reporting problems as values.
361    ///
362    /// # Errors
363    ///
364    /// * [`BuildError::NoSink`] if no sink was installed.
365    /// * [`BuildError::InvalidFilter`] if a filter directive (including
366    ///   one read from the environment) failed to parse.
367    pub fn try_build(self) -> Result<Logger, BuildError> {
368        self.into_logger(true)
369    }
370
371    fn into_logger(self, strict: bool) -> Result<Logger, BuildError> {
372        if self.sinks.is_empty() {
373            return Err(BuildError::NoSink);
374        }
375        if strict {
376            if let Some(e) = self.filter_error {
377                return Err(BuildError::InvalidFilter(e));
378            }
379        }
380        Ok(Logger::from_inner(LoggerInner {
381            filter: self.filter,
382            sinks: self.sinks.into_boxed_slice(),
383            defaults: self.defaults.into_boxed_slice(),
384            default_target: self.default_target.into_boxed_str(),
385            capture_timestamps: self.capture_timestamps,
386            capture_context: self.capture_context,
387            flush_level: self.flush_level,
388            on_error: self.on_error,
389            redact: self.redact.into_boxed_slice(),
390        }))
391    }
392}
393
394// Format shortcuts for the most common sinks.
395
396#[cfg(feature = "json")]
397impl LoggerBuilder {
398    /// Install a standard-output sink with [`crate::format::JsonFormat`].
399    pub fn stdout_json(self) -> Self {
400        self.stdout(crate::format::JsonFormat::new())
401    }
402
403    /// Install a standard-error sink with [`crate::format::JsonFormat`].
404    pub fn stderr_json(self) -> Self {
405        self.stderr(crate::format::JsonFormat::new())
406    }
407}
408
409#[cfg(feature = "logfmt")]
410impl LoggerBuilder {
411    /// Install a standard-output sink with [`crate::format::LogfmtFormat`].
412    pub fn stdout_logfmt(self) -> Self {
413        self.stdout(crate::format::LogfmtFormat::new())
414    }
415
416    /// Install a standard-error sink with [`crate::format::LogfmtFormat`].
417    pub fn stderr_logfmt(self) -> Self {
418        self.stderr(crate::format::LogfmtFormat::new())
419    }
420}
421
422#[cfg(feature = "human")]
423impl LoggerBuilder {
424    /// Install a standard-output sink with [`crate::format::HumanFormat`],
425    /// coloured when standard output is a terminal (see
426    /// [`crate::format::ColorChoice::Auto`]).
427    pub fn stdout_human(self) -> Self {
428        let color = crate::format::ColorChoice::Auto.enabled_for_stdout();
429        self.stdout(crate::format::HumanFormat::new().color(color))
430    }
431
432    /// Install a standard-error sink with [`crate::format::HumanFormat`],
433    /// coloured when standard error is a terminal (see
434    /// [`crate::format::ColorChoice::Auto`]).
435    pub fn stderr_human(self) -> Self {
436        let color = crate::format::ColorChoice::Auto.enabled_for_stderr();
437        self.stderr(crate::format::HumanFormat::new().color(color))
438    }
439}
440
441impl core::fmt::Debug for LoggerBuilder {
442    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
443        f.debug_struct("LoggerBuilder")
444            .field("filter", &self.filter)
445            .field("filter_error", &self.filter_error)
446            .field("sinks", &self.sinks.len())
447            .field("default_fields", &self.defaults.len())
448            .field("default_target", &self.default_target)
449            .field("capture_timestamps", &self.capture_timestamps)
450            .field("capture_context", &self.capture_context)
451            .field("flush_level", &self.flush_level)
452            .field("redacted_keys", &self.redact)
453            .field("on_error", &self.on_error.is_some())
454            .finish()
455    }
456}
457
458#[cfg(all(test, feature = "json", feature = "logfmt", feature = "human"))]
459mod tests {
460    use super::*;
461    use crate::filter::ParseFilterErrorKind;
462    use crate::sink::NullSink;
463
464    #[test]
465    fn test_try_build_without_sink_is_no_sink_error() {
466        let err = Logger::builder().try_build().unwrap_err();
467        assert_eq!(err, BuildError::NoSink);
468        assert!(err.to_string().contains("no sink"));
469        assert!(std::error::Error::source(&err).is_none());
470    }
471
472    #[test]
473    fn test_build_without_sink_panics() {
474        let result = std::panic::catch_unwind(|| Logger::builder().build());
475        assert!(result.is_err());
476    }
477
478    #[test]
479    fn test_invalid_directive_is_reported_by_try_build_only() {
480        let err = Logger::builder()
481            .filter_directive("info,app=loud")
482            .with_sink(NullSink::new())
483            .try_build()
484            .unwrap_err();
485        let BuildError::InvalidFilter(inner) = &err else {
486            panic!("unexpected {err:?}");
487        };
488        assert_eq!(inner.kind(), ParseFilterErrorKind::InvalidLevel);
489        assert!(std::error::Error::source(&err).is_some());
490        assert!(err.to_string().contains("app=loud"));
491
492        let lenient = Logger::builder()
493            .level(Level::Warn)
494            .filter_directive("info,app=loud")
495            .with_sink(NullSink::new())
496            .build();
497        assert_eq!(
498            lenient.filter().default_level(),
499            Level::Warn,
500            "previous filter kept"
501        );
502    }
503
504    #[test]
505    fn test_try_filter_directive_fails_fast() {
506        assert!(Logger::builder().try_filter_directive("nope").is_err());
507        let builder = Logger::builder().try_filter_directive("debug").unwrap();
508        let logger = builder.with_sink(NullSink::new()).build();
509        assert_eq!(logger.filter().default_level(), Level::Debug);
510    }
511
512    #[test]
513    fn test_blank_directive_changes_nothing() {
514        let logger = Logger::builder()
515            .level(Level::Warn)
516            .filter_directive("   ")
517            .with_sink(NullSink::new())
518            .try_build()
519            .unwrap();
520        assert_eq!(logger.filter().default_level(), Level::Warn);
521    }
522
523    #[test]
524    fn test_level_after_directive_keeps_rules() {
525        let logger = Logger::builder()
526            .filter_directive("warn,app=trace")
527            .level(Level::Error)
528            .with_sink(NullSink::new())
529            .build();
530        assert_eq!(logger.threshold_for("app"), Level::Trace);
531        assert_eq!(logger.threshold_for("other"), Level::Error);
532    }
533
534    #[test]
535    fn test_env_filter_values() {
536        let base = || {
537            Logger::builder()
538                .level(Level::Warn)
539                .with_sink(NullSink::new())
540        };
541        let unset = base().apply_env_value(None).try_build().unwrap();
542        assert_eq!(unset.filter().default_level(), Level::Warn);
543        let empty = base().apply_env_value(Some("".into())).try_build().unwrap();
544        assert_eq!(empty.filter().default_level(), Level::Warn);
545        let set = base()
546            .apply_env_value(Some("debug,hyper=off".into()))
547            .try_build()
548            .unwrap();
549        assert_eq!(set.filter().default_level(), Level::Debug);
550        assert_eq!(set.threshold_for("hyper"), Level::Off);
551        let bad = base().apply_env_value(Some("loud".into())).try_build();
552        assert!(matches!(bad, Err(BuildError::InvalidFilter(_))));
553        // A variable that is almost certainly unset.
554        let real = base()
555            .env_filter("LOG_IO_TEST_VARIABLE_THAT_IS_NOT_SET_7f3a")
556            .try_build()
557            .unwrap();
558        assert_eq!(real.filter().default_level(), Level::Warn);
559    }
560
561    #[cfg(any(unix, windows))]
562    #[test]
563    fn test_env_filter_rejects_non_unicode() {
564        #[cfg(unix)]
565        let raw = {
566            use std::os::unix::ffi::OsStringExt;
567            OsString::from_vec(vec![0x66, 0xff, 0x6f])
568        };
569        #[cfg(windows)]
570        let raw = {
571            use std::os::windows::ffi::OsStringExt;
572            OsString::from_wide(&[0x0066, 0xd800, 0x006f])
573        };
574        let err = Logger::builder()
575            .with_sink(NullSink::new())
576            .apply_env_value(Some(raw))
577            .try_build()
578            .unwrap_err();
579        let BuildError::InvalidFilter(inner) = err else {
580            panic!("unexpected {err:?}");
581        };
582        assert_eq!(inner.kind(), ParseFilterErrorKind::NotUnicode);
583    }
584
585    #[test]
586    fn test_default_fields_same_key_replaces() {
587        let builder = Logger::builder()
588            .with_default_field("service", "a")
589            .with_default_field("service", "b");
590        assert_eq!(builder.defaults.len(), 1);
591        assert_eq!(builder.defaults[0].as_field().value, Value::Str("b"));
592    }
593
594    #[test]
595    fn test_redact_keys_are_lowercased_and_deduplicated() {
596        let builder = Logger::builder()
597            .redact_key("*Token*")
598            .redact_key("*token*")
599            .redact_common_secrets();
600        assert_eq!(
601            builder.redact.len(),
602            LoggerBuilder::COMMON_SECRET_KEYS.len(),
603            "*token* appears once"
604        );
605        let text = format!("{builder:?}");
606        assert!(text.contains("token"), "{text}");
607    }
608
609    #[test]
610    fn test_shortcuts_install_sinks() {
611        let logger = Logger::builder()
612            .stdout_json()
613            .stderr_json()
614            .stdout_logfmt()
615            .stderr_logfmt()
616            .stdout_human()
617            .stderr_human()
618            .stdout(crate::format::JsonFormat::new())
619            .writer(Vec::<u8>::new(), crate::format::JsonFormat::new())
620            .build();
621        assert_eq!(logger.sink_count(), 8);
622    }
623}