1use std::path::PathBuf;
9use std::time::Duration;
10
11use tracing_appender::rolling::{Builder as AppenderBuilder, Rotation};
12use tracing_subscriber::layer::SubscriberExt;
13use tracing_subscriber::util::SubscriberInitExt;
14use tracing_subscriber::{reload, EnvFilter, Layer};
15
16use crate::error::{Error, Result};
17use crate::layer::{DigJsonLayer, OwnedStatics};
18use crate::writer::{LossyWriter, WriterGuard};
19use crate::{correlation, dirs, filter, janitor, Service};
20
21const MAINTENANCE_INTERVAL: Duration = Duration::from_secs(3600);
23
24type FilterSetter = Box<dyn Fn(&str) -> Result<()> + Send + Sync>;
26
27pub struct LogGuard {
30 _writer: WriterGuard,
31 dir: PathBuf,
32 set_filter: FilterSetter,
33}
34
35impl LogGuard {
36 pub fn log_dir(&self) -> &std::path::Path {
38 &self.dir
39 }
40
41 pub fn set_filter(&self, directive: &str) -> Result<()> {
43 (self.set_filter)(directive)
44 }
45}
46
47pub fn init(service: Service) -> Result<LogGuard> {
50 let dir = dirs::log_dir(service.name);
51 std::fs::create_dir_all(&dir).map_err(|source| Error::LogDir {
52 path: dir.clone(),
53 source,
54 })?;
55
56 let env = |key: &str| std::env::var(key).ok();
57 let retention = janitor::retention_days(env);
58 let max_bytes = janitor::max_bytes(env);
59 janitor::enforce_byte_cap(&dir, service.name, max_bytes);
60
61 let appender = AppenderBuilder::new()
62 .rotation(Rotation::DAILY)
63 .filename_prefix(format!("{}.jsonl", service.name))
64 .max_log_files(retention)
65 .build(&dir)
66 .map_err(|source| Error::Appender {
67 path: dir.clone(),
68 source,
69 })?;
70 let (file_writer, writer_guard) = crate::writer::spawn(appender);
71
72 let statics = OwnedStatics {
73 service: service.name.to_string(),
74 service_version: service.version.to_string(),
75 run_context: service.run_context.as_str().to_string(),
76 run_id: correlation::new_run_id(),
77 parent_op_id: correlation::parent_op_id_from_env(),
78 };
79
80 let directive = filter::resolve_filter_from_env(filter::read_persisted_level(&dir).as_deref());
81 let env_filter = EnvFilter::try_new(&directive).map_err(|e| Error::Filter {
82 directive: directive.clone(),
83 message: e.to_string(),
84 })?;
85 let (filter_layer, reload_handle) = reload::Layer::new(env_filter);
86
87 let json_layer = DigJsonLayer::new(statics, file_writer.clone());
88 let stderr_layer = tracing_subscriber::fmt::layer()
89 .with_writer(std::io::stderr)
90 .compact();
91
92 tracing_subscriber::registry()
93 .with(filter_layer)
94 .with(json_layer)
95 .with(stderr_layer.boxed())
96 .try_init()
97 .map_err(|_| Error::AlreadyInitialized)?;
98
99 spawn_maintenance(dir.clone(), service.name, max_bytes, file_writer);
100
101 let set_filter = Box::new(move |directive: &str| -> Result<()> {
102 let new = EnvFilter::try_new(directive).map_err(|e| Error::Filter {
103 directive: directive.to_string(),
104 message: e.to_string(),
105 })?;
106 reload_handle.reload(new).map_err(|e| Error::Filter {
107 directive: directive.to_string(),
108 message: e.to_string(),
109 })
110 });
111
112 Ok(LogGuard {
113 _writer: writer_guard,
114 dir,
115 set_filter,
116 })
117}
118
119fn spawn_maintenance(dir: PathBuf, service: &'static str, max_bytes: u64, writer: LossyWriter) {
122 std::thread::Builder::new()
123 .name("dig-logging-maintenance".into())
124 .spawn(move || {
125 let mut last_dropped = 0u64;
126 loop {
127 std::thread::sleep(MAINTENANCE_INTERVAL);
128 janitor::enforce_byte_cap(&dir, service, max_bytes);
129 let dropped = writer.dropped();
130 if dropped > last_dropped {
131 tracing::warn!(
132 target: "dig_logging",
133 dropped,
134 "log lines dropped under backpressure since start"
135 );
136 last_dropped = dropped;
137 }
138 }
139 })
140 .expect("spawn dig-logging maintenance thread");
141}