shared_logging/
schema.rs

1//! Standard event schema for consistent log formatting.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Standard log levels.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum Level {
10    Trace,
11    Debug,
12    Info,
13    Warn,
14    Error,
15}
16
17impl Level {
18    /// Convert to tracing level.
19    pub fn to_tracing_level(&self) -> tracing::Level {
20        match self {
21            Level::Trace => tracing::Level::TRACE,
22            Level::Debug => tracing::Level::DEBUG,
23            Level::Info => tracing::Level::INFO,
24            Level::Warn => tracing::Level::WARN,
25            Level::Error => tracing::Level::ERROR,
26        }
27    }
28}
29
30impl fmt::Display for Level {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            Level::Trace => write!(f, "trace"),
34            Level::Debug => write!(f, "debug"),
35            Level::Info => write!(f, "info"),
36            Level::Warn => write!(f, "warn"),
37            Level::Error => write!(f, "error"),
38        }
39    }
40}
41
42/// Standard field names used across all log events.
43pub struct StandardFields;
44
45impl StandardFields {
46    /// Service name field
47    pub const SERVICE: &'static str = "service";
48    /// Module/component name field
49    pub const MODULE: &'static str = "module";
50    /// Log level field
51    pub const LEVEL: &'static str = "level";
52    /// Message field
53    pub const MESSAGE: &'static str = "message";
54    /// Timestamp field
55    pub const TIMESTAMP: &'static str = "timestamp";
56    /// Error field (for error details)
57    pub const ERROR: &'static str = "error";
58    /// Error type field
59    pub const ERROR_TYPE: &'static str = "error_type";
60    /// Error message field
61    pub const ERROR_MESSAGE: &'static str = "error_message";
62    /// Error stack trace field
63    pub const ERROR_STACK: &'static str = "error_stack";
64    /// Trace ID field
65    pub const TRACE_ID: &'static str = "trace_id";
66    /// Span ID field
67    pub const SPAN_ID: &'static str = "span_id";
68    /// Request ID field
69    pub const REQUEST_ID: &'static str = "request_id";
70    /// User ID field
71    pub const USER_ID: &'static str = "user_id";
72    /// Tenant ID field
73    pub const TENANT_ID: &'static str = "tenant_id";
74    /// HTTP method field
75    pub const HTTP_METHOD: &'static str = "http.method";
76    /// HTTP path field
77    pub const HTTP_PATH: &'static str = "http.path";
78    /// HTTP status code field
79    pub const HTTP_STATUS: &'static str = "http.status";
80    /// HTTP duration field
81    pub const HTTP_DURATION_MS: &'static str = "http.duration_ms";
82    /// Client IP field
83    pub const CLIENT_IP: &'static str = "client.ip";
84    /// User agent field
85    pub const USER_AGENT: &'static str = "user_agent";
86}
87
88/// Standard log event structure.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct Event {
91    /// Service name
92    #[serde(rename = "service")]
93    pub service: String,
94    /// Module/component name
95    #[serde(rename = "module")]
96    pub module: Option<String>,
97    /// Log level
98    #[serde(rename = "level")]
99    pub level: Level,
100    /// Log message
101    #[serde(rename = "message")]
102    pub message: String,
103    /// Timestamp (ISO 8601)
104    #[serde(rename = "timestamp")]
105    pub timestamp: String,
106    /// Additional fields
107    #[serde(flatten)]
108    pub fields: serde_json::Value,
109}
110
111impl Event {
112    /// Create a new event.
113    pub fn new(
114        service: impl Into<String>,
115        module: Option<String>,
116        level: Level,
117        message: impl Into<String>,
118    ) -> Self {
119        Self {
120            service: service.into(),
121            module,
122            level,
123            message: message.into(),
124            timestamp: chrono::Utc::now().to_rfc3339(),
125            fields: serde_json::Value::Object(serde_json::Map::new()),
126        }
127    }
128
129    /// Format an error for logging.
130    pub fn format_error(error: &dyn std::error::Error) -> serde_json::Value {
131        let mut error_obj = serde_json::Map::new();
132        error_obj.insert(
133            StandardFields::ERROR_TYPE.to_string(),
134            serde_json::Value::String(std::any::type_name_of_val(error).to_string()),
135        );
136        error_obj.insert(
137            StandardFields::ERROR_MESSAGE.to_string(),
138            serde_json::Value::String(error.to_string()),
139        );
140
141        // Try to get source chain
142        let mut source_chain = Vec::new();
143        let mut current: Option<&dyn std::error::Error> = Some(error);
144        while let Some(err) = current {
145            source_chain.push(err.to_string());
146            current = err.source();
147        }
148
149        if source_chain.len() > 1 {
150            error_obj.insert(
151                StandardFields::ERROR_STACK.to_string(),
152                serde_json::Value::Array(
153                    source_chain
154                        .into_iter()
155                        .map(serde_json::Value::String)
156                        .collect(),
157                ),
158            );
159        }
160
161        serde_json::Value::Object(error_obj)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_level_display() {
171        assert_eq!(Level::Info.to_string(), "info");
172        assert_eq!(Level::Error.to_string(), "error");
173    }
174
175    #[test]
176    fn test_event_creation() {
177        let event = Event::new("my-service", Some("my-module".to_string()), Level::Info, "test");
178        assert_eq!(event.service, "my-service");
179        assert_eq!(event.module, Some("my-module".to_string()));
180        assert_eq!(event.level, Level::Info);
181        assert_eq!(event.message, "test");
182    }
183}
184