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}