1#![doc = include_str!("../README.md")]
2
3mod format;
4mod hook;
5mod rate_limit;
6
7use std::{io, path::Path, time::Duration};
8
9use tracing_subscriber::{filter::Targets, fmt, layer::SubscriberExt, util::SubscriberInitExt};
10
11use format::Formatter;
12use hook::{HookLayer, LOG_HOOK};
13use rate_limit::{RateLimitedFile, is_log_file};
14
15const MAX_LOG_AGE_DAYS: u64 = 7;
17
18pub fn init(dir: Option<&Path>) -> io::Result<()> {
25 #[cfg(debug_assertions)]
26 const DEFAULT_LEVEL: &str = "debug";
27 #[cfg(not(debug_assertions))]
28 const DEFAULT_LEVEL: &str = "info";
29
30 init_with_default_level(dir, DEFAULT_LEVEL)
31}
32
33pub fn init_with_default_level(dir: Option<&Path>, default_level: &str) -> io::Result<()> {
38 tracing_log::LogTracer::init().ok();
39 install_panic_hook();
40
41 let level = std::env::var("LOG").unwrap_or_else(|_| default_level.to_string());
42
43 let directives = std::env::var("RUST_LOG").unwrap_or_else(|_| {
44 format!(
45 "{level},bitcoin=off,corepc=off,tracing=off,aide=off,fjall=off,lsm_tree=off,tower_http=off"
46 )
47 });
48
49 let filter: Targets = directives
50 .parse()
51 .unwrap_or_else(|_| Targets::new().with_default(tracing::Level::INFO));
52
53 let registry = tracing_subscriber::registry()
54 .with(filter)
55 .with(fmt::layer().event_format(Formatter::<true>))
56 .with(HookLayer);
57
58 if let Some(dir) = dir {
59 let writer = RateLimitedFile::new(dir)?;
60
61 cleanup_old_logs(dir);
62
63 registry
64 .with(
65 fmt::layer()
66 .event_format(Formatter::<false>)
67 .with_writer(writer),
68 )
69 .init();
70 } else {
71 registry.init();
72 }
73
74 Ok(())
75}
76
77fn install_panic_hook() {
78 std::panic::set_hook(Box::new(|info| {
79 let location = info
80 .location()
81 .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
82 .unwrap_or_else(|| "unknown".to_string());
83 let payload = info.payload();
84 let msg = payload
85 .downcast_ref::<&str>()
86 .copied()
87 .map(str::to_owned)
88 .or_else(|| payload.downcast_ref::<String>().cloned())
89 .unwrap_or_else(|| "Box<dyn Any>".to_owned());
90 let backtrace = std::backtrace::Backtrace::capture();
91 tracing::error!(location, backtrace = %backtrace, "panic: {msg}");
92 }));
93}
94
95pub fn register_hook<F>(hook: F) -> Result<(), &'static str>
97where
98 F: Fn(&str) + Send + Sync + 'static,
99{
100 LOG_HOOK
101 .set(Box::new(hook))
102 .map_err(|_| "Hook already registered")
103}
104
105fn cleanup_old_logs(dir: &Path) {
106 let max_age = Duration::from_secs(MAX_LOG_AGE_DAYS * 24 * 60 * 60);
107 let Ok(entries) = std::fs::read_dir(dir) else {
108 return;
109 };
110
111 for entry in entries.flatten() {
112 let path = entry.path();
113 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
114 continue;
115 };
116 if !is_log_file(name) {
117 continue;
118 }
119
120 if let Ok(meta) = path.metadata()
121 && let Ok(modified) = meta.modified()
122 && let Ok(age) = modified.elapsed()
123 && age > max_age
124 {
125 let _ = std::fs::remove_file(&path);
126 }
127 }
128}