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