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