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;
14
15const MAX_LOG_AGE_DAYS: u64 = 7;
17
18pub fn init(path: Option<&Path>) -> io::Result<()> {
19 tracing_log::LogTracer::init().ok();
20
21 #[cfg(debug_assertions)]
22 const DEFAULT_LEVEL: &str = "debug";
23 #[cfg(not(debug_assertions))]
24 const DEFAULT_LEVEL: &str = "info";
25
26 let level = std::env::var("LOG").unwrap_or_else(|_| DEFAULT_LEVEL.to_string());
27
28 let directives = std::env::var("RUST_LOG").unwrap_or_else(|_| {
29 format!(
30 "{level},bitcoin=off,bitcoincore_rpc=off,corepc=off,fjall=off,brk_fjall=off,lsm_tree=off,brk_rolldown=off,rolldown=off,tracing=off,aide=off,rustls=off,notify=off,oxc_resolver=off,tower_http=off"
31 )
32 });
33
34 let filter: Targets = directives
35 .parse()
36 .unwrap_or_else(|_| Targets::new().with_default(tracing::Level::INFO));
37
38 let registry = tracing_subscriber::registry()
39 .with(filter)
40 .with(fmt::layer().event_format(Formatter::<true>))
41 .with(HookLayer);
42
43 if let Some(path) = path {
44 let dir = path.parent().unwrap_or(Path::new("."));
45 let prefix = path
46 .file_name()
47 .and_then(|s| s.to_str())
48 .unwrap_or("app.log");
49
50 cleanup_old_logs(dir, prefix);
51
52 let writer = RateLimitedFile::new(dir, prefix);
53
54 registry
55 .with(
56 fmt::layer()
57 .event_format(Formatter::<false>)
58 .with_writer(writer),
59 )
60 .init();
61 } else {
62 registry.init();
63 }
64
65 Ok(())
66}
67
68pub fn register_hook<F>(hook: F) -> Result<(), &'static str>
70where
71 F: Fn(&str) + Send + Sync + 'static,
72{
73 LOG_HOOK
74 .set(Box::new(hook))
75 .map_err(|_| "Hook already registered")
76}
77
78fn cleanup_old_logs(dir: &Path, prefix: &str) {
79 let max_age = Duration::from_secs(MAX_LOG_AGE_DAYS * 24 * 60 * 60);
80 let Ok(entries) = std::fs::read_dir(dir) else {
81 return;
82 };
83
84 for entry in entries.flatten() {
85 let path = entry.path();
86 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
87 continue;
88 };
89
90 if !name.starts_with(prefix) || name == prefix {
91 continue;
92 }
93
94 if let Ok(meta) = path.metadata()
95 && let Ok(modified) = meta.modified()
96 && let Ok(age) = modified.elapsed()
97 && age > max_age
98 {
99 let _ = std::fs::remove_file(&path);
100 }
101 }
102}