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 self::stack::CONTEXT_STACK;
33pub use self::{context::LogContext, future::FutureExt, value::ContextValue};
34
35mod context;
36pub mod future;
37pub mod guard;
38mod stack;
39mod value;
40
41type StaticCowStr = Cow<'static, str>;
42
43/// A logger wrapper that enhances log records with contextual properties.
44///
45/// `ContextLogger` wraps an existing logging implementation and adds additional
46/// context properties to log records. These context properties are taken from the
47/// current context stack, which is managed by the [`LogContext`] type.
48///
49/// # Example
50///
51/// ```
52/// use log::{info, LevelFilter};
53/// use context_logger::{ContextLogger, LogContext};
54///
55/// // Create a logger.
56/// let env_logger = env_logger::builder().build();
57/// let max_level = env_logger.filter();
58/// // Wrap it with ContextLogger to enable context propagation.
59/// let context_logger = ContextLogger::new(env_logger);
60/// // Initialize the resulting logger.
61/// context_logger.init(max_level);
62///
63/// // Create a context with properties
64/// let ctx = LogContext::new()
65///     .record("request_id", "req-123")
66///     .record("user_id", 42);
67///
68/// // Use the context while logging
69/// let _guard = ctx.enter();
70/// info!("Processing request"); // Will include request_id and user_id properties
71/// ```
72///
73/// See [`LogContext`] for more information on how to create and manage context properties.
74pub struct ContextLogger {
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 context properties
82    /// from the current context stack.
83    pub fn new<L>(inner: L) -> Self
84    where
85        L: log::Log + 'static,
86    {
87        Self {
88            inner: Box::new(inner),
89        }
90    }
91
92    /// Initializes the global logger with the context logger.
93    ///
94    /// This should be called early in the execution of a Rust program. Any log events that occur before initialization will be ignored.
95    ///
96    /// # Panics
97    ///
98    /// Panics if a logger has already been set.
99    pub fn init(self, max_level: log::LevelFilter) {
100        self.try_init(max_level)
101            .expect("ContextLogger::init should not be called after logger initialization");
102    }
103
104    /// Initializes the global logger with the context logger.
105    ///
106    /// This should be called early in the execution of a Rust program. Any log events that occur before initialization will be ignored.
107    ///
108    /// # Errors
109    ///
110    /// Returns an error if a logger has already been set.
111    pub fn try_init(self, max_level: log::LevelFilter) -> Result<(), log::SetLoggerError> {
112        log::set_max_level(max_level);
113        log::set_boxed_logger(Box::new(self))
114    }
115}
116
117impl std::fmt::Debug for ContextLogger {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("ContextLogger").finish_non_exhaustive()
120    }
121}
122
123impl log::Log for ContextLogger {
124    fn enabled(&self, metadata: &log::Metadata) -> bool {
125        self.inner.enabled(metadata)
126    }
127
128    fn log(&self, record: &log::Record) {
129        let error = CONTEXT_STACK.try_with(|stack| {
130            if let Some(top) = stack.top() {
131                let extra_properties = ExtraProperties {
132                    source: &record.key_values(),
133                    properties: &*top,
134                };
135                let new_record = record.to_builder().key_values(&extra_properties).build();
136
137                self.inner.log(&new_record);
138            } else {
139                self.inner.log(record);
140            }
141        });
142
143        if let Err(err) = error {
144            // If the context stack is not available, log the original record.
145            self.inner.log(record);
146            // We can't use `log::error!` here because we are in the middle of logging and
147            // this invocation becomes recursive.
148            eprintln!("Error accessing context stack: {err}");
149        }
150    }
151
152    fn flush(&self) {
153        self.inner.flush();
154    }
155}
156
157struct ExtraProperties<'a, I> {
158    source: &'a dyn log::kv::Source,
159    properties: I,
160}
161
162impl<'a, I> log::kv::Source for ExtraProperties<'a, I>
163where
164    I: IntoIterator<Item = &'a (StaticCowStr, ContextValue)> + Copy,
165{
166    fn visit<'kvs>(
167        &'kvs self,
168        visitor: &mut dyn log::kv::VisitSource<'kvs>,
169    ) -> Result<(), log::kv::Error> {
170        for (key, value) in self.properties {
171            visitor.visit_pair(log::kv::Key::from_str(key), value.as_log_value())?;
172        }
173        self.source.visit(visitor)
174    }
175}