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] = &[
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 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 {
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 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 pub fn on_error(mut self, handler: ErrorHandler) -> Self {
304 self.on_error = Some(handler);
305 self
306 }
307
308 pub fn with_sink(mut self, sink: impl Sink + 'static) -> Self {
310 self.sinks.push(Box::new(sink));
311 self
312 }
313
314 pub fn stdout<F: Format + 'static>(self, format: F) -> Self {
316 self.with_sink(StdoutSink::new(format))
317 }
318
319 pub fn stderr<F: Format + 'static>(self, format: F) -> Self {
321 self.with_sink(StderrSink::new(format))
322 }
323
324 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 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 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#[cfg(feature = "json")]
389impl LoggerBuilder {
390 pub fn stdout_json(self) -> Self {
392 self.stdout(crate::format::JsonFormat::new())
393 }
394
395 pub fn stderr_json(self) -> Self {
397 self.stderr(crate::format::JsonFormat::new())
398 }
399}
400
401#[cfg(feature = "logfmt")]
402impl LoggerBuilder {
403 pub fn stdout_logfmt(self) -> Self {
405 self.stdout(crate::format::LogfmtFormat::new())
406 }
407
408 pub fn stderr_logfmt(self) -> Self {
410 self.stderr(crate::format::LogfmtFormat::new())
411 }
412}
413
414#[cfg(feature = "human")]
415impl LoggerBuilder {
416 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 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 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}