first commit, mlog (logfmt)
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2025-01-10 15:01:48 +01:00
commit a8e09718d4
6 changed files with 649 additions and 0 deletions

3
src/lib.rs Normal file
View File

@ -0,0 +1,3 @@
mod logger;
pub use logger::LoggerBuilder;

171
src/logger.rs Normal file
View File

@ -0,0 +1,171 @@
use chrono::Local;
use env_logger::Builder;
use log::kv::{Error, Key, Value, VisitSource};
use std::io::Write;
/// Struct for visiting key-value pairs and formatting them as logfmt.
struct LogFmtVisitor<'a> {
log: &'a mut String,
}
impl<'kvs> VisitSource<'kvs> for LogFmtVisitor<'kvs> {
fn visit_pair(&mut self, key: Key<'kvs>, value: Value<'kvs>) -> Result<(), Error> {
self.log.push_str(&format!(" {}=\"{}\"", key, value));
Ok(())
}
}
/// Builder for configuring the logfmt logger.
pub struct LoggerBuilder {
skip_messages: Vec<String>,
include_date: bool,
include_target: bool,
}
impl LoggerBuilder {
/// Creates a new `LoggerBuilder` with default settings.
pub fn new() -> Self {
Self {
skip_messages: vec![],
include_date: true,
include_target: true,
}
}
/// Adds a message pattern to skip in the logs.
pub fn skip_message(mut self, message: &str) -> Self {
self.skip_messages.push(message.to_string());
self
}
/// Enables or disables the inclusion of the date in logs.
pub fn include_date(mut self, include: bool) -> Self {
self.include_date = include;
self
}
/// Enables or disables the inclusion of the target in logs.
pub fn include_target(mut self, include: bool) -> Self {
self.include_target = include;
self
}
/// Formats a log record into a string based on the configuration.
fn format_log(&self, record: &log::Record) -> Option<String> {
let msg = record.args().to_string();
if self.skip_messages.iter().any(|skip| msg.contains(skip)) {
return None;
}
let mut log = String::new();
if self.include_date {
log.push_str(&format!(
"date=\"{}\" ",
Local::now().format("%Y-%m-%d %H:%M:%S")
));
}
log.push_str(&format!(
"level=\"{}\" msg=\"{}\"",
record.level().as_str().to_lowercase(),
msg
));
let _ = record
.key_values()
.visit(&mut LogFmtVisitor { log: &mut log });
if self.include_target {
log.push_str(&format!(" target=\"{}\"", record.target()));
}
Some(log)
}
/// Initializes the logger with the configured settings.
pub fn init(self) {
Builder::from_default_env()
.format(move |buf, record| {
if let Some(log) = self.format_log(record) {
writeln!(buf, "{}", log)
} else {
Ok(())
}
})
.init();
}
}
impl Default for LoggerBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::LoggerBuilder;
use chrono::Local;
use log::{Level, Record};
#[test]
fn test_format_log_with_date_and_target() {
let builder = LoggerBuilder::new().include_date(true).include_target(true);
let record = Record::builder()
.args(format_args!("Test message"))
.level(Level::Info)
.target("my_target")
.build();
let formatted = builder.format_log(&record).unwrap();
let expected_date = Local::now().format("%Y-%m-%d").to_string();
assert!(formatted.contains(&format!("date=\"{}", expected_date)));
assert!(formatted.contains("level=\"info\""));
assert!(formatted.contains("msg=\"Test message\""));
assert!(formatted.contains("target=\"my_target\""));
}
#[test]
fn test_format_log_without_date_and_target() {
let builder = LoggerBuilder::new()
.include_date(false)
.include_target(false);
let record = Record::builder()
.args(format_args!("Another test message"))
.level(Level::Error)
.target("no_target")
.build();
let formatted = builder.format_log(&record).unwrap();
assert!(!formatted.contains("date="));
assert!(formatted.contains("level=\"error\""));
assert!(formatted.contains("msg=\"Another test message\""));
assert!(!formatted.contains("target=\"no_target\""));
}
#[test]
fn test_format_log_skip_message() {
let builder = LoggerBuilder::new().skip_message("skip_this");
let record_skip = Record::builder()
.args(format_args!("skip_this: This should not appear"))
.level(Level::Warn)
.target("skipped_target")
.build();
let record_show = Record::builder()
.args(format_args!("This should appear"))
.level(Level::Warn)
.target("shown_target")
.build();
assert!(builder.format_log(&record_skip).is_none());
assert!(builder.format_log(&record_show).is_some());
}
}