Skip to main content

rusty_cat/
log.rs

1//! Flow-level debug logging utilities.
2//!
3//! At most one global listener can be registered. When no listener is present,
4//! `emit`/`emit_lazy` return quickly with minimal overhead. Listener panics are
5//! isolated with `catch_unwind` so SDK internal logic remains safe.
6
7use std::fmt;
8use std::panic::{self, AssertUnwindSafe};
9use std::sync::{Arc, Mutex, OnceLock};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12/// Log severity level for flow debug entries.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum LogLevel {
15    Debug,
16    Info,
17    Warn,
18}
19
20impl fmt::Display for LogLevel {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        let s = match self {
23            LogLevel::Debug => "DEBUG",
24            LogLevel::Info => "INFO",
25            LogLevel::Warn => "WARN",
26        };
27        f.write_str(s)
28    }
29}
30
31/// One structured log record that can be printed or persisted externally.
32#[derive(Debug, Clone)]
33pub struct Log {
34    /// Unix epoch timestamp in milliseconds.
35    timestamp_ms: u64,
36    /// Log severity level.
37    level: LogLevel,
38    /// Static tag such as `"meow_client"` or `"enqueue"` for filtering.
39    tag: &'static str,
40    /// Human-readable message content.
41    message: String,
42}
43
44impl Log {
45    /// Creates a log entry with explicit level and tag.
46    ///
47    /// # Examples
48    ///
49    /// ```no_run
50    /// use rusty_cat::api::{Log, LogLevel};
51    ///
52    /// let log = Log::new(LogLevel::Info, "demo", "hello");
53    /// assert_eq!(log.level(), LogLevel::Info);
54    /// ```
55    pub fn new(level: LogLevel, tag: &'static str, message: impl Into<String>) -> Self {
56        let timestamp_ms = SystemTime::now()
57            .duration_since(UNIX_EPOCH)
58            .map(|d| d.as_millis() as u64)
59            .unwrap_or(0);
60        Self {
61            timestamp_ms,
62            level,
63            tag,
64            message: message.into(),
65        }
66    }
67
68    /// Creates a debug-level log entry.
69    ///
70    /// # Examples
71    ///
72    /// ```no_run
73    /// use rusty_cat::api::Log;
74    ///
75    /// let log = Log::debug("demo", "debug message");
76    /// assert_eq!(log.tag(), "demo");
77    /// ```
78    pub fn debug(tag: &'static str, message: impl Into<String>) -> Self {
79        Self::new(LogLevel::Debug, tag, message)
80    }
81
82    /// Returns timestamp in milliseconds.
83    ///
84    /// # Examples
85    ///
86    /// ```no_run
87    /// use rusty_cat::api::Log;
88    ///
89    /// let log = Log::debug("demo", "message");
90    /// let _ts = log.timestamp_ms();
91    /// ```
92    pub fn timestamp_ms(&self) -> u64 {
93        self.timestamp_ms
94    }
95
96    /// Returns log level.
97    ///
98    /// # Examples
99    ///
100    /// ```no_run
101    /// use rusty_cat::api::{Log, LogLevel};
102    ///
103    /// let log = Log::new(LogLevel::Warn, "demo", "warn");
104    /// assert_eq!(log.level(), LogLevel::Warn);
105    /// ```
106    pub fn level(&self) -> LogLevel {
107        self.level
108    }
109
110    /// Returns static tag.
111    ///
112    /// # Examples
113    ///
114    /// ```no_run
115    /// use rusty_cat::api::Log;
116    ///
117    /// let log = Log::debug("network", "retry");
118    /// assert_eq!(log.tag(), "network");
119    /// ```
120    pub fn tag(&self) -> &'static str {
121        self.tag
122    }
123
124    /// Returns message by shared reference.
125    ///
126    /// # Examples
127    ///
128    /// ```no_run
129    /// use rusty_cat::api::Log;
130    ///
131    /// let log = Log::debug("demo", "hello");
132    /// assert_eq!(log.message(), "hello");
133    /// ```
134    pub fn message(&self) -> &str {
135        &self.message
136    }
137
138    /// Consumes the entry and returns owned message.
139    ///
140    /// # Examples
141    ///
142    /// ```no_run
143    /// use rusty_cat::api::Log;
144    ///
145    /// let log = Log::debug("demo", "hello");
146    /// let msg = log.into_message();
147    /// assert_eq!(msg, "hello");
148    /// ```
149    pub fn into_message(self) -> String {
150        self.message
151    }
152}
153
154impl fmt::Display for Log {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        write!(
157            f,
158            "[{}] {} [{}] {}",
159            self.timestamp_ms, self.level, self.tag, self.message
160        )
161    }
162}
163
164/// Callback type for global debug log listener.
165pub type DebugLogListener = Arc<dyn Fn(Log) + Send + Sync + 'static>;
166
167static DEBUG_LOG_LISTENER: OnceLock<Mutex<Option<DebugLogListener>>> = OnceLock::new();
168
169fn debug_log_listener_slot() -> &'static Mutex<Option<DebugLogListener>> {
170    DEBUG_LOG_LISTENER.get_or_init(|| Mutex::new(None))
171}
172
173/// Returns whether a debug log listener is currently registered.
174///
175/// Useful on hot paths to avoid constructing log objects unnecessarily.
176///
177/// # Examples
178///
179/// ```no_run
180/// use rusty_cat::api::debug_log_listener_active;
181///
182/// let _active = debug_log_listener_active();
183/// ```
184#[inline]
185pub fn debug_log_listener_active() -> bool {
186    match debug_log_listener_slot().lock() {
187        Ok(g) => g.is_some(),
188        Err(_) => false,
189    }
190}
191
192/// Sets or clears the global debug log listener.
193///
194/// - `Some(listener)`: set or replace current listener.
195/// - `None`: clear listener (unregister).
196///
197/// # Errors
198///
199/// Returns [`DebugLogListenerError`] when internal listener storage lock is
200/// poisoned.
201///
202/// # Examples
203///
204/// ```no_run
205/// use std::sync::Arc;
206/// use rusty_cat::api::{set_debug_log_listener, DebugLogListener, Log};
207///
208/// let listener: DebugLogListener = Arc::new(|log: Log| println!("{log}"));
209/// set_debug_log_listener(Some(listener))?;
210/// set_debug_log_listener(None)?;
211/// # Ok::<(), rusty_cat::api::DebugLogListenerError>(())
212/// ```
213pub fn set_debug_log_listener(
214    listener: Option<DebugLogListener>,
215) -> Result<(), DebugLogListenerError> {
216    let mut g = debug_log_listener_slot()
217        .lock()
218        .map_err(|_| DebugLogListenerError(()))?;
219    *g = listener;
220    Ok(())
221}
222
223/// Registers a global singleton debug log listener.
224///
225/// Returns `Err` if a listener is already registered.
226///
227/// # Errors
228///
229/// Returns [`DebugLogListenerError`] when:
230/// - a listener is already registered, or
231/// - internal listener storage lock is poisoned.
232///
233/// # Examples
234///
235/// ```no_run
236/// use rusty_cat::api::{try_set_debug_log_listener, Log};
237///
238/// let _ = try_set_debug_log_listener(|log: Log| {
239///     println!("{log}");
240/// });
241/// ```
242pub fn try_set_debug_log_listener<F>(f: F) -> Result<(), DebugLogListenerError>
243where
244    F: Fn(Log) + Send + Sync + 'static,
245{
246    let mut g = debug_log_listener_slot()
247        .lock()
248        .map_err(|_| DebugLogListenerError(()))?;
249    if g.is_some() {
250        return Err(DebugLogListenerError(()));
251    }
252    *g = Some(Arc::new(f));
253    Ok(())
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub struct DebugLogListenerError(());
258
259impl fmt::Display for DebugLogListenerError {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        f.write_str("debug log listener already set")
262    }
263}
264
265impl std::error::Error for DebugLogListenerError {}
266
267/// Emits one log entry.
268///
269/// Returns immediately when no listener is set. Listener panics are caught and
270/// discarded.
271///
272/// # Panics
273///
274/// This function does not panic. Listener panics are caught internally.
275///
276/// # Examples
277///
278/// ```no_run
279/// use rusty_cat::api::{emit, Log};
280///
281/// emit(Log::debug("demo", "manual emit"));
282/// ```
283pub fn emit(log: Log) {
284    let cb_opt = debug_log_listener_slot()
285        .lock()
286        .ok()
287        .and_then(|g| g.as_ref().map(Arc::clone));
288    let Some(cb) = cb_opt else {
289        return;
290    };
291    let _ = panic::catch_unwind(AssertUnwindSafe(move || {
292        cb(log);
293    }));
294}
295
296/// Lazily emits a log entry only when listener is active.
297///
298/// This avoids formatting/allocation overhead when logging is disabled.
299///
300/// # Panics
301///
302/// This function does not panic. Any panic from listener callback is caught by
303/// [`emit`].
304///
305/// # Examples
306///
307/// ```no_run
308/// use rusty_cat::api::{emit_lazy, Log};
309///
310/// emit_lazy(|| Log::debug("demo", format!("computed {}", 42)));
311/// ```
312#[inline]
313pub fn emit_lazy<F>(f: F)
314where
315    F: FnOnce() -> Log,
316{
317    if !debug_log_listener_active() {
318        return;
319    }
320    emit(f());
321}
322
323/// Internal flow debug logging macro.
324///
325/// The `format!` expression is evaluated only when listener is active.
326/// crate::meow_flow_log!(
327///     "enqueue",
328///    "task_id={:?} offset={} total={}",
329///     task_id,
330///     offset,
331///    total
332/// );
333#[macro_export]
334macro_rules! meow_flow_log {
335    ($tag:expr, $($arg:tt)*) => {
336        $crate::log::emit_lazy(|| {
337            $crate::log::Log::debug($tag, format!($($arg)*))
338        });
339    };
340}