use std::{
collections::{HashMap, VecDeque},
fmt::{Arguments, Debug},
fs::write,
path::{Path, PathBuf},
process::Command,
};
use log::kv::{Error, Key, Value, VisitSource};
use log::{LevelFilter, Log, Metadata, Record, SetLoggerError};
#[cfg(feature = "timestamps")]
use chrono::{DateTime, Local, Utc};
#[cfg(feature = "timestamps")]
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(feature = "timestamps")]
#[derive(PartialEq, Debug)]
pub enum TimestampFormat {
UtcEpochMs,
UtcEpochUs,
Utc,
Local,
}
pub struct CallLogger {
level: LevelFilter,
levels: Vec<(String, log::LevelFilter)>,
call_target: String,
#[cfg(feature = "timestamps")]
timestamp: TimestampFormat,
#[cfg(feature = "timestamps")]
format_string: Option<String>,
file: Option<PathBuf>,
formatter: Box<Formatter>,
echo: bool,
}
impl CallLogger {
pub fn new() -> CallLogger {
CallLogger {
level: LevelFilter::Trace,
levels: Vec::new(),
call_target: "echo".into(),
#[cfg(feature = "timestamps")]
timestamp: TimestampFormat::Utc,
#[cfg(feature = "timestamps")]
format_string: None,
file: None,
echo: false,
formatter: Box::new(Self::json_formatter),
}
}
#[inline]
#[must_use = "You must call init() before logging"]
pub fn with_level(mut self, level: LevelFilter) -> CallLogger {
self.level = level;
log::set_max_level(self.level);
self
}
#[inline]
#[must_use = "You must call init() before logging"]
pub fn with_level_for<T: Into<String>>(mut self, target: T, level: log::LevelFilter) -> Self {
self.levels.push((target.into(), level));
self
}
#[inline]
#[must_use = "You must call init() before logging"]
pub fn with_call_target<T>(mut self, call_target: T) -> CallLogger
where
T: Into<String>,
{
self.call_target = call_target.into();
self
}
#[inline]
#[must_use = "You must call init() before logging"]
#[cfg(feature = "timestamps")]
pub fn with_epoch_ms_timestamp(mut self) -> CallLogger {
self.timestamp = TimestampFormat::UtcEpochMs;
self
}
#[inline]
#[must_use = "You must call init() before logging"]
#[cfg(feature = "timestamps")]
pub fn with_epoch_us_timestamp(mut self) -> CallLogger {
self.timestamp = TimestampFormat::UtcEpochUs;
self
}
#[inline]
#[must_use = "You must call init() before logging"]
#[cfg(feature = "timestamps")]
pub fn with_utc_timestamp(mut self) -> CallLogger {
self.timestamp = TimestampFormat::Utc;
self
}
#[inline]
#[must_use = "You must call init() before logging"]
#[cfg(feature = "timestamps")]
pub fn with_local_timestamp(mut self) -> CallLogger {
self.timestamp = TimestampFormat::Local;
self
}
#[inline]
#[must_use = "You must call init() before logging"]
#[cfg(feature = "timestamps")]
pub fn with_formatted_timestamp<T>(
mut self,
timestamp_format: TimestampFormat,
format_string: T,
) -> CallLogger
where
T: Into<String>,
{
self.timestamp = timestamp_format;
self.format_string = Some(format_string.into());
self
}
#[inline]
#[must_use = "You must call init() before logging"]
pub fn echo(mut self) -> CallLogger {
self.echo = true;
self
}
#[inline]
#[must_use = "You must call init() before logging"]
pub fn to_file<P>(mut self, file: P) -> CallLogger
where
P: AsRef<Path>,
{
self.file = Some(PathBuf::from(file.as_ref()));
self
}
#[inline]
#[cfg(feature = "timestamps")]
pub fn format<F>(mut self, formatter: F) -> Self
where
F: Fn(String, &Arguments, &log::Record) -> String + Sync + Send + 'static,
{
self.formatter = Box::new(formatter);
self
}
#[inline]
#[cfg(not(feature = "timestamps"))]
pub fn format<F>(mut self, formatter: F) -> Self
where
F: Fn(&Arguments, &log::Record) -> String + Sync + Send + 'static,
{
self.formatter = Box::new(formatter);
self
}
pub fn init(self) -> Result<(), SetLoggerError> {
log::set_boxed_logger(Box::new(self))?;
Ok(())
}
#[cfg(feature = "timestamps")]
fn format_timestamp(&self, time: SystemTime) -> String {
if let Some(format_string) = &self.format_string {
match &self.timestamp {
TimestampFormat::Local => Into::<DateTime<Local>>::into(time)
.format(format_string)
.to_string(),
_ => Into::<DateTime<Utc>>::into(time)
.format(format_string)
.to_string(),
}
} else {
match &self.timestamp {
TimestampFormat::UtcEpochMs => time
.duration_since(UNIX_EPOCH)
.expect("Leap second or time went backwards")
.as_millis()
.to_string(),
TimestampFormat::UtcEpochUs => time
.duration_since(UNIX_EPOCH)
.expect("Leap second or time went backwards")
.as_micros()
.to_string(),
TimestampFormat::Utc => Into::<DateTime<Utc>>::into(time).to_rfc3339().to_string(),
TimestampFormat::Local => {
Into::<DateTime<Local>>::into(time).to_rfc3339().to_string()
}
}
}
}
#[cfg(not(feature = "timestamps"))]
fn json_formatter(message: &Arguments, record: &log::Record) -> String {
Self::json_formatter_inner(String::new(), message, record)
}
#[cfg(feature = "timestamps")]
fn json_formatter(timestamp: String, message: &Arguments, record: &log::Record) -> String {
Self::json_formatter_inner(timestamp.to_string(), message, record)
}
fn json_formatter_inner(
timestamp: String,
message: &Arguments,
record: &log::Record,
) -> String {
let timestamp = format!("\"ts\":\"{timestamp}\",");
let level = format!("\"level\":\"{}\",", record.level());
let file = match record.file() {
Some(file) => format!("\"file\":\"{file}\","),
None => "".to_string(),
};
let line = match record.line() {
Some(line) => format!("\"line\":\"{line}\","),
None => "".to_string(),
};
let module_path = match record.module_path() {
Some(module_path) => format!("\"module_path\":\"{module_path}\","),
None => "".to_string(),
};
let mut visitor = LogVisitor {
map: HashMap::new(),
};
let kv_str = if let Ok(()) = record.key_values().visit(&mut visitor) {
let mut msg = String::new();
for (key, value) in visitor.map {
msg.push_str(&format!("\"{key}\":\"{value}\","));
}
msg
} else {
"".to_string()
};
let msg = format!(
"\"msg\":\"{}\"",
message
.to_string()
.replace('\\', "\\\\")
.replace('\"', "\\\"")
);
format!("{{{timestamp}{level}{file}{line}{module_path}{kv_str}{msg}}}")
}
fn get_level_for_module(&self, target: String) -> &LevelFilter {
self.levels
.iter()
.find(|(module, _)| target.contains(module))
.map(|(_, level)| level)
.unwrap_or(&self.level)
}
}
impl Default for CallLogger {
fn default() -> Self {
Self::new()
}
}
impl Log for CallLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= *self.get_level_for_module(metadata.target().to_string())
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let formatter = &self.formatter;
#[cfg(feature = "timestamps")]
let params = formatter(
self.format_timestamp(SystemTime::now()),
record.args(),
record,
);
#[cfg(not(feature = "timestamps"))]
let params = formatter(record.args(), record);
if self.call_target.starts_with("http://") || self.call_target.starts_with("https://") {
if self.echo {
println!("Calling: `{}\n\t{params}`", self.call_target);
}
let avoid_overflow = match record.module_path() {
Some(module_path) => {
module_path.starts_with("ureq::") | module_path.starts_with("ureq_proto::")
|| module_path.starts_with("rustls::")
}
None => false,
};
if !avoid_overflow {
if let Err(x) = ureq::post(&self.call_target)
.header("Content-Type", "application/json")
.send(params.as_str())
{
println!("logging call to {} failed {x}", self.call_target);
}
}
} else {
let mut args = if let Some((header, trailer)) = self.call_target.split_once("{}") {
let mut args = header.split(' ').collect::<VecDeque<&str>>();
args.push_back(params.as_str());
for arg in trailer.split(' ') {
args.push_back(arg);
}
args
} else {
let mut args = self.call_target.split(' ').collect::<VecDeque<&str>>();
args.push_back(params.as_str());
args
};
if self.echo {
println!("Calling: `{}`", Vec::from(args.clone()).join(" "));
}
let call_target = args.pop_front().unwrap();
match self.file {
Some(_) => match Command::new(call_target).args(args).output() {
Ok(output) => {
if let Some(file) = &self.file {
let _ = write(file, &output.stdout);
}
}
Err(x) => {
println!("logging call to {} failed {x}", self.call_target);
}
},
None => match Command::new(call_target).args(args).spawn() {
Ok(_) => {}
Err(x) => {
println!("logging call to {} failed {x}", self.call_target);
}
},
}
}
}
}
fn flush(&self) {
log::logger().flush()
}
}
impl Debug for CallLogger {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
struct LevelsDebug<'a>(&'a [(String, LevelFilter)]);
impl Debug for LevelsDebug<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_map()
.entries(self.0.iter().map(|t| (&t.0, t.1)))
.finish()
}
}
let mut f = f.debug_struct("CallLogger");
let f = f
.field("call-target", &self.call_target)
.field("level", &self.level)
.field("levels", &LevelsDebug(&self.levels))
.field("echo", &self.echo)
.field("file", &self.file)
.field("formatter", &"Box<Formatter>");
#[cfg(feature = "timestamps")]
let f = f.field("timestamp", &self.timestamp);
f.finish()
}
}
struct LogVisitor {
map: HashMap<String, String>,
}
impl<'kvs> VisitSource<'kvs> for LogVisitor {
fn visit_pair(&mut self, key: Key<'kvs>, value: Value<'kvs>) -> Result<(), Error> {
self.map.insert(key.to_string(), value.to_string());
Ok(())
}
}
#[cfg(feature = "timestamps")]
pub type Formatter = dyn Fn(String, &Arguments, &log::Record) -> String + Sync + Send + 'static;
#[cfg(not(feature = "timestamps"))]
pub type Formatter = dyn Fn(&Arguments, &log::Record) -> String + Sync + Send + 'static;
#[cfg(test)]
mod test;