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