use fastrace::collector::{Config, Reporter, SpanRecord};
use serde_json::{Map, Value, json};
use std::borrow::Cow;
use std::env;
use std::io::{self, Write};
use std::sync::OnceLock;
type PropPairs = [(Cow<'static, str>, Cow<'static, str>)];
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Level {
Off = 0,
Error = 1,
Warn = 2,
Info = 3,
Debug = 4,
Trace = 5,
}
impl Level {
fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"off" | "0" => Some(Self::Off),
"error" | "err" => Some(Self::Error),
"warn" | "warning" => Some(Self::Warn),
"info" => Some(Self::Info),
"debug" => Some(Self::Debug),
"trace" => Some(Self::Trace),
_ => None,
}
}
}
pub fn current_level() -> Level {
static LEVEL: OnceLock<Level> = OnceLock::new();
*LEVEL.get_or_init(|| {
env::var("AFTERBURNER_LOG")
.ok()
.and_then(|v| Level::parse(&v))
.unwrap_or(Level::Warn)
})
}
#[inline]
pub fn enabled(level: Level) -> bool {
level <= current_level()
}
#[macro_export]
macro_rules! ab_event {
($level:expr, $name:expr) => {{
if $crate::log::enabled($level) {
::fastrace::local::LocalSpan::add_event(
::fastrace::Event::new($name)
.with_property(|| ("level", $crate::log::level_str($level)))
);
}
}};
($level:expr, $name:expr, $($k:expr => $v:expr),+ $(,)?) => {{
if $crate::log::enabled($level) {
let level_label = $crate::log::level_str($level);
::fastrace::local::LocalSpan::add_event(
::fastrace::Event::new($name).with_properties(|| {
let pairs: ::std::vec::Vec<(::std::borrow::Cow<'static, str>, ::std::borrow::Cow<'static, str>)> = vec![
(::std::borrow::Cow::Borrowed("level"), ::std::borrow::Cow::Borrowed(level_label)),
$((::std::borrow::Cow::Borrowed($k), ::std::borrow::Cow::Owned($v.to_string()))),+
];
pairs
})
);
}
}};
}
pub fn level_str(level: Level) -> &'static str {
match level {
Level::Off => "off",
Level::Error => "error",
Level::Warn => "warn",
Level::Info => "info",
Level::Debug => "debug",
Level::Trace => "trace",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Text,
Json,
}
impl Format {
fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"text" | "human" | "" => Some(Self::Text),
"json" => Some(Self::Json),
_ => None,
}
}
fn from_env() -> Self {
env::var("AFTERBURNER_LOG_FORMAT")
.ok()
.and_then(|v| Self::parse(&v))
.unwrap_or(Self::Text)
}
}
static INIT_GUARD: OnceLock<()> = OnceLock::new();
pub fn init() {
init_with_format(Format::from_env());
}
pub fn init_with_format(format: Format) {
INIT_GUARD.get_or_init(|| {
if current_level() == Level::Off {
return;
}
match format {
Format::Text => fastrace::set_reporter(TextReporter, Config::default()),
Format::Json => fastrace::set_reporter(JsonReporter, Config::default()),
}
});
}
pub struct TextReporter;
impl Reporter for TextReporter {
fn report(&mut self, spans: Vec<SpanRecord>) {
let stderr = io::stderr();
let mut out = stderr.lock();
for span in spans {
let _ = writeln!(
out,
"[afterburner] span={} duration_us={} trace={:032x} props={}",
span.name,
span.duration_ns / 1_000,
span.trace_id.0,
fmt_props(&span.properties),
);
for event in span.events {
let _ = writeln!(
out,
"[afterburner] event={} props={}",
event.name,
fmt_props(&event.properties),
);
}
}
}
}
fn fmt_props(p: &PropPairs) -> String {
if p.is_empty() {
return String::from("{}");
}
let mut s = String::with_capacity(64);
s.push('{');
let mut first = true;
for (k, v) in p {
if !first {
s.push_str(", ");
}
first = false;
s.push_str(k);
s.push('=');
s.push_str(v);
}
s.push('}');
s
}
pub struct JsonReporter;
impl Reporter for JsonReporter {
fn report(&mut self, spans: Vec<SpanRecord>) {
let stdout = io::stdout();
let mut out = stdout.lock();
for span in spans {
let value = json!({
"name": span.name,
"trace_id": format!("{:032x}", span.trace_id.0),
"span_id": span.span_id.0,
"parent_span_id": span.parent_id.0,
"begin_unix_ns": span.begin_time_unix_ns,
"duration_ns": span.duration_ns,
"properties": props_to_json(&span.properties),
"events": span.events.iter().map(|e| json!({
"name": e.name,
"timestamp_unix_ns": e.timestamp_unix_ns,
"properties": props_to_json(&e.properties),
})).collect::<Vec<_>>(),
});
let _ = writeln!(out, "{value}");
}
}
}
fn props_to_json(p: &PropPairs) -> Map<String, Value> {
let mut m = Map::with_capacity(p.len());
for (k, v) in p {
m.insert(k.to_string(), Value::String(v.to_string()));
}
m
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn level_parse_round_trip() {
for (s, expect) in [
("off", Level::Off),
("ERROR", Level::Error),
("warn", Level::Warn),
("Info", Level::Info),
("debug", Level::Debug),
("TRACE", Level::Trace),
] {
assert_eq!(Level::parse(s), Some(expect));
}
assert_eq!(Level::parse("nonsense"), None);
}
#[test]
fn enabled_compares_correctly() {
assert!(Level::Error <= Level::Warn);
assert!(Level::Warn <= Level::Info);
assert!(Level::Debug > Level::Warn);
}
}