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}