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}