1use 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#[derive(Debug, Clone, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum BuildError {
27 NoSink,
31 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#[cfg_attr(all(feature = "human"), doc = "```")]
65#[cfg_attr(not(all(feature = "human")), doc = "```ignore")]
66#[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 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 pub fn level(mut self, level: Level) -> Self {
142 self.filter = self.filter.with_default_level(level);
143 self
144 }
145
146 pub fn filter(mut self, filter: Filter) -> Self {
148 self.filter = filter;
149 self
150 }
151
152 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 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 pub fn try_filter_directive(mut self, directive: &str) -> Result<Self, ParseFilterError> {
183 self.filter = Filter::parse(directive)?;
184 Ok(self)
185 }
186
187 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 pub fn default_target(mut self, target: impl Into<String>) -> Self {
220 self.default_target = target.into();
221 self
222 }
223
224 pub fn no_timestamps(mut self) -> Self {
227 self.capture_timestamps = false;
228 self
229 }
230
231 pub fn no_context(mut self) -> Self {
233 self.capture_context = false;
234 self
235 }
236
237 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 pub fn flush_on(mut self, level: Level) -> Self {
274 self.flush_level = level;
275 self
276 }
277
278 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 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 pub fn on_error(mut self, handler: ErrorHandler) -> Self {
312 self.on_error = Some(handler);
313 self
314 }
315
316 pub fn with_sink(mut self, sink: impl Sink + 'static) -> Self {
318 self.sinks.push(Box::new(sink));
319 self
320 }
321
322 pub fn stdout<F: Format + 'static>(self, format: F) -> Self {
324 self.with_sink(StdoutSink::new(format))
325 }
326
327 pub fn stderr<F: Format + 'static>(self, format: F) -> Self {
329 self.with_sink(StderrSink::new(format))
330 }
331
332 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 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 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#[cfg(feature = "json")]
397impl LoggerBuilder {
398 pub fn stdout_json(self) -> Self {
400 self.stdout(crate::format::JsonFormat::new())
401 }
402
403 pub fn stderr_json(self) -> Self {
405 self.stderr(crate::format::JsonFormat::new())
406 }
407}
408
409#[cfg(feature = "logfmt")]
410impl LoggerBuilder {
411 pub fn stdout_logfmt(self) -> Self {
413 self.stdout(crate::format::LogfmtFormat::new())
414 }
415
416 pub fn stderr_logfmt(self) -> Self {
418 self.stderr(crate::format::LogfmtFormat::new())
419 }
420}
421
422#[cfg(feature = "human")]
423impl LoggerBuilder {
424 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 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 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}