1use std::io;
7use tracing_subscriber::{
8 fmt::Layer,
9 layer::SubscriberExt,
10 util::SubscriberInitExt,
11 EnvFilter,
12};
13use serde_json::{json, Value};
14
15#[derive(Debug, Clone)]
17pub struct LoggingConfig {
18 pub level: String,
20 pub json_format: bool,
22 pub pretty_print: bool,
24 pub include_location: bool,
26 pub include_timestamp: bool,
28 pub global_fields: serde_json::Map<String, Value>,
30 pub env_filter: Option<String>,
32 pub service_name: Option<String>,
34 pub service_version: Option<String>,
36}
37
38impl Default for LoggingConfig {
39 fn default() -> Self {
40 Self {
41 level: "info".to_string(),
42 json_format: false,
43 pretty_print: true,
44 include_location: false,
45 include_timestamp: true,
46 global_fields: serde_json::Map::new(),
47 env_filter: None,
48 service_name: None,
49 service_version: None,
50 }
51 }
52}
53
54impl LoggingConfig {
55 pub fn production() -> Self {
57 Self {
58 level: "info".to_string(),
59 json_format: true,
60 pretty_print: false,
61 include_location: false,
62 include_timestamp: true,
63 global_fields: {
64 let mut fields = serde_json::Map::new();
65 fields.insert("env".to_string(), json!("production"));
66 fields
67 },
68 env_filter: Some("elif=info,tower=warn,axum=warn".to_string()),
69 service_name: None,
70 service_version: None,
71 }
72 }
73
74 pub fn development() -> Self {
76 Self {
77 level: "debug".to_string(),
78 json_format: false,
79 pretty_print: true,
80 include_location: true,
81 include_timestamp: true,
82 global_fields: {
83 let mut fields = serde_json::Map::new();
84 fields.insert("env".to_string(), json!("development"));
85 fields
86 },
87 env_filter: Some("elif=debug,tower=debug,axum=debug".to_string()),
88 service_name: None,
89 service_version: None,
90 }
91 }
92
93 pub fn test() -> Self {
95 Self {
96 level: "error".to_string(),
97 json_format: false,
98 pretty_print: false,
99 include_location: false,
100 include_timestamp: false,
101 global_fields: {
102 let mut fields = serde_json::Map::new();
103 fields.insert("env".to_string(), json!("test"));
104 fields
105 },
106 env_filter: Some("elif=error".to_string()),
107 service_name: None,
108 service_version: None,
109 }
110 }
111
112 pub fn with_global_field<K, V>(mut self, key: K, value: V) -> Self
114 where
115 K: Into<String>,
116 V: Into<Value>,
117 {
118 self.global_fields.insert(key.into(), value.into());
119 self
120 }
121
122 pub fn with_service(mut self, name: &str, version: &str) -> Self {
124 self.service_name = Some(name.to_string());
125 self.service_version = Some(version.to_string());
126 self
127 }
128
129 pub fn with_env_filter<S: Into<String>>(mut self, filter: S) -> Self {
131 self.env_filter = Some(filter.into());
132 self
133 }
134}
135
136pub fn init_logging(config: LoggingConfig) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
138 let env_filter = config
139 .env_filter
140 .as_deref()
141 .unwrap_or(&config.level);
142
143 let filter = EnvFilter::try_from_default_env()
144 .or_else(|_| EnvFilter::try_new(env_filter))?;
145
146 if config.json_format {
147 tracing_subscriber::registry()
149 .with(filter)
150 .with(Layer::new().with_writer(io::stdout).json())
151 .init();
152 } else if config.pretty_print {
153 tracing_subscriber::registry()
155 .with(filter)
156 .with(Layer::new().with_writer(io::stdout).pretty())
157 .init();
158 } else {
159 tracing_subscriber::registry()
161 .with(filter)
162 .with(Layer::new().with_writer(io::stdout))
163 .init();
164 }
165
166 if !config.global_fields.is_empty() {
168 let mut init_msg = json!({
169 "message": "Structured logging initialized",
170 "config": {
171 "level": config.level,
172 "json_format": config.json_format,
173 "pretty_print": config.pretty_print,
174 "include_location": config.include_location,
175 "include_timestamp": config.include_timestamp,
176 }
177 });
178
179 if let Some(name) = config.service_name {
181 init_msg["service_name"] = json!(name);
182 }
183 if let Some(version) = config.service_version {
184 init_msg["service_version"] = json!(version);
185 }
186
187 for (key, value) in config.global_fields {
189 init_msg[key] = value;
190 }
191
192 tracing::info!(target: "elif::logging", "{}", init_msg);
193 } else {
194 tracing::info!(
195 target: "elif::logging",
196 "Structured logging initialized (level: {}, format: {})",
197 config.level,
198 if config.json_format { "JSON" } else { "text" }
199 );
200 }
201
202 Ok(())
203}
204
205#[macro_export]
207macro_rules! log_with_context {
208 ($level:expr, $($field:tt)*) => {
209 tracing::event!($level, $($field)*)
210 };
211}
212
213#[macro_export]
215macro_rules! info_structured {
216 ($($field:tt)*) => {
217 $crate::log_with_context!(tracing::Level::INFO, $($field)*)
218 };
219}
220
221#[macro_export]
223macro_rules! error_structured {
224 ($($field:tt)*) => {
225 $crate::log_with_context!(tracing::Level::ERROR, $($field)*)
226 };
227}
228
229#[macro_export]
231macro_rules! debug_structured {
232 ($($field:tt)*) => {
233 $crate::log_with_context!(tracing::Level::DEBUG, $($field)*)
234 };
235}
236
237pub fn log_startup_info(service_name: &str, service_version: &str) {
239 let startup_info = json!({
240 "event": "application_startup",
241 "service": service_name,
242 "version": service_version,
243 "pid": std::process::id(),
244 "rust_version": env!("CARGO_PKG_RUST_VERSION"),
245 "timestamp": chrono::Utc::now().to_rfc3339(),
246 "os": std::env::consts::OS,
247 "arch": std::env::consts::ARCH,
248 });
249
250 tracing::info!(target: "elif::startup", "{}", startup_info);
251}
252
253pub fn log_shutdown_info(service_name: &str) {
255 let shutdown_info = json!({
256 "event": "application_shutdown",
257 "service": service_name,
258 "timestamp": chrono::Utc::now().to_rfc3339(),
259 });
260
261 tracing::info!(target: "elif::shutdown", "{}", shutdown_info);
262}
263
264#[derive(Debug, Clone)]
266pub struct LoggingContext {
267 pub correlation_id: String,
268 pub request_id: Option<String>,
269 pub user_id: Option<String>,
270 pub session_id: Option<String>,
271 pub custom_fields: serde_json::Map<String, Value>,
272}
273
274impl LoggingContext {
275 pub fn new(correlation_id: String) -> Self {
276 Self {
277 correlation_id,
278 request_id: None,
279 user_id: None,
280 session_id: None,
281 custom_fields: serde_json::Map::new(),
282 }
283 }
284
285 pub fn with_request_id(mut self, request_id: String) -> Self {
286 self.request_id = Some(request_id);
287 self
288 }
289
290 pub fn with_user_id(mut self, user_id: String) -> Self {
291 self.user_id = Some(user_id);
292 self
293 }
294
295 pub fn with_session_id(mut self, session_id: String) -> Self {
296 self.session_id = Some(session_id);
297 self
298 }
299
300 pub fn with_custom_field<K, V>(mut self, key: K, value: V) -> Self
301 where
302 K: Into<String>,
303 V: Into<Value>,
304 {
305 self.custom_fields.insert(key.into(), value.into());
306 self
307 }
308
309 pub fn to_json(&self) -> Value {
311 let mut context = json!({
312 "correlation_id": self.correlation_id,
313 });
314
315 if let Some(request_id) = &self.request_id {
316 context["request_id"] = json!(request_id);
317 }
318
319 if let Some(user_id) = &self.user_id {
320 context["user_id"] = json!(user_id);
321 }
322
323 if let Some(session_id) = &self.session_id {
324 context["session_id"] = json!(session_id);
325 }
326
327 for (key, value) in &self.custom_fields {
328 context[key] = value.clone();
329 }
330
331 context
332 }
333}
334
335pub mod structured {
337 use super::*;
338 use tracing::{info, warn, error, debug};
339
340 pub fn log_http_request(
342 context: &LoggingContext,
343 method: &str,
344 path: &str,
345 status: u16,
346 duration_ms: u128,
347 user_agent: Option<&str>,
348 ) {
349 let mut log_data = json!({
350 "event": "http_request",
351 "method": method,
352 "path": path,
353 "status": status,
354 "duration_ms": duration_ms,
355 });
356
357 let context_json = context.to_json();
359 for (key, value) in context_json.as_object().unwrap() {
360 log_data[key] = value.clone();
361 }
362
363 if let Some(ua) = user_agent {
364 log_data["user_agent"] = json!(ua);
365 }
366
367 if status >= 500 {
368 error!(target: "elif::http", "{}", log_data);
369 } else if status >= 400 {
370 warn!(target: "elif::http", "{}", log_data);
371 } else {
372 info!(target: "elif::http", "{}", log_data);
373 }
374 }
375
376 pub fn log_database_query(
378 context: &LoggingContext,
379 query: &str,
380 duration_ms: u128,
381 affected_rows: Option<u64>,
382 ) {
383 let mut log_data = json!({
384 "event": "database_query",
385 "query": query,
386 "duration_ms": duration_ms,
387 });
388
389 let context_json = context.to_json();
391 for (key, value) in context_json.as_object().unwrap() {
392 log_data[key] = value.clone();
393 }
394
395 if let Some(rows) = affected_rows {
396 log_data["affected_rows"] = json!(rows);
397 }
398
399 if duration_ms > 1000 {
400 warn!(target: "elif::database", "Slow query: {}", log_data);
401 } else {
402 debug!(target: "elif::database", "{}", log_data);
403 }
404 }
405
406 pub fn log_application_error(
408 context: &LoggingContext,
409 error_type: &str,
410 error_message: &str,
411 error_details: Option<&str>,
412 ) {
413 let mut log_data = json!({
414 "event": "application_error",
415 "error_type": error_type,
416 "error_message": error_message,
417 });
418
419 let context_json = context.to_json();
421 for (key, value) in context_json.as_object().unwrap() {
422 log_data[key] = value.clone();
423 }
424
425 if let Some(details) = error_details {
426 log_data["error_details"] = json!(details);
427 }
428
429 error!(target: "elif::error", "{}", log_data);
430 }
431
432 pub fn log_security_event(
434 context: &LoggingContext,
435 event_type: &str,
436 severity: &str,
437 details: &str,
438 ip_address: Option<&str>,
439 ) {
440 let mut log_data = json!({
441 "event": "security_event",
442 "event_type": event_type,
443 "severity": severity,
444 "details": details,
445 });
446
447 let context_json = context.to_json();
449 for (key, value) in context_json.as_object().unwrap() {
450 log_data[key] = value.clone();
451 }
452
453 if let Some(ip) = ip_address {
454 log_data["ip_address"] = json!(ip);
455 }
456
457 match severity {
458 "high" | "critical" => error!(target: "elif::security", "{}", log_data),
459 "medium" => warn!(target: "elif::security", "{}", log_data),
460 _ => info!(target: "elif::security", "{}", log_data),
461 }
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468
469 #[test]
470 fn test_logging_config_presets() {
471 let prod = LoggingConfig::production();
472 assert!(prod.json_format);
473 assert!(!prod.pretty_print);
474 assert_eq!(prod.level, "info");
475 assert!(prod.global_fields.contains_key("env"));
476
477 let dev = LoggingConfig::development();
478 assert!(!dev.json_format);
479 assert!(dev.pretty_print);
480 assert_eq!(dev.level, "debug");
481 assert!(dev.include_location);
482
483 let test = LoggingConfig::test();
484 assert_eq!(test.level, "error");
485 assert!(!test.include_timestamp);
486 }
487
488 #[test]
489 fn test_logging_config_builder() {
490 let config = LoggingConfig::default()
491 .with_global_field("app", "test-app")
492 .with_service("test-service", "1.0.0")
493 .with_env_filter("debug");
494
495 assert_eq!(config.global_fields.get("app").unwrap(), "test-app");
496 assert_eq!(config.service_name.unwrap(), "test-service");
497 assert_eq!(config.service_version.unwrap(), "1.0.0");
498 assert_eq!(config.env_filter.unwrap(), "debug");
499 }
500
501 #[test]
502 fn test_logging_context() {
503 let context = LoggingContext::new("test-correlation-123".to_string())
504 .with_request_id("req-456".to_string())
505 .with_user_id("user-789".to_string())
506 .with_custom_field("component", "test");
507
508 let json = context.to_json();
509 assert_eq!(json["correlation_id"], "test-correlation-123");
510 assert_eq!(json["request_id"], "req-456");
511 assert_eq!(json["user_id"], "user-789");
512 assert_eq!(json["component"], "test");
513 }
514
515 #[test]
516 fn test_structured_logging_utilities() {
517 use structured::*;
518
519 let context = LoggingContext::new("test-123".to_string())
520 .with_user_id("user-456".to_string());
521
522 log_http_request(&context, "GET", "/api/users", 200, 150, Some("test-agent"));
525 log_database_query(&context, "SELECT * FROM users", 25, Some(5));
526 log_application_error(&context, "ValidationError", "Invalid input", Some("Field 'email' is required"));
527 log_security_event(&context, "failed_login", "medium", "Multiple failed attempts", Some("192.168.1.100"));
528 }
529}