Skip to main content

fastapi_core/
logging.rs

1//! Structured logging infrastructure for fastapi_rust.
2//!
3//! This module provides structured logging that integrates with asupersync's
4//! observability system and automatically propagates 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**: Delegates to asupersync's observability module
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// Re-export asupersync's log types for convenience
95// In full implementation, these would come from asupersync::observability
96// For now, we define our own compatible types
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            // In full implementation, this would emit a span-end event
346            // to asupersync's DiagnosticContext
347        }
348        duration
349    }
350
351    /// Returns a JSON representation of the span timing.
352    #[must_use]
353    pub fn to_json(&self) -> String {
354        let duration = self.elapsed();
355        let mut json = format!(
356            r#"{{"span_id":{},"name":"{}","request_id":{},"duration_us":{}"#,
357            self.span_id,
358            escape_json(&self.name),
359            self.request_id,
360            duration.as_micros()
361        );
362
363        if let Some(parent) = self.parent_id {
364            json.push_str(&format!(r#","parent_id":{parent}"#));
365        }
366
367        json.push('}');
368        json
369    }
370}
371
372impl Drop for Span {
373    fn drop(&mut self) {
374        if !self.ended {
375            self.end();
376        }
377    }
378}
379
380/// Auto-ending span that logs duration on drop.
381///
382/// Unlike [`Span`], this type automatically logs its duration
383/// when it goes out of scope, making it ideal for RAII-style usage.
384pub struct AutoSpan {
385    inner: Span,
386    ctx_request_id: u64,
387}
388
389impl AutoSpan {
390    /// Creates a new auto-ending span.
391    #[must_use]
392    pub fn new(ctx: &RequestContext, name: impl Into<String>) -> Self {
393        Self {
394            inner: Span::new(ctx, name),
395            ctx_request_id: ctx.request_id(),
396        }
397    }
398}
399
400impl Drop for AutoSpan {
401    fn drop(&mut self) {
402        let duration = self.inner.end();
403        // In full implementation, would emit a log entry like:
404        // log_debug!(ctx, "Span ended", span => self.inner.name(), duration_us => duration.as_micros());
405        let _ = (duration, self.ctx_request_id); // Suppress warnings for now
406    }
407}
408
409/// Configuration for the logging system.
410#[derive(Debug, Clone)]
411pub struct LogConfig {
412    /// Minimum log level to emit.
413    pub min_level: LogLevel,
414    /// Whether to output JSON (true) or compact format (false).
415    pub json_output: bool,
416    /// Whether to include the target module path.
417    pub include_target: bool,
418    /// Maximum number of structured fields per log entry.
419    pub max_fields: usize,
420}
421
422impl Default for LogConfig {
423    fn default() -> Self {
424        Self {
425            min_level: LogLevel::Info,
426            json_output: true,
427            include_target: true,
428            max_fields: 16,
429        }
430    }
431}
432
433impl LogConfig {
434    /// Creates a new configuration with defaults.
435    #[must_use]
436    pub fn new() -> Self {
437        Self::default()
438    }
439
440    /// Sets the minimum log level.
441    #[must_use]
442    pub fn level(mut self, level: LogLevel) -> Self {
443        self.min_level = level;
444        self
445    }
446
447    /// Sets whether to output JSON format.
448    #[must_use]
449    pub fn json_output(mut self, json: bool) -> Self {
450        self.json_output = json;
451        self
452    }
453
454    /// Sets whether to include the target module path.
455    #[must_use]
456    pub fn include_target(mut self, include: bool) -> Self {
457        self.include_target = include;
458        self
459    }
460
461    /// Sets the maximum structured fields per log.
462    #[must_use]
463    pub fn max_fields(mut self, max: usize) -> Self {
464        self.max_fields = max;
465        self
466    }
467
468    /// Returns a development configuration (verbose, compact output).
469    #[must_use]
470    pub fn development() -> Self {
471        Self {
472            min_level: LogLevel::Debug,
473            json_output: false,
474            include_target: true,
475            max_fields: 16,
476        }
477    }
478
479    /// Returns a production configuration (info+, JSON output).
480    #[must_use]
481    pub fn production() -> Self {
482        Self {
483            min_level: LogLevel::Info,
484            json_output: true,
485            include_target: true,
486            max_fields: 16,
487        }
488    }
489
490    /// Returns a testing configuration (trace level, JSON output).
491    #[must_use]
492    pub fn testing() -> Self {
493        Self {
494            min_level: LogLevel::Trace,
495            json_output: true,
496            include_target: true,
497            max_fields: 16,
498        }
499    }
500}
501
502// ============================================================================
503// Request Logger
504// ============================================================================
505
506use std::sync::atomic::AtomicUsize;
507
508/// Global log level for fast level checks.
509///
510/// This allows macros to skip log construction entirely when
511/// the level is below the configured minimum (zero overhead).
512static GLOBAL_LOG_LEVEL: AtomicUsize = AtomicUsize::new(LogLevel::Info as usize);
513
514/// Returns the current global log level.
515#[inline]
516#[must_use]
517pub fn global_log_level() -> LogLevel {
518    match GLOBAL_LOG_LEVEL.load(Ordering::Relaxed) {
519        0 => LogLevel::Trace,
520        1 => LogLevel::Debug,
521        2 => LogLevel::Info,
522        3 => LogLevel::Warn,
523        _ => LogLevel::Error,
524    }
525}
526
527/// Sets the global log level.
528///
529/// This affects all future log macro calls.
530pub fn set_global_log_level(level: LogLevel) {
531    GLOBAL_LOG_LEVEL.store(level as usize, Ordering::Relaxed);
532}
533
534/// Returns true if the given level is enabled.
535#[inline]
536#[must_use]
537pub fn level_enabled(level: LogLevel) -> bool {
538    level >= global_log_level()
539}
540
541/// A per-request logger that captures context and emits logs.
542///
543/// This struct is typically created once per request and provides
544/// logging methods that automatically include request context.
545///
546/// # Example
547///
548/// ```ignore
549/// use fastapi_core::logging::RequestLogger;
550///
551/// async fn handler(ctx: &RequestContext) -> impl IntoResponse {
552///     let logger = RequestLogger::new(ctx, LogConfig::production());
553///
554///     logger.info("Request started");
555///     logger.debug_with_fields("Processing", |e| e.field("path", ctx.path()));
556///
557///     "ok"
558/// }
559/// ```
560pub struct RequestLogger<'a> {
561    ctx: &'a RequestContext,
562    config: LogConfig,
563}
564
565impl<'a> RequestLogger<'a> {
566    /// Creates a new request logger.
567    #[must_use]
568    pub fn new(ctx: &'a RequestContext, config: LogConfig) -> Self {
569        Self { ctx, config }
570    }
571
572    /// Returns true if the given log level is enabled.
573    #[inline]
574    #[must_use]
575    pub fn is_enabled(&self, level: LogLevel) -> bool {
576        level >= self.config.min_level && level_enabled(level)
577    }
578
579    /// Emits a log entry if the level is enabled.
580    pub fn emit(&self, entry: LogEntry) {
581        if !self.is_enabled(entry.level) {
582            return;
583        }
584
585        let output = if self.config.json_output {
586            entry.to_json()
587        } else {
588            entry.to_compact()
589        };
590
591        // In production, this would delegate to asupersync's observability
592        // or a configured log sink. For now, print to stderr.
593        eprintln!("{output}");
594    }
595
596    /// Logs a message at TRACE level.
597    pub fn trace(&self, message: impl Into<String>) {
598        if self.is_enabled(LogLevel::Trace) {
599            self.emit(LogEntry::new(self.ctx, LogLevel::Trace, message));
600        }
601    }
602
603    /// Logs a message at DEBUG level.
604    pub fn debug(&self, message: impl Into<String>) {
605        if self.is_enabled(LogLevel::Debug) {
606            self.emit(LogEntry::new(self.ctx, LogLevel::Debug, message));
607        }
608    }
609
610    /// Logs a message at INFO level.
611    pub fn info(&self, message: impl Into<String>) {
612        if self.is_enabled(LogLevel::Info) {
613            self.emit(LogEntry::new(self.ctx, LogLevel::Info, message));
614        }
615    }
616
617    /// Logs a message at WARN level.
618    pub fn warn(&self, message: impl Into<String>) {
619        if self.is_enabled(LogLevel::Warn) {
620            self.emit(LogEntry::new(self.ctx, LogLevel::Warn, message));
621        }
622    }
623
624    /// Logs a message at ERROR level.
625    pub fn error(&self, message: impl Into<String>) {
626        if self.is_enabled(LogLevel::Error) {
627            self.emit(LogEntry::new(self.ctx, LogLevel::Error, message));
628        }
629    }
630
631    /// Logs with custom field builder at TRACE level.
632    pub fn trace_with_fields<F>(&self, message: impl Into<String>, f: F)
633    where
634        F: FnOnce(LogEntry) -> LogEntry,
635    {
636        if self.is_enabled(LogLevel::Trace) {
637            let entry = f(LogEntry::new(self.ctx, LogLevel::Trace, message));
638            self.emit(entry);
639        }
640    }
641
642    /// Logs with custom field builder at DEBUG level.
643    pub fn debug_with_fields<F>(&self, message: impl Into<String>, f: F)
644    where
645        F: FnOnce(LogEntry) -> LogEntry,
646    {
647        if self.is_enabled(LogLevel::Debug) {
648            let entry = f(LogEntry::new(self.ctx, LogLevel::Debug, message));
649            self.emit(entry);
650        }
651    }
652
653    /// Logs with custom field builder at INFO level.
654    pub fn info_with_fields<F>(&self, message: impl Into<String>, f: F)
655    where
656        F: FnOnce(LogEntry) -> LogEntry,
657    {
658        if self.is_enabled(LogLevel::Info) {
659            let entry = f(LogEntry::new(self.ctx, LogLevel::Info, message));
660            self.emit(entry);
661        }
662    }
663
664    /// Logs with custom field builder at WARN level.
665    pub fn warn_with_fields<F>(&self, message: impl Into<String>, f: F)
666    where
667        F: FnOnce(LogEntry) -> LogEntry,
668    {
669        if self.is_enabled(LogLevel::Warn) {
670            let entry = f(LogEntry::new(self.ctx, LogLevel::Warn, message));
671            self.emit(entry);
672        }
673    }
674
675    /// Logs with custom field builder at ERROR level.
676    pub fn error_with_fields<F>(&self, message: impl Into<String>, f: F)
677    where
678        F: FnOnce(LogEntry) -> LogEntry,
679    {
680        if self.is_enabled(LogLevel::Error) {
681            let entry = f(LogEntry::new(self.ctx, LogLevel::Error, message));
682            self.emit(entry);
683        }
684    }
685
686    /// Creates a timing span.
687    #[must_use]
688    pub fn span(&self, name: impl Into<String>) -> Span {
689        Span::new(self.ctx, name)
690    }
691
692    /// Creates an auto-ending span.
693    #[must_use]
694    pub fn span_auto(&self, name: impl Into<String>) -> AutoSpan {
695        AutoSpan::new(self.ctx, name)
696    }
697}
698
699// ============================================================================
700// Logging Macros
701// ============================================================================
702
703/// Logs a message at the TRACE level with request context.
704///
705/// Returns a [`LogEntry`] that can be emitted or inspected.
706/// For zero-overhead logging, use [`RequestLogger`].
707///
708/// # Example
709///
710/// ```ignore
711/// log_trace!(ctx, "Entering function");
712/// log_trace!(ctx, "Processing item {}", item_id);
713/// log_trace!(ctx, "With fields", key => value, another => thing);
714/// ```
715#[macro_export]
716macro_rules! log_trace {
717    ($ctx:expr, $msg:expr) => {
718        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Trace, $msg)
719            .target(module_path!())
720    };
721    ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
722        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Trace, $msg)
723            .target(module_path!())
724            $(.field(stringify!($key), $value))+
725    };
726    ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
727        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Trace, format!($fmt, $($arg)*))
728            .target(module_path!())
729    };
730}
731
732/// Logs a message at the DEBUG level with request context.
733#[macro_export]
734macro_rules! log_debug {
735    ($ctx:expr, $msg:expr) => {
736        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Debug, $msg)
737            .target(module_path!())
738    };
739    ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
740        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Debug, $msg)
741            .target(module_path!())
742            $(.field(stringify!($key), $value))+
743    };
744    ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
745        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Debug, format!($fmt, $($arg)*))
746            .target(module_path!())
747    };
748}
749
750/// Logs a message at the INFO level with request context.
751#[macro_export]
752macro_rules! log_info {
753    ($ctx:expr, $msg:expr) => {
754        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Info, $msg)
755            .target(module_path!())
756    };
757    ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
758        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Info, $msg)
759            .target(module_path!())
760            $(.field(stringify!($key), $value))+
761    };
762    ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
763        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Info, format!($fmt, $($arg)*))
764            .target(module_path!())
765    };
766}
767
768/// Logs a message at the WARN level with request context.
769#[macro_export]
770macro_rules! log_warn {
771    ($ctx:expr, $msg:expr) => {
772        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Warn, $msg)
773            .target(module_path!())
774    };
775    ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
776        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Warn, $msg)
777            .target(module_path!())
778            $(.field(stringify!($key), $value))+
779    };
780    ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
781        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Warn, format!($fmt, $($arg)*))
782            .target(module_path!())
783    };
784}
785
786/// Logs a message at the ERROR level with request context.
787#[macro_export]
788macro_rules! log_error {
789    ($ctx:expr, $msg:expr) => {
790        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Error, $msg)
791            .target(module_path!())
792    };
793    ($ctx:expr, $msg:expr, $($key:ident => $value:expr),+ $(,)?) => {
794        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Error, $msg)
795            .target(module_path!())
796            $(.field(stringify!($key), $value))+
797    };
798    ($ctx:expr, $fmt:expr, $($arg:tt)*) => {
799        $crate::logging::LogEntry::new($ctx, $crate::logging::LogLevel::Error, format!($fmt, $($arg)*))
800            .target(module_path!())
801    };
802}
803
804#[cfg(test)]
805mod tests {
806    use super::*;
807    use asupersync::Cx;
808
809    fn test_context() -> crate::context::RequestContext {
810        let cx = Cx::for_testing();
811        crate::context::RequestContext::new(cx, 12345)
812    }
813
814    #[test]
815    fn log_level_ordering() {
816        assert!(LogLevel::Trace < LogLevel::Debug);
817        assert!(LogLevel::Debug < LogLevel::Info);
818        assert!(LogLevel::Info < LogLevel::Warn);
819        assert!(LogLevel::Warn < LogLevel::Error);
820    }
821
822    #[test]
823    fn log_level_display() {
824        assert_eq!(LogLevel::Info.as_str(), "info");
825        assert_eq!(LogLevel::Error.as_char(), 'E');
826    }
827
828    #[test]
829    fn log_entry_json() {
830        let ctx = test_context();
831        let entry = LogEntry::new(&ctx, LogLevel::Info, "Test message")
832            .target("test::module")
833            .field("user_id", 42)
834            .field("action", "login");
835
836        let json = entry.to_json();
837        assert!(json.contains(r#""level":"info""#));
838        assert!(json.contains(r#""message":"Test message""#));
839        assert!(json.contains(r#""request_id":12345"#));
840        assert!(json.contains(r#""target":"test::module""#));
841        assert!(json.contains(r#""user_id":"42""#));
842        assert!(json.contains(r#""action":"login""#));
843    }
844
845    #[test]
846    fn log_entry_compact() {
847        let ctx = test_context();
848        let entry =
849            LogEntry::new(&ctx, LogLevel::Warn, "Something happened").field("error_code", "E001");
850
851        let compact = entry.to_compact();
852        assert!(compact.starts_with("[W] req=12345"));
853        assert!(compact.contains("Something happened"));
854        assert!(compact.contains("error_code=E001"));
855    }
856
857    #[test]
858    fn span_timing() {
859        let ctx = test_context();
860        let mut span = Span::new(&ctx, "test_operation");
861
862        std::thread::sleep(std::time::Duration::from_millis(1));
863        let duration = span.end();
864
865        assert!(duration.as_micros() >= 1000);
866        assert!(span.ended);
867    }
868
869    #[test]
870    fn span_child() {
871        let ctx = test_context();
872        let parent = Span::new(&ctx, "parent");
873        let child = parent.child(&ctx, "child");
874
875        assert_eq!(child.parent_id(), Some(parent.span_id()));
876    }
877
878    #[test]
879    fn span_json() {
880        let ctx = test_context();
881        let span = Span::new(&ctx, "db_query");
882
883        let json = span.to_json();
884        assert!(json.contains(r#""name":"db_query""#));
885        assert!(json.contains(r#""request_id":12345"#));
886    }
887
888    #[test]
889    fn log_config_presets() {
890        let dev = LogConfig::development();
891        assert_eq!(dev.min_level, LogLevel::Debug);
892        assert!(!dev.json_output);
893
894        let prod = LogConfig::production();
895        assert_eq!(prod.min_level, LogLevel::Info);
896        assert!(prod.json_output);
897
898        let test = LogConfig::testing();
899        assert_eq!(test.min_level, LogLevel::Trace);
900    }
901
902    #[test]
903    fn escape_json_special_chars() {
904        assert_eq!(escape_json("hello\nworld"), "hello\\nworld");
905        assert_eq!(escape_json(r#"say "hi""#), r#"say \"hi\""#);
906        assert_eq!(escape_json("tab\there"), "tab\\there");
907    }
908
909    #[test]
910    fn log_macro_basic() {
911        let ctx = test_context();
912        let entry = log_info!(&ctx, "Basic message");
913        assert_eq!(entry.level, LogLevel::Info);
914        assert_eq!(entry.message, "Basic message");
915        assert_eq!(entry.request_id, 12345);
916    }
917
918    #[test]
919    fn log_macro_with_fields() {
920        let ctx = test_context();
921        let entry = log_info!(&ctx, "With fields", user_id => 42, action => "login");
922        assert_eq!(entry.fields.len(), 2);
923        assert_eq!(entry.fields[0], ("user_id".to_string(), "42".to_string()));
924        assert_eq!(entry.fields[1], ("action".to_string(), "login".to_string()));
925    }
926
927    #[test]
928    fn log_macro_format_string() {
929        let ctx = test_context();
930        let item_id = 99;
931        let entry = log_debug!(&ctx, "Processing item {}", item_id);
932        assert_eq!(entry.level, LogLevel::Debug);
933        assert_eq!(entry.message, "Processing item 99");
934    }
935
936    // =========================================================================
937    // AutoSpan and Span nesting tests (bd-sdrz)
938    // =========================================================================
939
940    #[test]
941    fn autospan_creates_and_ends_on_drop() {
942        let ctx = test_context();
943        let start = std::time::Instant::now();
944
945        {
946            let _span = AutoSpan::new(&ctx, "short_operation");
947            std::thread::sleep(std::time::Duration::from_millis(1));
948            // AutoSpan should end when dropped here
949        }
950
951        let elapsed = start.elapsed();
952        // Should have completed in a reasonable time
953        assert!(elapsed.as_millis() >= 1);
954    }
955
956    #[test]
957    fn autospan_captures_request_id() {
958        let ctx = test_context();
959        let span = AutoSpan::new(&ctx, "test_span");
960
961        // AutoSpan stores the context's request ID
962        assert_eq!(span.ctx_request_id, 12345);
963    }
964
965    #[test]
966    fn span_nested_three_levels_deep() {
967        let ctx = test_context();
968
969        let parent = Span::new(&ctx, "level_1");
970        let child = parent.child(&ctx, "level_2");
971        let grandchild = child.child(&ctx, "level_3");
972
973        // Verify parent-child relationships
974        assert!(parent.parent_id().is_none());
975        assert_eq!(child.parent_id(), Some(parent.span_id()));
976        assert_eq!(grandchild.parent_id(), Some(child.span_id()));
977
978        // Verify names
979        assert_eq!(parent.name(), "level_1");
980        assert_eq!(child.name(), "level_2");
981        assert_eq!(grandchild.name(), "level_3");
982    }
983
984    #[test]
985    fn span_nested_five_levels_deep() {
986        let ctx = test_context();
987
988        let level1 = Span::new(&ctx, "request_handler");
989        let level2 = level1.child(&ctx, "middleware");
990        let level3 = level2.child(&ctx, "auth_check");
991        let level4 = level3.child(&ctx, "token_validation");
992        let level5 = level4.child(&ctx, "signature_verify");
993
994        // Verify the full chain
995        assert!(level1.parent_id().is_none());
996        assert_eq!(level2.parent_id(), Some(level1.span_id()));
997        assert_eq!(level3.parent_id(), Some(level2.span_id()));
998        assert_eq!(level4.parent_id(), Some(level3.span_id()));
999        assert_eq!(level5.parent_id(), Some(level4.span_id()));
1000    }
1001
1002    #[test]
1003    fn span_sibling_spans_have_same_parent() {
1004        let ctx = test_context();
1005
1006        let parent = Span::new(&ctx, "parent");
1007        let sibling1 = parent.child(&ctx, "sibling_a");
1008        let sibling2 = parent.child(&ctx, "sibling_b");
1009        let sibling3 = parent.child(&ctx, "sibling_c");
1010
1011        // All siblings share the same parent
1012        assert_eq!(sibling1.parent_id(), Some(parent.span_id()));
1013        assert_eq!(sibling2.parent_id(), Some(parent.span_id()));
1014        assert_eq!(sibling3.parent_id(), Some(parent.span_id()));
1015
1016        // But each has a unique span_id
1017        assert_ne!(sibling1.span_id(), sibling2.span_id());
1018        assert_ne!(sibling2.span_id(), sibling3.span_id());
1019        assert_ne!(sibling1.span_id(), sibling3.span_id());
1020    }
1021
1022    #[test]
1023    fn span_ids_are_globally_unique() {
1024        let ctx = test_context();
1025
1026        let span1 = Span::new(&ctx, "span1");
1027        let span2 = Span::new(&ctx, "span2");
1028        let span3 = Span::new(&ctx, "span3");
1029
1030        assert_ne!(span1.span_id(), span2.span_id());
1031        assert_ne!(span2.span_id(), span3.span_id());
1032        assert_ne!(span1.span_id(), span3.span_id());
1033    }
1034
1035    #[test]
1036    fn span_request_id_propagates_to_children() {
1037        let ctx = test_context();
1038
1039        let parent = Span::new(&ctx, "parent");
1040        let child = parent.child(&ctx, "child");
1041        let grandchild = child.child(&ctx, "grandchild");
1042
1043        // All spans should have the same request_id from context
1044        assert_eq!(parent.request_id, 12345);
1045        assert_eq!(child.request_id, 12345);
1046        assert_eq!(grandchild.request_id, 12345);
1047    }
1048
1049    #[test]
1050    fn span_json_includes_parent_id_when_present() {
1051        let ctx = test_context();
1052
1053        let parent = Span::new(&ctx, "parent");
1054        let child = parent.child(&ctx, "child");
1055
1056        // Parent JSON should not have parent_id
1057        let parent_json = parent.to_json();
1058        assert!(!parent_json.contains("parent_id"));
1059
1060        // Child JSON should include parent_id
1061        let child_json = child.to_json();
1062        assert!(child_json.contains("parent_id"));
1063        assert!(child_json.contains(&parent.span_id().to_string()));
1064    }
1065
1066    #[test]
1067    fn span_elapsed_increases_over_time() {
1068        let ctx = test_context();
1069        let span = Span::new(&ctx, "timing_test");
1070
1071        let elapsed1 = span.elapsed();
1072        std::thread::sleep(std::time::Duration::from_millis(2));
1073        let elapsed2 = span.elapsed();
1074
1075        assert!(elapsed2 > elapsed1);
1076    }
1077
1078    #[test]
1079    fn span_end_returns_final_duration() {
1080        let ctx = test_context();
1081        let mut span = Span::new(&ctx, "duration_test");
1082
1083        std::thread::sleep(std::time::Duration::from_millis(1));
1084        let duration = span.end();
1085
1086        // Duration should be at least 1ms
1087        assert!(duration.as_millis() >= 1);
1088        // Span should be marked as ended
1089        assert!(span.ended);
1090    }
1091
1092    #[test]
1093    fn span_multiple_end_calls_idempotent() {
1094        let ctx = test_context();
1095        let mut span = Span::new(&ctx, "multi_end_test");
1096
1097        std::thread::sleep(std::time::Duration::from_millis(1));
1098        let duration1 = span.end();
1099        let duration2 = span.end();
1100        let duration3 = span.end();
1101
1102        // All calls after first should return similar durations (no panic)
1103        // Note: duration may vary slightly due to elapsed() calls
1104        assert!(duration1.as_micros() > 0);
1105        assert!(duration2.as_micros() > 0);
1106        assert!(duration3.as_micros() > 0);
1107    }
1108
1109    #[test]
1110    fn span_drop_ends_span_automatically() {
1111        let ctx = test_context();
1112
1113        // Create a span and let it drop
1114        {
1115            let span = Span::new(&ctx, "auto_end_test");
1116            assert!(!span.ended);
1117            // Span should be ended when dropped
1118        }
1119        // We can't directly verify ended after drop, but the Drop impl
1120        // should not panic and should mark it ended
1121    }
1122
1123    #[test]
1124    fn nested_spans_timing_accumulates_correctly() {
1125        let ctx = test_context();
1126
1127        let start = std::time::Instant::now();
1128
1129        let mut parent = Span::new(&ctx, "parent");
1130        std::thread::sleep(std::time::Duration::from_millis(2));
1131
1132        {
1133            let mut child = parent.child(&ctx, "child");
1134            std::thread::sleep(std::time::Duration::from_millis(2));
1135            let child_duration = child.end();
1136
1137            // Child duration should be ~2ms
1138            assert!(child_duration.as_millis() >= 2);
1139        }
1140
1141        let parent_duration = parent.end();
1142
1143        // Parent duration should include child time (~4ms total)
1144        assert!(parent_duration.as_millis() >= 4);
1145
1146        let total = start.elapsed();
1147        assert!(total.as_millis() >= 4);
1148    }
1149}