1use crate::Config;
2use crate::model::errors::{LbResult, core_err_unexpected};
3use crate::service::debug::{generate_panic_content, generate_panic_filename};
4use std::backtrace::Backtrace;
5use std::fs::OpenOptions;
6use std::io::Write;
7use std::{env, panic};
8use tracing::Subscriber;
9use tracing::metadata::LevelFilter;
10use tracing_subscriber::fmt::format::FmtSpan;
11use tracing_subscriber::prelude::*;
12use tracing_subscriber::registry::LookupSpan;
13use tracing_subscriber::{Layer, filter, fmt};
14
15pub static LOG_FILE: &str = "lockbook.log";
16
17pub static TRACE_ENV_VAR: &str = "LB_TRACE";
20
21fn log_targets(metadata: &tracing::Metadata<'_>) -> bool {
22 let t = metadata.target();
23 t.starts_with("lb_rs")
24 || t.starts_with("dbrs")
25 || t.starts_with("workspace")
26 || t.starts_with("lb_fs")
27}
28
29#[cfg(not(target_family = "wasm"))]
30fn trace_targets(metadata: &tracing::Metadata<'_>) -> bool {
31 log_targets(metadata) || metadata.target().starts_with("lockbook_desktop")
32}
33
34fn fmt_layers<S>(config: &Config) -> Vec<Box<dyn Layer<S> + Send + Sync + 'static>>
35where
36 S: Subscriber + for<'a> LookupSpan<'a>,
37{
38 let lockbook_log_level = env::var("LOG_LEVEL")
39 .ok()
40 .and_then(|s| s.as_str().parse().ok())
41 .unwrap_or(LevelFilter::DEBUG);
42
43 let mut layers = Vec::with_capacity(2);
44
45 #[cfg(not(target_arch = "wasm32"))]
46 layers.push(
47 fmt::Layer::new()
48 .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
49 .with_ansi(config.colored_logs)
50 .with_target(true)
51 .with_writer(tracing_appender::rolling::never(&config.writeable_path, LOG_FILE))
52 .with_filter(lockbook_log_level)
53 .with_filter(filter::filter_fn(log_targets))
54 .boxed(),
55 );
56
57 if config.stdout_logs {
58 #[cfg(not(target_os = "android"))]
59 layers.push(
60 fmt::Layer::new()
61 .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
62 .with_ansi(config.colored_logs)
63 .with_target(true)
64 .with_filter(lockbook_log_level)
65 .with_filter(filter::filter_fn(log_targets))
66 .boxed(),
67 );
68 #[cfg(target_os = "android")]
69 if let Some(writer) =
70 tracing_logcat::LogcatMakeWriter::new(tracing_logcat::LogcatTag::Target).ok()
71 {
72 layers.push(
73 fmt::Layer::new()
74 .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
75 .with_ansi(false)
76 .with_writer(writer)
77 .with_filter(lockbook_log_level)
78 .with_filter(filter::filter_fn(|metadata| {
79 let t = metadata.target();
80 t.starts_with("lb_rs")
81 || t.starts_with("workspace")
82 || t.starts_with("lb_java")
83 }))
84 .boxed(),
85 );
86 }
87 }
88
89 layers
90}
91
92#[cfg(not(target_family = "wasm"))]
93static TRACE_GUARD: std::sync::OnceLock<tracing_perfetto_file::FlushGuard> =
94 std::sync::OnceLock::new();
95
96#[cfg(not(target_family = "wasm"))]
97fn trace_path(config: &Config) -> Option<std::path::PathBuf> {
98 let requested = env::var(TRACE_ENV_VAR).ok()?;
99 match requested.as_str() {
100 "" | "0" | "false" => None,
101 "1" | "true" => {
102 let stamp = chrono::Local::now().format("%Y%m%d-%H%M%S");
103 Some(
104 std::path::Path::new(&config.writeable_path)
105 .join("traces")
106 .join(format!("{stamp}.pftrace")),
107 )
108 }
109 path => Some(std::path::PathBuf::from(path)),
110 }
111}
112
113#[cfg(not(target_family = "wasm"))]
114fn trace_file(path: &std::path::Path) -> std::io::Result<std::fs::File> {
115 if let Some(dir) = path.parent().filter(|dir| !dir.as_os_str().is_empty()) {
116 std::fs::create_dir_all(dir)?;
117 }
118 std::fs::File::create(path)
119}
120
121#[cfg(not(target_family = "wasm"))]
122fn trace_layer<S>(config: &Config) -> Option<Box<dyn Layer<S> + Send + Sync + 'static>>
123where
124 S: Subscriber + for<'a> LookupSpan<'a>,
125{
126 use tracing_perfetto_file::{PerfettoLayer, SpanMode};
127
128 let path = trace_path(config)?;
129
130 let file = match trace_file(&path) {
131 Ok(file) => file,
132 Err(err) => {
133 eprintln!("{TRACE_ENV_VAR}: failed to open {}: {err}", path.display());
134 return None;
135 }
136 };
137
138 let (layer, guard) = PerfettoLayer::builder(file)
139 .span_mode(SpanMode::Both)
140 .with_debug_annotations()
141 .with_source_locations()
142 .with_counters()
143 .build();
144 let _ = TRACE_GUARD.set(guard);
145
146 eprintln!("{TRACE_ENV_VAR}: writing {} (open in https://ui.perfetto.dev)", path.display());
147
148 Some(
149 layer
150 .with_filter(LevelFilter::TRACE)
151 .with_filter(filter::filter_fn(trace_targets))
152 .boxed(),
153 )
154}
155
156pub fn traces_enabled() -> bool {
158 #[cfg(not(target_family = "wasm"))]
159 {
160 TRACE_GUARD.get().is_some()
161 }
162 #[cfg(target_family = "wasm")]
163 {
164 false
165 }
166}
167
168pub fn flush_traces() {
171 #[cfg(not(target_family = "wasm"))]
172 if let Some(guard) = TRACE_GUARD.get() {
173 let _ = guard.flush();
174 }
175}
176
177pub fn init(config: &Config) -> LbResult<()> {
181 if !config.logs {
182 return Ok(());
183 }
184 if tracing::dispatcher::has_been_set() {
185 return Ok(());
186 }
187 if !config.writeable_path.is_empty() {
188 std::fs::create_dir_all(&config.writeable_path).map_err(core_err_unexpected)?;
189 }
190
191 let subscriber = tracing_subscriber::Registry::default().with(fmt_layers(config));
192
193 #[cfg(not(target_family = "wasm"))]
194 let subscriber = subscriber.with(trace_layer(config));
195
196 tracing::subscriber::set_global_default(subscriber).map_err(core_err_unexpected)?;
197 panic_capture(config);
198 Ok(())
199}
200
201fn panic_capture(config: &Config) {
202 let path = config.writeable_path.clone();
203 panic::set_hook(Box::new(move |error_header| {
204 let bt = Backtrace::force_capture();
205 tracing::error!("panic detected: {error_header} {}", bt);
206 eprintln!("panic detected and logged: {error_header} {bt}");
207 let file_name = generate_panic_filename(&path);
208 let content = generate_panic_content(&error_header.to_string(), &bt.to_string());
209
210 let mut file = OpenOptions::new()
211 .create(true)
212 .append(true)
213 .open(&file_name)
214 .unwrap();
215
216 file.write_all(content.as_bytes()).unwrap();
217
218 flush_traces();
219 }));
220}
221
222#[cfg(all(test, not(target_family = "wasm")))]
223mod tests {
224 use super::*;
225 use crate::model::core_config::ClientType;
226
227 #[test]
228 fn lb_trace_writes_a_pftrace() {
229 let path = std::env::temp_dir().join("lockbook-lb-trace-smoke.pftrace");
230 let _ = std::fs::remove_file(&path);
231 env::set_var(TRACE_ENV_VAR, &path);
232
233 let config = Config {
234 writeable_path: String::new(),
235 background_work: false,
236 logs: true,
237 stdout_logs: false,
238 colored_logs: false,
239 client_type: ClientType::Unknown,
240 };
241 let subscriber = tracing_subscriber::Registry::default().with(trace_layer(&config));
242 env::remove_var(TRACE_ENV_VAR);
243
244 tracing::subscriber::with_default(subscriber, || {
245 let _span = tracing::trace_span!("smoke").entered();
246 tracing::trace!(counter.rss_bytes = 1u64, "sample");
247 });
248 flush_traces();
249
250 let len = std::fs::metadata(&path).expect("stat trace file").len();
251 assert!(len > 0, "expected a non-empty .pftrace");
252 let _ = std::fs::remove_file(&path);
253 }
254}