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