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}