use std::fmt;
use std::panic::{self, AssertUnwindSafe};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LogLevel {
Trace,
Debug,
Info,
Key,
Warn,
Error,
}
impl LogLevel {
#[inline]
pub fn persist_recommended(&self) -> bool {
matches!(self, LogLevel::Key | LogLevel::Warn | LogLevel::Error)
}
pub fn as_str(&self) -> &'static str {
match self {
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Key => "KEY",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
}
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct Log {
timestamp_ms: u64,
level: LogLevel,
tag: &'static str,
message: String,
task_id: Option<String>,
object_key: Option<String>,
part_index: Option<u64>,
offset: Option<u64>,
byte_len: Option<u64>,
http_status: Option<u16>,
attempt: Option<u32>,
max_retries: Option<u32>,
error_code: Option<i32>,
url: Option<String>,
}
impl Log {
pub fn new(level: LogLevel, tag: &'static str, message: impl Into<String>) -> Self {
let timestamp_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
Self {
timestamp_ms,
level,
tag,
message: message.into(),
task_id: None,
object_key: None,
part_index: None,
offset: None,
byte_len: None,
http_status: None,
attempt: None,
max_retries: None,
error_code: None,
url: None,
}
}
pub fn trace(tag: &'static str, message: impl Into<String>) -> Self {
Self::new(LogLevel::Trace, tag, message)
}
pub fn debug(tag: &'static str, message: impl Into<String>) -> Self {
Self::new(LogLevel::Debug, tag, message)
}
pub fn info(tag: &'static str, message: impl Into<String>) -> Self {
Self::new(LogLevel::Info, tag, message)
}
pub fn key(tag: &'static str, message: impl Into<String>) -> Self {
Self::new(LogLevel::Key, tag, message)
}
pub fn warn(tag: &'static str, message: impl Into<String>) -> Self {
Self::new(LogLevel::Warn, tag, message)
}
pub fn error(tag: &'static str, message: impl Into<String>) -> Self {
Self::new(LogLevel::Error, tag, message)
}
#[must_use]
pub fn with_task_id(mut self, task_id: impl Into<String>) -> Self {
self.task_id = Some(task_id.into());
self
}
#[must_use]
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.object_key = Some(key.into());
self
}
#[must_use]
pub fn with_part(mut self, part_index: u64) -> Self {
self.part_index = Some(part_index);
self
}
#[must_use]
pub fn with_offset(mut self, offset: u64) -> Self {
self.offset = Some(offset);
self
}
#[must_use]
pub fn with_byte_len(mut self, byte_len: u64) -> Self {
self.byte_len = Some(byte_len);
self
}
#[must_use]
pub fn with_range(mut self, offset: u64, byte_len: u64) -> Self {
self.offset = Some(offset);
self.byte_len = Some(byte_len);
self
}
#[must_use]
pub fn with_http_status(mut self, status: u16) -> Self {
self.http_status = Some(status);
self
}
#[must_use]
pub fn with_attempt(mut self, attempt: u32) -> Self {
self.attempt = Some(attempt);
self
}
#[must_use]
pub fn with_max_retries(mut self, max_retries: u32) -> Self {
self.max_retries = Some(max_retries);
self
}
#[must_use]
pub fn with_error_code(mut self, code: i32) -> Self {
self.error_code = Some(code);
self
}
#[must_use]
pub fn with_url(mut self, url: impl AsRef<str>) -> Self {
self.url = Some(sanitize_url(url.as_ref()));
self
}
pub fn timestamp_ms(&self) -> u64 {
self.timestamp_ms
}
pub fn level(&self) -> LogLevel {
self.level
}
pub fn tag(&self) -> &'static str {
self.tag
}
pub fn message(&self) -> &str {
&self.message
}
pub fn task_id(&self) -> Option<&str> {
self.task_id.as_deref()
}
pub fn object_key(&self) -> Option<&str> {
self.object_key.as_deref()
}
pub fn part_index(&self) -> Option<u64> {
self.part_index
}
pub fn offset(&self) -> Option<u64> {
self.offset
}
pub fn byte_len(&self) -> Option<u64> {
self.byte_len
}
pub fn http_status(&self) -> Option<u16> {
self.http_status
}
pub fn attempt(&self) -> Option<u32> {
self.attempt
}
pub fn max_retries(&self) -> Option<u32> {
self.max_retries
}
pub fn error_code(&self) -> Option<i32> {
self.error_code
}
pub fn url(&self) -> Option<&str> {
self.url.as_deref()
}
pub fn into_message(self) -> String {
self.message
}
}
impl fmt::Display for Log {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] {} [{}] {}",
self.timestamp_ms, self.level, self.tag, self.message
)?;
if let Some(v) = &self.task_id {
write!(f, " task_id={v}")?;
}
if let Some(v) = &self.object_key {
write!(f, " key={v}")?;
}
if let Some(v) = self.part_index {
write!(f, " part={v}")?;
}
if let Some(v) = self.offset {
write!(f, " offset={v}")?;
}
if let Some(v) = self.byte_len {
write!(f, " len={v}")?;
}
if let Some(v) = self.http_status {
write!(f, " http_status={v}")?;
}
if let Some(v) = self.attempt {
write!(f, " attempt={v}")?;
}
if let Some(v) = self.max_retries {
write!(f, " max_retries={v}")?;
}
if let Some(v) = self.error_code {
write!(f, " error_code={v}")?;
}
if let Some(v) = &self.url {
write!(f, " url={v}")?;
}
Ok(())
}
}
fn is_sensitive_param(key: &str) -> bool {
let k = key.trim().to_ascii_lowercase();
const EXACT: &[&str] = &[
"sig",
"signature",
"x-oss-signature",
"ossaccesskeyid",
"x-oss-credential",
"x-oss-security-token",
"security-token",
"x-amz-signature",
"x-amz-credential",
"x-amz-security-token",
];
if EXACT.contains(&k.as_str()) {
return true;
}
k.contains("sig")
|| k.contains("credential")
|| k.contains("token")
|| k.contains("secret")
|| k.contains("password")
|| k.contains("accesskey")
}
pub fn sanitize_url(url: &str) -> String {
let Some((base, query)) = url.split_once('?') else {
return url.to_string();
};
let mut out = String::with_capacity(url.len());
out.push_str(base);
out.push('?');
let mut first = true;
for pair in query.split('&') {
if !first {
out.push('&');
}
first = false;
match pair.split_once('=') {
Some((k, _)) if is_sensitive_param(k) => {
out.push_str(k);
out.push_str("=REDACTED");
}
_ => out.push_str(pair),
}
}
out
}
fn is_token_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '-'
}
fn is_value_delim(c: char) -> bool {
c.is_whitespace()
|| matches!(
c,
'&' | '"' | '\'' | ',' | ';' | ')' | '}' | ']' | '<' | '>' | '|'
)
}
pub fn redact_secrets(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
let mut token = String::new();
while let Some(c) = chars.next() {
if is_token_char(c) {
token.push(c);
continue;
}
if c == '=' && !token.is_empty() && is_sensitive_param(&token) {
out.push_str(&token);
out.push_str("=REDACTED");
token.clear();
while let Some(&n) = chars.peek() {
if is_value_delim(n) {
break;
}
chars.next();
}
} else {
out.push_str(&token);
token.clear();
out.push(c);
}
}
out.push_str(&token);
out
}
pub type DebugLogListener = Arc<dyn Fn(Log) + Send + Sync + 'static>;
static DEBUG_LOG_LISTENER: OnceLock<Mutex<Option<DebugLogListener>>> = OnceLock::new();
fn debug_log_listener_slot() -> &'static Mutex<Option<DebugLogListener>> {
DEBUG_LOG_LISTENER.get_or_init(|| Mutex::new(None))
}
#[inline]
pub fn debug_log_listener_active() -> bool {
match debug_log_listener_slot().lock() {
Ok(g) => g.is_some(),
Err(_) => false,
}
}
pub fn set_debug_log_listener(
listener: Option<DebugLogListener>,
) -> Result<(), DebugLogListenerError> {
let mut g = debug_log_listener_slot()
.lock()
.map_err(|_| DebugLogListenerError(()))?;
*g = listener;
Ok(())
}
pub fn try_set_debug_log_listener<F>(f: F) -> Result<(), DebugLogListenerError>
where
F: Fn(Log) + Send + Sync + 'static,
{
let mut g = debug_log_listener_slot()
.lock()
.map_err(|_| DebugLogListenerError(()))?;
if g.is_some() {
return Err(DebugLogListenerError(()));
}
*g = Some(Arc::new(f));
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DebugLogListenerError(());
impl fmt::Display for DebugLogListenerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("debug log listener already set")
}
}
impl std::error::Error for DebugLogListenerError {}
pub fn emit(log: Log) {
let cb_opt = debug_log_listener_slot()
.lock()
.ok()
.and_then(|g| g.as_ref().map(Arc::clone));
let Some(cb) = cb_opt else {
return;
};
let _ = panic::catch_unwind(AssertUnwindSafe(move || {
cb(log);
}));
}
#[inline]
pub fn emit_lazy<F>(f: F)
where
F: FnOnce() -> Log,
{
if !debug_log_listener_active() {
return;
}
emit(f());
}
#[macro_export]
macro_rules! meow_flow_log {
($tag:expr, $($arg:tt)*) => {
$crate::log::emit_lazy(|| {
$crate::log::Log::debug($tag, format!($($arg)*))
});
};
}
#[macro_export]
macro_rules! meow_trace_log {
($tag:expr, $($arg:tt)*) => {
$crate::log::emit_lazy(|| {
$crate::log::Log::new($crate::log::LogLevel::Trace, $tag, format!($($arg)*))
});
};
}
#[macro_export]
macro_rules! meow_key_log {
($tag:expr, $($arg:tt)*) => {
$crate::log::emit_lazy(|| {
$crate::log::Log::new($crate::log::LogLevel::Key, $tag, format!($($arg)*))
});
};
}
#[macro_export]
macro_rules! meow_warn_log {
($tag:expr, $($arg:tt)*) => {
$crate::log::emit_lazy(|| {
$crate::log::Log::new($crate::log::LogLevel::Warn, $tag, format!($($arg)*))
});
};
}
#[macro_export]
macro_rules! meow_error_log {
($tag:expr, $($arg:tt)*) => {
$crate::log::emit_lazy(|| {
$crate::log::Log::new($crate::log::LogLevel::Error, $tag, format!($($arg)*))
});
};
}