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//!
7//! # Log levels and persistence
8//!
9//! Consumers register a single listener via [`set_debug_log_listener`] and
10//! receive every [`Log`] the SDK emits. Each entry carries a [`LogLevel`] that
11//! tells the consumer how to treat it. Use [`LogLevel::persist_recommended`] to
12//! decide what to keep:
13//!
14//! | Level | Meaning | Persist? |
15//! |-------|---------|----------|
16//! | [`LogLevel::Trace`] | per-chunk / per-poll / per-retry hot-loop noise | **No** — drop or sample only |
17//! | [`LogLevel::Debug`] | low-volume diagnostics | optional / short-term only |
18//! | [`LogLevel::Info`] | normal operational information | optional |
19//! | [`LogLevel::Key`] | key-node checkpoint for troubleshooting | **Yes** — keep and forward to the SDK author |
20//! | [`LogLevel::Warn`] | recoverable anomaly / misuse / backpressure | **Yes** |
21//! | [`LogLevel::Error`] | failure with full detail, or a caught panic | **Yes** |
22//!
23//! The author-forwardable troubleshooting stream is everything at
24//! `>= LogLevel::Key`, i.e. `Key | Warn | Error`. Never persist a high-frequency
25//! `Trace` entry, and never persist any URL or task dump that has not been run
26//! through [`sanitize_url`].
27
28use std::fmt;
29use std::panic::{self, AssertUnwindSafe};
30use std::sync::{Arc, Mutex, OnceLock};
31use std::time::{SystemTime, UNIX_EPOCH};
32
33/// Log severity level for flow debug entries.
34///
35/// Ordered from least to most important: `Trace < Debug < Info < Key < Warn <
36/// Error`. Consumers can filter the author-forwardable stream with
37/// `level >= LogLevel::Key`.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub enum LogLevel {
40    /// Very high frequency, per-chunk / per-poll / per-retry hot-loop noise.
41    /// Consumers MUST NOT persist these (drop or sample only).
42    Trace,
43    /// Low-volume diagnostics. Persisting is optional / short-term only.
44    Debug,
45    /// Normal operational information. Persisting is optional.
46    Info,
47    /// Key-node checkpoint the SDK author reviews when a consumer reports a bug.
48    /// Consumers SHOULD persist these and forward them for troubleshooting.
49    Key,
50    /// Recoverable anomaly, misuse, or backpressure. Consumers SHOULD persist.
51    Warn,
52    /// Failure with full detail — a chunk/part upload or download failed, a task
53    /// failed, an HTTP error response, a signing failure — or a caught panic.
54    /// Consumers SHOULD persist these together with their full context.
55    Error,
56}
57
58impl LogLevel {
59    /// Returns whether consumers are recommended to persist entries at this
60    /// level. True for [`LogLevel::Key`], [`LogLevel::Warn`] and
61    /// [`LogLevel::Error`]; false for the higher-frequency / lower-value
62    /// [`LogLevel::Trace`], [`LogLevel::Debug`] and [`LogLevel::Info`].
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use rusty_cat::api::LogLevel;
68    ///
69    /// assert!(LogLevel::Error.persist_recommended());
70    /// assert!(LogLevel::Key.persist_recommended());
71    /// assert!(!LogLevel::Trace.persist_recommended());
72    /// ```
73    #[inline]
74    pub fn persist_recommended(&self) -> bool {
75        matches!(self, LogLevel::Key | LogLevel::Warn | LogLevel::Error)
76    }
77
78    /// Returns the static upper-case label for this level.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use rusty_cat::api::LogLevel;
84    ///
85    /// assert_eq!(LogLevel::Error.as_str(), "ERROR");
86    /// ```
87    pub fn as_str(&self) -> &'static str {
88        match self {
89            LogLevel::Trace => "TRACE",
90            LogLevel::Debug => "DEBUG",
91            LogLevel::Info => "INFO",
92            LogLevel::Key => "KEY",
93            LogLevel::Warn => "WARN",
94            LogLevel::Error => "ERROR",
95        }
96    }
97}
98
99impl fmt::Display for LogLevel {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.write_str(self.as_str())
102    }
103}
104
105/// One structured log record that can be printed or persisted externally.
106///
107/// Besides the human-readable [`Log::message`], an entry may carry optional
108/// structured context for triage — task id, object key, part index, byte
109/// offset/length, HTTP status, retry attempt and a sanitized URL. These are set
110/// through the `with_*` builder methods and are most valuable on
111/// [`LogLevel::Error`] and [`LogLevel::Key`] entries. [`Log::Display`] appends
112/// every present field as ` key=value`, so a persisted log line is
113/// self-describing.
114#[derive(Debug, Clone)]
115pub struct Log {
116    /// Unix epoch timestamp in milliseconds.
117    timestamp_ms: u64,
118    /// Log severity level.
119    level: LogLevel,
120    /// Static tag such as `"meow_client"` or `"enqueue"` for filtering.
121    tag: &'static str,
122    /// Human-readable message content.
123    message: String,
124    /// Owning task id, when known.
125    task_id: Option<String>,
126    /// Object / blob key being transferred, when known.
127    object_key: Option<String>,
128    /// Zero-based part or chunk index, when known.
129    part_index: Option<u64>,
130    /// Byte offset within the file, when known.
131    offset: Option<u64>,
132    /// Byte length of the chunk/range, when known.
133    byte_len: Option<u64>,
134    /// HTTP status code associated with a failure, when known.
135    http_status: Option<u16>,
136    /// Retry attempt number, when known.
137    attempt: Option<u32>,
138    /// Maximum retry attempts configured, when known.
139    max_retries: Option<u32>,
140    /// SDK error code, when known.
141    error_code: Option<i32>,
142    /// URL associated with the entry — ALWAYS sanitized via [`sanitize_url`].
143    url: Option<String>,
144}
145
146impl Log {
147    /// Creates a log entry with explicit level and tag (no structured context).
148    ///
149    /// # Examples
150    ///
151    /// ```no_run
152    /// use rusty_cat::api::{Log, LogLevel};
153    ///
154    /// let log = Log::new(LogLevel::Info, "demo", "hello");
155    /// assert_eq!(log.level(), LogLevel::Info);
156    /// ```
157    pub fn new(level: LogLevel, tag: &'static str, message: impl Into<String>) -> Self {
158        let timestamp_ms = SystemTime::now()
159            .duration_since(UNIX_EPOCH)
160            .map(|d| d.as_millis() as u64)
161            .unwrap_or(0);
162        Self {
163            timestamp_ms,
164            level,
165            tag,
166            message: message.into(),
167            task_id: None,
168            object_key: None,
169            part_index: None,
170            offset: None,
171            byte_len: None,
172            http_status: None,
173            attempt: None,
174            max_retries: None,
175            error_code: None,
176            url: None,
177        }
178    }
179
180    /// Creates a [`LogLevel::Trace`] entry (high-frequency; do not persist).
181    ///
182    /// # Examples
183    ///
184    /// ```no_run
185    /// use rusty_cat::api::Log;
186    ///
187    /// let log = Log::trace("download_chunk", "wrote chunk");
188    /// assert_eq!(log.tag(), "download_chunk");
189    /// ```
190    pub fn trace(tag: &'static str, message: impl Into<String>) -> Self {
191        Self::new(LogLevel::Trace, tag, message)
192    }
193
194    /// Creates a [`LogLevel::Debug`] entry.
195    ///
196    /// # Examples
197    ///
198    /// ```no_run
199    /// use rusty_cat::api::Log;
200    ///
201    /// let log = Log::debug("demo", "debug message");
202    /// assert_eq!(log.tag(), "demo");
203    /// ```
204    pub fn debug(tag: &'static str, message: impl Into<String>) -> Self {
205        Self::new(LogLevel::Debug, tag, message)
206    }
207
208    /// Creates a [`LogLevel::Info`] entry.
209    ///
210    /// # Examples
211    ///
212    /// ```no_run
213    /// use rusty_cat::api::Log;
214    ///
215    /// let log = Log::info("demo", "info message");
216    /// assert_eq!(log.tag(), "demo");
217    /// ```
218    pub fn info(tag: &'static str, message: impl Into<String>) -> Self {
219        Self::new(LogLevel::Info, tag, message)
220    }
221
222    /// Creates a [`LogLevel::Key`] entry (key-node checkpoint; persist & forward).
223    ///
224    /// # Examples
225    ///
226    /// ```no_run
227    /// use rusty_cat::api::Log;
228    ///
229    /// let log = Log::key("enqueue", "task enqueued");
230    /// assert_eq!(log.tag(), "enqueue");
231    /// ```
232    pub fn key(tag: &'static str, message: impl Into<String>) -> Self {
233        Self::new(LogLevel::Key, tag, message)
234    }
235
236    /// Creates a [`LogLevel::Warn`] entry.
237    ///
238    /// # Examples
239    ///
240    /// ```no_run
241    /// use rusty_cat::api::Log;
242    ///
243    /// let log = Log::warn("cleanup", "abort failed");
244    /// assert_eq!(log.tag(), "cleanup");
245    /// ```
246    pub fn warn(tag: &'static str, message: impl Into<String>) -> Self {
247        Self::new(LogLevel::Warn, tag, message)
248    }
249
250    /// Creates a [`LogLevel::Error`] entry (failure; persist with full context).
251    ///
252    /// # Examples
253    ///
254    /// ```no_run
255    /// use rusty_cat::api::Log;
256    ///
257    /// let log = Log::error("upload_part", "part upload failed").with_part(3);
258    /// assert_eq!(log.part_index(), Some(3));
259    /// ```
260    pub fn error(tag: &'static str, message: impl Into<String>) -> Self {
261        Self::new(LogLevel::Error, tag, message)
262    }
263
264    /// Attaches the owning task id.
265    #[must_use]
266    pub fn with_task_id(mut self, task_id: impl Into<String>) -> Self {
267        self.task_id = Some(task_id.into());
268        self
269    }
270
271    /// Attaches the object / blob key being transferred.
272    #[must_use]
273    pub fn with_key(mut self, key: impl Into<String>) -> Self {
274        self.object_key = Some(key.into());
275        self
276    }
277
278    /// Attaches the zero-based part / chunk index.
279    #[must_use]
280    pub fn with_part(mut self, part_index: u64) -> Self {
281        self.part_index = Some(part_index);
282        self
283    }
284
285    /// Attaches the byte offset within the file.
286    #[must_use]
287    pub fn with_offset(mut self, offset: u64) -> Self {
288        self.offset = Some(offset);
289        self
290    }
291
292    /// Attaches the byte length of the chunk / range.
293    #[must_use]
294    pub fn with_byte_len(mut self, byte_len: u64) -> Self {
295        self.byte_len = Some(byte_len);
296        self
297    }
298
299    /// Attaches both the byte offset and the byte length of a range.
300    #[must_use]
301    pub fn with_range(mut self, offset: u64, byte_len: u64) -> Self {
302        self.offset = Some(offset);
303        self.byte_len = Some(byte_len);
304        self
305    }
306
307    /// Attaches the HTTP status code associated with a failure.
308    #[must_use]
309    pub fn with_http_status(mut self, status: u16) -> Self {
310        self.http_status = Some(status);
311        self
312    }
313
314    /// Attaches the retry attempt number.
315    #[must_use]
316    pub fn with_attempt(mut self, attempt: u32) -> Self {
317        self.attempt = Some(attempt);
318        self
319    }
320
321    /// Attaches the configured maximum number of retry attempts.
322    #[must_use]
323    pub fn with_max_retries(mut self, max_retries: u32) -> Self {
324        self.max_retries = Some(max_retries);
325        self
326    }
327
328    /// Attaches the SDK error code.
329    #[must_use]
330    pub fn with_error_code(mut self, code: i32) -> Self {
331        self.error_code = Some(code);
332        self
333    }
334
335    /// Attaches a URL, automatically passing it through [`sanitize_url`] so that
336    /// SAS tokens, signatures and credentials never reach a persisted log.
337    #[must_use]
338    pub fn with_url(mut self, url: impl AsRef<str>) -> Self {
339        self.url = Some(sanitize_url(url.as_ref()));
340        self
341    }
342
343    /// Returns timestamp in milliseconds.
344    ///
345    /// # Examples
346    ///
347    /// ```no_run
348    /// use rusty_cat::api::Log;
349    ///
350    /// let log = Log::debug("demo", "message");
351    /// let _ts = log.timestamp_ms();
352    /// ```
353    pub fn timestamp_ms(&self) -> u64 {
354        self.timestamp_ms
355    }
356
357    /// Returns log level.
358    ///
359    /// # Examples
360    ///
361    /// ```no_run
362    /// use rusty_cat::api::{Log, LogLevel};
363    ///
364    /// let log = Log::new(LogLevel::Warn, "demo", "warn");
365    /// assert_eq!(log.level(), LogLevel::Warn);
366    /// ```
367    pub fn level(&self) -> LogLevel {
368        self.level
369    }
370
371    /// Returns static tag.
372    ///
373    /// # Examples
374    ///
375    /// ```no_run
376    /// use rusty_cat::api::Log;
377    ///
378    /// let log = Log::debug("network", "retry");
379    /// assert_eq!(log.tag(), "network");
380    /// ```
381    pub fn tag(&self) -> &'static str {
382        self.tag
383    }
384
385    /// Returns message by shared reference.
386    ///
387    /// # Examples
388    ///
389    /// ```no_run
390    /// use rusty_cat::api::Log;
391    ///
392    /// let log = Log::debug("demo", "hello");
393    /// assert_eq!(log.message(), "hello");
394    /// ```
395    pub fn message(&self) -> &str {
396        &self.message
397    }
398
399    /// Returns the owning task id, if set.
400    pub fn task_id(&self) -> Option<&str> {
401        self.task_id.as_deref()
402    }
403
404    /// Returns the object / blob key, if set.
405    pub fn object_key(&self) -> Option<&str> {
406        self.object_key.as_deref()
407    }
408
409    /// Returns the part / chunk index, if set.
410    pub fn part_index(&self) -> Option<u64> {
411        self.part_index
412    }
413
414    /// Returns the byte offset, if set.
415    pub fn offset(&self) -> Option<u64> {
416        self.offset
417    }
418
419    /// Returns the byte length, if set.
420    pub fn byte_len(&self) -> Option<u64> {
421        self.byte_len
422    }
423
424    /// Returns the HTTP status code, if set.
425    pub fn http_status(&self) -> Option<u16> {
426        self.http_status
427    }
428
429    /// Returns the retry attempt number, if set.
430    pub fn attempt(&self) -> Option<u32> {
431        self.attempt
432    }
433
434    /// Returns the configured maximum retry attempts, if set.
435    pub fn max_retries(&self) -> Option<u32> {
436        self.max_retries
437    }
438
439    /// Returns the SDK error code, if set.
440    pub fn error_code(&self) -> Option<i32> {
441        self.error_code
442    }
443
444    /// Returns the sanitized URL, if set.
445    pub fn url(&self) -> Option<&str> {
446        self.url.as_deref()
447    }
448
449    /// Consumes the entry and returns owned message.
450    ///
451    /// # Examples
452    ///
453    /// ```no_run
454    /// use rusty_cat::api::Log;
455    ///
456    /// let log = Log::debug("demo", "hello");
457    /// let msg = log.into_message();
458    /// assert_eq!(msg, "hello");
459    /// ```
460    pub fn into_message(self) -> String {
461        self.message
462    }
463}
464
465impl fmt::Display for Log {
466    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467        write!(
468            f,
469            "[{}] {} [{}] {}",
470            self.timestamp_ms, self.level, self.tag, self.message
471        )?;
472        if let Some(v) = &self.task_id {
473            write!(f, " task_id={v}")?;
474        }
475        if let Some(v) = &self.object_key {
476            write!(f, " key={v}")?;
477        }
478        if let Some(v) = self.part_index {
479            write!(f, " part={v}")?;
480        }
481        if let Some(v) = self.offset {
482            write!(f, " offset={v}")?;
483        }
484        if let Some(v) = self.byte_len {
485            write!(f, " len={v}")?;
486        }
487        if let Some(v) = self.http_status {
488            write!(f, " http_status={v}")?;
489        }
490        if let Some(v) = self.attempt {
491            write!(f, " attempt={v}")?;
492        }
493        if let Some(v) = self.max_retries {
494            write!(f, " max_retries={v}")?;
495        }
496        if let Some(v) = self.error_code {
497            write!(f, " error_code={v}")?;
498        }
499        if let Some(v) = &self.url {
500            write!(f, " url={v}")?;
501        }
502        Ok(())
503    }
504}
505
506/// Returns `true` when a URL query parameter name carries a secret (signature,
507/// credential, security token, access key) that must never be logged.
508fn is_sensitive_param(key: &str) -> bool {
509    let k = key.trim().to_ascii_lowercase();
510    const EXACT: &[&str] = &[
511        "sig",
512        "signature",
513        "x-oss-signature",
514        "ossaccesskeyid",
515        "x-oss-credential",
516        "x-oss-security-token",
517        "security-token",
518        "x-amz-signature",
519        "x-amz-credential",
520        "x-amz-security-token",
521    ];
522    if EXACT.contains(&k.as_str()) {
523        return true;
524    }
525    k.contains("sig")
526        || k.contains("credential")
527        || k.contains("token")
528        || k.contains("secret")
529        || k.contains("password")
530        || k.contains("accesskey")
531}
532
533/// Redacts secret query parameters from a URL so it is safe to log or persist.
534///
535/// The path and non-secret query parameters (for example a SAS `se` expiry or
536/// `sp` permissions, the Aliyun `Expires`, an OSS `partNumber`) are kept intact;
537/// the *values* of signature / credential / security-token / access-key
538/// parameters are replaced with `REDACTED`. URLs without a query string are
539/// returned unchanged.
540///
541/// # Examples
542///
543/// ```
544/// use rusty_cat::api::sanitize_url;
545///
546/// let safe = sanitize_url("https://x.blob.core.windows.net/c/b?sv=2021&se=2030-01-01&sig=AbC%2Bsecret");
547/// assert!(safe.contains("sv=2021"));
548/// assert!(safe.contains("se=2030-01-01"));
549/// assert!(safe.contains("sig=REDACTED"));
550/// assert!(!safe.contains("AbC"));
551/// ```
552pub fn sanitize_url(url: &str) -> String {
553    let Some((base, query)) = url.split_once('?') else {
554        return url.to_string();
555    };
556    let mut out = String::with_capacity(url.len());
557    out.push_str(base);
558    out.push('?');
559    let mut first = true;
560    for pair in query.split('&') {
561        if !first {
562            out.push('&');
563        }
564        first = false;
565        match pair.split_once('=') {
566            Some((k, _)) if is_sensitive_param(k) => {
567                out.push_str(k);
568                out.push_str("=REDACTED");
569            }
570            _ => out.push_str(pair),
571        }
572    }
573    out
574}
575
576fn is_token_char(c: char) -> bool {
577    c.is_ascii_alphanumeric() || c == '_' || c == '-'
578}
579
580fn is_value_delim(c: char) -> bool {
581    c.is_whitespace()
582        || matches!(
583            c,
584            '&' | '"' | '\'' | ',' | ';' | ')' | '}' | ']' | '<' | '>' | '|'
585        )
586}
587
588/// Redacts secret `key=value` pairs (signatures, credentials, security tokens,
589/// access keys) from arbitrary free text — an error chain message, or a provider
590/// response body that may echo a signed URL — so the text is safe to log.
591///
592/// The value of any sensitive key is replaced with `REDACTED`; all other text is
593/// preserved verbatim. Use this on any provider body or error string before
594/// putting it into a [`Log`] message. For a whole URL prefer [`sanitize_url`].
595///
596/// # Examples
597///
598/// ```
599/// use rusty_cat::api::redact_secrets;
600///
601/// let safe = redact_secrets("error: GET https://h/o?sv=2021&sig=AbC%2Bsecret failed");
602/// assert!(safe.contains("sv=2021"));
603/// assert!(safe.contains("sig=REDACTED"));
604/// assert!(!safe.contains("AbC"));
605/// ```
606pub fn redact_secrets(text: &str) -> String {
607    let mut out = String::with_capacity(text.len());
608    let mut chars = text.chars().peekable();
609    let mut token = String::new();
610    while let Some(c) = chars.next() {
611        if is_token_char(c) {
612            token.push(c);
613            continue;
614        }
615        if c == '=' && !token.is_empty() && is_sensitive_param(&token) {
616            out.push_str(&token);
617            out.push_str("=REDACTED");
618            token.clear();
619            while let Some(&n) = chars.peek() {
620                if is_value_delim(n) {
621                    break;
622                }
623                chars.next();
624            }
625        } else {
626            out.push_str(&token);
627            token.clear();
628            out.push(c);
629        }
630    }
631    out.push_str(&token);
632    out
633}
634
635/// Callback type for global debug log listener.
636pub type DebugLogListener = Arc<dyn Fn(Log) + Send + Sync + 'static>;
637
638static DEBUG_LOG_LISTENER: OnceLock<Mutex<Option<DebugLogListener>>> = OnceLock::new();
639
640fn debug_log_listener_slot() -> &'static Mutex<Option<DebugLogListener>> {
641    DEBUG_LOG_LISTENER.get_or_init(|| Mutex::new(None))
642}
643
644/// Returns whether a debug log listener is currently registered.
645///
646/// Useful on hot paths to avoid constructing log objects unnecessarily.
647///
648/// # Examples
649///
650/// ```no_run
651/// use rusty_cat::api::debug_log_listener_active;
652///
653/// let _active = debug_log_listener_active();
654/// ```
655#[inline]
656pub fn debug_log_listener_active() -> bool {
657    match debug_log_listener_slot().lock() {
658        Ok(g) => g.is_some(),
659        Err(_) => false,
660    }
661}
662
663/// Sets or clears the global debug log listener.
664///
665/// - `Some(listener)`: set or replace current listener.
666/// - `None`: clear listener (unregister).
667///
668/// # Errors
669///
670/// Returns [`DebugLogListenerError`] when internal listener storage lock is
671/// poisoned.
672///
673/// # Examples
674///
675/// ```no_run
676/// use std::sync::Arc;
677/// use rusty_cat::api::{set_debug_log_listener, DebugLogListener, Log};
678///
679/// let listener: DebugLogListener = Arc::new(|log: Log| println!("{log}"));
680/// set_debug_log_listener(Some(listener))?;
681/// set_debug_log_listener(None)?;
682/// # Ok::<(), rusty_cat::api::DebugLogListenerError>(())
683/// ```
684pub fn set_debug_log_listener(
685    listener: Option<DebugLogListener>,
686) -> Result<(), DebugLogListenerError> {
687    let mut g = debug_log_listener_slot()
688        .lock()
689        .map_err(|_| DebugLogListenerError(()))?;
690    *g = listener;
691    Ok(())
692}
693
694/// Registers a global singleton debug log listener.
695///
696/// Returns `Err` if a listener is already registered.
697///
698/// # Errors
699///
700/// Returns [`DebugLogListenerError`] when:
701/// - a listener is already registered, or
702/// - internal listener storage lock is poisoned.
703///
704/// # Examples
705///
706/// ```no_run
707/// use rusty_cat::api::{try_set_debug_log_listener, Log};
708///
709/// let _ = try_set_debug_log_listener(|log: Log| {
710///     println!("{log}");
711/// });
712/// ```
713pub fn try_set_debug_log_listener<F>(f: F) -> Result<(), DebugLogListenerError>
714where
715    F: Fn(Log) + Send + Sync + 'static,
716{
717    let mut g = debug_log_listener_slot()
718        .lock()
719        .map_err(|_| DebugLogListenerError(()))?;
720    if g.is_some() {
721        return Err(DebugLogListenerError(()));
722    }
723    *g = Some(Arc::new(f));
724    Ok(())
725}
726
727#[derive(Debug, Clone, Copy, PartialEq, Eq)]
728pub struct DebugLogListenerError(());
729
730impl fmt::Display for DebugLogListenerError {
731    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732        f.write_str("debug log listener already set")
733    }
734}
735
736impl std::error::Error for DebugLogListenerError {}
737
738/// Emits one log entry.
739///
740/// Returns immediately when no listener is set. Listener panics are caught and
741/// discarded.
742///
743/// # Panics
744///
745/// This function does not panic. Listener panics are caught internally.
746///
747/// # Examples
748///
749/// ```no_run
750/// use rusty_cat::api::{emit, Log};
751///
752/// emit(Log::debug("demo", "manual emit"));
753/// ```
754pub fn emit(log: Log) {
755    let cb_opt = debug_log_listener_slot()
756        .lock()
757        .ok()
758        .and_then(|g| g.as_ref().map(Arc::clone));
759    let Some(cb) = cb_opt else {
760        return;
761    };
762    let _ = panic::catch_unwind(AssertUnwindSafe(move || {
763        cb(log);
764    }));
765}
766
767/// Lazily emits a log entry only when listener is active.
768///
769/// This avoids formatting/allocation overhead when logging is disabled.
770///
771/// # Panics
772///
773/// This function does not panic. Any panic from listener callback is caught by
774/// [`emit`].
775///
776/// # Examples
777///
778/// ```no_run
779/// use rusty_cat::api::{emit_lazy, Log};
780///
781/// emit_lazy(|| Log::debug("demo", format!("computed {}", 42)));
782/// ```
783#[inline]
784pub fn emit_lazy<F>(f: F)
785where
786    F: FnOnce() -> Log,
787{
788    if !debug_log_listener_active() {
789        return;
790    }
791    emit(f());
792}
793
794/// Internal flow debug logging macro ([`LogLevel::Debug`]).
795///
796/// The `format!` expression is evaluated only when listener is active.
797/// crate::meow_flow_log!(
798///     "enqueue",
799///    "task_id={:?} offset={} total={}",
800///     task_id,
801///     offset,
802///    total
803/// );
804#[macro_export]
805macro_rules! meow_flow_log {
806    ($tag:expr, $($arg:tt)*) => {
807        $crate::log::emit_lazy(|| {
808            $crate::log::Log::debug($tag, format!($($arg)*))
809        });
810    };
811}
812
813/// Internal trace logging macro ([`LogLevel::Trace`]).
814///
815/// Same lazy evaluation semantics as [`meow_flow_log`], but emits at
816/// [`LogLevel::Trace`]. Use it for VERY high frequency events that fire inside
817/// hot loops — per chunk, per poll tick, per retry attempt. Consumers must not
818/// persist these (drop or sample only).
819///
820/// # Examples
821///
822/// ```
823/// rusty_cat::meow_trace_log!("download_chunk", "wrote {} bytes at {}", 1024, 0);
824/// ```
825#[macro_export]
826macro_rules! meow_trace_log {
827    ($tag:expr, $($arg:tt)*) => {
828        $crate::log::emit_lazy(|| {
829            $crate::log::Log::new($crate::log::LogLevel::Trace, $tag, format!($($arg)*))
830        });
831    };
832}
833
834/// Internal key-node logging macro ([`LogLevel::Key`]).
835///
836/// Same lazy evaluation semantics as [`meow_flow_log`], but emits at
837/// [`LogLevel::Key`]. Use it for task/executor lifecycle checkpoints (created,
838/// enqueued, started, prepared, completed, paused, resumed, cancelled, closed)
839/// that the SDK author reviews to reconstruct a run. Consumers should persist
840/// these and forward them for troubleshooting.
841///
842/// # Examples
843///
844/// ```
845/// rusty_cat::meow_key_log!("enqueue", "task enqueued key={}", 7);
846/// ```
847#[macro_export]
848macro_rules! meow_key_log {
849    ($tag:expr, $($arg:tt)*) => {
850        $crate::log::emit_lazy(|| {
851            $crate::log::Log::new($crate::log::LogLevel::Key, $tag, format!($($arg)*))
852        });
853    };
854}
855
856/// Internal warning logging macro.
857///
858/// Same lazy evaluation semantics as [`meow_flow_log`], but emits at
859/// [`LogLevel::Warn`]. Use it for non-fatal failures that a consumer would want
860/// to surface even when filtering out debug noise — for example a remote cleanup
861/// (multipart abort) that failed and may leave billable orphaned parts behind.
862///
863/// # Examples
864///
865/// ```
866/// rusty_cat::meow_warn_log!("cleanup", "abort failed for key={}", 7);
867/// ```
868#[macro_export]
869macro_rules! meow_warn_log {
870    ($tag:expr, $($arg:tt)*) => {
871        $crate::log::emit_lazy(|| {
872            $crate::log::Log::new($crate::log::LogLevel::Warn, $tag, format!($($arg)*))
873        });
874    };
875}
876
877/// Internal error logging macro ([`LogLevel::Error`]).
878///
879/// Same lazy evaluation semantics as [`meow_flow_log`], but emits at
880/// [`LogLevel::Error`]. Use it for failures with full detail — a chunk/part
881/// upload or download failed, a task failed, an HTTP error response, a signing
882/// failure — and for caught panics. For rich triage context (ids, byte range,
883/// HTTP status, attempt, sanitized URL) build the entry with the [`Log`] `with_*`
884/// builder methods and emit it via [`emit_lazy`] instead of this macro.
885///
886/// # Examples
887///
888/// ```
889/// rusty_cat::meow_error_log!("upload_part", "part {} upload failed", 3);
890/// ```
891#[macro_export]
892macro_rules! meow_error_log {
893    ($tag:expr, $($arg:tt)*) => {
894        $crate::log::emit_lazy(|| {
895            $crate::log::Log::new($crate::log::LogLevel::Error, $tag, format!($($arg)*))
896        });
897    };
898}