use std::fmt;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde_json::{Map, Value};
use tracing::field::{Field, Visit};
use tracing::{Event, Subscriber};
use tracing_subscriber::fmt::FmtContext;
use tracing_subscriber::fmt::format::{FormatEvent, FormatFields, Writer};
use tracing_subscriber::fmt::time::FormatTime;
use tracing_subscriber::registry::LookupSpan;
pub(crate) struct OtelLogFormat {
service_name: &'static str,
service_version: &'static str,
deployment_environment: Option<String>,
}
impl OtelLogFormat {
pub(crate) fn new() -> Self {
Self {
service_name: "aviso-cli",
service_version: aviso::VERSION,
deployment_environment: std::env::var("AVISO_DEPLOYMENT_ENV")
.ok()
.filter(|s| !s.is_empty()),
}
}
}
impl<S, N> FormatEvent<S, N> for OtelLogFormat
where
S: Subscriber + for<'lookup> LookupSpan<'lookup>,
N: for<'writer> FormatFields<'writer> + 'static,
{
fn format_event(
&self,
_ctx: &FmtContext<'_, S, N>,
mut writer: Writer<'_>,
event: &Event<'_>,
) -> fmt::Result {
let metadata = event.metadata();
let level = *metadata.level();
let mut visitor = OtelFieldVisitor::default();
event.record(&mut visitor);
let mut record = Map::new();
record.insert("timestamp".into(), Value::String(now_rfc3339_micros()));
record.insert(
"severityText".into(),
Value::String(severity_text(level).into()),
);
record.insert(
"severityNumber".into(),
Value::Number(severity_number(level).into()),
);
record.insert("body".into(), Value::String(visitor.body));
let mut resource = Map::new();
resource.insert(
"service.name".into(),
Value::String(self.service_name.into()),
);
resource.insert(
"service.version".into(),
Value::String(self.service_version.into()),
);
if let Some(env) = &self.deployment_environment {
resource.insert("deployment.environment".into(), Value::String(env.clone()));
}
record.insert("resource".into(), Value::Object(resource));
if !visitor.attributes.is_empty() {
record.insert("attributes".into(), Value::Object(visitor.attributes));
}
let line = serde_json::to_string(&record).map_err(|_| fmt::Error)?;
writeln!(writer, "{line}")
}
}
fn now_rfc3339_micros() -> String {
humantime::format_rfc3339_micros(SystemTime::now()).to_string()
}
pub(crate) struct ShortClockTimer;
impl FormatTime for ShortClockTimer {
fn format_time(&self, w: &mut Writer<'_>) -> fmt::Result {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO);
let secs_today = now.as_secs() % 86_400;
let h = (secs_today / 3_600) % 24;
let m = (secs_today / 60) % 60;
let s = secs_today % 60;
let millis = now.subsec_millis();
write!(w, "{h:02}:{m:02}:{s:02}.{millis:03}")
}
}
fn severity_text(level: tracing::Level) -> &'static str {
match level {
tracing::Level::ERROR => "ERROR",
tracing::Level::WARN => "WARN",
tracing::Level::INFO => "INFO",
tracing::Level::DEBUG => "DEBUG",
tracing::Level::TRACE => "TRACE",
}
}
fn f64_to_value(value: f64) -> Value {
match serde_json::Number::from_f64(value) {
Some(n) => Value::Number(n),
None if value.is_nan() => Value::String("NaN".into()),
None if value.is_sign_negative() => Value::String("-Infinity".into()),
None => Value::String("Infinity".into()),
}
}
fn severity_number(level: tracing::Level) -> u8 {
match level {
tracing::Level::TRACE => 1,
tracing::Level::DEBUG => 5,
tracing::Level::INFO => 9,
tracing::Level::WARN => 13,
tracing::Level::ERROR => 17,
}
}
#[derive(Default)]
struct OtelFieldVisitor {
body: String,
attributes: Map<String, Value>,
}
impl Visit for OtelFieldVisitor {
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == "message" {
self.body = value.to_string();
} else {
self.attributes
.insert(field.name().into(), Value::String(value.into()));
}
}
fn record_bool(&mut self, field: &Field, value: bool) {
self.attributes
.insert(field.name().into(), Value::Bool(value));
}
fn record_i64(&mut self, field: &Field, value: i64) {
self.attributes
.insert(field.name().into(), Value::Number(value.into()));
}
fn record_u64(&mut self, field: &Field, value: u64) {
self.attributes
.insert(field.name().into(), Value::Number(value.into()));
}
fn record_f64(&mut self, field: &Field, value: f64) {
self.attributes
.insert(field.name().into(), f64_to_value(value));
}
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
let formatted = format!("{value:?}");
if field.name() == "message" {
self.body = formatted;
} else {
self.attributes
.insert(field.name().into(), Value::String(formatted));
}
}
fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
self.attributes
.insert(field.name().into(), Value::String(value.to_string()));
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "test code: unwrap/expect/panic on JSON roundtrip is the expected diagnostic"
)]
mod tests {
use super::*;
#[test]
fn severity_text_uppercase_matches_otel_spec() {
assert_eq!(severity_text(tracing::Level::ERROR), "ERROR");
assert_eq!(severity_text(tracing::Level::WARN), "WARN");
assert_eq!(severity_text(tracing::Level::INFO), "INFO");
assert_eq!(severity_text(tracing::Level::DEBUG), "DEBUG");
assert_eq!(severity_text(tracing::Level::TRACE), "TRACE");
}
#[test]
fn severity_number_lowest_in_each_otel_band() {
assert_eq!(severity_number(tracing::Level::TRACE), 1);
assert_eq!(severity_number(tracing::Level::DEBUG), 5);
assert_eq!(severity_number(tracing::Level::INFO), 9);
assert_eq!(severity_number(tracing::Level::WARN), 13);
assert_eq!(severity_number(tracing::Level::ERROR), 17);
}
#[test]
fn timestamp_is_rfc3339_with_microsecond_precision() {
let ts = now_rfc3339_micros();
assert!(ts.ends_with('Z'), "expected trailing Z: {ts}");
let dot = ts.find('.').expect("expected . separator");
let frac = &ts[dot + 1..ts.len() - 1];
assert_eq!(
frac.len(),
6,
"expected 6-digit fractional seconds, got {frac:?}"
);
}
#[test]
fn visitor_default_is_empty() {
let v = OtelFieldVisitor::default();
assert!(v.body.is_empty());
assert!(v.attributes.is_empty());
}
#[test]
fn f64_to_value_emits_nan_and_infinity_as_distinct_strings() {
assert_eq!(f64_to_value(f64::NAN), Value::String("NaN".into()));
assert_eq!(
f64_to_value(f64::INFINITY),
Value::String("Infinity".into())
);
assert_eq!(
f64_to_value(f64::NEG_INFINITY),
Value::String("-Infinity".into())
);
}
#[test]
fn f64_to_value_emits_finite_values_as_numbers() {
match f64_to_value(1.5) {
Value::Number(n) => assert!((n.as_f64().expect("finite") - 1.5).abs() < f64::EPSILON),
other => panic!("expected Number for 1.5, got {other:?}"),
}
match f64_to_value(0.0) {
Value::Number(n) => assert_eq!(n.as_f64(), Some(0.0)),
other => panic!("expected Number for 0.0, got {other:?}"),
}
}
}