1use 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#[cfg_attr(all(feature = "logfmt"), doc = "```")]
34#[cfg_attr(not(all(feature = "logfmt")), doc = "```ignore")]
35pub type ErrorHandler = Arc<dyn Fn(&Error) + Send + Sync>;
43
44pub const REDACTED: &str = "[REDACTED]";
46
47#[cfg_attr(all(feature = "json"), doc = "```")]
90#[cfg_attr(not(all(feature = "json")), doc = "```ignore")]
91#[derive(Clone)]
103pub struct Logger {
104 inner: Arc<LoggerInner>,
105}
106
107impl 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 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 static DISPATCH_DEPTH: Cell<u32> = const { Cell::new(0) };
141 static IN_HANDLER: Cell<bool> = const { Cell::new(false) };
143 static MESSAGE_BUF: RefCell<String> = const { RefCell::new(String::new()) };
145}
146
147pub(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 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 #[must_use]
202 pub fn threshold_for(&self, target: &str) -> Level {
203 self.inner.filter.threshold_for(target)
204 }
205
206 #[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 #[must_use]
218 pub fn min_level(&self) -> Level {
219 self.inner.filter.min_level()
220 }
221
222 #[must_use]
224 pub fn filter(&self) -> &Filter {
225 &self.inner.filter
226 }
227
228 #[must_use]
230 pub fn sink_count(&self) -> usize {
231 self.inner.sinks.len()
232 }
233
234 #[must_use]
237 pub fn default_target(&self) -> &str {
238 &self.inner.default_target
239 }
240
241 #[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 #[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 #[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 pub fn flush(&self) -> Result<()> {
298 self.for_each_sink(|sink| sink.flush())
299 }
300
301 pub fn shutdown(&self) -> Result<()> {
309 self.for_each_sink(|sink| sink.shutdown())
310 }
311
312 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 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 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 self.report(&Error::Format(fmt::Error));
379 }
380 }
381
382 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 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 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 fn merge_ambient(
463 &self,
464 metadata: Metadata<'_>,
465 message: &str,
466 fields: &[Field<'_>],
467 ctx: &[Field<'_>],
468 extra: &[Field<'_>],
469 ) -> Result<()> {
470 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 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
552pub(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#[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#[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 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#[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#[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)] let class = hash as u32;
658 class
659}
660
661#[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
691const INLINE_FIELDS: usize = 16;
693
694const EMPTY_FIELD: Field<'static> = Field {
695 key: "",
696 value: Value::Null,
697};
698
699struct 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 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 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 #[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 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 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 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}