use std::io::Write;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use tracing::field::{Field, Visit};
use tracing_subscriber::layer::Context;
use tracing_subscriber::Layer;
pub const WIRE_TARGET: &str = "matter_wire";
pub struct JsonlLayer<W: Write + Send + 'static> {
seq: AtomicU64,
writer: Mutex<W>,
}
impl<W: Write + Send + 'static> JsonlLayer<W> {
pub fn new(writer: W) -> Self {
Self {
seq: AtomicU64::new(0),
writer: Mutex::new(writer),
}
}
}
#[derive(Default)]
struct WireVisitor {
dir: Option<String>,
session_id: Option<u64>,
exchange_id: Option<u64>,
protocol: Option<u64>,
opcode: Option<u64>,
payload: Option<String>,
}
impl Visit for WireVisitor {
fn record_u64(&mut self, field: &Field, value: u64) {
match field.name() {
"session_id" => self.session_id = Some(value),
"exchange_id" => self.exchange_id = Some(value),
"protocol" => self.protocol = Some(value),
"opcode" => self.opcode = Some(value),
_ => {}
}
}
fn record_i64(&mut self, field: &Field, value: i64) {
if let Ok(v) = u64::try_from(value) {
self.record_u64(field, v);
}
}
fn record_str(&mut self, field: &Field, value: &str) {
match field.name() {
"dir" => self.dir = Some(value.to_owned()),
"payload" => self.payload = Some(value.to_owned()),
_ => {}
}
}
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if matches!(field.name(), "dir" | "payload") {
let rendered = format!("{value:?}");
let trimmed = rendered.trim_matches('"').to_owned();
match field.name() {
"dir" => self.dir = Some(trimmed),
"payload" => self.payload = Some(trimmed),
_ => {}
}
}
}
}
impl<S, W> Layer<S> for JsonlLayer<W>
where
S: tracing::Subscriber,
W: Write + Send + 'static,
{
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
if event.metadata().target() != WIRE_TARGET {
return;
}
let mut v = WireVisitor::default();
event.record(&mut v);
let (
Some(dir),
Some(session_id),
Some(exchange),
Some(protocol),
Some(opcode),
Some(payload),
) = (
v.dir,
v.session_id,
v.exchange_id,
v.protocol,
v.opcode,
v.payload,
)
else {
return;
};
if let Ok(mut w) = self.writer.lock() {
let seq = self.seq.fetch_add(1, Ordering::Relaxed);
let line = serde_json::json!({
"seq": seq,
"dir": dir,
"session_id": session_id,
"exchange": exchange,
"protocol": protocol,
"opcode": opcode,
"payload": payload,
});
let _ = writeln!(w, "{line}");
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
use std::sync::{Arc, Mutex};
use tracing_subscriber::layer::SubscriberExt as _;
use super::*;
#[derive(Clone)]
struct SharedBuf(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for SharedBuf {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn layer_serializes_wire_events_and_ignores_others() {
let buf = Arc::new(Mutex::new(Vec::new()));
let layer = JsonlLayer::new(SharedBuf(buf.clone()));
let subscriber = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(subscriber, || {
tracing::debug!(
target: "matter_wire",
dir = "tx",
session_id = 0_u64,
exchange_id = 1_u64,
protocol = 0_u64,
opcode = 0x20_u64,
payload = %"15300120aa18",
"wire"
);
tracing::debug!(unrelated = 1, "noise");
tracing::debug!(
target: "matter_wire",
dir = "rx",
session_id = 0_u64,
exchange_id = 1_u64,
protocol = 0_u64,
opcode = 0x21_u64,
payload = %"153001",
"wire"
);
});
let bytes = buf.lock().unwrap().clone();
let text = String::from_utf8(bytes).unwrap();
let lines: Vec<serde_json::Value> = text
.lines()
.map(|l| serde_json::from_str(l).unwrap())
.collect();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0]["seq"], 0);
assert_eq!(lines[0]["dir"], "tx");
assert_eq!(lines[0]["session_id"], 0);
assert_eq!(lines[0]["exchange"], 1);
assert_eq!(lines[0]["protocol"], 0);
assert_eq!(lines[0]["opcode"], 0x20);
assert_eq!(lines[0]["payload"], "15300120aa18");
assert_eq!(lines[1]["seq"], 1);
assert_eq!(lines[1]["dir"], "rx");
}
#[test]
fn wire_event_missing_schema_field_is_dropped() {
let buf = Arc::new(Mutex::new(Vec::new()));
let layer = JsonlLayer::new(SharedBuf(buf.clone()));
let subscriber = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(subscriber, || {
tracing::debug!(
target: "matter_wire",
dir = "tx",
session_id = 0_u64,
exchange_id = 1_u64,
protocol = 0_u64,
payload = %"deadbeef",
"wire"
);
});
let bytes = buf.lock().unwrap().clone();
assert!(bytes.is_empty(), "expected no output for incomplete event");
}
#[test]
fn empty_str_payload_serializes_as_empty_string() {
let buf = Arc::new(Mutex::new(Vec::new()));
let layer = JsonlLayer::new(SharedBuf(buf.clone()));
let subscriber = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(subscriber, || {
tracing::debug!(
target: "matter_wire",
dir = "tx",
session_id = 0_u64,
exchange_id = 1_u64,
protocol = 0_u64,
opcode = 0x10_u64,
payload = "", "wire"
);
});
let bytes = buf.lock().unwrap().clone();
let text = String::from_utf8(bytes).unwrap();
let lines: Vec<serde_json::Value> = text
.lines()
.map(|l| serde_json::from_str(l).unwrap())
.collect();
assert_eq!(lines.len(), 1, "expected exactly one line");
assert_eq!(lines[0]["payload"], "", "payload must be empty string");
}
}