Skip to main content

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, collections::HashMap};
31
32use crate::records::LogRecordRef;
33
34mod context;
35pub mod future;
36mod records;
37mod scope;
38mod value;
39
40type LogValueFn = Box<dyn Fn(&log::Record) -> LogValue + Send + Sync>;
41
42pub use self::{
43    context::LogContext,
44    future::FutureExt,
45    records::LogRecords,
46    scope::{LogContextExt, LogScope},
47    value::LogValue,
48};
49
50/// A logger wrapper that enhances log records with scope records.
51///
52/// `ContextLogger` wraps an existing logging implementation and adds additional
53/// scope records to log records. These records are taken from the
54/// current scope stack, which is managed by [`LogScope`].
55///
56/// # Example
57///
58/// ```
59/// use log::{info, LevelFilter};
60/// use context_logger::{ContextLogger, LogContext, LogScope};
61///
62/// // Create a logger.
63/// let env_logger = env_logger::builder().build();
64/// let max_level = env_logger.filter();
65/// // Wrap it with ContextLogger to enable context propagation.
66/// let context_logger = ContextLogger::new(env_logger);
67/// // Initialize the resulting logger.
68/// context_logger.init(max_level);
69///
70/// // Create a context with properties
71/// let ctx = LogContext::new()
72///     .with_local_record("request_id", "req-123")
73///     .with_local_record("user_id", 42);
74///
75/// // Use the context while logging
76/// let _guard = LogScope::enter(ctx);
77/// info!("Processing request"); // Will include request_id and user_id records
78/// ```
79///
80/// See [`LogContext`] for more information on how to create and manage scope records.
81pub struct ContextLogger {
82    inner: Box<dyn log::Log>,
83    default_records: LogRecords,
84    dynamic_default_records: HashMap<Cow<'static, str>, LogValueFn>,
85}
86
87impl ContextLogger {
88    /// Creates a new [`ContextLogger`] that wraps the given logging implementation.
89    ///
90    /// The inner logger will receive log records enhanced with scope records
91    /// from the current scope stack.
92    pub fn new<L>(inner: L) -> Self
93    where
94        L: log::Log + 'static,
95    {
96        Self {
97            inner: Box::new(inner),
98            default_records: LogRecords::new(),
99            dynamic_default_records: HashMap::new(),
100        }
101    }
102
103    /// Initializes the global logger with the context logger.
104    ///
105    /// This should be called early in the execution of a Rust program. Any log events that occur before initialization will be ignored.
106    ///
107    /// # Panics
108    ///
109    /// Panics if a logger has already been set.
110    pub fn init(self, max_level: log::LevelFilter) {
111        self.try_init(max_level)
112            .expect("ContextLogger::init should not be called after logger initialization");
113    }
114
115    /// Initializes the global logger with the context logger.
116    ///
117    /// This should be called early in the execution of a Rust program. Any log events that occur before initialization will be ignored.
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if a logger has already been set.
122    pub fn try_init(self, max_level: log::LevelFilter) -> Result<(), log::SetLoggerError> {
123        log::set_max_level(max_level);
124        log::set_boxed_logger(Box::new(self))
125    }
126
127    /// Adds a default record that will be included in all log entries.
128    ///
129    /// Default records are automatically added to all log entries, regardless of
130    /// the current context. They are defined when the logger is created and remain
131    /// constant throughout the application's lifetime.
132    ///
133    /// # Behavior with Duplicate Keys
134    ///
135    /// When logging, default records are added first, followed by records from the current
136    /// context. If multiple records with the same key exist, the behavior depends on the
137    /// underlying logger implementation. In most implementations, later records with the
138    /// same key will typically replace earlier ones.
139    ///
140    /// # Example
141    ///
142    /// ```
143    /// use log::{info, LevelFilter};
144    /// use context_logger::{ContextLogger, LogContext, LogScope};
145    ///
146    /// // Create a logger with default records
147    /// let logger = ContextLogger::new(env_logger::builder().build())
148    ///     .with_default_record("service", "api")
149    ///     .with_default_record("version", "1.0.0");
150    /// // Initialize it
151    /// logger.init(LevelFilter::Info);
152    /// // Context records are added after default records
153    /// let _guard = LogScope::enter(LogContext::new()
154    ///     .with_local_record("request_id", "123"));
155    ///
156    /// info!("Processing request"); // Will include service="api", version="1.0.0", request_id="123"
157    /// ```
158    #[must_use]
159    pub fn with_default_record(
160        mut self,
161        key: impl Into<Cow<'static, str>>,
162        value: impl Into<LogValue>,
163    ) -> Self {
164        self.default_records.insert(key, value);
165        self
166    }
167
168    /// Adds a dynamic default record computed by the given closure for each log entry.
169    ///
170    /// Like [`Self::with_default_record`], the record is included in all log entries.
171    /// However, unlike the static variant, the value is *computed at log time* by invoking the
172    /// provided closure with the current [`log::Record`]. This makes it suitable for fields
173    /// whose values are not known upfront, such as timestamps or thread IDs.
174    ///
175    /// **Note!** *The order in which dynamic default record functions are evaluated is not guaranteed.*
176    ///
177    /// # Example
178    ///
179    /// Adding a current timestamp.
180    ///
181    /// ```
182    /// use chrono::Utc;
183    /// use log::{info, LevelFilter};
184    /// use context_logger::{ContextLogger, LogValue};
185    ///
186    /// let logger = ContextLogger::new(env_logger::builder().build())
187    ///         .with_default_record_fn("timestamp", |_record| {
188    ///          Utc::now().to_rfc3339().to_string()
189    ///         });
190    /// logger.init(LevelFilter::Info);
191    ///
192    /// info!("Hello"); // The "timestamp" field will contain an RFC 3339 timestamp
193    /// ```
194    #[must_use]
195    pub fn with_default_record_fn<V: Into<LogValue>>(
196        mut self,
197        key: impl Into<Cow<'static, str>>,
198        f: impl Fn(&log::Record) -> V + Send + Sync + 'static,
199    ) -> Self {
200        self.dynamic_default_records
201            .insert(key.into(), Box::new(move |record| f(record).into()));
202        self
203    }
204}
205
206impl std::fmt::Debug for ContextLogger {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        f.debug_struct("ContextLogger").finish_non_exhaustive()
209    }
210}
211
212impl log::Log for ContextLogger {
213    fn enabled(&self, metadata: &log::Metadata) -> bool {
214        self.inner.enabled(metadata)
215    }
216
217    fn log(&self, record: &log::Record) {
218        if !self.enabled(record.metadata()) {
219            return;
220        }
221
222        let error = scope::stack::SCOPE_STACK.try_with(|stack| {
223            let dynamic_default_records = self
224                .dynamic_default_records
225                .iter()
226                .map(|(key, f)| (key, f(record)))
227                .collect::<Vec<_>>();
228            let default_records = self
229                .default_records
230                .iter()
231                .chain(dynamic_default_records.iter().map(|(k, v)| (*k, v)));
232
233            // Only the top frame is read here intentionally: inherited records from
234            // outer scopes are copied into each newly entered frame on `enter()`,
235            // so the top frame always contains a complete, flat view of active records.
236            if let Some(top) = stack.top() {
237                self.inner.log(
238                    &record
239                        .to_builder()
240                        .key_values(&SourceWithRecords {
241                            source: &record.key_values(),
242                            records: default_records.chain(top.records()),
243                        })
244                        .build(),
245                );
246            } else {
247                self.inner.log(
248                    &record
249                        .to_builder()
250                        .key_values(&SourceWithRecords {
251                            source: &record.key_values(),
252                            records: default_records,
253                        })
254                        .build(),
255                );
256            }
257        });
258
259        if let Err(err) = error {
260            // If the context stack is not available, log the original record.
261            self.inner.log(record);
262            // We can't use `log::error!` here because we are in the middle of logging and
263            // this invocation becomes recursive.
264            eprintln!("Error accessing context stack: {err}");
265        }
266    }
267
268    fn flush(&self) {
269        self.inner.flush();
270    }
271}
272
273struct SourceWithRecords<'a, I> {
274    source: &'a dyn log::kv::Source,
275    records: I,
276}
277
278impl<'a, I> log::kv::Source for SourceWithRecords<'a, I>
279where
280    I: Iterator<Item = LogRecordRef<'a>> + Clone,
281{
282    fn visit<'kvs>(
283        &'kvs self,
284        visitor: &mut dyn log::kv::VisitSource<'kvs>,
285    ) -> Result<(), log::kv::Error> {
286        for (key, value) in self.records.clone() {
287            visitor.visit_pair(log::kv::Key::from_str(key), value.as_log_value())?;
288        }
289        self.source.visit(visitor)
290    }
291}
292
293mod private {
294    pub trait Sealed {}
295
296    impl<F: Future> Sealed for F {}
297    impl Sealed for crate::LogContext {}
298}