Skip to main content

minifly_logging/
lib.rs

1use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
2use uuid::Uuid;
3
4/// Logging configuration for Minifly services
5#[derive(Debug, Clone)]
6pub struct LoggingConfig {
7    pub service_name: String,
8    pub service_version: String,
9    pub environment: String,
10    pub format: LogFormat,
11    pub level: String,
12}
13
14/// Log output format options
15#[derive(Debug, Clone)]
16pub enum LogFormat {
17    /// Human-readable format for development
18    Human,
19    /// JSON format for production and log aggregation
20    Json,
21}
22
23impl Default for LoggingConfig {
24    fn default() -> Self {
25        Self {
26            service_name: "minifly".to_string(),
27            service_version: env!("CARGO_PKG_VERSION").to_string(),
28            environment: "development".to_string(),
29            format: LogFormat::Human,
30            level: "info".to_string(),
31        }
32    }
33}
34
35impl LoggingConfig {
36    /// Create a new logging configuration
37    pub fn new(service_name: &str) -> Self {
38        Self {
39            service_name: service_name.to_string(),
40            ..Default::default()
41        }
42    }
43
44    /// Set the log format
45    pub fn with_format(mut self, format: LogFormat) -> Self {
46        self.format = format;
47        self
48    }
49
50    /// Set the log level
51    pub fn with_level(mut self, level: &str) -> Self {
52        self.level = level.to_string();
53        self
54    }
55
56    /// Set the environment
57    pub fn with_environment(mut self, environment: &str) -> Self {
58        self.environment = environment.to_string();
59        self
60    }
61
62    /// Build config from environment variables
63    pub fn from_env(service_name: &str) -> Self {
64        let format = match std::env::var("MINIFLY_LOG_FORMAT").as_deref() {
65            Ok("json") => LogFormat::Json,
66            _ => LogFormat::Human,
67        };
68
69        let level = std::env::var("MINIFLY_LOG_LEVEL").unwrap_or_else(|_| "info".to_string());
70        let environment = std::env::var("MINIFLY_ENVIRONMENT").unwrap_or_else(|_| "development".to_string());
71
72        Self {
73            service_name: service_name.to_string(),
74            service_version: env!("CARGO_PKG_VERSION").to_string(),
75            environment,
76            format,
77            level,
78        }
79    }
80}
81
82/// Initialize structured logging for a Minifly service
83pub fn init_logging(config: LoggingConfig) -> anyhow::Result<()> {
84    let env_filter = EnvFilter::try_from_default_env()
85        .unwrap_or_else(|_| EnvFilter::new(&config.level));
86
87    let subscriber = tracing_subscriber::registry()
88        .with(env_filter);
89
90    match config.format {
91        LogFormat::Json => {
92            subscriber
93                .with(
94                    tracing_subscriber::fmt::layer()
95                        .json()
96                        .with_current_span(false)
97                        .with_span_list(true)
98                        .with_target(true)
99                        .with_thread_ids(true)
100                        .with_thread_names(true)
101                )
102                .init();
103        }
104        LogFormat::Human => {
105            subscriber
106                .with(
107                    tracing_subscriber::fmt::layer()
108                        .pretty()
109                        .with_target(true)
110                        .with_thread_ids(false)
111                        .with_thread_names(false)
112                )
113                .init();
114        }
115    }
116
117    // Set global context
118    tracing::info!(
119        service.name = %config.service_name,
120        service.version = %config.service_version,
121        environment = %config.environment,
122        log.format = ?config.format,
123        log.level = %config.level,
124        "Structured logging initialized"
125    );
126
127    Ok(())
128}
129
130/// Standard field names for consistent logging across all Minifly components
131pub mod fields {
132    // Identity and correlation fields
133    pub const CORRELATION_ID: &str = "correlation_id";
134    pub const REQUEST_ID: &str = "request_id";
135    pub const USER_ID: &str = "user_id";
136    pub const SESSION_ID: &str = "session_id";
137
138    // Business entities
139    pub const APP_NAME: &str = "app.name";
140    pub const MACHINE_ID: &str = "machine.id";
141    pub const REGION: &str = "region";
142    pub const IMAGE: &str = "image";
143    pub const CONTAINER_ID: &str = "container.id";
144
145    // Operations
146    pub const OPERATION: &str = "operation";
147    pub const OPERATION_TYPE: &str = "operation.type";
148    pub const OPERATION_STATUS: &str = "operation.status";
149    pub const DURATION_MS: &str = "duration_ms";
150
151    // HTTP context
152    pub const HTTP_METHOD: &str = "http.method";
153    pub const HTTP_PATH: &str = "http.path";
154    pub const HTTP_STATUS: &str = "http.status";
155    pub const HTTP_USER_AGENT: &str = "http.user_agent";
156
157    // Error context
158    pub const ERROR_TYPE: &str = "error.type";
159    pub const ERROR_MESSAGE: &str = "error.message";
160    pub const ERROR_STACK: &str = "error.stack";
161    pub const ERROR_ID: &str = "error.id";
162
163    // LiteFS specific
164    pub const LITEFS_MOUNT_PATH: &str = "litefs.mount_path";
165    pub const LITEFS_IS_PRIMARY: &str = "litefs.is_primary";
166    pub const LITEFS_CONFIG_PATH: &str = "litefs.config_path";
167
168    // Docker specific
169    pub const DOCKER_IMAGE: &str = "docker.image";
170    pub const DOCKER_CONTAINER_NAME: &str = "docker.container.name";
171    pub const DOCKER_NETWORK: &str = "docker.network";
172}
173
174/// Generate a new correlation ID
175pub fn new_correlation_id() -> String {
176    Uuid::new_v4().to_string()
177}
178
179/// Generate a new request ID
180pub fn new_request_id() -> String {
181    Uuid::new_v4().to_string()
182}
183
184/// Generate a new error ID
185pub fn new_error_id() -> String {
186    Uuid::new_v4().to_string()
187}
188
189/// Macro for creating structured operation spans
190#[macro_export]
191macro_rules! operation_span {
192    ($operation:expr, $($field:ident = $value:expr),* $(,)?) => {
193        tracing::info_span!(
194            "operation",
195            operation = $operation,
196            correlation_id = %$crate::new_correlation_id(),
197            $($field = $value,)*
198        )
199    };
200}
201
202/// Macro for logging operation completion
203#[macro_export]
204macro_rules! log_operation_result {
205    ($result:expr, $success_msg:expr, $error_msg:expr) => {
206        match &$result {
207            Ok(_) => {
208                tracing::info!(
209                    operation.status = "success",
210                    $success_msg
211                );
212            }
213            Err(e) => {
214                tracing::error!(
215                    operation.status = "failed",
216                    error.message = %e,
217                    $error_msg
218                );
219            }
220        }
221    };
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_logging_config_default() {
230        let config = LoggingConfig::default();
231        assert_eq!(config.service_name, "minifly");
232        assert_eq!(config.environment, "development");
233        assert!(matches!(config.format, LogFormat::Human));
234    }
235
236    #[test]
237    fn test_logging_config_builder() {
238        let config = LoggingConfig::new("test-service")
239            .with_format(LogFormat::Json)
240            .with_level("debug")
241            .with_environment("production");
242
243        assert_eq!(config.service_name, "test-service");
244        assert_eq!(config.level, "debug");
245        assert_eq!(config.environment, "production");
246        assert!(matches!(config.format, LogFormat::Json));
247    }
248
249    #[test]
250    fn test_correlation_id_generation() {
251        let id1 = new_correlation_id();
252        let id2 = new_correlation_id();
253        
254        assert_ne!(id1, id2);
255        assert!(uuid::Uuid::parse_str(&id1).is_ok());
256        assert!(uuid::Uuid::parse_str(&id2).is_ok());
257    }
258}