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    /// Keys redacted by [`LoggerBuilder::redact_common_secrets`].
94    ///
95    /// Matching treats `-` and `_` alike, so `api_key` also covers the
96    /// HTTP header names `api-key` and `x-api-key` (via `x_api_key`),
97    /// and `set_cookie` covers `set-cookie`. Matching is otherwise
98    /// exact: `user_password` is not covered by `password`; add such
99    /// keys with [`LoggerBuilder::redact_key`].
100    pub const COMMON_SECRET_KEYS: &'static [&'static str] = &[
101        "password",
102        "passwd",
103        "secret",
104        "secret_key",
105        "client_secret",
106        "token",
107        "access_token",
108        "refresh_token",
109        "id_token",
110        "auth_token",
111        "session_token",
112        "jwt",
113        "api_key",
114        "apikey",
115        "x_api_key",
116        "authorization",
117        "proxy_authorization",
118        "cookie",
119        "set_cookie",
120        "private_key",
121        "privatekey",
122        "credentials",
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    pub fn redact_key(mut self, key: impl Into<String>) -> Self {
287        let key = crate::logger::normalize_redact_key(&key.into()).into_boxed_str();
288        if !self.redact.contains(&key) {
289            self.redact.push(key);
290        }
291        self
292    }
293
294    /// Redact every key in [`Self::COMMON_SECRET_KEYS`].
295    pub fn redact_common_secrets(self) -> Self {
296        Self::COMMON_SECRET_KEYS
297            .iter()
298            .fold(self, |builder, key| builder.redact_key(*key))
299    }
300
301    /// Install a callback for sink, flush, and formatting errors. See
302    /// [`ErrorHandler`].
303    pub fn on_error(mut self, handler: ErrorHandler) -> Self {
304        self.on_error = Some(handler);
305        self
306    }
307
308    /// Install a sink.
309    pub fn with_sink(mut self, sink: impl Sink + 'static) -> Self {
310        self.sinks.push(Box::new(sink));
311        self
312    }
313
314    /// Install a standard-output sink with `format`.
315    pub fn stdout<F: Format + 'static>(self, format: F) -> Self {
316        self.with_sink(StdoutSink::new(format))
317    }
318
319    /// Install a standard-error sink with `format`.
320    pub fn stderr<F: Format + 'static>(self, format: F) -> Self {
321        self.with_sink(StderrSink::new(format))
322    }
323
324    /// Install a sink that writes to `writer` with `format`.
325    pub fn writer<W, F>(self, writer: W, format: F) -> Self
326    where
327        W: Write + Send + 'static,
328        F: Format + 'static,
329    {
330        self.with_sink(WriterSink::new(writer, format))
331    }
332
333    /// Finish configuration.
334    ///
335    /// An invalid filter directive recorded by
336    /// [`Self::filter_directive`] or [`Self::env_filter`] is ignored
337    /// (the previous filter stays in effect); use [`Self::try_build`]
338    /// to surface it.
339    ///
340    /// # Panics
341    ///
342    /// Panics if no sink has been installed, which is a programming
343    /// error rather than a runtime condition. Use [`Self::try_build`]
344    /// when the sinks depend on configuration.
345    pub fn build(self) -> Logger {
346        match self.into_logger(false) {
347            Ok(logger) => logger,
348            Err(e) => panic!("log_io: {e}"),
349        }
350    }
351
352    /// Finish configuration, reporting problems as values.
353    ///
354    /// # Errors
355    ///
356    /// * [`BuildError::NoSink`] if no sink was installed.
357    /// * [`BuildError::InvalidFilter`] if a filter directive (including
358    ///   one read from the environment) failed to parse.
359    pub fn try_build(self) -> Result<Logger, BuildError> {
360        self.into_logger(true)
361    }
362
363    fn into_logger(self, strict: bool) -> Result<Logger, BuildError> {
364        if self.sinks.is_empty() {
365            return Err(BuildError::NoSink);
366        }
367        if strict {
368            if let Some(e) = self.filter_error {
369                return Err(BuildError::InvalidFilter(e));
370            }
371        }
372        Ok(Logger::from_inner(LoggerInner {
373            filter: self.filter,
374            sinks: self.sinks.into_boxed_slice(),
375            defaults: self.defaults.into_boxed_slice(),
376            default_target: self.default_target.into_boxed_str(),
377            capture_timestamps: self.capture_timestamps,
378            capture_context: self.capture_context,
379            flush_level: self.flush_level,
380            on_error: self.on_error,
381            redact: self.redact.into_boxed_slice(),
382        }))
383    }
384}
385
386// Format shortcuts for the most common sinks.
387
388#[cfg(feature = "json")]
389impl LoggerBuilder {
390    /// Install a standard-output sink with [`crate::format::JsonFormat`].
391    pub fn stdout_json(self) -> Self {
392        self.stdout(crate::format::JsonFormat::new())
393    }
394
395    /// Install a standard-error sink with [`crate::format::JsonFormat`].
396    pub fn stderr_json(self) -> Self {
397        self.stderr(crate::format::JsonFormat::new())
398    }
399}
400
401#[cfg(feature = "logfmt")]
402impl LoggerBuilder {
403    /// Install a standard-output sink with [`crate::format::LogfmtFormat`].
404    pub fn stdout_logfmt(self) -> Self {
405        self.stdout(crate::format::LogfmtFormat::new())
406    }
407
408    /// Install a standard-error sink with [`crate::format::LogfmtFormat`].
409    pub fn stderr_logfmt(self) -> Self {
410        self.stderr(crate::format::LogfmtFormat::new())
411    }
412}
413
414#[cfg(feature = "human")]
415impl LoggerBuilder {
416    /// Install a standard-output sink with [`crate::format::HumanFormat`],
417    /// coloured when standard output is a terminal (see
418    /// [`crate::format::ColorChoice::Auto`]).
419    pub fn stdout_human(self) -> Self {
420        let color = crate::format::ColorChoice::Auto.enabled_for_stdout();
421        self.stdout(crate::format::HumanFormat::new().color(color))
422    }
423
424    /// Install a standard-error sink with [`crate::format::HumanFormat`],
425    /// coloured when standard error is a terminal (see
426    /// [`crate::format::ColorChoice::Auto`]).
427    pub fn stderr_human(self) -> Self {
428        let color = crate::format::ColorChoice::Auto.enabled_for_stderr();
429        self.stderr(crate::format::HumanFormat::new().color(color))
430    }
431}
432
433impl core::fmt::Debug for LoggerBuilder {
434    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
435        f.debug_struct("LoggerBuilder")
436            .field("filter", &self.filter)
437            .field("filter_error", &self.filter_error)
438            .field("sinks", &self.sinks.len())
439            .field("default_fields", &self.defaults.len())
440            .field("default_target", &self.default_target)
441            .field("capture_timestamps", &self.capture_timestamps)
442            .field("capture_context", &self.capture_context)
443            .field("flush_level", &self.flush_level)
444            .field("redacted_keys", &self.redact)
445            .field("on_error", &self.on_error.is_some())
446            .finish()
447    }
448}
449
450#[cfg(all(test, feature = "json", feature = "logfmt", feature = "human"))]
451mod tests {
452    use super::*;
453    use crate::filter::ParseFilterErrorKind;
454    use crate::sink::NullSink;
455
456    #[test]
457    fn test_try_build_without_sink_is_no_sink_error() {
458        let err = Logger::builder().try_build().unwrap_err();
459        assert_eq!(err, BuildError::NoSink);
460        assert!(err.to_string().contains("no sink"));
461        assert!(std::error::Error::source(&err).is_none());
462    }
463
464    #[test]
465    fn test_build_without_sink_panics() {
466        let result = std::panic::catch_unwind(|| Logger::builder().build());
467        assert!(result.is_err());
468    }
469
470    #[test]
471    fn test_invalid_directive_is_reported_by_try_build_only() {
472        let err = Logger::builder()
473            .filter_directive("info,app=loud")
474            .with_sink(NullSink::new())
475            .try_build()
476            .unwrap_err();
477        let BuildError::InvalidFilter(inner) = &err else {
478            panic!("unexpected {err:?}");
479        };
480        assert_eq!(inner.kind(), ParseFilterErrorKind::InvalidLevel);
481        assert!(std::error::Error::source(&err).is_some());
482        assert!(err.to_string().contains("app=loud"));
483
484        let lenient = Logger::builder()
485            .level(Level::Warn)
486            .filter_directive("info,app=loud")
487            .with_sink(NullSink::new())
488            .build();
489        assert_eq!(
490            lenient.filter().default_level(),
491            Level::Warn,
492            "previous filter kept"
493        );
494    }
495
496    #[test]
497    fn test_try_filter_directive_fails_fast() {
498        assert!(Logger::builder().try_filter_directive("nope").is_err());
499        let builder = Logger::builder().try_filter_directive("debug").unwrap();
500        let logger = builder.with_sink(NullSink::new()).build();
501        assert_eq!(logger.filter().default_level(), Level::Debug);
502    }
503
504    #[test]
505    fn test_blank_directive_changes_nothing() {
506        let logger = Logger::builder()
507            .level(Level::Warn)
508            .filter_directive("   ")
509            .with_sink(NullSink::new())
510            .try_build()
511            .unwrap();
512        assert_eq!(logger.filter().default_level(), Level::Warn);
513    }
514
515    #[test]
516    fn test_level_after_directive_keeps_rules() {
517        let logger = Logger::builder()
518            .filter_directive("warn,app=trace")
519            .level(Level::Error)
520            .with_sink(NullSink::new())
521            .build();
522        assert_eq!(logger.threshold_for("app"), Level::Trace);
523        assert_eq!(logger.threshold_for("other"), Level::Error);
524    }
525
526    #[test]
527    fn test_env_filter_values() {
528        let base = || {
529            Logger::builder()
530                .level(Level::Warn)
531                .with_sink(NullSink::new())
532        };
533        let unset = base().apply_env_value(None).try_build().unwrap();
534        assert_eq!(unset.filter().default_level(), Level::Warn);
535        let empty = base().apply_env_value(Some("".into())).try_build().unwrap();
536        assert_eq!(empty.filter().default_level(), Level::Warn);
537        let set = base()
538            .apply_env_value(Some("debug,hyper=off".into()))
539            .try_build()
540            .unwrap();
541        assert_eq!(set.filter().default_level(), Level::Debug);
542        assert_eq!(set.threshold_for("hyper"), Level::Off);
543        let bad = base().apply_env_value(Some("loud".into())).try_build();
544        assert!(matches!(bad, Err(BuildError::InvalidFilter(_))));
545        // A variable that is almost certainly unset.
546        let real = base()
547            .env_filter("LOG_IO_TEST_VARIABLE_THAT_IS_NOT_SET_7f3a")
548            .try_build()
549            .unwrap();
550        assert_eq!(real.filter().default_level(), Level::Warn);
551    }
552
553    #[cfg(any(unix, windows))]
554    #[test]
555    fn test_env_filter_rejects_non_unicode() {
556        #[cfg(unix)]
557        let raw = {
558            use std::os::unix::ffi::OsStringExt;
559            OsString::from_vec(vec![0x66, 0xff, 0x6f])
560        };
561        #[cfg(windows)]
562        let raw = {
563            use std::os::windows::ffi::OsStringExt;
564            OsString::from_wide(&[0x0066, 0xd800, 0x006f])
565        };
566        let err = Logger::builder()
567            .with_sink(NullSink::new())
568            .apply_env_value(Some(raw))
569            .try_build()
570            .unwrap_err();
571        let BuildError::InvalidFilter(inner) = err else {
572            panic!("unexpected {err:?}");
573        };
574        assert_eq!(inner.kind(), ParseFilterErrorKind::NotUnicode);
575    }
576
577    #[test]
578    fn test_default_fields_same_key_replaces() {
579        let builder = Logger::builder()
580            .with_default_field("service", "a")
581            .with_default_field("service", "b");
582        assert_eq!(builder.defaults.len(), 1);
583        assert_eq!(builder.defaults[0].as_field().value, Value::Str("b"));
584    }
585
586    #[test]
587    fn test_redact_keys_are_lowercased_and_deduplicated() {
588        let builder = Logger::builder()
589            .redact_key("Token")
590            .redact_key("token")
591            .redact_common_secrets();
592        assert_eq!(
593            builder.redact.len(),
594            LoggerBuilder::COMMON_SECRET_KEYS.len(),
595            "token appears once"
596        );
597        let text = format!("{builder:?}");
598        assert!(text.contains("token"), "{text}");
599    }
600
601    #[test]
602    fn test_shortcuts_install_sinks() {
603        let logger = Logger::builder()
604            .stdout_json()
605            .stderr_json()
606            .stdout_logfmt()
607            .stderr_logfmt()
608            .stdout_human()
609            .stderr_human()
610            .stdout(crate::format::JsonFormat::new())
611            .writer(Vec::<u8>::new(), crate::format::JsonFormat::new())
612            .build();
613        assert_eq!(logger.sink_count(), 8);
614    }
615}