foxy/logging/
structured.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Structured logging implementation for Foxy.
6
7use slog::{Drain, Logger, o};
8use slog_async::Async;
9use slog_term::{TermDecorator, CompactFormat};
10use slog_json::Json;
11use std::sync::Arc;
12use std::io;
13use uuid::Uuid;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16/// Logger configuration
17#[derive(Debug, Clone)]
18pub struct LoggerConfig {
19    /// Log format (terminal or json)
20    pub format: LogFormat,
21    /// Log level
22    pub level: slog::Level,
23    /// Include source code location
24    pub include_location: bool,
25    /// Include thread ID
26    pub include_thread_id: bool,
27    /// Static fields to include in all logs
28    pub static_fields: std::collections::HashMap<String, String>,
29}
30
31impl Default for LoggerConfig {
32    fn default() -> Self {
33        Self {
34            format: LogFormat::Terminal,
35            level: slog::Level::Info,
36            include_location: true,
37            include_thread_id: true,
38            static_fields: std::collections::HashMap::new(),
39        }
40    }
41}
42
43/// Log format
44#[derive(Debug, Clone, PartialEq)]
45pub enum LogFormat {
46    /// Human-readable terminal output
47    Terminal,
48    /// Machine-parseable JSON output
49    Json,
50}
51
52/// Request information for logging
53#[derive(Debug, Clone)]
54pub struct RequestInfo {
55    /// Trace ID for request correlation
56    pub trace_id: String,
57    /// HTTP method
58    pub method: String,
59    /// Request path
60    pub path: String,
61    /// Remote address
62    pub remote_addr: String,
63    /// User agent
64    pub user_agent: String,
65    /// Request start time (milliseconds since epoch)
66    pub start_time_ms: u128,
67}
68
69impl RequestInfo {
70    /// Calculate elapsed time in milliseconds
71    pub fn elapsed_ms(&self) -> u128 {
72        SystemTime::now()
73            .duration_since(UNIX_EPOCH)
74            .unwrap_or_default()
75            .as_millis()
76            .saturating_sub(self.start_time_ms)
77    }
78}
79
80/// Generate a new trace ID
81pub fn generate_trace_id() -> String {
82    Uuid::new_v4().to_string()
83}
84
85/// Initialize the global logger
86pub fn init_global_logger(config: &LoggerConfig) -> LoggerGuard {
87    let drain = match config.format {
88        LogFormat::Terminal => {
89            let decorator = TermDecorator::new().build();
90            let drain = CompactFormat::new(decorator).build().fuse();
91            Async::new(drain).build().fuse()
92        }
93        LogFormat::Json => {
94            // Create a custom JSON drain with our specific key names
95            let drain = Json::new(io::stdout())
96                .set_pretty(false)
97                .set_newlines(true)
98                // Use @timestamp for timestamp
99                .add_key_value(o!("@timestamp" => slog::PushFnValue(|_record, ser| {
100                    let time = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
101                    ser.emit(time)
102                })))
103                // Use message for the message
104                .add_key_value(o!("message" => slog::PushFnValue(|record, ser| {
105                    ser.emit(record.msg())
106                })))
107                // Add level without any prefix
108                .add_key_value(o!("level" => slog::PushFnValue(|record, ser| {
109                    let level = record.level().as_str();
110                    ser.emit(level)
111                })))
112                .build()
113                .fuse();
114            Async::new(drain).build().fuse()
115        }
116    };
117
118    let drain = drain.filter_level(config.level).fuse();
119
120    // Add static fields
121    let mut logger = Logger::root(drain, o!());
122    for (key, value) in &config.static_fields {
123        let key_str: &'static str = Box::leak(key.clone().into_boxed_str());
124        logger = logger.new(o!(key_str => value.clone()));
125    }
126
127    // Set up the global logger
128    let guard = slog_scope::set_global_logger(logger);
129
130    let log_level_filter = match config.level {
131        slog::Level::Trace => log::Level::Trace,
132        slog::Level::Debug => log::Level::Debug,
133        slog::Level::Info => log::Level::Info,
134        slog::Level::Warning => log::Level::Warn,
135        slog::Level::Error => log::Level::Error,
136        slog::Level::Critical => log::Level::Error,
137    };
138
139    slog_stdlog::init_with_level(log_level_filter).unwrap();
140
141    LoggerGuard { _guard: guard }
142}
143
144/// Guard for the global logger
145pub struct LoggerGuard {
146    _guard: slog_scope::GlobalLoggerGuard,
147}