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