1use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
2use uuid::Uuid;
3
4#[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#[derive(Debug, Clone)]
16pub enum LogFormat {
17 Human,
19 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 pub fn new(service_name: &str) -> Self {
38 Self {
39 service_name: service_name.to_string(),
40 ..Default::default()
41 }
42 }
43
44 pub fn with_format(mut self, format: LogFormat) -> Self {
46 self.format = format;
47 self
48 }
49
50 pub fn with_level(mut self, level: &str) -> Self {
52 self.level = level.to_string();
53 self
54 }
55
56 pub fn with_environment(mut self, environment: &str) -> Self {
58 self.environment = environment.to_string();
59 self
60 }
61
62 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
82pub 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 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
130pub mod fields {
132 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 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 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 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 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 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 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
174pub fn new_correlation_id() -> String {
176 Uuid::new_v4().to_string()
177}
178
179pub fn new_request_id() -> String {
181 Uuid::new_v4().to_string()
182}
183
184pub fn new_error_id() -> String {
186 Uuid::new_v4().to_string()
187}
188
189#[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_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}