glsdk/logging.rs
1// SDK logging — apps install a LogListener to receive log output
2// emitted by this SDK and by the underlying gl-client library.
3//
4// The bridge sits on top of the `log` crate facade: gl-sdk's own
5// `tracing` calls are routed via `tracing`'s `log` feature, and
6// gl-client's direct `log::*!` calls flow through the same channel.
7
8use crate::Error;
9use log::{Level, LevelFilter, Log, Metadata, Record};
10
11/// Log level for filtering messages.
12#[derive(Clone, Copy, Debug, uniffi::Enum)]
13pub enum LogLevel {
14 Error,
15 Warn,
16 Info,
17 Debug,
18 Trace,
19}
20
21impl From<Level> for LogLevel {
22 fn from(level: Level) -> Self {
23 match level {
24 Level::Error => LogLevel::Error,
25 Level::Warn => LogLevel::Warn,
26 Level::Info => LogLevel::Info,
27 Level::Debug => LogLevel::Debug,
28 Level::Trace => LogLevel::Trace,
29 }
30 }
31}
32
33impl From<LogLevel> for LevelFilter {
34 fn from(level: LogLevel) -> Self {
35 match level {
36 LogLevel::Error => LevelFilter::Error,
37 LogLevel::Warn => LevelFilter::Warn,
38 LogLevel::Info => LevelFilter::Info,
39 LogLevel::Debug => LevelFilter::Debug,
40 LogLevel::Trace => LevelFilter::Trace,
41 }
42 }
43}
44
45/// A single log message from the SDK.
46#[derive(Clone, Debug, uniffi::Record)]
47pub struct LogEntry {
48 pub level: LogLevel,
49 pub message: String,
50 /// The module that produced this log (e.g. "gl_client::scheduler").
51 pub target: String,
52 /// Source file path, if the log macro recorded one.
53 pub file: Option<String>,
54 /// Source line number, if the log macro recorded one.
55 pub line: Option<u32>,
56}
57
58/// Callback interface for receiving log messages.
59///
60/// `on_log` is invoked on the thread that emitted the log — which can
61/// be any tokio worker or background thread inside the SDK. Keep the
62/// implementation cheap and non-blocking; if you need UI updates,
63/// hand the entry off to your app's main thread.
64#[uniffi::export(callback_interface)]
65pub trait LogListener: Send + Sync {
66 fn on_log(&self, entry: LogEntry);
67}
68
69struct SdkLogger {
70 listener: Box<dyn LogListener>,
71}
72
73impl Log for SdkLogger {
74 fn enabled(&self, metadata: &Metadata) -> bool {
75 // Use the `log` crate's global max level so `set_log_level`
76 // can change the filter at runtime. `log::max_level()` is an
77 // atomic read that every log macro also consults.
78 metadata.level() <= log::max_level()
79 }
80
81 fn log(&self, record: &Record) {
82 if self.enabled(record.metadata()) {
83 self.listener.on_log(LogEntry {
84 level: record.level().into(),
85 message: record.args().to_string(),
86 target: record.target().to_string(),
87 file: record.file().map(|s| s.to_string()),
88 line: record.line(),
89 });
90 }
91 }
92
93 fn flush(&self) {}
94}
95
96/// Install a log listener. Call this once, as early as possible, so
97/// logs emitted during node bring-up are captured.
98///
99/// Returns `Err` if a logger is already installed in the process
100/// (either from an earlier successful call to `set_logger` or from a
101/// different crate). To change the filter after installation use
102/// `set_log_level`.
103pub fn set_logger(level: LogLevel, listener: Box<dyn LogListener>) -> Result<(), Error> {
104 let filter: LevelFilter = level.into();
105 log::set_boxed_logger(Box::new(SdkLogger { listener })).map_err(|e| {
106 Error::other(format!("a `log` logger is already installed: {e}"))
107 })?;
108 log::set_max_level(filter);
109 Ok(())
110}
111
112/// Change the log filter at runtime without reinstalling the listener.
113///
114/// Useful for features like a "verbose logs" toggle in app settings.
115/// Safe to call before `set_logger` — it adjusts the `log` crate's
116/// global max level immediately; the listener (if any) picks it up
117/// on the next emitted message.
118pub fn set_log_level(level: LogLevel) {
119 log::set_max_level(level.into());
120}