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