Skip to main content

fastapi_core/
logging.rs

1//! Structured logging infrastructure for fastapi_rust.
2//!
3//! This module provides structured logging designed to integrate with
4//! asupersync observability APIs and automatically propagate request context.
5//!
6//! # Design Principles
7//!
8//! 1. **Context propagation**: Log macros auto-inject request_id, region_id, task_id
9//! 2. **Structured output**: All logs are JSON-formatted for production
10//! 3. **Span-based timing**: Instrument operations with hierarchical spans
11//! 4. **asupersync integration**: Designed to integrate with asupersync observability APIs
12//! 5. **Zero-allocation fast path**: Critical paths avoid heap allocation
13//!
14//! # Usage
15//!
16//! ## Basic Logging
17//!
18//! ```ignore
19//! use fastapi_core::logging::*;
20//!
21//! async fn handler(ctx: &RequestContext) -> impl IntoResponse {
22//!     log_info!(ctx, "Processing request");
23//!     log_debug!(ctx, "Request path: {}", ctx.request().path());
24//!
25//!     // With structured fields
26//!     log_info!(ctx, "User authenticated",
27//!         user_id => user.id,
28//!         role => user.role
29//!     );
30//!
31//!     "ok"
32//! }
33//! ```
34//!
35//! ## Timing Spans
36//!
37//! ```ignore
38//! use fastapi_core::logging::*;
39//!
40//! async fn handler(ctx: &RequestContext) -> impl IntoResponse {
41//!     let span = ctx.span("database_query");
42//!     let result = db.query("SELECT ...").await?;
43//!     span.end(); // Logs duration
44//!
45//!     // Or with auto-end on drop
46//!     {
47//!         let _span = ctx.span_auto("serialize");
48//!         serde_json::to_string(&result)?
49//!     } // Span ends here
50//! }
51//! ```
52//!
53//! # JSON Output Schema
54//!
55//! ```json
56//! {
57//!     "timestamp": "2024-01-17T10:30:45.123456789Z",
58//!     "level": "info",
59//!     "message": "User authenticated",
60//!     "request_id": 12345,
61//!     "region_id": 1,
62//!     "task_id": 42,
63//!     "target": "my_app::handlers::auth",
64//!     "fields": {
65//!         "user_id": 67890,
66//!         "role": "admin"
67//!     }
68//! }
69//! ```
70//!
71//! # Configuration
72//!
73//! Logging is configured via `LogConfig`:
74//!
75//! ```ignore
76//! use fastapi_core::logging::{LogConfig, LogLevel};
77//!
78//! let config = LogConfig::new()
79//!     .level(LogLevel::Debug)          // Minimum level to emit
80//!     .json_output(true)               // JSON format (false = compact)
81//!     .include_target(true)            // Include module path
82//!     .max_fields(16);                 // Max structured fields per log
83//!
84//! let app = App::new()
85//!     .with_logging(config);
86//! ```
87
88use std::fmt;
89use std::sync::atomic::{AtomicU64, Ordering};
90use std::time::Instant;
91
92use crate::context::RequestContext;
93
94// These types mirror an intended asupersync observability surface. The current
95// implementation provides a minimal built-in sink (stderr) and can be wired to
96// asupersync observability when that API is available.
97
98/// Log levels matching asupersync's observability module.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
100pub enum LogLevel {
101    /// Most verbose, for detailed debugging.
102    Trace = 0,
103    /// Debug information, not shown in production.
104    Debug = 1,
105    /// General information about normal operation.
106    Info = 2,
107    /// Something unexpected but recoverable.
108    Warn = 3,
109    /// An error that affected request processing.
110    Error = 4,
111}
112
113impl LogLevel {
114    /// Returns the level as a lowercase string.
115    #[must_use]
116    pub const fn as_str(&self) -> &'static str {
117        match self {
118            Self::Trace => "trace",
119            Self::Debug => "debug",
120            Self::Info => "info",
121            Self::Warn => "warn",
122            Self::Error => "error",
123        }
124    }
125
126    /// Returns a single character representation.
127    #[must_use]
128    pub const fn as_char(&self) -> char {
129        match self {
130            Self::Trace => 'T',
131            Self::Debug => 'D',
132            Self::Info => 'I',
133            Self::Warn => 'W',
134            Self::Error => 'E',
135        }
136    }
137}
138
139impl fmt::Display for LogLevel {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        write!(f, "{}", self.as_str())
142    }
143}
144
145/// A structured log entry with context.
146///
147/// Logs are created via macros that auto-inject request context,
148/// then emitted through the configured log sink.
149#[derive(Debug)]
150pub struct LogEntry {
151    /// The log level.
152    pub level: LogLevel,
153    /// The log message.
154    pub message: String,
155    /// Unique request identifier.
156    pub request_id: u64,
157    /// asupersync region ID (formatted as string for serialization).
158    pub region_id: String,
159    /// asupersync task ID (formatted as string for serialization).
160    pub task_id: String,
161    /// Module/target path (optional).
162    pub target: Option<String>,
163    /// Structured key-value fields (max 16).
164    pub fields: Vec<(String, String)>,
165    /// Nanosecond timestamp from asupersync's virtual time.
166    pub timestamp_ns: u64,
167}
168
169impl LogEntry {
170    /// Creates a new log entry with context from RequestContext.
171    #[must_use]
172    pub fn new(ctx: &RequestContext, level: LogLevel, message: impl Into<String>) -> Self {
173        Self {
174            level,
175            message: message.into(),
176            request_id: ctx.request_id(),
177            region_id: format!("{:?}", ctx.region_id()),
178            task_id: format!("{:?}", ctx.task_id()),
179            target: None,
180            fields: Vec::new(),
181            timestamp_ns: 0, // Will be set by asupersync's virtual time
182        }
183    }
184
185    /// Sets the target module path.
186    #[must_use]
187    pub fn target(mut self, target: impl Into<String>) -> Self {
188        self.target = Some(target.into());
189        self
190    }
191
192    /// Adds a structured field.
193    ///
194    /// Fields beyond the max (16) are silently dropped.
195    #[must_use]
196    pub fn field(mut self, key: impl Into<String>, value: impl fmt::Display) -> Self {
197        if self.fields.len() < 16 {
198            self.fields.push((key.into(), value.to_string()));
199        }
200        self
201    }
202
203    /// Formats the log entry as JSON.
204    #[must_use]
205    pub fn to_json(&self) -> String {
206        let mut json = format!(
207            r#"{{"timestamp_ns":{},"level":"{}","message":"{}","request_id":{},"region_id":"{}","task_id":"{}""#,
208            self.timestamp_ns,
209            self.level,
210            escape_json(&self.message),
211            self.request_id,
212            escape_json(&self.region_id),
213            escape_json(&self.task_id)
214        );
215
216        if let Some(ref target) = self.target {
217            json.push_str(&format!(r#","target":"{}""#, escape_json(target)));
218        }
219
220        if !self.fields.is_empty() {
221            json.push_str(r#","fields":{"#);
222            for (i, (k, v)) in self.fields.iter().enumerate() {
223                if i > 0 {
224                    json.push(',');
225                }
226                json.push_str(&format!(r#""{}":"{}""#, escape_json(k), escape_json(v)));
227            }
228            json.push('}');
229        }
230
231        json.push('}');
232        json
233    }
234
235    /// Formats the log entry in compact format.
236    #[must_use]
237    pub fn to_compact(&self) -> String {
238        let mut output = format!(
239            "[{}] req={} {}",
240            self.level.as_char(),
241            self.request_id,
242            self.message
243        );
244
245        if !self.fields.is_empty() {
246            output.push_str(" {");
247            for (i, (k, v)) in self.fields.iter().enumerate() {
248                if i > 0 {
249                    output.push_str(", ");
250                }
251                output.push_str(&format!("{k}={v}"));
252            }
253            output.push('}');
254        }
255
256        output
257    }
258}
259
260/// Escapes a string for JSON output.
261fn escape_json(s: &str) -> String {
262    s.replace('\\', "\\\\")
263        .replace('"', "\\\"")
264        .replace('\n', "\\n")
265        .replace('\r', "\\r")
266        .replace('\t', "\\t")
267}
268
269/// A timing span for instrumentation.
270///
271/// Spans track the duration of operations and can be nested hierarchically.
272/// They integrate with asupersync's DiagnosticContext for distributed tracing.
273pub struct Span {
274    name: String,
275    request_id: u64,
276    start: Instant,
277    span_id: u64,
278    parent_id: Option<u64>,
279    ended: bool,
280}
281
282impl Span {
283    /// Creates a new span.
284    #[must_use]
285    pub fn new(ctx: &RequestContext, name: impl Into<String>) -> Self {
286        static SPAN_COUNTER: AtomicU64 = AtomicU64::new(1);
287
288        Self {
289            name: name.into(),
290            request_id: ctx.request_id(),
291            start: Instant::now(),
292            span_id: SPAN_COUNTER.fetch_add(1, Ordering::SeqCst),
293            parent_id: None,
294            ended: false,
295        }
296    }
297
298    /// Creates a child span under this span.
299    #[must_use]
300    pub fn child(&self, ctx: &RequestContext, name: impl Into<String>) -> Self {
301        static SPAN_COUNTER: AtomicU64 = AtomicU64::new(1);
302
303        Self {
304            name: name.into(),
305            request_id: ctx.request_id(),
306            start: Instant::now(),
307            span_id: SPAN_COUNTER.fetch_add(1, Ordering::SeqCst),
308            parent_id: Some(self.span_id),
309            ended: false,
310        }
311    }
312
313    /// Returns the span name.
314    #[must_use]
315    pub fn name(&self) -> &str {
316        &self.name
317    }
318
319    /// Returns the span ID.
320    #[must_use]
321    pub fn span_id(&self) -> u64 {
322        self.span_id
323    }
324
325    /// Returns the parent span ID if this is a child span.
326    #[must_use]
327    pub fn parent_id(&self) -> Option<u64> {
328        self.parent_id
329    }
330
331    /// Returns the elapsed duration since the span started.
332    #[must_use]
333    pub fn elapsed(&self) -> std::time::Duration {
334        self.start.elapsed()
335    }
336
337    /// Ends the span and returns the duration.
338    ///
339    /// Call this to explicitly end the span and log its duration.
340    /// If not called, the span will be ended when dropped.
341    pub fn end(&mut self) -> std::time::Duration {
342        let duration = self.elapsed();
343        if !self.ended {
344            self.ended = true;
345        }
346        duration
347    }
348
349    /// Returns a JSON representation of the span timing.
350    #[must_use]
351    pub fn to_json(&self) -> String {
352        let duration = self.elapsed();
353        let mut json = format!(
354            r#"{{"span_id":{},"name":"{}","request_id":{},"duration_us":{}"#,
355            self.span_id,
356            escape_json(&self.name),
357            self.request_id,
358            duration.as_micros()
359        );
360
361        if let Some(parent) = self.parent_id {
362            json.push_str(&format!(r#","parent_id":{parent}"#));
363        }
364
365        json.push('}');
366        json
367    }
368}
369
370impl Drop for Span {
371    fn drop(&mut self) {
372        if !self.ended {
373            self.end();
374        }
375    }
376}
377
378/// Auto-ending span that logs duration on drop.
379///
380/// Unlike [`Span`], this type automatically logs its duration
381/// when it goes out of scope, making it ideal for RAII-style usage.
382pub struct AutoSpan {
383    inner: Span,
384    ctx: RequestContext,
385    config: LogConfig,
386}
387
388impl AutoSpan {
389    /// Creates a new auto-ending span using the default [`LogConfig`].
390    ///
391    /// The span end event is emitted at DEBUG level, so it will only be visible
392    /// if DEBUG logging is enabled.
393    #[must_use]
394    pub fn new(ctx: &RequestContext, name: impl Into<String>) -> Self {
395        Self::new_with_config(ctx, LogConfig::default(), name)
396    }
397
398    /// Creates a new auto-ending span using an explicit [`LogConfig`].
399    ///
400    /// The span end event is emitted at DEBUG level, so it will only be visible
401    /// if DEBUG logging is enabled.
402    #[must_use]
403    pub fn new_with_config(
404        ctx: &RequestContext,
405        config: LogConfig,
406        name: impl Into<String>,
407    ) -> Self {
408        Self {
409            inner: Span::new(ctx, name),
410            ctx: ctx.clone(),
411            config,
412        }
413    }
414}
415
416impl Drop for AutoSpan {
417    fn drop(&mut self) {
418        let duration = self.inner.end();
419        let logger = RequestLogger::new(&self.ctx, self.config.clone());
420        logger.debug_with_fields("Span ended", |e| {
421            let mut e = e.target(module_path!());
422            e = e.field("span", self.inner.name());
423            e = e.field("span_id", self.inner.span_id());
424            if let Some(parent_id) = self.inner.parent_id() {
425                e = e.field("parent_id", parent_id);
426            }
427            e.field("duration_us", duration.as_micros())
428        });
429    }
430}
431
432/// Configuration for the logging system.
433#[derive(Debug, Clone)]
434pub struct LogConfig {
435    /// Minimum log level to emit.
436    pub min_level: LogLevel,
437    /// Whether to output JSON (true) or compact format (false).
438    pub json_output: bool,
439    /// Whether to include the target module path.
440    pub include_target: bool,
441    /// Maximum number of structured fields per log entry.
442    pub max_fields: usize,
443}
444
445impl Default for LogConfig {
446    fn default() -> Self {
447        Self {
448            min_level: LogLevel::Info,
449            json_output: true,
450            include_target: true,
451            max_fields: 16,
452        }
453    }
454}
455
456impl LogConfig {
457    /// Creates a new configuration with defaults.
458    #[must_use]
459    pub fn new() -> Self {
460        Self::default()
461    }
462
463    /// Sets the minimum log level.
464    #[must_use]
465    pub fn level(mut self, level: LogLevel) -> Self {
466        self.min_level = level;
467        self
468    }
469
470    /// Sets whether to output JSON format.
471    #[must_use]
472    pub fn json_output(mut self, json: bool) -> Self {
473        self.json_output = json;
474        self
475    }
476
477    /// Sets whether to include the target module path.
478    #[must_use]
479    pub fn include_target(mut self, include: bool) -> Self {
480        self.include_target = include;
481        self
482    }
483
484    /// Sets the maximum structured fields per log.
485    #[must_use]
486    pub fn max_fields(mut self, max: usize) -> Self {
487        self.max_fields = max;
488        self
489    }
490
491    /// Returns a development configuration (verbose, compact output).
492    #[must_use]
493    pub fn development() -> Self {
494        Self {
495            min_level: LogLevel::Debug,
496            json_output: false,
497            include_target: true,
498            max_fields: 16,
499        }
500    }
501
502    /// Returns a production configuration (info+, JSON output).
503    #[must_use]
504    pub fn production() -> Self {
505        Self {
506            min_level: LogLevel::Info,
507            json_output: true,
508            include_target: true,
509            max_fields: 16,
510        }
511    }
512
513    /// Returns a testing configuration (trace level, JSON output).
514    #[must_use]
515    pub fn testing() -> Self {
516        Self {
517            min_level: LogLevel::Trace,
518            json_output: true,
519            include_target: true,
520            max_fields: 16,
521        }
522    }
523}
524
525// ============================================================================
526// Request Logger
527// ============================================================================
528
529use std::sync::atomic::AtomicUsize;
530
531/// Global log level for fast level checks.
532///
533/// This allows macros to skip log construction entirely when
534/// the level is below the configured minimum (zero overhead).
535static GLOBAL_LOG_LEVEL: AtomicUsize = AtomicUsize::new(LogLevel::Info as usize);
536
537/// Returns the current global log level.
538#[inline]
539#[must_use]
540pub fn global_log_level() -> LogLevel {
541    match GLOBAL_LOG_LEVEL.load(Ordering::Relaxed) {
542        0 => LogLevel::Trace,
543        1 => LogLevel::Debug,
544        2 => LogLevel::Info,
545        3 => LogLevel::Warn,
546        _ => LogLevel::Error,
547    }
548}
549
550/// Sets the global log level.
551///
552/// This affects all future log macro calls.
553pub fn set_global_log_level(level: LogLevel) {
554    GLOBAL_LOG_LEVEL.store(level as usize, Ordering::Relaxed);
555}
556
557/// Returns true if the given level is enabled.
558#[inline]
559#[must_use]
560pub fn level_enabled(level: LogLevel) -> bool {
561    level >= global_log_level()
562}
563
564/// A per-request logger that captures context and emits logs.
565///
566/// This struct is typically created once per request and provides
567/// logging methods that automatically include request context.
568///
569/// # Example
570///
571/// ```ignore
572/// use fastapi_core::logging::RequestLogger;
573///
574/// async fn handler(ctx: &RequestContext) -> impl IntoResponse {
575///     let logger = RequestLogger::new(ctx, LogConfig::production());
576///
577///     logger.info("Request started");
578///     logger.debug_with_fields("Processing", |e| e.field("path", ctx.path()));
579///
580///     "ok"
581/// }
582/// ```
583pub struct RequestLogger<'a> {
584    ctx: &'a RequestContext,
585    config: LogConfig,
586}
587
588impl<'a> RequestLogger<'a> {
589    /// Creates a new request logger.
590    #[must_use]
591    pub fn new(ctx: &'a RequestContext, config: LogConfig) -> Self {
592        Self { ctx, config }
593    }
594
595    /// Returns true if the given log level is enabled.
596    #[inline]
597    #[must_use]
598    pub fn is_enabled(&self, level: LogLevel) -> bool {
599        level >= self.config.min_level && level_enabled(level)
600    }
601
602    /// Emits a log entry if the level is enabled.
603    pub fn emit(&self, entry: LogEntry) {
604        if !self.is_enabled(entry.level) {
605            return;
606        }
607
608        let output = if self.config.json_output {
609            entry.to_json()
610        } else {
611            entry.to_compact()
612        };
613
614        // Current default sink: print to stderr. A configurable sink/export path
615        // can be added without changing call sites.
616        eprintln!("{output}");
617    }
618
619    /// Logs a message at TRACE level.
620    pub fn trace(&self, message: impl Into<String>) {
621        if self.is_enabled(LogLevel::Trace) {
622            self.emit(LogEntry::new(self.ctx, LogLevel::Trace, message));
623        }
624    }
625
626    /// Logs a message at DEBUG level.
627    pub fn debug(&self, message: impl Into<String>) {
628        if self.is_enabled(LogLevel::Debug) {
629            self.emit(LogEntry::new(self.ctx, LogLevel::Debug, message));
630        }
631    }
632
633    /// Logs a message at INFO level.
634    pub fn info(&self, message: impl Into<String>) {
635        if self.is_enabled(LogLevel::Info) {
636            self.emit(LogEntry::new(self.ctx, LogLevel::Info, message));
637        }
638    }
639
640    /// Logs a message at WARN level.
641    pub fn warn(&self, message: impl Into<String>) {
642        if self.is_enabled(LogLevel::Warn) {
643            self.emit(LogEntry::new(self.ctx, LogLevel::Warn, message));
644        }
645    }
646
647    /// Logs a message at ERROR level.
648    pub fn error(&self, message: impl Into<String>) {
649        if self.is_enabled(LogLevel::Error) {
650            self.emit(LogEntry::new(self.ctx, LogLevel::Error, message));
651        }
652    }
653
654    /// Logs with custom field builder at TRACE level.
655    pub fn trace_with_fields<F>(&self, message: impl Into<String>, f: F)
656    where
657        F: FnOnce(LogEntry) -> LogEntry,
658    {
659        if self.is_enabled(LogLevel::Trace) {
660            let entry = f(LogEntry::new(self.ctx, LogLevel::Trace, message));
661            self.emit(entry);
662        }
663    }
664
665    /// Logs with custom field builder at DEBUG level.
666    pub fn debug_with_fields<F>(&self, message: impl Into<String>, f: F)
667    where
668        F: FnOnce(LogEntry) -> LogEntry,
669    {
670        if self.is_enabled(LogLevel::Debug) {
671            let entry = f(LogEntry::new(self.ctx, LogLevel::Debug, message));
672            self.emit(entry);
673        }
674    }
675
676    /// Logs with custom field builder at INFO level.
677    pub fn info_with_fields<F>(&self, message: impl Into<String>, f: F)
678    where
679        F: FnOnce(LogEntry) -> LogEntry,
680    {
681        if self.is_enabled(LogLevel::Info) {
682            let entry = f(LogEntry::new(self.ctx, LogLevel::Info, message));
683            self.emit(entry);
684        }
685    }
686
687    /// Logs with custom field builder at WARN level.
688    pub fn warn_with_fields<F>(&self, message: impl Into<String>, f: F)
689    where
690        F: FnOnce(LogEntry) -> LogEntry,
691    {
692        if self.is_enabled(LogLevel::Warn) {
693            let entry = f(LogEntry::new(self.ctx, LogLevel::Warn, message));
694            self.emit(entry);
695        }
696    }
697
698    /// Logs with custom field builder at ERROR level.
699    pub fn error_with_fields<F>(&self, message: impl Into<String>, f: F)
700    where
701        F: FnOnce(LogEntry) -> LogEntry,
702    {
703        if self.is_enabled(LogLevel::Error) {
704            let entry = f(LogEntry::new(self.ctx, LogLevel::Error, message));
705            self.emit(entry);
706        }
707    }
708
709    /// Creates a timing span.
710    #[must_use]
711    pub fn span(&self, name: impl Into<String>) -> Span {
712        Span::new(self.ctx, name)
713    }
714
715    /// Creates an auto-ending span.
716    #[must_use]
717    pub fn span_auto(&self, name: impl Into<String>) -> AutoSpan {
718        AutoSpan::new_with_config(self.ctx, self.config.clone(), name)
719    }
720}
721
722// ============================================================================
723// Logging Macros
724// ============================================================================
725
726/// Logs a message at the TRACE level with request context.
727///
728/// Returns a [`LogEntry`] that can be emitted or inspected.
729/// For zero-overhead logging, use [`RequestLogger`].
730///
731/// # Example
732///
733/// ```ignore
734/// log_trace!(ctx, "Entering function");
735/// log_trace!(ctx, "Processing item {}", item_id);
736/// log_trace!(ctx, "With fields", key => value, another => thing);
737/// ```
738#[macro_export]
739macro_rules! log_trace {
740    ($ctx:expr, $msg:expr) => {
741        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Trace, $msg)
742            .target(module_path!())
743    };
744    ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
745        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Trace, $msg)
746            .target(module_path!())
747            $(.field(stringify!($key), $value))+
748    };
749    ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
750        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Trace, format!($fmt, $($arg)*))
751            .target(module_path!())
752    };
753}
754
755/// Logs a message at the DEBUG level with request context.
756#[macro_export]
757macro_rules! log_debug {
758    ($ctx:expr, $msg:expr) => {
759        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Debug, $msg)
760            .target(module_path!())
761    };
762    ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
763        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Debug, $msg)
764            .target(module_path!())
765            $(.field(stringify!($key), $value))+
766    };
767    ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
768        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Debug, format!($fmt, $($arg)*))
769            .target(module_path!())
770    };
771}
772
773/// Logs a message at the INFO level with request context.
774#[macro_export]
775macro_rules! log_info {
776    ($ctx:expr, $msg:expr) => {
777        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Info, $msg)
778            .target(module_path!())
779    };
780    ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
781        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Info, $msg)
782            .target(module_path!())
783            $(.field(stringify!($key), $value))+
784    };
785    ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
786        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Info, format!($fmt, $($arg)*))
787            .target(module_path!())
788    };
789}
790
791/// Logs a message at the WARN level with request context.
792#[macro_export]
793macro_rules! log_warn {
794    ($ctx:expr, $msg:expr) => {
795        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Warn, $msg)
796            .target(module_path!())
797    };
798    ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
799        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Warn, $msg)
800            .target(module_path!())
801            $(.field(stringify!($key), $value))+
802    };
803    ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
804        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Warn, format!($fmt, $($arg)*))
805            .target(module_path!())
806    };
807}
808
809/// Logs a message at the ERROR level with request context.
810#[macro_export]
811macro_rules! log_error {
812    ($ctx:expr, $msg:expr) => {
813        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Error, $msg)
814            .target(module_path!())
815    };
816    ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
817        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Error, $msg)
818            .target(module_path!())
819            $(.field(stringify!($key), $value))+
820    };
821    ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
822        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Error, format!($fmt, $($arg)*))
823            .target(module_path!())
824    };
825}
826
827#[cfg(test)]
828mod tests {
829    use super::*;
830    use asupersync::Cx;
831
832    fn test_context() -> crate::context::RequestContext {
833        let cx = Cx::for_testing();
834        crate::context::RequestContext::new(cx, 12345)
835    }
836
837    #[test]
838    fn log_level_ordering() {
839        assert!(LogLevel::Trace < LogLevel::Debug);
840        assert!(LogLevel::Debug < LogLevel::Info);
841        assert!(LogLevel::Info < LogLevel::Warn);
842        assert!(LogLevel::Warn < LogLevel::Error);
843    }
844
845    #[test]
846    fn log_level_display() {
847        assert_eq!(LogLevel::Info.as_str(), "info");
848        assert_eq!(LogLevel::Error.as_char(), 'E');
849    }
850
851    #[test]
852    fn log_entry_json() {
853        let ctx = test_context();
854        let entry = LogEntry::new(&ctx, LogLevel::Info, "Test message")
855            .target("test::module")
856            .field("user_id", 42)
857            .field("action", "login");
858
859        let json = entry.to_json();
860        assert!(json.contains(r#""level":"info""#));
861        assert!(json.contains(r#""message":"Test message""#));
862        assert!(json.contains(r#""request_id":12345"#));
863        assert!(json.contains(r#""target":"test::module""#));
864        assert!(json.contains(r#""user_id":"42""#));
865        assert!(json.contains(r#""action":"login""#));
866    }
867
868    #[test]
869    fn log_entry_compact() {
870        let ctx = test_context();
871        let entry =
872            LogEntry::new(&ctx, LogLevel::Warn, "Something happened").field("error_code", "E001");
873
874        let compact = entry.to_compact();
875        assert!(compact.starts_with("[W] req=12345"));
876        assert!(compact.contains("Something happened"));
877        assert!(compact.contains("error_code=E001"));
878    }
879
880    #[test]
881    fn span_timing() {
882        let ctx = test_context();
883        let mut span = Span::new(&ctx, "test_operation");
884
885        std::thread::sleep(std::time::Duration::from_millis(1));
886        let duration = span.end();
887
888        assert!(duration.as_micros() >= 1000);
889        assert!(span.ended);
890    }
891
892    #[test]
893    fn span_child() {
894        let ctx = test_context();
895        let parent = Span::new(&ctx, "parent");
896        let child = parent.child(&ctx, "child");
897
898        assert_eq!(child.parent_id(), Some(parent.span_id()));
899    }
900
901    #[test]
902    fn span_json() {
903        let ctx = test_context();
904        let span = Span::new(&ctx, "db_query");
905
906        let json = span.to_json();
907        assert!(json.contains(r#""name":"db_query""#));
908        assert!(json.contains(r#""request_id":12345"#));
909    }
910
911    #[test]
912    fn log_config_presets() {
913        let dev = LogConfig::development();
914        assert_eq!(dev.min_level, LogLevel::Debug);
915        assert!(!dev.json_output);
916
917        let prod = LogConfig::production();
918        assert_eq!(prod.min_level, LogLevel::Info);
919        assert!(prod.json_output);
920
921        let test = LogConfig::testing();
922        assert_eq!(test.min_level, LogLevel::Trace);
923    }
924
925    #[test]
926    fn escape_json_special_chars() {
927        assert_eq!(escape_json("hello\nworld"), "hello\\nworld");
928        assert_eq!(escape_json(r#"say "hi""#), r#"say \"hi\""#);
929        assert_eq!(escape_json("tab\there"), "tab\\there");
930    }
931
932    #[test]
933    fn log_macro_basic() {
934        let ctx = test_context();
935        let entry = log_info!(&ctx, "Basic message");
936        assert_eq!(entry.level, LogLevel::Info);
937        assert_eq!(entry.message, "Basic message");
938        assert_eq!(entry.request_id, 12345);
939    }
940
941    #[test]
942    fn log_macro_with_fields() {
943        let ctx = test_context();
944        let entry = log_info!(&ctx, "With fields", user_id => 42, action => "login");
945        assert_eq!(entry.fields.len(), 2);
946        assert_eq!(entry.fields[0], ("user_id".to_string(), "42".to_string()));
947        assert_eq!(entry.fields[1], ("action".to_string(), "login".to_string()));
948    }
949
950    #[test]
951    fn log_macro_format_string() {
952        let ctx = test_context();
953        let item_id = 99;
954        let entry = log_debug!(&ctx, "Processing item {}", item_id);
955        assert_eq!(entry.level, LogLevel::Debug);
956        assert_eq!(entry.message, "Processing item 99");
957    }
958
959    // =========================================================================
960    // AutoSpan and Span nesting tests (bd-sdrz)
961    // =========================================================================
962
963    #[test]
964    fn autospan_creates_and_ends_on_drop() {
965        let ctx = test_context();
966        let start = std::time::Instant::now();
967
968        {
969            let _span = AutoSpan::new(&ctx, "short_operation");
970            std::thread::sleep(std::time::Duration::from_millis(1));
971            // AutoSpan should end when dropped here
972        }
973
974        let elapsed = start.elapsed();
975        // Should have completed in a reasonable time
976        assert!(elapsed.as_millis() >= 1);
977    }
978
979    #[test]
980    fn autospan_captures_request_id() {
981        let ctx = test_context();
982        let span = AutoSpan::new(&ctx, "test_span");
983
984        // AutoSpan stores a clone of the request context.
985        assert_eq!(span.ctx.request_id(), 12345);
986    }
987
988    #[test]
989    fn span_nested_three_levels_deep() {
990        let ctx = test_context();
991
992        let parent = Span::new(&ctx, "level_1");
993        let child = parent.child(&ctx, "level_2");
994        let grandchild = child.child(&ctx, "level_3");
995
996        // Verify parent-child relationships
997        assert!(parent.parent_id().is_none());
998        assert_eq!(child.parent_id(), Some(parent.span_id()));
999        assert_eq!(grandchild.parent_id(), Some(child.span_id()));
1000
1001        // Verify names
1002        assert_eq!(parent.name(), "level_1");
1003        assert_eq!(child.name(), "level_2");
1004        assert_eq!(grandchild.name(), "level_3");
1005    }
1006
1007    #[test]
1008    fn span_nested_five_levels_deep() {
1009        let ctx = test_context();
1010
1011        let level1 = Span::new(&ctx, "request_handler");
1012        let level2 = level1.child(&ctx, "middleware");
1013        let level3 = level2.child(&ctx, "auth_check");
1014        let level4 = level3.child(&ctx, "token_validation");
1015        let level5 = level4.child(&ctx, "signature_verify");
1016
1017        // Verify the full chain
1018        assert!(level1.parent_id().is_none());
1019        assert_eq!(level2.parent_id(), Some(level1.span_id()));
1020        assert_eq!(level3.parent_id(), Some(level2.span_id()));
1021        assert_eq!(level4.parent_id(), Some(level3.span_id()));
1022        assert_eq!(level5.parent_id(), Some(level4.span_id()));
1023    }
1024
1025    #[test]
1026    fn span_sibling_spans_have_same_parent() {
1027        let ctx = test_context();
1028
1029        let parent = Span::new(&ctx, "parent");
1030        let sibling1 = parent.child(&ctx, "sibling_a");
1031        let sibling2 = parent.child(&ctx, "sibling_b");
1032        let sibling3 = parent.child(&ctx, "sibling_c");
1033
1034        // All siblings share the same parent
1035        assert_eq!(sibling1.parent_id(), Some(parent.span_id()));
1036        assert_eq!(sibling2.parent_id(), Some(parent.span_id()));
1037        assert_eq!(sibling3.parent_id(), Some(parent.span_id()));
1038
1039        // But each has a unique span_id
1040        assert_ne!(sibling1.span_id(), sibling2.span_id());
1041        assert_ne!(sibling2.span_id(), sibling3.span_id());
1042        assert_ne!(sibling1.span_id(), sibling3.span_id());
1043    }
1044
1045    #[test]
1046    fn span_ids_are_globally_unique() {
1047        let ctx = test_context();
1048
1049        let span1 = Span::new(&ctx, "span1");
1050        let span2 = Span::new(&ctx, "span2");
1051        let span3 = Span::new(&ctx, "span3");
1052
1053        assert_ne!(span1.span_id(), span2.span_id());
1054        assert_ne!(span2.span_id(), span3.span_id());
1055        assert_ne!(span1.span_id(), span3.span_id());
1056    }
1057
1058    #[test]
1059    fn span_request_id_propagates_to_children() {
1060        let ctx = test_context();
1061
1062        let parent = Span::new(&ctx, "parent");
1063        let child = parent.child(&ctx, "child");
1064        let grandchild = child.child(&ctx, "grandchild");
1065
1066        // All spans should have the same request_id from context
1067        assert_eq!(parent.request_id, 12345);
1068        assert_eq!(child.request_id, 12345);
1069        assert_eq!(grandchild.request_id, 12345);
1070    }
1071
1072    #[test]
1073    fn span_json_includes_parent_id_when_present() {
1074        let ctx = test_context();
1075
1076        let parent = Span::new(&ctx, "parent");
1077        let child = parent.child(&ctx, "child");
1078
1079        // Parent JSON should not have parent_id
1080        let parent_json = parent.to_json();
1081        assert!(!parent_json.contains("parent_id"));
1082
1083        // Child JSON should include parent_id
1084        let child_json = child.to_json();
1085        assert!(child_json.contains("parent_id"));
1086        assert!(child_json.contains(&parent.span_id().to_string()));
1087    }
1088
1089    #[test]
1090    fn span_elapsed_increases_over_time() {
1091        let ctx = test_context();
1092        let span = Span::new(&ctx, "timing_test");
1093
1094        let elapsed1 = span.elapsed();
1095        std::thread::sleep(std::time::Duration::from_millis(2));
1096        let elapsed2 = span.elapsed();
1097
1098        assert!(elapsed2 > elapsed1);
1099    }
1100
1101    #[test]
1102    fn span_end_returns_final_duration() {
1103        let ctx = test_context();
1104        let mut span = Span::new(&ctx, "duration_test");
1105
1106        std::thread::sleep(std::time::Duration::from_millis(1));
1107        let duration = span.end();
1108
1109        // Duration should be at least 1ms
1110        assert!(duration.as_millis() >= 1);
1111        // Span should be marked as ended
1112        assert!(span.ended);
1113    }
1114
1115    #[test]
1116    fn span_multiple_end_calls_idempotent() {
1117        let ctx = test_context();
1118        let mut span = Span::new(&ctx, "multi_end_test");
1119
1120        std::thread::sleep(std::time::Duration::from_millis(1));
1121        let duration1 = span.end();
1122        let duration2 = span.end();
1123        let duration3 = span.end();
1124
1125        // All calls after first should return similar durations (no panic)
1126        // Note: duration may vary slightly due to elapsed() calls
1127        assert!(duration1.as_micros() > 0);
1128        assert!(duration2.as_micros() > 0);
1129        assert!(duration3.as_micros() > 0);
1130    }
1131
1132    #[test]
1133    fn span_drop_ends_span_automatically() {
1134        let ctx = test_context();
1135
1136        // Create a span and let it drop
1137        {
1138            let span = Span::new(&ctx, "auto_end_test");
1139            assert!(!span.ended);
1140            // Span should be ended when dropped
1141        }
1142        // We can't directly verify ended after drop, but the Drop impl
1143        // should not panic and should mark it ended
1144    }
1145
1146    #[test]
1147    fn nested_spans_timing_accumulates_correctly() {
1148        let ctx = test_context();
1149
1150        let start = std::time::Instant::now();
1151
1152        let mut parent = Span::new(&ctx, "parent");
1153        std::thread::sleep(std::time::Duration::from_millis(2));
1154
1155        {
1156            let mut child = parent.child(&ctx, "child");
1157            std::thread::sleep(std::time::Duration::from_millis(2));
1158            let child_duration = child.end();
1159
1160            // Child duration should be ~2ms
1161            assert!(child_duration.as_millis() >= 2);
1162        }
1163
1164        let parent_duration = parent.end();
1165
1166        // Parent duration should include child time (~4ms total)
1167        assert!(parent_duration.as_millis() >= 4);
1168
1169        let total = start.elapsed();
1170        assert!(total.as_millis() >= 4);
1171    }
1172}