with_async_context/logger.rs
1//! # Context Logger Implementation
2//!
3//! This module provides a logging implementation that automatically includes the current
4//! async context in log messages. It integrates with the standard `log` crate and allows
5//! customizing the log format and level.
6//!
7//! ## Features
8//!
9//! - Thread-safe logging with context information
10//! - Configurable log format with placeholders for level, context, message, and timestamp
11//! - Log level control via RUST_LOG environment variable
12//! - Implementation of the standard `log::Log` trait
13//!
14//! ## Usage Example
15//!
16//! ```rust,no_run
17//! use with_async_context::{ContextLogger, init_context_logger, with_async_context, from_context_mut};
18//! use log::{info, warn};
19//!
20//! #[derive(Clone)]
21//! struct MyContext {
22//! request_id: String
23//! }
24//!
25//! impl ToString for MyContext {
26//! fn to_string(&self) -> String {
27//! self.request_id.clone()
28//! }
29//! }
30//!
31//! // Initialize the logger with default format
32//! init_context_logger!(MyContext);
33//!
34//! // Or initialize with custom format
35//! // init_context_logger!(MyContext, "[{level}] Request {context}: {message}");
36//!
37//! async fn handle_request() {
38//! info!("Starting request processing");
39//! // Do some work
40//! warn!("Something unexpected happened");
41//! }
42//!
43//! # async fn example() {
44//! // Run function with context
45//! let ctx = MyContext { request_id: "req-123".to_string() };
46//! let (_, _) = with_async_context(ctx, handle_request()).await;
47//!
48//! // Run another request with different context
49//! let ctx2 = MyContext { request_id: "req-456".to_string() };
50//! let (_, _) = with_async_context(ctx2, handle_request()).await;
51//! # }
52//! ```
53
54use std::{env, io::Write, time::SystemTime};
55
56use crate::context_as_string;
57use log::{Level, Log, Metadata, Record};
58
59/// Formats a SystemTime as ISO 8601 timestamp string
60fn format_timestamp() -> String {
61 let now = SystemTime::now();
62 let duration = now.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default();
63 let secs = duration.as_secs();
64
65 // Calculate date/time components from Unix timestamp
66 let days = secs / 86400;
67 let time_of_day = secs % 86400;
68 let hours = time_of_day / 3600;
69 let minutes = (time_of_day % 3600) / 60;
70 let seconds = time_of_day % 60;
71 let millis = duration.subsec_millis();
72
73 // Calculate year, month, day from days since epoch (1970-01-01)
74 let mut year = 1970;
75 let mut remaining_days = days as i64;
76
77 loop {
78 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
79 if remaining_days < days_in_year {
80 break;
81 }
82 remaining_days -= days_in_year;
83 year += 1;
84 }
85
86 let days_in_months: [i64; 12] = if is_leap_year(year) {
87 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
88 } else {
89 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
90 };
91
92 let mut month = 1;
93 for &days_in_month in &days_in_months {
94 if remaining_days < days_in_month {
95 break;
96 }
97 remaining_days -= days_in_month;
98 month += 1;
99 }
100 let day = remaining_days + 1;
101
102 format!(
103 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
104 year, month, day, hours, minutes, seconds, millis
105 )
106}
107
108fn is_leap_year(year: i64) -> bool {
109 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
110}
111
112/// A logger implementation that includes async context information in log messages
113pub struct ContextLogger<C>
114where
115 C: 'static + ToString + std::marker::Send + std::marker::Sync,
116{
117 /// Phantom data to hold the context type parameter
118 pub _phantom: std::marker::PhantomData<C>,
119 /// Optional custom format string for log messages
120 format: Option<String>,
121 /// Current log level threshold
122 level: Option<Level>,
123}
124
125impl<C> ContextLogger<C>
126where
127 C: 'static + ToString + std::marker::Send + std::marker::Sync,
128{
129 /// Creates a new uninitialized context logger
130 pub const fn new() -> Self {
131 Self {
132 _phantom: std::marker::PhantomData,
133 format: None,
134 level: None,
135 }
136 }
137
138 /// Initializes the logger with an optional custom format
139 ///
140 /// # Arguments
141 ///
142 /// * `format` - Optional custom format string with {level}, {context}, {message}, and {timestamp} placeholders
143 pub fn init(&mut self, format: Option<String>) {
144 let level = match env::var("RUST_LOG")
145 .unwrap_or_default()
146 .to_uppercase()
147 .as_str()
148 {
149 "ERROR" => Level::Error,
150 "WARN" => Level::Warn,
151 "INFO" => Level::Info,
152 "DEBUG" => Level::Debug,
153 "TRACE" => Level::Trace,
154 _ => Level::Info,
155 };
156
157 self.format =
158 Some(format.unwrap_or_else(|| String::from("{timestamp} {level} - {context} - {message}")));
159 self.level = Some(level);
160 }
161}
162
163impl<C> Log for ContextLogger<C>
164where
165 C: 'static + ToString + std::marker::Send + std::marker::Sync,
166{
167 /// Checks if a log level is enabled
168 fn enabled(&self, metadata: &Metadata) -> bool {
169 if let Some(level) = self.level {
170 metadata.level() <= level
171 } else {
172 false
173 }
174 }
175
176 /// Logs a message with the current context
177 fn log(&self, record: &Record) {
178 if self.enabled(record.metadata()) {
179 let context = context_as_string::<C>();
180
181 if let Some(format) = &self.format {
182 let msg = format
183 .replace("{timestamp}", &format_timestamp())
184 .replace("{level}", &record.level().to_string())
185 .replace("{context}", &context)
186 .replace("{message}", &record.args().to_string());
187
188 let _ = std::io::stderr().write_all(msg.as_bytes());
189 let _ = std::io::stderr().write_all(b"\n");
190 }
191 }
192 }
193
194 /// Flushes any buffered records
195 fn flush(&self) {}
196}
197
198/// Macro to initialize the context logger
199///
200/// # Arguments
201///
202/// * `$context_type` - The type implementing the context
203/// * `$format` - Optional custom format string
204///
205/// # Examples
206///
207/// ```rust,no_run
208/// use with_async_context::{ContextLogger, init_context_logger, with_async_context, from_context_mut};
209/// use log::{info, warn};
210///
211/// #[derive(Debug)]
212/// struct MyContext {
213/// function_name: String,
214/// context_id: String,
215/// }
216///
217/// impl ToString for MyContext {
218/// fn to_string(&self) -> String {
219/// format!("[{}:{}]", self.function_name, self.context_id)
220/// }
221/// }
222///
223/// // Initialize with default format
224/// init_context_logger!(MyContext);
225///
226/// // Initialize with custom format
227/// // init_context_logger!(MyContext, "{level} [{context}] {message}");
228///
229/// async fn example_function() {
230/// from_context_mut(|ctx: Option<&mut MyContext>| {
231/// if let Some(ctx) = ctx {
232/// ctx.function_name = "example_function".to_string();
233/// }
234/// });
235/// info!("Inside example function");
236/// warn!("Something to warn about");
237/// }
238///
239/// async fn another_function() {
240/// from_context_mut(|ctx: Option<&mut MyContext>| {
241/// if let Some(ctx) = ctx {
242/// ctx.function_name = "another_function".to_string();
243/// }
244/// });
245/// info!("Inside another function");
246/// }
247///
248/// # async fn run_example() {
249/// let ctx = MyContext {
250/// function_name: String::new(),
251/// context_id: "ctx1".to_string()
252/// };
253/// let (_, _) = with_async_context(ctx, example_function()).await;
254///
255/// let ctx2 = MyContext {
256/// function_name: String::new(),
257/// context_id: "ctx2".to_string()
258/// };
259/// let (_, _) = with_async_context(ctx2, another_function()).await;
260/// # }
261/// ```
262#[macro_export]
263macro_rules! init_context_logger {
264 ($context_type:ty) => {{
265 use with_async_context::ContextLogger;
266 static mut LOGGER: ContextLogger<$context_type> = ContextLogger::<$context_type>::new();
267 unsafe {
268 LOGGER.init(None);
269 let max_level = match std::env::var("RUST_LOG")
270 .unwrap_or_default()
271 .to_uppercase()
272 .as_str()
273 {
274 "ERROR" => log::LevelFilter::Error,
275 "WARN" => log::LevelFilter::Warn,
276 "INFO" => log::LevelFilter::Info,
277 "DEBUG" => log::LevelFilter::Debug,
278 "TRACE" => log::LevelFilter::Trace,
279 _ => log::LevelFilter::Info,
280 };
281 let _ = log::set_logger(&LOGGER).map(|()| log::set_max_level(max_level));
282 }
283 }};
284 ($context_type:ty, $format:expr) => {{
285 use with_async_context::ContextLogger;
286 static mut LOGGER: ContextLogger<$context_type> = ContextLogger::<$context_type>::new();
287 unsafe {
288 LOGGER.init(Some($format.to_string()));
289 let max_level = match std::env::var("RUST_LOG")
290 .unwrap_or_default()
291 .to_uppercase()
292 .as_str()
293 {
294 "ERROR" => log::LevelFilter::Error,
295 "WARN" => log::LevelFilter::Warn,
296 "INFO" => log::LevelFilter::Info,
297 "DEBUG" => log::LevelFilter::Debug,
298 "TRACE" => log::LevelFilter::Trace,
299 _ => log::LevelFilter::Info,
300 };
301 let _ = log::set_logger(&LOGGER).map(|()| log::set_max_level(max_level));
302 }
303 }};
304}