use std::fmt::Write;
use std::path::{PathBuf, absolute};
use std::sync::OnceLock;
use log::{LevelFilter, Record};
use time::OffsetDateTime;
use crate::logger::FileTarget;
pub(crate) const DEFAULT_BUF_CAPACITY: usize = 4 * 1024;
pub(crate) const MAX_BUF_CAPACITY: usize = 1024 * 1024;
pub(crate) const DEFAULT_FLUSH_MS: u64 = 1_000;
pub(crate) const MAX_FLUSH_MS: u64 = 60 * 60 * 1_000;
pub(crate) const DEFAULT_LOG_FORMAT: &str =
"{timestamp} [{level:<5}] T[{thread_name}] [{target}] {args}";
pub(crate) const MAX_FORMAT_TEMPLATE_LEN: usize = 8 * 1024;
pub(crate) const MAX_FORMAT_FIELD_WIDTH: usize = 4 * 1024;
pub(crate) const MAX_FILTERS: usize = 128;
pub(crate) const MAX_FILTER_TARGET_LEN: usize = 256;
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct ReloadConfig {
pub(crate) default_level: LevelFilter,
pub(crate) filters: Vec<TargetFilter>,
pub(crate) file_path: Option<String>,
pub(crate) buf_capacity: usize,
pub(crate) flush_ms: u64,
pub(crate) format_template: String,
}
#[derive(Clone)]
pub(crate) struct ActiveConfig {
pub(crate) reload: ReloadConfig,
pub(crate) format: LogFormat,
pub(crate) max_level: LevelFilter,
}
impl ActiveConfig {
pub(crate) fn from_reload(reload: ReloadConfig) -> Self {
let max_level = reload
.filters
.iter()
.map(|f| f.level)
.fold(reload.default_level, |acc, level| acc.max(level));
ActiveConfig {
format: LogFormat::parse(&reload.format_template),
reload,
max_level,
}
}
#[inline]
pub(crate) fn level_for(&self, target: &str) -> LevelFilter {
self.reload
.filters
.iter()
.find(|f| target.starts_with(f.target.as_str()))
.map(|f| f.level)
.unwrap_or(self.reload.default_level)
}
}
pub struct MinimalLoggerConfig {
pub(crate) level: Option<LevelFilter>,
pub(crate) filters: Option<Vec<(String, LevelFilter)>>,
pub(crate) file: Option<FileTarget>,
pub(crate) buf_capacity: Option<usize>,
pub(crate) flush_ms: Option<u64>,
pub(crate) format: Option<String>,
}
impl Default for MinimalLoggerConfig {
fn default() -> Self {
Self::new()
}
}
fn bounded_buf_capacity(bytes: usize) -> usize {
if bytes > MAX_BUF_CAPACITY {
eprintln!("[minimal_logger] Buffer size {bytes} exceeds max {MAX_BUF_CAPACITY} — clamping");
MAX_BUF_CAPACITY
} else {
bytes
}
}
fn bounded_flush_ms(ms: u64) -> u64 {
if ms > MAX_FLUSH_MS {
eprintln!("[minimal_logger] Flush interval {ms}ms exceeds max {MAX_FLUSH_MS}ms — clamping");
MAX_FLUSH_MS
} else {
ms
}
}
fn bounded_format_template(template: String) -> String {
if template.len() > MAX_FORMAT_TEMPLATE_LEN {
eprintln!(
"[minimal_logger] Format template is {} bytes; max is {MAX_FORMAT_TEMPLATE_LEN} — using default",
template.len()
);
DEFAULT_LOG_FORMAT.to_string()
} else {
template
}
}
fn bounded_format_width(width: usize) -> usize {
if width > MAX_FORMAT_FIELD_WIDTH {
eprintln!(
"[minimal_logger] Format field width {width} exceeds max {MAX_FORMAT_FIELD_WIDTH} — clamping"
);
MAX_FORMAT_FIELD_WIDTH
} else {
width
}
}
impl MinimalLoggerConfig {
pub fn new() -> Self {
MinimalLoggerConfig {
level: None,
filters: None,
file: None,
buf_capacity: None,
flush_ms: None,
format: None,
}
}
pub fn level(mut self, level: LevelFilter) -> Self {
self.level = Some(level);
self
}
pub fn filter(mut self, target: impl Into<String>, level: LevelFilter) -> Self {
let target = target.into();
if target.len() > MAX_FILTER_TARGET_LEN {
eprintln!(
"[minimal_logger] Filter target {:?} exceeds max {MAX_FILTER_TARGET_LEN} bytes — skipping",
target
);
return self;
}
let filters = self.filters.get_or_insert_with(Vec::new);
if filters.len() >= MAX_FILTERS {
eprintln!("[minimal_logger] Filter count exceeds max {MAX_FILTERS} — skipping");
return self;
}
filters.push((target, level));
self
}
pub fn file(mut self, path: impl Into<PathBuf>) -> Self {
self.file = Some(FileTarget::Path(path.into()));
self
}
pub fn stderr(mut self) -> Self {
self.file = Some(FileTarget::Stderr);
self
}
pub fn buf_capacity(mut self, bytes: usize) -> Self {
self.buf_capacity = Some(bounded_buf_capacity(bytes));
self
}
pub fn flush_ms(mut self, ms: u64) -> Self {
self.flush_ms = Some(bounded_flush_ms(ms));
self
}
pub fn format(mut self, template: impl Into<String>) -> Self {
self.format = Some(bounded_format_template(template.into()));
self
}
pub fn get_level(&self) -> Option<LevelFilter> {
self.level
}
pub fn get_filters(&self) -> &[(String, LevelFilter)] {
self.filters.as_deref().unwrap_or(&[])
}
pub fn get_file_path(&self) -> Option<&std::path::Path> {
match &self.file {
Some(FileTarget::Path(p)) => Some(p.as_path()),
_ => None,
}
}
pub fn get_buf_capacity(&self) -> Option<usize> {
self.buf_capacity
}
pub fn get_flush_ms(&self) -> Option<u64> {
self.flush_ms
}
pub fn get_format(&self) -> Option<&str> {
self.format.as_deref()
}
pub fn from_env() -> MinimalLoggerConfig {
let rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
let file = std::env::var("RUST_LOG_FILE")
.ok()
.map(|path| FileTarget::Path(PathBuf::from(path)));
let buf_capacity = std::env::var("RUST_LOG_BUFFER_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.map(bounded_buf_capacity);
let flush_ms = std::env::var("RUST_LOG_FLUSH_MS")
.ok()
.and_then(|s| s.parse().ok())
.map(bounded_flush_ms);
let format = std::env::var("RUST_LOG_FORMAT")
.ok()
.map(bounded_format_template);
let mut level: Option<LevelFilter> = None;
let mut filters: Vec<(String, LevelFilter)> = Vec::new();
for directive in rust_log.split(',').map(str::trim).filter(|s| !s.is_empty()) {
match directive.split_once('=') {
Some((target, level_str)) => {
let target = target.trim();
if filters.len() >= MAX_FILTERS {
eprintln!(
"[minimal_logger] RUST_LOG: too many filters — ignoring remaining"
);
break;
}
if target.len() > MAX_FILTER_TARGET_LEN {
eprintln!(
"[minimal_logger] RUST_LOG: filter target {:?} exceeds max {MAX_FILTER_TARGET_LEN} bytes — skipping",
target
);
continue;
}
match level_str.trim().parse::<LevelFilter>() {
Ok(l) => filters.push((target.to_string(), l)),
Err(_) => eprintln!(
"[minimal_logger] RUST_LOG: unknown level {:?} — skipping",
level_str
),
}
}
None => match directive.parse::<LevelFilter>() {
Ok(l) => level = Some(l),
Err(_) => eprintln!(
"[minimal_logger] RUST_LOG: unknown directive {:?} — skipping",
directive
),
},
}
}
MinimalLoggerConfig {
level,
filters: if filters.is_empty() {
None
} else {
Some(filters)
},
file,
buf_capacity,
flush_ms,
format,
}
}
pub(crate) fn into_reload(self, current: Option<&ReloadConfig>) -> ReloadConfig {
let default_level = self
.level
.unwrap_or_else(|| current.map_or(LevelFilter::Info, |c| c.default_level));
let filters = match self.filters {
Some(vec) => {
let mut tf: Vec<TargetFilter> = vec
.into_iter()
.map(|(target, level)| TargetFilter { target, level })
.collect();
tf.sort_unstable_by(|a, b| b.target.len().cmp(&a.target.len()));
tf
}
None => current.map_or_else(Vec::new, |c| c.filters.clone()),
};
let file_path = match self.file {
Some(FileTarget::Path(p)) => {
let abs = match absolute(&p) {
Ok(a) => a,
Err(e) => {
eprintln!(
"[minimal_logger] Could not resolve absolute path for {:?}: {e} — using path as-is",
p
);
p
}
};
Some(abs.display().to_string())
}
Some(FileTarget::Stderr) => None,
None => current.and_then(|c| c.file_path.clone()),
};
let buf_capacity = bounded_buf_capacity(
self.buf_capacity
.unwrap_or_else(|| current.map_or(DEFAULT_BUF_CAPACITY, |c| c.buf_capacity)),
);
let flush_ms = bounded_flush_ms(
self.flush_ms
.unwrap_or_else(|| current.map_or(DEFAULT_FLUSH_MS, |c| c.flush_ms)),
);
let format_template = bounded_format_template(self.format.unwrap_or_else(|| {
current.map_or_else(
|| DEFAULT_LOG_FORMAT.to_string(),
|c| c.format_template.clone(),
)
}));
ReloadConfig {
default_level,
filters,
file_path,
buf_capacity,
flush_ms,
format_template,
}
}
}
#[deprecated(
since = "0.3.0",
note = "Use `MinimalLoggerConfig::from_env()` instead"
)]
pub fn config_from_env() -> MinimalLoggerConfig {
MinimalLoggerConfig::from_env()
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct TargetFilter {
pub(crate) target: String,
pub(crate) level: LevelFilter,
}
#[derive(Clone, Copy)]
enum Align {
Left,
Right,
}
#[derive(Clone, Copy)]
struct FormatSpec {
align: Align,
width: Option<usize>,
}
#[derive(Clone, Copy)]
enum LogField {
Timestamp,
ThreadName,
Level,
Target,
Args,
ModulePath,
File,
Line,
}
#[derive(Clone)]
enum FormatPiece {
Literal(String),
Placeholder { field: LogField, spec: FormatSpec },
}
#[derive(Clone)]
pub(crate) struct LogFormat {
pieces: Vec<FormatPiece>,
}
impl LogFormat {
pub(crate) fn parse(format: &str) -> Self {
let mut pieces = Vec::new();
let mut literal = String::new();
let mut chars = format.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'{' => {
if chars.peek() == Some(&'{') {
chars.next();
literal.push('{');
continue;
}
if !literal.is_empty() {
pieces.push(FormatPiece::Literal(std::mem::take(&mut literal)));
}
let mut token = String::new();
for next in chars.by_ref() {
if next == '}' {
break;
}
token.push(next);
}
let piece = if token.is_empty() {
FormatPiece::Literal("{}".to_string())
} else {
parse_placeholder(&token)
};
pieces.push(piece);
}
'}' => {
if chars.peek() == Some(&'}') {
chars.next();
literal.push('}');
} else {
literal.push('}');
}
}
other => literal.push(other),
}
}
if !literal.is_empty() {
pieces.push(FormatPiece::Literal(literal));
}
LogFormat { pieces }
}
pub(crate) fn render(&self, record: &Record) -> String {
let mut output = String::new();
for piece in &self.pieces {
match piece {
FormatPiece::Literal(text) => output.push_str(text),
FormatPiece::Placeholder { field, spec } => {
write_field(&mut output, *field, *spec, record);
}
}
}
if !output.ends_with('\n') {
output.push('\n');
}
output
}
}
fn parse_placeholder(token: &str) -> FormatPiece {
let (name, spec_text) = token.split_once(':').unwrap_or((token, ""));
let spec = parse_format_spec(spec_text);
let field = match name {
"timestamp" => LogField::Timestamp,
"thread_name" => LogField::ThreadName,
"level" => LogField::Level,
"target" => LogField::Target,
"args" | "message" => LogField::Args,
"module_path" => LogField::ModulePath,
"file" => LogField::File,
"line" => LogField::Line,
_ => {
return FormatPiece::Literal(format!("{{{}}}", token));
}
};
FormatPiece::Placeholder { field, spec }
}
fn parse_format_spec(spec: &str) -> FormatSpec {
if let Some(width_text) = spec.strip_prefix('<')
&& let Ok(width) = width_text.parse::<usize>()
{
return FormatSpec {
align: Align::Left,
width: Some(bounded_format_width(width)),
};
}
if let Some(width_text) = spec.strip_prefix('>')
&& let Ok(width) = width_text.parse::<usize>()
{
return FormatSpec {
align: Align::Right,
width: Some(bounded_format_width(width)),
};
}
FormatSpec {
align: Align::Left,
width: None,
}
}
static TIMESTAMP_FMT: OnceLock<time::format_description::OwnedFormatItem> = OnceLock::new();
fn write_field(out: &mut String, field: LogField, spec: FormatSpec, record: &Record) {
fn write_padded(out: &mut String, value: &str, spec: FormatSpec) {
match spec.width {
Some(w) if value.len() < w => {
let pad = w - value.len();
match spec.align {
Align::Left => {
out.push_str(value);
for _ in 0..pad {
out.push(' ');
}
}
Align::Right => {
for _ in 0..pad {
out.push(' ');
}
out.push_str(value);
}
}
}
_ => out.push_str(value),
}
}
match field {
LogField::Timestamp => {
let fmt = TIMESTAMP_FMT.get_or_init(|| {
time::format_description::parse_owned::<1>(
"[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:6]Z",
)
.expect("timestamp format string is valid")
});
let now = OffsetDateTime::now_utc();
match now.format(fmt) {
Ok(ts) => write_padded(out, &ts, spec),
Err(_) => write_padded(out, "unknown-time", spec),
}
}
LogField::ThreadName => {
let t = std::thread::current();
write_padded(out, t.name().unwrap_or("unnamed"), spec);
}
LogField::Level => {
write_padded(out, record.level().as_str(), spec);
}
LogField::Target => {
write_padded(out, record.target(), spec);
}
LogField::Args => {
match spec.width {
None => {
let _ = write!(out, "{}", record.args());
}
Some(_) => {
let s = record.args().to_string();
write_padded(out, &s, spec);
}
}
}
LogField::ModulePath => {
write_padded(out, record.module_path().unwrap_or_default(), spec);
}
LogField::File => {
write_padded(out, record.file().unwrap_or_default(), spec);
}
LogField::Line => {
if let Some(n) = record.line() {
if spec.width.is_none() {
let _ = write!(out, "{n}");
} else {
let s = n.to_string();
write_padded(out, &s, spec);
}
}
}
}
}