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}