#![forbid(unsafe_code)]
#![allow(clippy::nursery)]
#![allow(clippy::pedantic)]
use backtrace::Backtrace;
use lipgloss::{Color, Style};
use std::collections::HashMap;
use std::fmt;
use std::io::{self, Write};
use std::sync::{Arc, RwLock};
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum Level {
Debug = -4,
Info = 0,
Warn = 4,
Error = 8,
Fatal = 12,
}
impl Level {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Debug => "debug",
Self::Info => "info",
Self::Warn => "warn",
Self::Error => "error",
Self::Fatal => "fatal",
}
}
#[must_use]
pub fn as_upper_str(&self) -> &'static str {
match self {
Self::Debug => "DEBU",
Self::Info => "INFO",
Self::Warn => "WARN",
Self::Error => "ERRO",
Self::Fatal => "FATA",
}
}
}
impl PartialOrd for Level {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Level {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(*self as i32).cmp(&(*other as i32))
}
}
impl fmt::Display for Level {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl std::str::FromStr for Level {
type Err = ParseLevelError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"debug" => Ok(Self::Debug),
"info" => Ok(Self::Info),
"warn" => Ok(Self::Warn),
"error" => Ok(Self::Error),
"fatal" => Ok(Self::Fatal),
_ => Err(ParseLevelError(s.to_string())),
}
}
}
#[derive(Error, Debug, Clone)]
#[error("invalid level: {0:?}")]
pub struct ParseLevelError(String);
pub type ParseResult<T> = std::result::Result<T, ParseLevelError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Formatter {
#[default]
Text,
Json,
Logfmt,
}
pub mod keys {
pub const TIMESTAMP: &str = "time";
pub const MESSAGE: &str = "msg";
pub const LEVEL: &str = "level";
pub const CALLER: &str = "caller";
pub const PREFIX: &str = "prefix";
}
pub const DEFAULT_TIME_FORMAT: &str = "%Y/%m/%d %H:%M:%S";
#[derive(Debug, Clone)]
pub struct Styles {
pub timestamp: Style,
pub caller: Style,
pub prefix: Style,
pub message: Style,
pub key: Style,
pub value: Style,
pub separator: Style,
pub levels: HashMap<Level, Style>,
pub keys: HashMap<String, Style>,
pub values: HashMap<String, Style>,
}
impl Default for Styles {
fn default() -> Self {
Self::new()
}
}
impl Styles {
#[must_use]
pub fn new() -> Self {
let mut levels = HashMap::new();
levels.insert(
Level::Debug,
Style::new().bold().foreground_color(Color::from("63")),
);
levels.insert(
Level::Info,
Style::new().bold().foreground_color(Color::from("86")),
);
levels.insert(
Level::Warn,
Style::new().bold().foreground_color(Color::from("192")),
);
levels.insert(
Level::Error,
Style::new().bold().foreground_color(Color::from("204")),
);
levels.insert(
Level::Fatal,
Style::new().bold().foreground_color(Color::from("134")),
);
Self {
timestamp: Style::new(),
caller: Style::new().faint(),
prefix: Style::new().bold().faint(),
message: Style::new(),
key: Style::new().faint(),
value: Style::new(),
separator: Style::new().faint(),
levels,
keys: HashMap::new(),
values: HashMap::new(),
}
}
}
pub type TimeFunction = fn(std::time::SystemTime) -> std::time::SystemTime;
#[must_use]
pub fn now_utc(t: SystemTime) -> SystemTime {
t }
pub type CallerFormatter = fn(&str, u32, &str) -> String;
pub type ErrorHandler = Arc<dyn Fn(io::Error) + Send + Sync>;
#[must_use]
pub fn short_caller_formatter(file: &str, line: u32, _fn_name: &str) -> String {
let trimmed = trim_caller_path(file, 2);
format!("{trimmed}:{line}")
}
#[must_use]
pub fn long_caller_formatter(file: &str, line: u32, _fn_name: &str) -> String {
format!("{file}:{line}")
}
fn trim_caller_path(path: &str, n: usize) -> &str {
if n == 0 {
return path;
}
let mut last_idx = path.len();
for _ in 0..n {
if let Some(idx) = path[..last_idx].rfind('/') {
last_idx = idx;
} else {
return path;
}
}
&path[last_idx + 1..]
}
#[derive(Debug, Clone)]
pub struct CallerInfo {
pub file: String,
pub line: u32,
pub function: String,
}
impl CallerInfo {
#[must_use]
pub fn capture(skip: usize) -> Option<Self> {
let bt = Backtrace::new();
let frames: Vec<_> = bt.frames().iter().collect();
let skip_total = skip + 4;
for frame in frames.iter().skip(skip_total) {
for symbol in frame.symbols() {
let fn_name = symbol
.name()
.map(|n| n.to_string())
.unwrap_or_else(|| "<unknown>".to_string());
if fn_name.contains("charmed_log::") || fn_name.contains("backtrace::") {
continue;
}
let file = symbol
.filename()
.and_then(|p| p.to_str())
.unwrap_or("<unknown>")
.to_string();
let line = symbol.lineno().unwrap_or(0);
return Some(Self {
file,
line,
function: fn_name,
});
}
}
None
}
}
#[derive(Clone)]
pub struct Options {
pub time_function: TimeFunction,
pub time_format: String,
pub level: Level,
pub prefix: String,
pub report_timestamp: bool,
pub report_caller: bool,
pub caller_formatter: CallerFormatter,
pub caller_offset: usize,
pub fields: Vec<(String, String)>,
pub formatter: Formatter,
}
impl Default for Options {
fn default() -> Self {
Self {
time_function: now_utc,
time_format: DEFAULT_TIME_FORMAT.to_string(),
level: Level::Info,
prefix: String::new(),
report_timestamp: false,
report_caller: false,
caller_formatter: short_caller_formatter,
caller_offset: 0,
fields: Vec::new(),
formatter: Formatter::Text,
}
}
}
struct LoggerInner {
writer: Box<dyn Write + Send + Sync>,
level: Level,
prefix: String,
time_function: TimeFunction,
time_format: String,
caller_offset: usize,
caller_formatter: CallerFormatter,
formatter: Formatter,
report_timestamp: bool,
report_caller: bool,
fields: Vec<(String, String)>,
styles: Styles,
error_handler: Option<ErrorHandler>,
has_warned_io_failure: bool,
warned_caller_overhead: bool,
suppress_caller_warning: bool,
}
pub struct Logger {
inner: Arc<RwLock<LoggerInner>>,
}
impl Default for Logger {
fn default() -> Self {
Self::new()
}
}
impl Clone for Logger {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl fmt::Debug for Logger {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
f.debug_struct("Logger")
.field("level", &inner.level)
.field("prefix", &inner.prefix)
.field("formatter", &inner.formatter)
.field("report_timestamp", &inner.report_timestamp)
.field("report_caller", &inner.report_caller)
.finish()
}
}
impl Logger {
#[must_use]
pub fn new() -> Self {
Self::with_options(Options::default())
}
#[must_use]
pub fn with_options(opts: Options) -> Self {
Self {
inner: Arc::new(RwLock::new(LoggerInner {
writer: Box::new(io::stderr()),
level: opts.level,
prefix: opts.prefix,
time_function: opts.time_function,
time_format: opts.time_format,
caller_offset: opts.caller_offset,
caller_formatter: opts.caller_formatter,
formatter: opts.formatter,
report_timestamp: opts.report_timestamp,
report_caller: opts.report_caller,
fields: opts.fields,
styles: Styles::new(),
error_handler: None,
has_warned_io_failure: false,
warned_caller_overhead: false,
suppress_caller_warning: false,
})),
}
}
pub fn set_level(&self, level: Level) {
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
inner.level = level;
}
#[must_use]
pub fn level(&self) -> Level {
let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
inner.level
}
pub fn set_prefix(&self, prefix: impl Into<String>) {
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
inner.prefix = prefix.into();
}
#[must_use]
pub fn prefix(&self) -> String {
let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
inner.prefix.clone()
}
pub fn set_report_timestamp(&self, report: bool) {
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
inner.report_timestamp = report;
}
pub fn set_report_caller(&self, report: bool) {
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
inner.report_caller = report;
}
pub fn suppress_caller_warning(&self) {
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
inner.suppress_caller_warning = true;
}
pub fn set_time_format(&self, format: impl Into<String>) {
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
inner.time_format = format.into();
}
pub fn set_formatter(&self, formatter: Formatter) {
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
inner.formatter = formatter;
}
pub fn set_styles(&self, styles: Styles) {
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
inner.styles = styles;
}
#[must_use]
pub fn with_fields(&self, fields: &[(&str, &str)]) -> Self {
let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
let mut new_fields = inner.fields.clone();
new_fields.extend(fields.iter().map(|(k, v)| (k.to_string(), v.to_string())));
Self {
inner: Arc::new(RwLock::new(LoggerInner {
writer: Box::new(io::stderr()),
level: inner.level,
prefix: inner.prefix.clone(),
time_function: inner.time_function,
time_format: inner.time_format.clone(),
caller_offset: inner.caller_offset,
caller_formatter: inner.caller_formatter,
formatter: inner.formatter,
report_timestamp: inner.report_timestamp,
report_caller: inner.report_caller,
fields: new_fields,
styles: inner.styles.clone(),
error_handler: inner.error_handler.clone(),
has_warned_io_failure: false, warned_caller_overhead: false, suppress_caller_warning: inner.suppress_caller_warning, })),
}
}
#[must_use]
pub fn with(&self, fields: &[(&str, &str)]) -> Self {
self.with_fields(fields)
}
#[must_use]
pub fn with_prefix(&self, prefix: impl Into<String>) -> Self {
let new_logger = self.with_fields(&[]);
new_logger.set_prefix(prefix);
new_logger
}
#[must_use]
pub fn with_error_handler<F>(self, handler: F) -> Self
where
F: Fn(io::Error) + Send + Sync + 'static,
{
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
inner.error_handler = Some(Arc::new(handler));
drop(inner);
self
}
pub fn log(&self, level: Level, msg: &str, keyvals: &[(&str, &str)]) {
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
if inner.report_caller && !inner.warned_caller_overhead && !inner.suppress_caller_warning {
inner.warned_caller_overhead = true;
let _ = io::stderr().write_all(
b"[charmed_log] PERF WARNING: caller reporting enabled - expect 100-1000x slowdown\n",
);
}
if level < inner.level {
return;
}
let mut output = String::new();
match inner.formatter {
Formatter::Text => Self::format_text_inner(&inner, level, msg, keyvals, &mut output),
Formatter::Json => Self::format_json_inner(&inner, level, msg, keyvals, &mut output),
Formatter::Logfmt => {
Self::format_logfmt_inner(&inner, level, msg, keyvals, &mut output);
}
}
if let Err(e) = inner.writer.write_all(output.as_bytes()) {
if let Some(ref handler) = inner.error_handler {
let handler = Arc::clone(handler);
drop(inner);
handler(e);
} else if !inner.has_warned_io_failure {
inner.has_warned_io_failure = true;
drop(inner);
let _ =
io::stderr().write_all(format!("charmed_log: write failed: {e}\n").as_bytes());
}
}
}
fn format_text_inner(
inner: &LoggerInner,
level: Level,
msg: &str,
keyvals: &[(&str, &str)],
output: &mut String,
) {
let styles = &inner.styles;
let mut first = true;
if inner.report_timestamp {
let ts = (inner.time_function)(SystemTime::now());
if let Ok(duration) = ts.duration_since(UNIX_EPOCH) {
let secs = duration.as_secs();
let ts_str = format_timestamp(secs, &inner.time_format);
let styled = styles.timestamp.render(&ts_str);
if !first {
output.push(' ');
}
output.push_str(&styled);
first = false;
}
}
if let Some(level_style) = styles.levels.get(&level) {
let lvl = level_style.render(level.as_upper_str());
if !first {
output.push(' ');
}
output.push_str(&lvl);
first = false;
}
if inner.report_caller {
let caller_str = if let Some(info) = CallerInfo::capture(inner.caller_offset) {
(inner.caller_formatter)(&info.file, info.line, &info.function)
} else {
(inner.caller_formatter)("unknown", 0, "unknown")
};
let styled = styles.caller.render(&format!("<{caller_str}>"));
if !first {
output.push(' ');
}
output.push_str(&styled);
first = false;
}
if !inner.prefix.is_empty() {
let styled = styles.prefix.render(&format!("{}:", inner.prefix));
if !first {
output.push(' ');
}
output.push_str(&styled);
first = false;
}
if !msg.is_empty() {
let styled = styles.message.render(msg);
if !first {
output.push(' ');
}
output.push_str(&styled);
first = false;
}
for (key, value) in &inner.fields {
Self::format_text_keyval_inner(styles, key, value, &mut first, output);
}
for (key, value) in keyvals {
Self::format_text_keyval_inner(styles, key, value, &mut first, output);
}
output.push('\n');
}
fn format_text_keyval_inner(
styles: &Styles,
key: &str,
value: &str,
first: &mut bool,
output: &mut String,
) {
let sep = styles.separator.render("=");
let key_styled = if let Some(style) = styles.keys.get(key) {
style.render(key)
} else {
styles.key.render(key)
};
let value_styled = if let Some(style) = styles.values.get(key) {
style.render(value)
} else {
styles.value.render(value)
};
if !*first {
output.push(' ');
}
output.push_str(&key_styled);
output.push_str(&sep);
output.push_str(&value_styled);
*first = false;
}
fn format_json_inner(
inner: &LoggerInner,
level: Level,
msg: &str,
keyvals: &[(&str, &str)],
output: &mut String,
) {
output.push('{');
let mut first = true;
if inner.report_timestamp {
let ts = (inner.time_function)(SystemTime::now());
if let Ok(duration) = ts.duration_since(UNIX_EPOCH) {
let secs = duration.as_secs();
let ts_str = format_timestamp(secs, &inner.time_format);
write_json_field(output, keys::TIMESTAMP, &ts_str, &mut first);
}
}
write_json_field(output, keys::LEVEL, level.as_str(), &mut first);
if !inner.prefix.is_empty() {
write_json_field(output, keys::PREFIX, &inner.prefix, &mut first);
}
if !msg.is_empty() {
write_json_field(output, keys::MESSAGE, msg, &mut first);
}
for (key, value) in &inner.fields {
write_json_field(output, key, value, &mut first);
}
for (key, value) in keyvals {
write_json_field(output, key, value, &mut first);
}
output.push_str("}\n");
}
fn format_logfmt_inner(
inner: &LoggerInner,
level: Level,
msg: &str,
keyvals: &[(&str, &str)],
output: &mut String,
) {
let mut first = true;
if inner.report_timestamp {
let ts = (inner.time_function)(SystemTime::now());
if let Ok(duration) = ts.duration_since(UNIX_EPOCH) {
let secs = duration.as_secs();
let ts_str = format_timestamp(secs, &inner.time_format);
write_logfmt_field(output, keys::TIMESTAMP, &ts_str, &mut first);
}
}
write_logfmt_field(output, keys::LEVEL, level.as_str(), &mut first);
if !inner.prefix.is_empty() {
write_logfmt_field(output, keys::PREFIX, &inner.prefix, &mut first);
}
if !msg.is_empty() {
write_logfmt_field(output, keys::MESSAGE, msg, &mut first);
}
for (key, value) in &inner.fields {
write_logfmt_field(output, key, value, &mut first);
}
for (key, value) in keyvals {
write_logfmt_field(output, key, value, &mut first);
}
output.push('\n');
}
#[expect(dead_code, reason = "Kept for API compatibility")]
fn format_text(
&self,
inner: &LoggerInner,
level: Level,
msg: &str,
keyvals: &[(&str, &str)],
output: &mut String,
) {
Self::format_text_inner(inner, level, msg, keyvals, output);
}
#[expect(dead_code, reason = "Kept for API compatibility")]
fn format_text_keyval(
&self,
styles: &Styles,
key: &str,
value: &str,
first: &mut bool,
output: &mut String,
) {
Self::format_text_keyval_inner(styles, key, value, first, output);
}
#[expect(dead_code, reason = "Kept for API compatibility")]
fn format_json(
&self,
inner: &LoggerInner,
level: Level,
msg: &str,
keyvals: &[(&str, &str)],
output: &mut String,
) {
Self::format_json_inner(inner, level, msg, keyvals, output);
}
#[expect(dead_code, reason = "Kept for API compatibility")]
fn format_logfmt(
&self,
inner: &LoggerInner,
level: Level,
msg: &str,
keyvals: &[(&str, &str)],
output: &mut String,
) {
Self::format_logfmt_inner(inner, level, msg, keyvals, output);
}
pub fn debug(&self, msg: &str, keyvals: &[(&str, &str)]) {
self.log(Level::Debug, msg, keyvals);
}
pub fn info(&self, msg: &str, keyvals: &[(&str, &str)]) {
self.log(Level::Info, msg, keyvals);
}
pub fn warn(&self, msg: &str, keyvals: &[(&str, &str)]) {
self.log(Level::Warn, msg, keyvals);
}
pub fn error(&self, msg: &str, keyvals: &[(&str, &str)]) {
self.log(Level::Error, msg, keyvals);
}
pub fn fatal(&self, msg: &str, keyvals: &[(&str, &str)]) {
self.log(Level::Fatal, msg, keyvals);
}
pub fn logf(&self, level: Level, format: &str, args: &[&dyn fmt::Display]) {
let msg = format_args_simple(format, args);
self.log(level, &msg, &[]);
}
pub fn debugf(&self, format: &str, args: &[&dyn fmt::Display]) {
self.logf(Level::Debug, format, args);
}
pub fn infof(&self, format: &str, args: &[&dyn fmt::Display]) {
self.logf(Level::Info, format, args);
}
pub fn warnf(&self, format: &str, args: &[&dyn fmt::Display]) {
self.logf(Level::Warn, format, args);
}
pub fn errorf(&self, format: &str, args: &[&dyn fmt::Display]) {
self.logf(Level::Error, format, args);
}
pub fn fatalf(&self, format: &str, args: &[&dyn fmt::Display]) {
self.logf(Level::Fatal, format, args);
}
}
fn format_args_simple(format: &str, args: &[&dyn fmt::Display]) -> String {
use fmt::Write;
let mut result = String::with_capacity(format.len());
let mut arg_idx = 0;
let mut rest = format;
while let Some(pos) = rest.find("{}") {
result.push_str(&rest[..pos]);
if arg_idx < args.len() {
let _ = write!(result, "{}", args[arg_idx]);
arg_idx += 1;
} else {
result.push_str("{}");
}
rest = &rest[pos + 2..];
}
result.push_str(rest);
result
}
fn format_timestamp(secs: u64, format: &str) -> String {
use chrono::{DateTime, Utc};
if let Some(datetime) = DateTime::from_timestamp(secs as i64, 0) {
datetime.with_timezone(&Utc).format(format).to_string()
} else {
"INVALID TIMESTAMP".to_string()
}
}
fn write_json_field(output: &mut String, key: &str, value: &str, first: &mut bool) {
if !*first {
output.push(',');
}
output.push('"');
output.push_str(&escape_json(key));
output.push_str("\":\"");
output.push_str(&escape_json(value));
output.push('"');
*first = false;
}
fn escape_json(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
c if c.is_control() => {
let cp = c as u32;
if cp <= 0xFFFF {
result.push_str(&format!("\\u{cp:04x}"));
} else {
let s = cp - 0x10000;
let hi = 0xD800 + (s >> 10);
let lo = 0xDC00 + (s & 0x3FF);
result.push_str(&format!("\\u{hi:04x}\\u{lo:04x}"));
}
}
c => result.push(c),
}
}
result
}
fn write_logfmt_field(output: &mut String, key: &str, value: &str, first: &mut bool) {
if !*first {
output.push(' ');
}
output.push_str(key);
output.push('=');
if needs_quoting(value) {
output.push('"');
output.push_str(&escape_logfmt(value));
output.push('"');
} else {
output.push_str(value);
}
*first = false;
}
fn needs_quoting(s: &str) -> bool {
s.is_empty()
|| s.chars()
.any(|c| c.is_whitespace() || c == '"' || c == '=' || c.is_control())
}
fn escape_logfmt(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
c => result.push(c),
}
}
result
}
pub mod prelude {
pub use crate::{
CallerInfo, DEFAULT_TIME_FORMAT, ErrorHandler, Formatter, Level, Logger, Options,
ParseLevelError, ParseResult, Styles, keys, long_caller_formatter, now_utc,
short_caller_formatter,
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_level_ordering() {
assert!(Level::Debug < Level::Info);
assert!(Level::Info < Level::Warn);
assert!(Level::Warn < Level::Error);
assert!(Level::Error < Level::Fatal);
}
#[test]
fn test_level_display() {
assert_eq!(Level::Debug.to_string(), "debug");
assert_eq!(Level::Info.to_string(), "info");
assert_eq!(Level::Warn.to_string(), "warn");
assert_eq!(Level::Error.to_string(), "error");
assert_eq!(Level::Fatal.to_string(), "fatal");
}
#[test]
fn test_level_parse() {
assert_eq!("debug".parse::<Level>().unwrap(), Level::Debug);
assert_eq!("INFO".parse::<Level>().unwrap(), Level::Info);
assert_eq!("WARN".parse::<Level>().unwrap(), Level::Warn);
assert!("warning".parse::<Level>().is_err());
assert!("invalid".parse::<Level>().is_err());
}
#[test]
fn test_logger_new() {
let logger = Logger::new();
assert_eq!(logger.level(), Level::Info);
assert!(logger.prefix().is_empty());
}
#[test]
fn test_logger_set_level() {
let logger = Logger::new();
logger.set_level(Level::Debug);
assert_eq!(logger.level(), Level::Debug);
}
#[test]
fn test_logger_set_prefix() {
let logger = Logger::new();
logger.set_prefix("myapp");
assert_eq!(logger.prefix(), "myapp");
}
#[test]
fn test_logger_with_prefix() {
let logger = Logger::new();
let prefixed = logger.with_prefix("myapp");
assert_eq!(prefixed.prefix(), "myapp");
assert!(logger.prefix().is_empty()); }
#[test]
fn test_logger_with_fields() {
let logger = Logger::new();
let with_fields = logger.with_fields(&[("app", "test"), ("version", "1.0")]);
drop(with_fields);
}
#[test]
fn test_logger_with_method() {
let logger = Logger::new();
let ctx_logger = logger.with(&[("request_id", "abc123"), ("user", "alice")]);
ctx_logger.info("test message", &[]);
logger.info("another message", &[]);
}
#[test]
fn test_caller_info_capture() {
let info = CallerInfo::capture(0);
if let Some(caller) = info {
assert!(!caller.function.is_empty());
}
}
#[test]
fn test_styles_default() {
let styles = Styles::new();
assert!(styles.levels.contains_key(&Level::Debug));
assert!(styles.levels.contains_key(&Level::Info));
assert!(styles.levels.contains_key(&Level::Warn));
assert!(styles.levels.contains_key(&Level::Error));
assert!(styles.levels.contains_key(&Level::Fatal));
}
#[test]
fn test_trim_caller_path() {
assert_eq!(trim_caller_path("src/lib.rs", 1), "lib.rs");
assert_eq!(trim_caller_path("foo/bar/baz.rs", 2), "bar/baz.rs");
assert_eq!(trim_caller_path("baz.rs", 2), "baz.rs");
assert_eq!(trim_caller_path("foo/bar/baz.rs", 0), "foo/bar/baz.rs");
}
#[test]
fn test_short_caller_formatter() {
let result = short_caller_formatter("/home/user/project/src/main.rs", 42, "main");
assert!(result.contains(":42"));
}
#[test]
fn test_long_caller_formatter() {
let result = long_caller_formatter("/home/user/project/src/main.rs", 42, "main");
assert_eq!(result, "/home/user/project/src/main.rs:42");
}
#[test]
fn test_escape_json() {
assert_eq!(escape_json("hello"), "hello");
assert_eq!(escape_json("hello \"world\""), "hello \\\"world\\\"");
assert_eq!(escape_json("line1\nline2"), "line1\\nline2");
}
#[test]
fn test_needs_quoting() {
assert!(needs_quoting(""));
assert!(needs_quoting("hello world"));
assert!(needs_quoting("key=value"));
assert!(needs_quoting("has\"quote"));
assert!(!needs_quoting("simple"));
}
#[test]
fn test_escape_logfmt() {
assert_eq!(escape_logfmt("hello"), "hello");
assert_eq!(escape_logfmt("hello \"world\""), "hello \\\"world\\\"");
assert_eq!(escape_logfmt("line1\nline2"), "line1\\nline2");
}
#[test]
fn test_formatter_default() {
assert_eq!(Formatter::default(), Formatter::Text);
}
#[test]
fn test_options_default() {
let opts = Options::default();
assert_eq!(opts.level, Level::Info);
assert_eq!(opts.formatter, Formatter::Text);
assert!(!opts.report_timestamp);
assert!(!opts.report_caller);
}
#[test]
fn test_logger_with_options() {
let opts = Options {
level: Level::Debug,
prefix: "test".to_string(),
report_timestamp: true,
..Default::default()
};
let logger = Logger::with_options(opts);
assert_eq!(logger.level(), Level::Debug);
assert_eq!(logger.prefix(), "test");
}
struct FailingWriter;
impl Write for FailingWriter {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::other("simulated failure"))
}
fn flush(&mut self) -> io::Result<()> {
Err(io::Error::other("simulated failure"))
}
}
#[test]
fn test_error_handler_called_on_io_failure() {
use std::sync::atomic::{AtomicUsize, Ordering};
let error_count = Arc::new(AtomicUsize::new(0));
let counter = error_count.clone();
let logger = Logger::new().with_error_handler(move |_err| {
counter.fetch_add(1, Ordering::Relaxed);
});
{
let mut inner = logger.inner.write().unwrap();
inner.writer = Box::new(FailingWriter);
}
logger.info("test message", &[]);
assert_eq!(error_count.load(Ordering::Relaxed), 1);
}
#[test]
fn test_error_handler_receives_correct_error() {
use std::sync::Mutex;
let captured_error = Arc::new(Mutex::new(None::<String>));
let error_capture = captured_error.clone();
let logger = Logger::new().with_error_handler(move |err| {
*error_capture.lock().unwrap() = Some(err.to_string());
});
{
let mut inner = logger.inner.write().unwrap();
inner.writer = Box::new(FailingWriter);
}
logger.info("test", &[]);
let error_msg = captured_error.lock().unwrap();
assert!(error_msg.is_some());
assert!(error_msg.as_ref().unwrap().contains("simulated failure"));
}
#[test]
fn test_default_behavior_warns_once() {
let logger = Logger::new();
{
let mut inner = logger.inner.write().unwrap();
inner.writer = Box::new(FailingWriter);
}
logger.info("first message", &[]);
logger.info("second message", &[]);
logger.info("third message", &[]);
let inner = logger.inner.read().unwrap();
assert!(inner.has_warned_io_failure);
}
#[test]
fn test_error_handler_inherited_by_with_fields() {
use std::sync::atomic::{AtomicUsize, Ordering};
let error_count = Arc::new(AtomicUsize::new(0));
let counter = error_count.clone();
let logger = Logger::new().with_error_handler(move |_err| {
counter.fetch_add(1, Ordering::Relaxed);
});
let child_logger = logger.with_fields(&[("component", "test")]);
{
let mut inner = child_logger.inner.write().unwrap();
inner.writer = Box::new(FailingWriter);
}
child_logger.info("test message", &[]);
assert_eq!(error_count.load(Ordering::Relaxed), 1);
}
#[test]
fn test_with_error_handler_returns_same_logger() {
use std::sync::atomic::{AtomicBool, Ordering};
let called = Arc::new(AtomicBool::new(false));
let flag = called.clone();
let logger = Logger::new().with_error_handler(move |_| {
flag.store(true, Ordering::Relaxed);
});
assert_eq!(logger.level(), Level::Info);
let inner = logger.inner.read().unwrap();
assert!(inner.error_handler.is_some());
}
#[test]
fn test_caller_warning_flag_set_on_first_log() {
let logger = Logger::new();
logger.set_report_caller(true);
{
let inner = logger.inner.read().unwrap();
assert!(!inner.warned_caller_overhead);
}
logger.info("test message", &[]);
{
let inner = logger.inner.read().unwrap();
assert!(inner.warned_caller_overhead);
}
}
#[test]
fn test_caller_warning_suppressed() {
let logger = Logger::new();
logger.set_report_caller(true);
logger.suppress_caller_warning();
{
let inner = logger.inner.read().unwrap();
assert!(inner.suppress_caller_warning);
}
logger.info("test message", &[]);
{
let inner = logger.inner.read().unwrap();
assert!(!inner.warned_caller_overhead);
}
}
#[test]
fn test_caller_warning_not_triggered_when_caller_disabled() {
let logger = Logger::new();
logger.info("test message", &[]);
{
let inner = logger.inner.read().unwrap();
assert!(!inner.warned_caller_overhead);
}
}
#[test]
fn test_caller_warning_inherits_suppression_via_with_fields() {
let logger = Logger::new();
logger.set_report_caller(true);
logger.suppress_caller_warning();
let child = logger.with_fields(&[("key", "value")]);
{
let inner = child.inner.read().unwrap();
assert!(inner.suppress_caller_warning);
}
}
#[test]
fn test_caller_warning_resets_for_child_logger() {
let logger = Logger::new();
logger.set_report_caller(true);
logger.info("parent message", &[]);
let child = logger.with_fields(&[("key", "value")]);
{
let inner = child.inner.read().unwrap();
assert!(!inner.warned_caller_overhead);
}
}
}