Skip to main content

context_logger/
lib.rs

1//! # Overview
2#![doc = include_utils::include_md!("README.md:description")]
3//!
4//! ## How it works
5//!
6//! When a log record flows through `ContextLogger`, fields are resolved in this
7//! order:
8//!
9//! 1. Static default fields (e.g. `service`, `version`)
10//! 2. Computed default fields (e.g. `timestamp`, `level`)
11//! 3. Inherited fields from all parent scopes
12//! 4. Local fields of the active scope
13//!
14//! *These fields are merged into the [`log::Record`]'s key-value store. The
15//! exact behavior for duplicate keys depends on the underlying logger
16//! implementation — in most implementations, later fields with the same key
17//! replace earlier ones.*
18//!
19//! ## Concepts
20//!
21//! The `log` crate models structured logging through a [`log::kv::Source`] — a
22//! push-based iterator over key-value pairs attached to a [`log::Record`].
23//! Context logger adds an additional layer that injects scoped fields into that
24//! source.
25//!
26//! - **[`log::Record`]** — the log entry produced by `log::info!`, `log::warn!`, etc. Each record
27//!   carries a [`log::Record::key_values`] source that consumers visit to extract structured
28//!   attributes.
29//! - **Field** — a key-value pair ([`&str`], [`log::kv::Value`]) attached to a log record's
30//!   `Source`. Fields carry scoped context like `request_id` or `user_id`.
31//! - **[`LogFields`]** — a collection of fields that implements the extension logic for the log
32//!   record's source. When the record flows through [`ContextLogger`], its fields are merged into
33//!   the record's own `Source`.
34//! - **[`LogContext`]** — the blueprint for fields. It splits fields into
35//!   [`local`](LogContext::local) and [`inherited`](LogContext::inherited) categories with
36//!   different propagation semantics.
37//! - **[`LogScope`] guard** — activates a `LogContext`, pushing its fields onto the thread-local
38//!   scope stack. Fields are resolved when a log record flows through the logger.
39//!
40//! The scope stack is thread-local: each thread maintains its own independent
41//! stack ensuring thread-safety without expensive synchronization.
42//!
43//! ## Compatibility
44//!
45//! `ContextLogger` wraps any type implementing [`log::Log`]. For structured
46//! key-value output, pair it with a logger that supports the `kv` feature (e.g.
47//! [`env_logger`] with `features = ["kv"]` or  [`log4rs`]).
48//!
49//! ## Basic example
50#![doc = include_utils::include_md!("README.md:basic_example")]
51//!
52//! ## Async Context Propagation
53#![doc = include_utils::include_md!("README.md:async_example")]
54//!
55//! [`env_logger`]: https://docs.rs/env_logger/latest/env_logger
56//! [`log4rs`]: https://docs.rs/log4rs/latest/log4rs
57
58use std::{borrow::Cow, collections::HashMap};
59
60use crate::fields::LogFieldRef;
61
62mod context;
63mod fields;
64pub mod future;
65mod scope;
66mod value;
67
68type LogValueFn = Box<dyn Fn(&log::Record) -> LogValue + Send + Sync>;
69
70pub use self::{
71    context::LogContext,
72    fields::LogFields,
73    future::FutureExt,
74    scope::{LogContextExt, LogScope},
75    value::LogValue,
76};
77
78/// A logger wrapper that enhances [`log::Record`] with scoped fields.
79///
80/// `ContextLogger` wraps an existing logging implementation and merges
81/// additional fields from the current scope stack into each [`log::Record`].
82/// These fields are taken from the scope stack managed by [`LogScope`].
83///
84/// See the [crate-level docs](index.html) for an overview and examples.
85///
86/// See [`LogContext`] for more information on how to create and manage scope
87/// fields.
88pub struct ContextLogger {
89    inner: Box<dyn log::Log>,
90    default_fields: LogFields,
91    dynamic_default_fields: HashMap<Cow<'static, str>, LogValueFn>,
92}
93
94impl ContextLogger {
95    /// Creates a new [`ContextLogger`] that wraps the given logging
96    /// implementation.
97    ///
98    /// The inner logger will receive log records enhanced with scope fields
99    /// from the current scope stack.
100    pub fn new<L>(inner: L) -> Self
101    where
102        L: log::Log + 'static,
103    {
104        Self {
105            inner: Box::new(inner),
106            default_fields: LogFields::new(),
107            dynamic_default_fields: HashMap::new(),
108        }
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
114    /// events that occur before initialization will be ignored.
115    ///
116    /// # Panics
117    ///
118    /// Panics if a logger has already been set.
119    pub fn init(self, max_level: log::LevelFilter) {
120        self.try_init(max_level)
121            .expect("ContextLogger::init should not be called after logger initialization");
122    }
123
124    /// Initializes the global logger with the context logger.
125    ///
126    /// This should be called early in the execution of a Rust program. Any log
127    /// events that occur before initialization will be ignored.
128    ///
129    /// # Errors
130    ///
131    /// Returns an error if a logger has already been set.
132    pub fn try_init(self, max_level: log::LevelFilter) -> Result<(), log::SetLoggerError> {
133        log::set_max_level(max_level);
134        log::set_boxed_logger(Box::new(self))
135    }
136
137    /// Adds a default field that will be included in every [`log::Record`].
138    ///
139    /// Default fields are automatically merged into each log record, regardless
140    /// of the current context. They are defined when the logger is created
141    /// and remain constant throughout the application's lifetime.
142    ///
143    /// # Behavior with Duplicate Keys
144    ///
145    /// When logging, default fields are added first, followed by fields from
146    /// the current context. If multiple fields with the same key exist, the
147    /// behavior depends on the underlying logger implementation. In most
148    /// implementations, later fields with the same key will typically
149    /// replace earlier ones.
150    ///
151    /// # Example
152    ///
153    /// ```
154    /// use log::{info, LevelFilter};
155    /// use context_logger::{ContextLogger, LogContext, LogScope};
156    ///
157    /// // Create a logger with default fields
158    /// let logger = ContextLogger::new(env_logger::builder()
159    ///       .filter_level(log::LevelFilter::Info)
160    ///       .build())
161    ///       .with_default_field("service", "api")
162    ///     .with_default_field("version", "1.0.0");
163    /// // Initialize it
164    /// logger.init(LevelFilter::Info);
165    ///
166    /// info!("Processing request"); // Will include service="api", version="1.0.0"
167    /// ```
168    #[must_use]
169    pub fn with_default_field(
170        mut self,
171        key: impl Into<Cow<'static, str>>,
172        value: impl Into<LogValue>,
173    ) -> Self {
174        self.default_fields.insert(key, value);
175        self
176    }
177
178    /// Adds a dynamic default field computed by the given closure for every
179    /// [`log::Record`].
180    ///
181    /// Like [`Self::with_default_field`], the field is merged into each log
182    /// record. However, unlike the static variant, the value is *computed
183    /// at log time* by invoking the provided closure with the current
184    /// [`log::Record`] itself. This makes it suitable for fields whose
185    /// values are not known upfront, such as timestamps or thread IDs.
186    ///
187    /// **Note!** *The order in which dynamic default field functions are
188    /// evaluated is not guaranteed.*
189    ///
190    /// # Example
191    ///
192    /// Adding a current timestamp.
193    ///
194    /// ```
195    /// use chrono::Utc;
196    /// use log::{info, LevelFilter};
197    /// use context_logger::{ContextLogger, LogValue};
198    ///
199    /// let logger = ContextLogger::new(env_logger::builder()
200    ///       .filter_level(log::LevelFilter::Info)
201    ///       .build())
202    ///       .with_default_field_fn("timestamp", |_record| {
203    ///          Utc::now().to_rfc3339().to_string()
204    ///     });
205    /// logger.init(LevelFilter::Info);
206    ///
207    /// info!("Hello");  // Will include timestamp="..."
208    /// ```
209    #[must_use]
210    pub fn with_default_field_fn<V: Into<LogValue>>(
211        mut self,
212        key: impl Into<Cow<'static, str>>,
213        f: impl Fn(&log::Record) -> V + Send + Sync + 'static,
214    ) -> Self {
215        self.dynamic_default_fields
216            .insert(key.into(), Box::new(move |record| f(record).into()));
217        self
218    }
219}
220
221impl std::fmt::Debug for ContextLogger {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        f.debug_struct("ContextLogger").finish_non_exhaustive()
224    }
225}
226
227impl log::Log for ContextLogger {
228    fn enabled(&self, metadata: &log::Metadata) -> bool {
229        self.inner.enabled(metadata)
230    }
231
232    fn log(&self, record: &log::Record) {
233        if !self.enabled(record.metadata()) {
234            return;
235        }
236
237        let error = scope::stack::SCOPE_STACK.try_with(|stack| {
238            let dynamic_default_fields = self
239                .dynamic_default_fields
240                .iter()
241                .map(|(key, f)| (key, f(record)))
242                .collect::<Vec<_>>();
243
244            let default_fields = self
245                .default_fields
246                .iter()
247                .chain(dynamic_default_fields.iter().map(|(k, v)| (*k, v)));
248
249            // Only the top frame is read here intentionally: inherited fields from
250            // outer scopes are copied into each newly entered frame on `enter()`,
251            // so the top frame always contains a complete, flat view of active fields.
252            if let Some(top) = stack.top() {
253                self.inner.log(
254                    &record
255                        .to_builder()
256                        .key_values(&SourceWithFields {
257                            source: &record.key_values(),
258                            fields: default_fields.chain(top.fields()),
259                        })
260                        .build(),
261                );
262            } else {
263                self.inner.log(
264                    &record
265                        .to_builder()
266                        .key_values(&SourceWithFields {
267                            source: &record.key_values(),
268                            fields: default_fields,
269                        })
270                        .build(),
271                );
272            }
273        });
274
275        if let Err(err) = error {
276            // If the context stack is not available, log the original record.
277            self.inner.log(record);
278            // We can't use `log::error!` here because we are in the middle of logging and
279            // this invocation becomes recursive.
280            eprintln!("Error accessing context stack: {err}");
281        }
282    }
283
284    fn flush(&self) {
285        self.inner.flush();
286    }
287}
288
289struct SourceWithFields<'a, I> {
290    source: &'a dyn log::kv::Source,
291    fields: I,
292}
293
294impl<'a, I> log::kv::Source for SourceWithFields<'a, I>
295where
296    I: Iterator<Item = LogFieldRef<'a>> + Clone,
297{
298    fn visit<'kvs>(
299        &'kvs self,
300        visitor: &mut dyn log::kv::VisitSource<'kvs>,
301    ) -> Result<(), log::kv::Error> {
302        for (key, value) in self.fields.clone() {
303            visitor.visit_pair(log::kv::Key::from_str(key), value.as_log_value())?;
304        }
305        self.source.visit(visitor)
306    }
307}
308
309mod private {
310    pub trait Sealed {}
311
312    impl<F: Future> Sealed for F {}
313    impl Sealed for crate::LogContext {}
314}