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}