use std::io::{self, Write};
use tracing::field::{Field, Visit};
use tracing::span;
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::Layer;
#[derive(Clone, Copy)]
pub enum LogFormat {
Json,
Plain,
Yaml,
}
pub struct AfdataLayer {
format: LogFormat,
}
pub fn init_json(filter: tracing_subscriber::EnvFilter) {
init_with_format(filter, LogFormat::Json);
}
pub fn init_plain(filter: tracing_subscriber::EnvFilter) {
init_with_format(filter, LogFormat::Plain);
}
pub fn init_yaml(filter: tracing_subscriber::EnvFilter) {
init_with_format(filter, LogFormat::Yaml);
}
fn init_with_format(filter: tracing_subscriber::EnvFilter, format: LogFormat) {
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let _ = tracing_subscriber::registry()
.with(filter)
.with(AfdataLayer { format })
.try_init();
}
struct SpanFields(Vec<(String, serde_json::Value)>);
impl<S> Layer<S> for AfdataLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) {
let mut visitor = JsonVisitor::new();
attrs.record(&mut visitor);
if let Some(span) = ctx.span(id) {
span.extensions_mut().insert(SpanFields(visitor.fields));
}
}
fn on_record(&self, id: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) {
if let Some(span) = ctx.span(id) {
let mut visitor = JsonVisitor::new();
values.record(&mut visitor);
let mut extensions = span.extensions_mut();
if let Some(existing) = extensions.get_mut::<SpanFields>() {
existing.0.extend(visitor.fields);
} else {
extensions.insert(SpanFields(visitor.fields));
}
}
}
fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
let meta = event.metadata();
let mut visitor = JsonVisitor::new();
event.record(&mut visitor);
let mut map = serde_json::Map::with_capacity(4 + visitor.fields.len());
let default_code = match *meta.level() {
Level::TRACE => "trace",
Level::DEBUG => "debug",
Level::INFO => "info",
Level::WARN => "warn",
Level::ERROR => "error",
};
map.insert(
"timestamp_epoch_ms".into(),
serde_json::Value::Number(chrono::Utc::now().timestamp_millis().into()),
);
if let Some(msg) = visitor.message.take() {
map.insert("message".into(), serde_json::Value::String(msg));
}
map.insert(
"target".into(),
serde_json::Value::String(meta.target().to_string()),
);
if let Some(scope) = ctx.event_scope(event) {
for span in scope.from_root() {
let extensions = span.extensions();
if let Some(fields) = extensions.get::<SpanFields>() {
for (k, v) in &fields.0 {
map.insert(k.clone(), v.clone());
}
}
}
}
let mut has_code = false;
for (k, v) in visitor.fields {
if k == "code" {
has_code = true;
}
map.insert(k, v);
}
if !has_code {
map.insert(
"code".into(),
serde_json::Value::String(default_code.to_string()),
);
}
let value = serde_json::Value::Object(map);
let line = match self.format {
LogFormat::Json => crate::output_json(&value),
LogFormat::Plain => crate::output_plain(&value),
LogFormat::Yaml => crate::output_yaml(&value),
};
let mut out = io::stdout().lock();
let _ = out.write_all(line.as_bytes());
let _ = out.write_all(b"\n");
}
}
struct JsonVisitor {
message: Option<String>,
fields: Vec<(String, serde_json::Value)>,
}
impl JsonVisitor {
fn new() -> Self {
Self {
message: None,
fields: Vec::new(),
}
}
}
impl Visit for JsonVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
let val = format!("{:?}", value);
if field.name() == "message" {
self.message = Some(val);
} else {
let safe_val = sanitize_debug_field(field.name(), val);
self.fields.push((
field.name().to_string(),
serde_json::Value::String(safe_val),
));
}
}
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == "message" {
self.message = Some(value.to_string());
} else {
self.fields.push((
field.name().to_string(),
serde_json::Value::String(value.to_string()),
));
}
}
fn record_i64(&mut self, field: &Field, value: i64) {
self.fields.push((
field.name().to_string(),
serde_json::Value::Number(value.into()),
));
}
fn record_u64(&mut self, field: &Field, value: u64) {
self.fields.push((
field.name().to_string(),
serde_json::Value::Number(value.into()),
));
}
fn record_f64(&mut self, field: &Field, value: f64) {
if let Some(n) = serde_json::Number::from_f64(value) {
self.fields
.push((field.name().to_string(), serde_json::Value::Number(n)));
} else {
self.fields.push((
field.name().to_string(),
serde_json::Value::String(value.to_string()),
));
}
}
fn record_bool(&mut self, field: &Field, value: bool) {
self.fields
.push((field.name().to_string(), serde_json::Value::Bool(value)));
}
}
fn sanitize_debug_field(field_name: &str, raw_value: String) -> String {
if field_name.ends_with("_secret")
|| field_name.ends_with("_SECRET")
|| raw_value.contains("_secret")
|| raw_value.contains("_SECRET")
{
"***".to_string()
} else {
raw_value
}
}
#[cfg(test)]
mod tests {
use super::sanitize_debug_field;
#[test]
fn sanitize_debug_field_redacts_secret_key() {
let got = sanitize_debug_field("api_key_secret", "\"sk-live-123\"".to_string());
assert_eq!(got, "***");
}
#[test]
fn sanitize_debug_field_redacts_nested_secret_marker() {
let got = sanitize_debug_field(
"meta",
"Object {\"api_key_secret\": String(\"sk-live-123\")}".to_string(),
);
assert_eq!(got, "***");
}
#[test]
fn sanitize_debug_field_keeps_safe_debug_values() {
let got = sanitize_debug_field("attempt", "3".to_string());
assert_eq!(got, "3");
}
}