1use std::{fmt::Debug, fs::OpenOptions};
2use tracing::{
3 Event, Span, Subscriber,
4 field::{Field, Visit},
5 span::{Attributes, Id},
6};
7use tracing_subscriber::{
8 Layer,
9 filter::{LevelFilter, Targets},
10 fmt::{FormatFields, format::Writer},
11 layer::{Context, SubscriberExt},
12 registry::LookupSpan,
13 util::SubscriberInitExt,
14};
15
16use crate::{LogEntry, Logger, SdkError};
17
18pub const DEFAULT_FILTER: &str = concat!(
22 "info",
23 ",breez_sdk_spark=debug",
25 ",breez_sdk_common=debug",
26 ",breez_sdk_spark_wasm=debug",
27 ",breez_sdk_spark_bindings=debug",
28 ",spark=debug",
29 ",spark_wallet=debug",
30 ",spark_postgres=debug",
31 ",spark_mysql=debug",
32 ",flashnet=debug",
33 ",platform_utils=debug",
34 ",h2=warn",
36 ",rustls=warn",
37 ",rustyline=warn",
38 ",hyper=warn",
39 ",hyper_util=warn",
40 ",tower=warn",
41 ",Connection=warn",
42 ",tonic=warn",
43);
44
45const INSTANCE_SPAN_TARGET: &str = "breez_sdk_spark_instance";
48const INSTANCE_FIELD: &str = "instance";
49
50#[doc(hidden)]
54pub fn sdk_log_filter(filter: Option<&str>) -> Targets {
55 parse_log_filter(filter.unwrap_or(DEFAULT_FILTER))
56 .with_target(INSTANCE_SPAN_TARGET, LevelFilter::TRACE)
57}
58
59fn file_log_filter(filter: Option<&str>) -> Targets {
62 parse_log_filter(filter.unwrap_or(DEFAULT_FILTER))
63 .with_target(INSTANCE_SPAN_TARGET, LevelFilter::OFF)
64}
65
66pub(crate) fn instance_span(instance_id: &str) -> Span {
68 tracing::error_span!(target: INSTANCE_SPAN_TARGET, "sdk", instance = instance_id)
69}
70
71fn parse_log_filter(filter: &str) -> Targets {
76 let directives: Vec<_> = filter
77 .split(',')
78 .map(str::trim)
79 .filter(|d| !d.is_empty())
80 .filter_map(parse_directive)
81 .collect();
82 if directives.is_empty() {
84 return Targets::new().with_default(LevelFilter::ERROR);
85 }
86 directives
87 .into_iter()
88 .fold(Targets::new(), |targets, (target, level)| match target {
89 Some(target) => targets.with_target(target, level),
90 None => targets.with_default(level),
91 })
92}
93
94fn parse_directive(directive: &str) -> Option<(Option<&str>, LevelFilter)> {
97 let first = directive.chars().next()?;
98 let valid_start = first.is_alphanumeric() || matches!(first, '-' | ':' | '_');
99 if !valid_start || directive.contains(['[', '{']) {
100 return None;
101 }
102 match directive.split_once('=') {
103 Some((target, level)) => {
104 let level = match level.trim() {
105 "" => LevelFilter::TRACE,
106 level => level.parse().ok()?,
107 };
108 Some((Some(target.trim()), level))
109 }
110 None => Some(match directive.parse() {
111 Ok(level) => (None, level),
112 Err(_) => (Some(directive), LevelFilter::TRACE),
113 }),
114 }
115}
116
117struct InstanceId(String);
119
120#[derive(Default)]
121struct InstanceVisitor(Option<String>);
122
123impl Visit for InstanceVisitor {
124 fn record_str(&mut self, field: &Field, value: &str) {
125 if field.name() == INSTANCE_FIELD {
126 self.0 = Some(value.to_string());
127 }
128 }
129
130 fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
131 if field.name() == INSTANCE_FIELD {
132 self.0 = Some(format!("{value:?}"));
133 }
134 }
135}
136
137#[doc(hidden)]
140pub fn record_instance_id<S>(attrs: &Attributes<'_>, id: &Id, ctx: &Context<'_, S>)
141where
142 S: Subscriber + for<'a> LookupSpan<'a>,
143{
144 if attrs.metadata().target() != INSTANCE_SPAN_TARGET {
145 return;
146 }
147 let mut visitor = InstanceVisitor::default();
148 attrs.record(&mut visitor);
149 if let (Some(instance_id), Some(span)) = (visitor.0, ctx.span(id)) {
150 span.extensions_mut().replace(InstanceId(instance_id));
151 }
152}
153
154#[doc(hidden)]
157pub fn event_instance_id<S>(event: &Event<'_>, ctx: &Context<'_, S>) -> Option<String>
158where
159 S: Subscriber + for<'a> LookupSpan<'a>,
160{
161 ctx.event_scope(event)?.find_map(|span| {
162 span.extensions()
163 .get::<InstanceId>()
164 .map(|instance| instance.0.clone())
165 })
166}
167
168pub(crate) struct GlobalSdkLogger {
169 pub(crate) log_listener: Option<Box<dyn Logger>>,
171}
172
173impl<S> Layer<S> for GlobalSdkLogger
174where
175 S: Subscriber + for<'a> LookupSpan<'a>,
176{
177 fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
178 record_instance_id(attrs, id, &ctx);
179 }
180
181 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
182 if let Some(s) = self.log_listener.as_ref() {
183 let mut buf = String::new();
184 let writer = Writer::new(&mut buf);
185
186 if tracing_subscriber::fmt::format::DefaultFields::new()
187 .format_fields(writer, event)
188 .is_ok()
189 {
190 s.log(LogEntry {
191 line: buf,
192 level: event.metadata().level().to_string(),
193 instance_id: event_instance_id(event, &ctx),
194 });
195 }
196 }
197 }
198}
199
200pub(super) fn init_logging(
201 log_dir: Option<&str>,
202 app_logger: Option<Box<dyn Logger>>,
203 log_filter: Option<&str>,
204) -> Result<(), SdkError> {
205 let registry = tracing_subscriber::registry().with(
206 GlobalSdkLogger {
207 log_listener: app_logger,
208 }
209 .with_filter(sdk_log_filter(log_filter)),
210 );
211
212 if let Some(log_dir) = log_dir {
213 let log_file = OpenOptions::new()
214 .create(true)
215 .append(true)
216 .open(format!("{log_dir}/sdk.log"))
217 .map_err(|e| SdkError::Generic(e.to_string()))?;
218 let fmt_layer = tracing_subscriber::fmt::layer()
219 .with_ansi(false)
220 .with_line_number(true)
221 .with_writer(log_file);
222 #[cfg(feature = "span-trace")]
227 let fmt_layer = fmt_layer.with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE);
228 let fmt_layer = fmt_layer.with_filter(file_log_filter(log_filter));
229 registry.with(fmt_layer).try_init()?;
230 } else {
231 registry.try_init()?;
232 }
233
234 Ok(())
235}
236
237#[cfg(test)]
238mod tests {
239 use std::sync::{Arc, Mutex};
240
241 use tracing::{debug, error, info, trace, warn};
242 use tracing_subscriber::{EnvFilter, Registry, layer::Filter};
243
244 use super::{
245 Context, DEFAULT_FILTER, Event, GlobalSdkLogger, Layer, SubscriberExt, file_log_filter,
246 instance_span, parse_log_filter, sdk_log_filter,
247 };
248 use crate::{LogEntry, Logger};
249
250 struct CapturingLogger {
252 levels: Arc<Mutex<Vec<String>>>,
253 }
254
255 impl Logger for CapturingLogger {
256 fn log(&self, l: LogEntry) {
257 self.levels.lock().unwrap().push(l.level);
258 }
259 }
260
261 fn forwarded_levels(filter: &str, emit: impl FnOnce()) -> Vec<String> {
266 let levels = Arc::new(Mutex::new(Vec::new()));
267 let subscriber = tracing_subscriber::registry().with(
268 GlobalSdkLogger {
269 log_listener: Some(Box::new(CapturingLogger {
270 levels: levels.clone(),
271 })),
272 }
273 .with_filter(parse_log_filter(filter)),
274 );
275
276 tracing::subscriber::with_default(subscriber, emit);
277
278 levels.lock().unwrap().clone()
279 }
280
281 #[test]
282 fn external_logger_respects_filter_level() {
283 let levels = forwarded_levels("brz_logtest_debug=debug", || {
286 info!(target: "brz_logtest_debug", "info");
287 debug!(target: "brz_logtest_debug", "debug");
288 trace!(target: "brz_logtest_debug", "trace");
289 });
290 assert!(levels.contains(&"INFO".to_string()), "got {levels:?}");
291 assert!(levels.contains(&"DEBUG".to_string()), "got {levels:?}");
292 assert!(!levels.contains(&"TRACE".to_string()), "got {levels:?}");
293
294 let levels = forwarded_levels("brz_logtest_trace=trace", || {
296 debug!(target: "brz_logtest_trace", "debug");
297 trace!(target: "brz_logtest_trace", "trace");
298 });
299 assert!(levels.contains(&"DEBUG".to_string()), "got {levels:?}");
300 assert!(levels.contains(&"TRACE".to_string()), "got {levels:?}");
301 }
302
303 struct EventRecorder(Arc<Mutex<Vec<String>>>);
305
306 impl<S: tracing::Subscriber> Layer<S> for EventRecorder {
307 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
308 let meta = event.metadata();
309 self.0
310 .lock()
311 .unwrap()
312 .push(format!("{}:{}", meta.target(), meta.level()));
313 }
314 }
315
316 fn enabled_events(filter: impl Filter<Registry> + Send + Sync + 'static) -> Vec<String> {
319 let events = Arc::new(Mutex::new(Vec::new()));
320 let subscriber =
321 tracing_subscriber::registry().with(EventRecorder(events.clone()).with_filter(filter));
322 tracing::subscriber::with_default(subscriber, || {
323 macro_rules! at_all_levels {
324 ($($target:literal),*) => {$(
325 trace!(target: $target, "");
326 debug!(target: $target, "");
327 info!(target: $target, "");
328 warn!(target: $target, "");
329 error!(target: $target, "");
330 )*};
331 }
332 at_all_levels!(
333 "breez_sdk_spark",
334 "breez_sdk_spark::sdk",
335 "spark",
336 "spark::tree",
337 "sparkle",
338 "h2::codec",
339 "Connection",
340 "other"
341 );
342 });
343 events.lock().unwrap().clone()
344 }
345
346 #[test]
347 fn log_filter_matches_env_filter() {
348 let filters = [
349 DEFAULT_FILTER,
350 "",
351 "info",
352 "debug",
353 "TRACE",
354 "5",
355 "off",
356 "warn,spark=trace",
357 "spark",
358 "spark=",
359 "spark::=debug",
360 "spark=debug,spark=warn",
361 "debug,info",
362 "info,debug",
363 "info,,debug",
364 "spark=debug,",
365 "debug,spark=off",
366 "error,breez_sdk_spark::sdk=trace,breez_sdk_spark=info",
367 "garbage!!,debug",
368 "=debug",
369 "spark=verbose",
370 "info;spark=debug",
371 "Spark=debug",
372 ];
373 for filter in filters {
374 assert_eq!(
375 enabled_events(parse_log_filter(filter)),
376 enabled_events(EnvFilter::new(filter)),
377 "filter `{filter}`"
378 );
379 }
380 }
381
382 #[test]
383 fn log_filter_ignores_span_and_field_directives() {
384 assert_eq!(
385 enabled_events(parse_log_filter("warn,spark[sync]=debug,[{id=1}]=trace")),
386 enabled_events(parse_log_filter("warn"))
387 );
388 }
389
390 #[test]
391 fn log_filter_tolerates_whitespace() {
392 assert_eq!(
393 enabled_events(parse_log_filter("info, spark = debug ")),
394 enabled_events(parse_log_filter("info,spark=debug"))
395 );
396 }
397
398 #[derive(Clone, Default)]
400 struct EntryRecorder(Arc<Mutex<Vec<LogEntry>>>);
401
402 impl Logger for EntryRecorder {
403 fn log(&self, l: LogEntry) {
404 self.0.lock().unwrap().push(l);
405 }
406 }
407
408 impl EntryRecorder {
409 fn subscriber(&self, filter: &str) -> impl tracing::Subscriber + Send + Sync {
411 tracing_subscriber::registry().with(
412 GlobalSdkLogger {
413 log_listener: Some(Box::new(self.clone())),
414 }
415 .with_filter(sdk_log_filter(Some(filter))),
416 )
417 }
418
419 fn instance_of(&self, line: &str) -> Option<String> {
420 let entries = self.0.lock().unwrap();
421 let entry = entries
422 .iter()
423 .find(|entry| entry.line == line)
424 .unwrap_or_else(|| panic!("`{line}` not logged, got {entries:?}"));
425 entry.instance_id.clone()
426 }
427 }
428
429 #[test]
430 fn external_logger_receives_closest_instance() {
431 let recorder = EntryRecorder::default();
432 tracing::subscriber::with_default(recorder.subscriber("brz_logtest_instance=info"), || {
433 info!(target: "brz_logtest_instance", "outside");
434 instance_span("alice").in_scope(|| {
435 tracing::info_span!(target: "brz_logtest_instance", "inner").in_scope(|| {
437 info!(target: "brz_logtest_instance", "in alice");
438 });
439 instance_span("bob").in_scope(|| {
440 info!(target: "brz_logtest_instance", "in bob");
441 });
442 });
443 });
444 assert_eq!(recorder.instance_of("outside"), None);
445 assert_eq!(recorder.instance_of("in alice").as_deref(), Some("alice"));
446 assert_eq!(recorder.instance_of("in bob").as_deref(), Some("bob"));
447 }
448
449 #[test]
450 fn instance_survives_filters_that_exclude_its_span() {
451 let recorder = EntryRecorder::default();
452 let filter = "off,brz_logtest_filtered=warn";
455 tracing::subscriber::with_default(recorder.subscriber(filter), || {
456 instance_span("alice").in_scope(|| {
457 warn!(target: "brz_logtest_filtered", "warned");
458 });
459 });
460 assert_eq!(recorder.instance_of("warned").as_deref(), Some("alice"));
461 }
462
463 #[derive(Clone, Default)]
465 struct Buffer(Arc<Mutex<Vec<u8>>>);
466
467 impl std::io::Write for Buffer {
468 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
469 self.0.lock().unwrap().extend_from_slice(buf);
470 Ok(buf.len())
471 }
472
473 fn flush(&mut self) -> std::io::Result<()> {
474 Ok(())
475 }
476 }
477
478 impl Buffer {
479 fn subscriber(
482 &self,
483 filter: tracing_subscriber::filter::Targets,
484 ) -> impl tracing::Subscriber + Send + Sync {
485 let writer = self.clone();
486 tracing_subscriber::registry().with(
487 tracing_subscriber::fmt::layer()
488 .with_ansi(false)
489 .with_writer(move || writer.clone())
490 .with_filter(filter),
491 )
492 }
493
494 fn contents(&self) -> String {
495 String::from_utf8(self.0.lock().unwrap().clone()).unwrap()
496 }
497 }
498
499 #[test]
500 fn log_file_leaves_out_instance_spans() {
501 let buffer = Buffer::default();
502 tracing::subscriber::with_default(buffer.subscriber(file_log_filter(None)), || {
503 instance_span("alice").in_scope(|| {
504 tracing::info_span!(target: "brz_logtest_file", "inner").in_scope(|| {
505 info!(target: "brz_logtest_file", "logged");
506 });
507 });
508 });
509
510 let output = buffer.contents();
511 assert!(
512 output.contains("inner: brz_logtest_file: logged"),
513 "got {output}"
514 );
515 assert!(!output.contains("alice"), "got {output}");
516 }
517
518 struct Instance {
519 instance_id: String,
520 }
521
522 #[macros::instance_span]
523 impl Instance {
524 pub async fn log_here_and_in_task(&self) {
525 info!(target: "brz_logtest_macro", "in method");
526 platform_utils::spawn(async { info!(target: "brz_logtest_macro", "in task") })
527 .await
528 .unwrap();
529 }
530
531 #[tracing::instrument(
532 target = "brz_logtest_record",
533 skip_all,
534 fields(payment_id = tracing::field::Empty),
535 )]
536 pub fn record_in_own_span(&self) {
537 tracing::Span::current().record("payment_id", "abc");
538 info!(target: "brz_logtest_record", "recorded");
539 }
540 }
541
542 #[test]
543 fn instance_span_macro_leaves_method_span_current() {
544 let buffer = Buffer::default();
545 let subscriber = buffer.subscriber(sdk_log_filter(Some("brz_logtest_record=info")));
546 tracing::subscriber::with_default(subscriber, || {
547 Instance {
548 instance_id: "alice".to_string(),
549 }
550 .record_in_own_span();
551 });
552 let output = buffer.contents();
553 assert!(
554 output.contains("record_in_own_span{payment_id=\"abc\"}"),
555 "got {output}"
556 );
557 }
558
559 #[macros::async_test_all]
560 async fn instance_span_macro_attributes_method_and_spawned_tasks() {
561 let recorder = EntryRecorder::default();
562 let _guard =
563 tracing::subscriber::set_default(recorder.subscriber("brz_logtest_macro=info"));
564 Instance {
565 instance_id: "alice".to_string(),
566 }
567 .log_here_and_in_task()
568 .await;
569 assert_eq!(recorder.instance_of("in method").as_deref(), Some("alice"));
570 assert_eq!(recorder.instance_of("in task").as_deref(), Some("alice"));
571 }
572}