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