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