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