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::util::TryInitError;
use tracing_subscriber::Layer;
#[derive(Clone, Copy)]
pub enum LogFormat {
Json,
Plain,
Yaml,
}
pub struct AfdataLayer {
format: LogFormat,
redaction: crate::RedactionOptions,
}
pub fn init_json(filter: tracing_subscriber::EnvFilter) {
let _ = try_init_json(filter);
}
pub fn init_plain(filter: tracing_subscriber::EnvFilter) {
let _ = try_init_plain(filter);
}
pub fn init_yaml(filter: tracing_subscriber::EnvFilter) {
let _ = try_init_yaml(filter);
}
pub fn init_json_with_options(
filter: tracing_subscriber::EnvFilter,
redaction: crate::RedactionOptions,
) {
let _ = try_init_json_with_options(filter, redaction);
}
pub fn init_plain_with_options(
filter: tracing_subscriber::EnvFilter,
redaction: crate::RedactionOptions,
) {
let _ = try_init_plain_with_options(filter, redaction);
}
pub fn init_yaml_with_options(
filter: tracing_subscriber::EnvFilter,
redaction: crate::RedactionOptions,
) {
let _ = try_init_yaml_with_options(filter, redaction);
}
pub fn init_with_options(
filter: tracing_subscriber::EnvFilter,
format: LogFormat,
redaction: crate::RedactionOptions,
) {
let _ = try_init_with_options(filter, format, redaction);
}
pub fn try_init_json(filter: tracing_subscriber::EnvFilter) -> Result<(), TryInitError> {
try_init_json_with_options(filter, crate::RedactionOptions::default())
}
pub fn try_init_plain(filter: tracing_subscriber::EnvFilter) -> Result<(), TryInitError> {
try_init_plain_with_options(filter, crate::RedactionOptions::default())
}
pub fn try_init_yaml(filter: tracing_subscriber::EnvFilter) -> Result<(), TryInitError> {
try_init_yaml_with_options(filter, crate::RedactionOptions::default())
}
pub fn try_init_json_with_options(
filter: tracing_subscriber::EnvFilter,
redaction: crate::RedactionOptions,
) -> Result<(), TryInitError> {
try_init_with_options(filter, LogFormat::Json, redaction)
}
pub fn try_init_plain_with_options(
filter: tracing_subscriber::EnvFilter,
redaction: crate::RedactionOptions,
) -> Result<(), TryInitError> {
try_init_with_options(filter, LogFormat::Plain, redaction)
}
pub fn try_init_yaml_with_options(
filter: tracing_subscriber::EnvFilter,
redaction: crate::RedactionOptions,
) -> Result<(), TryInitError> {
try_init_with_options(filter, LogFormat::Yaml, redaction)
}
pub fn try_init_with_options(
filter: tracing_subscriber::EnvFilter,
format: LogFormat,
redaction: crate::RedactionOptions,
) -> Result<(), TryInitError> {
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
tracing_subscriber::registry()
.with(filter)
.with(AfdataLayer { format, redaction })
.try_init()
}
impl AfdataLayer {
fn output_options(&self) -> crate::OutputOptions {
crate::OutputOptions {
redaction: self.redaction.clone(),
style: crate::OutputStyle::Readable,
}
}
fn format_value(&self, value: &serde_json::Value) -> String {
let options = self.output_options();
match self.format {
LogFormat::Json => crate::output_json_with_options(value, &options),
LogFormat::Plain => crate::output_plain_with_options(value, &options),
LogFormat::Yaml => crate::output_yaml_with_options(value, &options),
}
}
}
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 level = 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));
}
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());
}
}
}
}
map.insert("code".into(), serde_json::Value::String("log".to_string()));
map.insert("level".into(), serde_json::Value::String(level.to_string()));
for (k, v) in visitor.fields {
if k == "code" {
continue;
}
map.insert(k, v);
}
let value = serde_json::Value::Object(map);
let line = self.format_value(&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 {
self.fields
.push((field.name().to_string(), serde_json::Value::String(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)));
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn secret_named_field_is_redacted_at_emit() {
let line = crate::output_json(&json!({
"code": "info",
"api_key_secret": "sk-live-123",
}));
assert!(line.contains("\"api_key_secret\":\"***\""), "{line}");
assert!(!line.contains("sk-live-123"), "{line}");
}
#[test]
fn non_secret_field_whose_value_mentions_secret_is_not_redacted() {
let line = crate::output_json(&json!({
"code": "info",
"note": "see the api_key_secret field in docs",
}));
assert!(
line.contains("see the api_key_secret field in docs"),
"{line}"
);
}
#[test]
fn secret_typed_field_is_redacted_regardless_of_record_path() {
let line = crate::output_json(&json!({
"code": "warn",
"db_password_secret": 1234,
}));
assert!(line.contains("\"db_password_secret\":\"***\""), "{line}");
}
#[test]
fn legacy_secret_names_are_redacted_when_layer_has_options() {
let value = json!({
"timestamp_epoch_ms": 1,
"message": "authorization appears in message but is not name-redacted",
"code": "log",
"level": "info",
"authorization": "Bearer legacy",
"request_url": "https://example.test/path?authorization=legacy&ok=1",
});
let redaction = crate::RedactionOptions {
secret_names: vec!["authorization".to_string()],
..crate::RedactionOptions::default()
};
for format in [LogFormat::Json, LogFormat::Plain, LogFormat::Yaml] {
let layer = AfdataLayer {
format,
redaction: redaction.clone(),
};
let line = layer.format_value(&value);
assert!(line.contains("***"), "{line}");
assert!(
!line.contains("Bearer legacy"),
"legacy field value should be redacted: {line}"
);
assert!(
!line.contains("authorization=legacy"),
"legacy URL query parameter should be redacted: {line}"
);
assert!(
line.contains("authorization appears in message"),
"message is free-form and should remain readable: {line}"
);
}
}
#[test]
fn legacy_secret_names_are_visible_without_layer_options() {
let value = json!({
"timestamp_epoch_ms": 1,
"message": "ready",
"code": "log",
"level": "info",
"authorization": "Bearer visible",
});
let layer = AfdataLayer {
format: LogFormat::Json,
redaction: crate::RedactionOptions::default(),
};
let line = layer.format_value(&value);
assert!(
line.contains("\"authorization\":\"Bearer visible\""),
"{line}"
);
}
}