use std::path::{PathBuf, absolute};
use log::{LevelFilter, Record};
use time::OffsetDateTime;
use crate::logger::FileTarget;
pub(crate) const DEFAULT_BUF_CAPACITY: usize = 4 * 1024;
pub(crate) const DEFAULT_FLUSH_MS: u64 = 1_000;
pub(crate) const DEFAULT_LOG_FORMAT: &str =
"{timestamp} [{level:<5}] T[{thread_name}] [{file}:{line}] {args}";
#[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()
}
}
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 {
self.filters
.get_or_insert_with(Vec::new)
.push((target.into(), 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(bytes);
self
}
pub fn flush_ms(mut self, ms: u64) -> Self {
self.flush_ms = Some(ms);
self
}
pub fn format(mut self, template: impl Into<String>) -> Self {
self.format = Some(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(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)) => Some(absolute(&p).unwrap_or(p).display().to_string()),
Some(FileTarget::Stderr) => None,
None => current.and_then(|c| c.file_path.clone()),
};
let buf_capacity = self
.buf_capacity
.unwrap_or_else(|| current.map_or(DEFAULT_BUF_CAPACITY, |c| c.buf_capacity));
let flush_ms = self
.flush_ms
.unwrap_or_else(|| current.map_or(DEFAULT_FLUSH_MS, |c| c.flush_ms));
let 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,
}
}
}
pub fn config_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());
let flush_ms = std::env::var("RUST_LOG_FLUSH_MS")
.ok()
.and_then(|s| s.parse().ok());
let format = std::env::var("RUST_LOG_FORMAT").ok();
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)) => match level_str.trim().parse::<LevelFilter>() {
Ok(l) => filters.push((target.trim().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) struct TargetFilter {
target: String,
level: LevelFilter,
}
impl Clone for TargetFilter {
fn clone(&self) -> Self {
Self {
target: self.target.clone(),
level: self.level,
}
}
}
impl PartialEq for TargetFilter {
fn eq(&self, other: &Self) -> bool {
self.target == other.target && self.level == other.level
}
}
impl Eq for TargetFilter {}
#[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();
while let Some(next) = chars.next() {
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 } => {
let raw = render_field(*field, record);
output.push_str(&apply_format_spec(&raw, *spec));
}
}
}
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('<') {
if let Ok(width) = width_text.parse::<usize>() {
return FormatSpec {
align: Align::Left,
width: Some(width),
};
}
}
if let Some(width_text) = spec.strip_prefix('>') {
if let Ok(width) = width_text.parse::<usize>() {
return FormatSpec {
align: Align::Right,
width: Some(width),
};
}
}
FormatSpec {
align: Align::Left,
width: None,
}
}
fn render_field(field: LogField, record: &Record) -> String {
match field {
LogField::Timestamp => {
let now = OffsetDateTime::now_utc();
now.format(
&time::format_description::parse(
"[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:6]Z",
)
.unwrap(),
)
.unwrap_or_else(|_| "unknown-time".to_string())
}
LogField::ThreadName => std::thread::current()
.name()
.unwrap_or("unnamed")
.to_string(),
LogField::Level => record.level().to_string(),
LogField::Target => record.target().to_string(),
LogField::Args => format!("{}", record.args()),
LogField::ModulePath => record.module_path().unwrap_or_default().to_string(),
LogField::File => record.file().unwrap_or_default().to_string(),
LogField::Line => record
.line()
.map(|line| line.to_string())
.unwrap_or_default(),
}
}
fn apply_format_spec(value: &str, spec: FormatSpec) -> String {
match spec.width {
Some(width) if value.len() < width => {
let pad = width - value.len();
match spec.align {
Align::Left => {
let mut result = String::with_capacity(width);
result.push_str(value);
result.extend(std::iter::repeat(' ').take(pad));
result
}
Align::Right => {
let mut result = String::with_capacity(width);
result.extend(std::iter::repeat(' ').take(pad));
result.push_str(value);
result
}
}
}
_ => value.to_string(),
}
}