use chrono::Utc;
use serde_json::Value;
use tracing::{debug, error, info, warn};
#[cfg_attr(coverage, coverage(off))]
#[inline(always)]
fn ensure_initialized() {
#[cfg(not(test))]
{
use std::sync::Once;
use tracing_subscriber::{prelude::*, EnvFilter};
static INIT: Once = Once::new();
INIT.call_once(|| {
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer().with_ansi(true))
.with(env_filter)
.init();
});
}
}
pub fn log(message: &str) {
ensure_initialized();
info!(message);
}
pub fn info(message: &str) {
ensure_initialized();
info!(message);
}
pub fn warn(message: &str) {
ensure_initialized();
warn!(message);
}
pub fn error(message: &str) {
ensure_initialized();
error!(message);
}
pub fn debug(message: &str) {
ensure_initialized();
debug!(message);
}
pub fn info_with(message: &str, data: impl Into<Value>) {
ensure_initialized();
let timestamp = Utc::now().to_rfc3339();
info!(%timestamp, message = %message, data = ?data.into());
}
pub fn warn_with(message: &str, data: impl Into<Value>) {
ensure_initialized();
let timestamp = Utc::now().to_rfc3339();
warn!(%timestamp, message = %message, data = ?data.into());
}
pub fn error_with(message: &str, data: impl Into<Value>) {
ensure_initialized();
let timestamp = Utc::now().to_rfc3339();
error!(%timestamp, message = %message, data = ?data.into());
}
pub fn debug_with(message: &str, data: impl Into<Value>) {
ensure_initialized();
let timestamp = Utc::now().to_rfc3339();
debug!(%timestamp, message = %message, data = ?data.into());
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::io;
use std::sync::{Arc, Mutex};
use tracing_subscriber::{filter::LevelFilter, fmt, layer::SubscriberExt, registry};
#[derive(Clone)]
struct TestWriter {
buf: Arc<Mutex<Vec<u8>>>,
}
impl TestWriter {
fn new() -> Self {
Self {
buf: Arc::new(Mutex::new(Vec::new())),
}
}
fn get_contents(&self) -> String {
let mut buf = self.buf.lock().unwrap();
let output = String::from_utf8(buf.clone()).expect("Logs should be valid UTF-8");
buf.clear();
output
}
}
#[cfg_attr(coverage, coverage(off))]
impl io::Write for TestWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buf.lock().unwrap().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.buf.lock().unwrap().flush()
}
}
#[test]
fn it_logs_all_levels() {
let writer = TestWriter::new();
let writer_clone = writer.clone();
let subscriber = registry()
.with(
fmt::layer()
.with_writer(move || writer_clone.clone())
.with_ansi(false),
)
.with(LevelFilter::TRACE);
tracing::subscriber::with_default(subscriber, || {
log("hello world");
info("this is info");
warn("a warning message");
error("an error message");
debug("a debug message");
});
let output = writer.get_contents();
assert!(output.contains("INFO") && output.contains("hello world"));
assert!(output.contains("INFO") && output.contains("this is info"));
assert!(output.contains("WARN") && output.contains("a warning message"));
assert!(output.contains("ERROR") && output.contains("an error message"));
assert!(output.contains("DEBUG") && output.contains("a debug message"));
}
#[test]
fn it_logs_all_levels_with_data() {
let writer = TestWriter::new();
let writer_clone = writer.clone();
let subscriber = registry()
.with(
fmt::layer()
.with_writer(move || writer_clone.clone())
.with_ansi(false),
)
.with(LevelFilter::TRACE);
tracing::subscriber::with_default(subscriber, || {
info_with("structured info", json!({ "k": "v" }));
warn_with("structured warn", json!({ "warn_level": 2 }));
error_with("structured error", json!({ "err": "boom" }));
debug_with("structured debug", json!({ "flag": true }));
});
let output = writer.get_contents();
assert!(output.contains("INFO") && output.contains("structured info"));
assert!(output.contains("WARN") && output.contains("structured warn"));
assert!(output.contains("ERROR") && output.contains("structured error"));
assert!(output.contains("DEBUG") && output.contains("structured debug"));
assert!(output.contains("timestamp")); assert!(output.contains('k') && output.contains('v'));
}
}