1use std::env;
28use std::path::PathBuf;
29use tracing_appender::non_blocking::WorkerGuard;
30use tracing_subscriber::{
31 fmt::{self, format::FmtSpan},
32 prelude::*,
33 EnvFilter,
34};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum TracingFormat {
39 #[default]
42 Pretty,
43 Compact,
46 Json,
49}
50
51impl std::str::FromStr for TracingFormat {
52 type Err = String;
53
54 fn from_str(s: &str) -> Result<Self, Self::Err> {
55 match s.to_lowercase().as_str() {
56 "pretty" => Ok(TracingFormat::Pretty),
57 "compact" => Ok(TracingFormat::Compact),
58 "json" => Ok(TracingFormat::Json),
59 _ => Err(format!(
60 "Unknown format: {}. Expected: pretty, compact, or json",
61 s
62 )),
63 }
64 }
65}
66
67impl TracingFormat {
68 pub fn parse(s: &str) -> Option<Self> {
70 s.parse().ok()
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct TracingConfig {
77 pub format: TracingFormat,
79 pub span_events: bool,
81 pub file_info: bool,
83 pub thread_ids: bool,
85 pub thread_names: bool,
87 pub target: bool,
89 pub default_filter: String,
91 pub log_file: Option<PathBuf>,
93 pub log_to_stdout: bool,
95}
96
97impl Default for TracingConfig {
98 fn default() -> Self {
99 Self {
100 format: TracingFormat::Pretty,
101 span_events: false,
102 file_info: false,
103 thread_ids: false,
104 thread_names: false,
105 target: true,
106 default_filter: "lgp=info".to_string(),
107 log_file: None,
108 log_to_stdout: true,
109 }
110 }
111}
112
113impl TracingConfig {
114 pub fn new() -> Self {
116 Self::default()
117 }
118
119 pub fn with_format(mut self, format: TracingFormat) -> Self {
121 self.format = format;
122 self
123 }
124
125 pub fn with_span_events(mut self, enabled: bool) -> Self {
127 self.span_events = enabled;
128 self
129 }
130
131 pub fn with_file_info(mut self, enabled: bool) -> Self {
133 self.file_info = enabled;
134 self
135 }
136
137 pub fn with_thread_ids(mut self, enabled: bool) -> Self {
139 self.thread_ids = enabled;
140 self
141 }
142
143 pub fn with_thread_names(mut self, enabled: bool) -> Self {
145 self.thread_names = enabled;
146 self
147 }
148
149 pub fn with_target(mut self, enabled: bool) -> Self {
151 self.target = enabled;
152 self
153 }
154
155 pub fn with_default_filter(mut self, filter: impl Into<String>) -> Self {
157 self.default_filter = filter.into();
158 self
159 }
160
161 pub fn with_log_file(mut self, path: impl Into<PathBuf>) -> Self {
163 self.log_file = Some(path.into());
164 self
165 }
166
167 pub fn with_stdout(mut self, enabled: bool) -> Self {
169 self.log_to_stdout = enabled;
170 self
171 }
172
173 pub fn verbose() -> Self {
175 Self {
176 format: TracingFormat::Pretty,
177 span_events: true,
178 file_info: true,
179 thread_ids: true,
180 thread_names: false,
181 target: true,
182 default_filter: "lgp=debug".to_string(),
183 log_file: None,
184 log_to_stdout: true,
185 }
186 }
187
188 pub fn production() -> Self {
190 Self {
191 format: TracingFormat::Json,
192 span_events: false,
193 file_info: false,
194 thread_ids: false,
195 thread_names: false,
196 target: true,
197 default_filter: "lgp=info".to_string(),
198 log_file: None,
199 log_to_stdout: true,
200 }
201 }
202}
203
204pub struct TracingGuard {
207 _guards: Vec<WorkerGuard>,
208}
209
210pub fn init_tracing(config: TracingConfig) -> TracingGuard {
234 let format = env::var("LGP_LOG_FORMAT")
236 .ok()
237 .and_then(|s| TracingFormat::parse(&s))
238 .unwrap_or(config.format);
239
240 let filter = EnvFilter::try_from_default_env()
242 .unwrap_or_else(|_| EnvFilter::new(&config.default_filter));
243
244 let span_events = if config.span_events {
246 FmtSpan::NEW | FmtSpan::CLOSE
247 } else {
248 FmtSpan::NONE
249 };
250
251 if let Some(log_path) = &config.log_file {
253 if let Some(parent) = log_path.parent() {
255 if !parent.as_os_str().is_empty() {
256 std::fs::create_dir_all(parent).ok();
257 }
258 }
259
260 let file = std::fs::OpenOptions::new()
262 .create(true)
263 .append(true)
264 .open(log_path)
265 .expect("Failed to open log file");
266
267 let (non_blocking, file_guard) = tracing_appender::non_blocking(file);
268
269 let mut guards = vec![file_guard];
271 if config.log_to_stdout {
272 let stdout_guard =
273 init_with_file_and_stdout(format, filter, span_events, &config, non_blocking);
274 guards.push(stdout_guard);
275 } else {
276 init_with_file_only(format, filter, span_events, &config, non_blocking);
277 }
278
279 return TracingGuard { _guards: guards };
280 }
281
282 let stdout_guard = init_stdout_only(format, filter, span_events, &config);
284 TracingGuard {
285 _guards: vec![stdout_guard],
286 }
287}
288
289fn init_with_file_only(
291 format: TracingFormat,
292 filter: EnvFilter,
293 span_events: FmtSpan,
294 config: &TracingConfig,
295 writer: tracing_appender::non_blocking::NonBlocking,
296) {
297 match format {
298 TracingFormat::Pretty => {
299 let subscriber = tracing_subscriber::registry().with(filter).with(
300 fmt::layer()
301 .with_writer(writer)
302 .with_ansi(false)
303 .pretty()
304 .with_span_events(span_events)
305 .with_file(config.file_info)
306 .with_line_number(config.file_info)
307 .with_thread_ids(config.thread_ids)
308 .with_thread_names(config.thread_names)
309 .with_target(config.target),
310 );
311 tracing::subscriber::set_global_default(subscriber)
312 .expect("Failed to set tracing subscriber");
313 }
314 TracingFormat::Compact => {
315 let subscriber = tracing_subscriber::registry().with(filter).with(
316 fmt::layer()
317 .with_writer(writer)
318 .with_ansi(false)
319 .compact()
320 .with_span_events(span_events)
321 .with_file(config.file_info)
322 .with_line_number(config.file_info)
323 .with_thread_ids(config.thread_ids)
324 .with_thread_names(config.thread_names)
325 .with_target(config.target),
326 );
327 tracing::subscriber::set_global_default(subscriber)
328 .expect("Failed to set tracing subscriber");
329 }
330 TracingFormat::Json => {
331 let subscriber = tracing_subscriber::registry().with(filter).with(
332 fmt::layer()
333 .with_writer(writer)
334 .json()
335 .with_span_events(span_events)
336 .with_file(config.file_info)
337 .with_line_number(config.file_info)
338 .with_thread_ids(config.thread_ids)
339 .with_thread_names(config.thread_names)
340 .with_target(config.target),
341 );
342 tracing::subscriber::set_global_default(subscriber)
343 .expect("Failed to set tracing subscriber");
344 }
345 }
346}
347
348fn init_with_file_and_stdout(
353 format: TracingFormat,
354 filter: EnvFilter,
355 span_events: FmtSpan,
356 config: &TracingConfig,
357 file_writer: tracing_appender::non_blocking::NonBlocking,
358) -> WorkerGuard {
359 let (nb_stdout, stdout_guard) = tracing_appender::non_blocking(std::io::stdout());
360
361 match format {
362 TracingFormat::Pretty => {
363 let file_layer = fmt::layer()
364 .with_writer(file_writer)
365 .with_ansi(false)
366 .pretty()
367 .with_span_events(span_events.clone())
368 .with_file(config.file_info)
369 .with_line_number(config.file_info)
370 .with_thread_ids(config.thread_ids)
371 .with_thread_names(config.thread_names)
372 .with_target(config.target);
373 let stdout_layer = fmt::layer()
374 .with_writer(nb_stdout)
375 .pretty()
376 .with_span_events(span_events)
377 .with_file(config.file_info)
378 .with_line_number(config.file_info)
379 .with_thread_ids(config.thread_ids)
380 .with_thread_names(config.thread_names)
381 .with_target(config.target);
382 let subscriber = tracing_subscriber::registry()
383 .with(filter)
384 .with(file_layer)
385 .with(stdout_layer);
386 tracing::subscriber::set_global_default(subscriber)
387 .expect("Failed to set tracing subscriber");
388 }
389 TracingFormat::Compact => {
390 let file_layer = fmt::layer()
391 .with_writer(file_writer)
392 .with_ansi(false)
393 .compact()
394 .with_span_events(span_events.clone())
395 .with_file(config.file_info)
396 .with_line_number(config.file_info)
397 .with_thread_ids(config.thread_ids)
398 .with_thread_names(config.thread_names)
399 .with_target(config.target);
400 let stdout_layer = fmt::layer()
401 .with_writer(nb_stdout)
402 .compact()
403 .with_span_events(span_events)
404 .with_file(config.file_info)
405 .with_line_number(config.file_info)
406 .with_thread_ids(config.thread_ids)
407 .with_thread_names(config.thread_names)
408 .with_target(config.target);
409 let subscriber = tracing_subscriber::registry()
410 .with(filter)
411 .with(file_layer)
412 .with(stdout_layer);
413 tracing::subscriber::set_global_default(subscriber)
414 .expect("Failed to set tracing subscriber");
415 }
416 TracingFormat::Json => {
417 let file_layer = fmt::layer()
418 .with_writer(file_writer)
419 .json()
420 .with_span_events(span_events.clone())
421 .with_file(config.file_info)
422 .with_line_number(config.file_info)
423 .with_thread_ids(config.thread_ids)
424 .with_thread_names(config.thread_names)
425 .with_target(config.target);
426 let stdout_layer = fmt::layer()
427 .with_writer(nb_stdout)
428 .json()
429 .with_span_events(span_events)
430 .with_file(config.file_info)
431 .with_line_number(config.file_info)
432 .with_thread_ids(config.thread_ids)
433 .with_thread_names(config.thread_names)
434 .with_target(config.target);
435 let subscriber = tracing_subscriber::registry()
436 .with(filter)
437 .with(file_layer)
438 .with(stdout_layer);
439 tracing::subscriber::set_global_default(subscriber)
440 .expect("Failed to set tracing subscriber");
441 }
442 }
443
444 stdout_guard
445}
446
447fn init_stdout_only(
452 format: TracingFormat,
453 filter: EnvFilter,
454 span_events: FmtSpan,
455 config: &TracingConfig,
456) -> WorkerGuard {
457 let (nb_stdout, guard) = tracing_appender::non_blocking(std::io::stdout());
458
459 match format {
460 TracingFormat::Pretty => {
461 let subscriber = tracing_subscriber::registry().with(filter).with(
462 fmt::layer()
463 .with_writer(nb_stdout)
464 .pretty()
465 .with_span_events(span_events)
466 .with_file(config.file_info)
467 .with_line_number(config.file_info)
468 .with_thread_ids(config.thread_ids)
469 .with_thread_names(config.thread_names)
470 .with_target(config.target),
471 );
472 tracing::subscriber::set_global_default(subscriber)
473 .expect("Failed to set tracing subscriber");
474 }
475 TracingFormat::Compact => {
476 let subscriber = tracing_subscriber::registry().with(filter).with(
477 fmt::layer()
478 .with_writer(nb_stdout)
479 .compact()
480 .with_span_events(span_events)
481 .with_file(config.file_info)
482 .with_line_number(config.file_info)
483 .with_thread_ids(config.thread_ids)
484 .with_thread_names(config.thread_names)
485 .with_target(config.target),
486 );
487 tracing::subscriber::set_global_default(subscriber)
488 .expect("Failed to set tracing subscriber");
489 }
490 TracingFormat::Json => {
491 let subscriber = tracing_subscriber::registry().with(filter).with(
492 fmt::layer()
493 .with_writer(nb_stdout)
494 .json()
495 .with_span_events(span_events)
496 .with_file(config.file_info)
497 .with_line_number(config.file_info)
498 .with_thread_ids(config.thread_ids)
499 .with_thread_names(config.thread_names)
500 .with_target(config.target),
501 );
502 tracing::subscriber::set_global_default(subscriber)
503 .expect("Failed to set tracing subscriber");
504 }
505 }
506
507 guard
508}
509
510pub fn try_init_tracing(config: TracingConfig) -> Result<(), Box<dyn std::error::Error>> {
514 let format = env::var("LGP_LOG_FORMAT")
516 .ok()
517 .and_then(|s| TracingFormat::parse(&s))
518 .unwrap_or(config.format);
519
520 let filter = EnvFilter::try_from_default_env()
522 .unwrap_or_else(|_| EnvFilter::new(&config.default_filter));
523
524 let span_events = if config.span_events {
526 FmtSpan::NEW | FmtSpan::CLOSE
527 } else {
528 FmtSpan::NONE
529 };
530
531 let result = match format {
533 TracingFormat::Pretty => {
534 let subscriber = tracing_subscriber::registry().with(filter).with(
535 fmt::layer()
536 .pretty()
537 .with_span_events(span_events)
538 .with_file(config.file_info)
539 .with_line_number(config.file_info)
540 .with_thread_ids(config.thread_ids)
541 .with_thread_names(config.thread_names)
542 .with_target(config.target),
543 );
544 tracing::subscriber::set_global_default(subscriber)
545 }
546 TracingFormat::Compact => {
547 let subscriber = tracing_subscriber::registry().with(filter).with(
548 fmt::layer()
549 .compact()
550 .with_span_events(span_events)
551 .with_file(config.file_info)
552 .with_line_number(config.file_info)
553 .with_thread_ids(config.thread_ids)
554 .with_thread_names(config.thread_names)
555 .with_target(config.target),
556 );
557 tracing::subscriber::set_global_default(subscriber)
558 }
559 TracingFormat::Json => {
560 let subscriber = tracing_subscriber::registry().with(filter).with(
561 fmt::layer()
562 .json()
563 .with_span_events(span_events)
564 .with_file(config.file_info)
565 .with_line_number(config.file_info)
566 .with_thread_ids(config.thread_ids)
567 .with_thread_names(config.thread_names)
568 .with_target(config.target),
569 );
570 tracing::subscriber::set_global_default(subscriber)
571 }
572 };
573
574 result.map_err(|e| e.into())
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580
581 #[test]
582 fn test_format_from_str() {
583 assert_eq!(TracingFormat::parse("pretty"), Some(TracingFormat::Pretty));
584 assert_eq!(TracingFormat::parse("PRETTY"), Some(TracingFormat::Pretty));
585 assert_eq!(
586 TracingFormat::parse("compact"),
587 Some(TracingFormat::Compact)
588 );
589 assert_eq!(TracingFormat::parse("json"), Some(TracingFormat::Json));
590 assert_eq!(TracingFormat::parse("invalid"), None);
591 }
592
593 #[test]
594 fn test_config_builder() {
595 let config = TracingConfig::new()
596 .with_format(TracingFormat::Json)
597 .with_span_events(true)
598 .with_file_info(true)
599 .with_thread_ids(true)
600 .with_default_filter("lgp=trace");
601
602 assert_eq!(config.format, TracingFormat::Json);
603 assert!(config.span_events);
604 assert!(config.file_info);
605 assert!(config.thread_ids);
606 assert_eq!(config.default_filter, "lgp=trace");
607 }
608
609 #[test]
610 fn test_verbose_config() {
611 let config = TracingConfig::verbose();
612 assert_eq!(config.format, TracingFormat::Pretty);
613 assert!(config.span_events);
614 assert!(config.file_info);
615 assert_eq!(config.default_filter, "lgp=debug");
616 }
617
618 #[test]
619 fn test_production_config() {
620 let config = TracingConfig::production();
621 assert_eq!(config.format, TracingFormat::Json);
622 assert!(!config.span_events);
623 assert!(!config.file_info);
624 assert_eq!(config.default_filter, "lgp=info");
625 }
626
627 #[test]
628 fn test_file_logging_config() {
629 let config = TracingConfig::new()
630 .with_log_file("/tmp/test.log")
631 .with_stdout(false);
632
633 assert_eq!(config.log_file, Some(PathBuf::from("/tmp/test.log")));
634 assert!(!config.log_to_stdout);
635
636 let default = TracingConfig::default();
638 assert!(default.log_file.is_none());
639 assert!(default.log_to_stdout);
640 }
641}