context_logger/lib.rs
1//! # Overview
2//!
3#![doc = include_utils::include_md!("README.md:description")]
4//!
5//! Modern applications often need rich, structured context in logs to provide
6//! insight into runtime behavior. This library simplifies the process by:
7//!
8//! - Adding structured context to logs without modifying the existing logging statements.
9//! - Propagating log context across async boundaries.
10//! - Allowing dynamic context updates.
11//! - Supporting nested contexts to build hierarchical relationships.
12//!
13//! This library provides a wrapper around other existing logger implementations,
14//! acting as a middleware layer that enriches log records with additional context before
15//! passing them to the underlying logger. It works with any logger that implements the
16//! standard [`Log`](log::Log) trait, making it compatible with popular logging frameworks like
17//! [`env_logger`], [`log4rs`] and others.
18//!
19//! ## Basic example
20//!
21#![doc = include_utils::include_md!("README.md:basic_example")]
22//!
23//! ## Async Context Propagation
24//!
25#![doc = include_utils::include_md!("README.md:async_example")]
26//!
27//! [`env_logger`]: https://docs.rs/env_logger/latest/env_logger
28//! [`log4rs`]: https://docs.rs/log4rs/latest/log4rs
29
30use std::borrow::Cow;
31
32use self::stack::CONTEXT_STACK;
33pub use self::{context::LogContext, future::FutureExt, value::ContextValue};
34
35mod context;
36pub mod future;
37pub mod guard;
38mod stack;
39mod value;
40
41type StaticCowStr = Cow<'static, str>;
42
43/// A logger wrapper that enhances log records with contextual properties.
44///
45/// `ContextLogger` wraps an existing logging implementation and adds additional
46/// context properties to log records. These context properties are taken from the
47/// current context stack, which is managed by the [`LogContext`] type.
48///
49/// # Example
50///
51/// ```
52/// use log::{info, LevelFilter};
53/// use context_logger::{ContextLogger, LogContext};
54///
55/// // Create a logger.
56/// let env_logger = env_logger::builder().build();
57/// let max_level = env_logger.filter();
58/// // Wrap it with ContextLogger to enable context propagation.
59/// let context_logger = ContextLogger::new(env_logger);
60/// // Initialize the resulting logger.
61/// context_logger.init(max_level);
62///
63/// // Create a context with properties
64/// let ctx = LogContext::new()
65/// .record("request_id", "req-123")
66/// .record("user_id", 42);
67///
68/// // Use the context while logging
69/// let _guard = ctx.enter();
70/// info!("Processing request"); // Will include request_id and user_id properties
71/// ```
72///
73/// See [`LogContext`] for more information on how to create and manage context properties.
74pub struct ContextLogger {
75 inner: Box<dyn log::Log>,
76}
77
78impl ContextLogger {
79 /// Creates a new [`ContextLogger`] that wraps the given logging implementation.
80 ///
81 /// The inner logger will receive log records enhanced with context properties
82 /// from the current context stack.
83 pub fn new<L>(inner: L) -> Self
84 where
85 L: log::Log + 'static,
86 {
87 Self {
88 inner: Box::new(inner),
89 }
90 }
91
92 /// Initializes the global logger with the context logger.
93 ///
94 /// This should be called early in the execution of a Rust program. Any log events that occur before initialization will be ignored.
95 ///
96 /// # Panics
97 ///
98 /// Panics if a logger has already been set.
99 pub fn init(self, max_level: log::LevelFilter) {
100 self.try_init(max_level)
101 .expect("ContextLogger::init should not be called after logger initialization");
102 }
103
104 /// Initializes the global logger with the context logger.
105 ///
106 /// This should be called early in the execution of a Rust program. Any log events that occur before initialization will be ignored.
107 ///
108 /// # Errors
109 ///
110 /// Returns an error if a logger has already been set.
111 pub fn try_init(self, max_level: log::LevelFilter) -> Result<(), log::SetLoggerError> {
112 log::set_max_level(max_level);
113 log::set_boxed_logger(Box::new(self))
114 }
115}
116
117impl std::fmt::Debug for ContextLogger {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.debug_struct("ContextLogger").finish_non_exhaustive()
120 }
121}
122
123impl log::Log for ContextLogger {
124 fn enabled(&self, metadata: &log::Metadata) -> bool {
125 self.inner.enabled(metadata)
126 }
127
128 fn log(&self, record: &log::Record) {
129 let error = CONTEXT_STACK.try_with(|stack| {
130 if let Some(top) = stack.top() {
131 let extra_properties = ExtraProperties {
132 source: &record.key_values(),
133 properties: &*top,
134 };
135 let new_record = record.to_builder().key_values(&extra_properties).build();
136
137 self.inner.log(&new_record);
138 } else {
139 self.inner.log(record);
140 }
141 });
142
143 if let Err(err) = error {
144 // If the context stack is not available, log the original record.
145 self.inner.log(record);
146 // We can't use `log::error!` here because we are in the middle of logging and
147 // this invocation becomes recursive.
148 eprintln!("Error accessing context stack: {err}");
149 }
150 }
151
152 fn flush(&self) {
153 self.inner.flush();
154 }
155}
156
157struct ExtraProperties<'a, I> {
158 source: &'a dyn log::kv::Source,
159 properties: I,
160}
161
162impl<'a, I> log::kv::Source for ExtraProperties<'a, I>
163where
164 I: IntoIterator<Item = &'a (StaticCowStr, ContextValue)> + Copy,
165{
166 fn visit<'kvs>(
167 &'kvs self,
168 visitor: &mut dyn log::kv::VisitSource<'kvs>,
169 ) -> Result<(), log::kv::Error> {
170 for (key, value) in self.properties {
171 visitor.visit_pair(log::kv::Key::from_str(key), value.as_log_value())?;
172 }
173 self.source.visit(visitor)
174 }
175}