1use std::io::{self, Write};
20
21use tracing::field::{Field, Visit};
22use tracing::span;
23use tracing::{Event, Level, Subscriber};
24use tracing_subscriber::Layer;
25use tracing_subscriber::layer::Context;
26use tracing_subscriber::registry::LookupSpan;
27use tracing_subscriber::util::TryInitError;
28
29#[derive(Clone, Copy, Debug)]
31pub enum LogFormat {
32 Json,
33 Plain,
34 Yaml,
35}
36
37pub struct AfdataLayer {
39 format: LogFormat,
40 redactor: crate::Redactor,
41}
42
43pub fn try_init(
54 filter: tracing_subscriber::EnvFilter,
55 format: LogFormat,
56 redactor: crate::Redactor,
57) -> Result<(), TryInitError> {
58 use tracing_subscriber::layer::SubscriberExt;
59 use tracing_subscriber::util::SubscriberInitExt;
60
61 tracing_subscriber::registry()
62 .with(filter)
63 .with(AfdataLayer { format, redactor })
64 .try_init()
65}
66
67impl AfdataLayer {
68 fn output_options(&self) -> crate::OutputOptions {
69 crate::OutputOptions {
70 redaction: self.redactor.clone(),
71 style: crate::OutputStyle::Readable,
72 }
73 }
74
75 fn format_value(&self, value: &serde_json::Value) -> String {
76 let options = self.output_options();
77 match self.format {
78 LogFormat::Json => crate::output_json_with_options(value, &options),
79 LogFormat::Plain => crate::output_plain_with_options(value, &options),
80 LogFormat::Yaml => crate::output_yaml_with_options(value, &options),
81 }
82 }
83}
84
85struct SpanFields(Vec<(String, serde_json::Value)>);
87
88impl<S> Layer<S> for AfdataLayer
89where
90 S: Subscriber + for<'a> LookupSpan<'a>,
91{
92 fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) {
93 let mut visitor = JsonVisitor::new();
94 attrs.record(&mut visitor);
95
96 if let Some(span) = ctx.span(id) {
97 span.extensions_mut().insert(SpanFields(visitor.fields));
98 }
99 }
100
101 fn on_record(&self, id: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) {
102 if let Some(span) = ctx.span(id) {
103 let mut visitor = JsonVisitor::new();
104 values.record(&mut visitor);
105
106 let mut extensions = span.extensions_mut();
107 if let Some(existing) = extensions.get_mut::<SpanFields>() {
108 existing.0.extend(visitor.fields);
109 } else {
110 extensions.insert(SpanFields(visitor.fields));
111 }
112 }
113 }
114
115 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
116 let meta = event.metadata();
117
118 let mut visitor = JsonVisitor::new();
120 event.record(&mut visitor);
121
122 let mut map = serde_json::Map::with_capacity(4 + visitor.fields.len());
124
125 let level = match *meta.level() {
126 Level::TRACE => "trace",
127 Level::DEBUG => "debug",
128 Level::INFO => "info",
129 Level::WARN => "warn",
130 Level::ERROR => "error",
131 };
132
133 map.insert(
134 "timestamp_epoch_ms".into(),
135 serde_json::Value::Number(chrono::Utc::now().timestamp_millis().into()),
136 );
137
138 if let Some(msg) = visitor.message.take() {
140 map.insert("message".into(), serde_json::Value::String(msg));
141 }
142
143 if let Some(scope) = ctx.event_scope(event) {
145 for span in scope.from_root() {
146 let extensions = span.extensions();
147 if let Some(fields) = extensions.get::<SpanFields>() {
148 for (k, v) in &fields.0 {
149 map.insert(k.clone(), v.clone());
150 }
151 }
152 }
153 }
154
155 map.insert("level".into(), serde_json::Value::String(level.to_string()));
156
157 for (k, v) in visitor.fields {
161 map.insert(k, v);
162 }
163
164 let message = map
167 .remove("message")
168 .and_then(|v| v.as_str().map(|s| s.to_string()))
169 .unwrap_or_else(|| "(no message)".to_string());
170 let level_str = map
171 .remove("level")
172 .and_then(|v| v.as_str().map(|s| s.to_string()))
173 .unwrap_or_else(|| "info".to_string());
174 let level = match level_str.as_str() {
175 "debug" => crate::LogLevel::Debug,
176 "info" => crate::LogLevel::Info,
177 "warn" => crate::LogLevel::Warn,
178 "error" => crate::LogLevel::Error,
179 _ => crate::LogLevel::Info,
180 };
181
182 map.insert(
183 "level".to_string(),
184 serde_json::Value::String(level.as_str().to_string()),
185 );
186 map.insert("message".to_string(), serde_json::Value::String(message));
187 let builder = crate::json_log(serde_json::Value::Object(map));
188 #[allow(clippy::expect_used)]
189 let value = builder.build().expect("log event builder failed");
190
191 let line = self.format_value(&value);
193
194 let mut out = io::stdout().lock();
195 let _ = out.write_all(line.as_bytes());
196 let _ = out.write_all(b"\n");
197 }
198}
199
200struct JsonVisitor {
202 message: Option<String>,
203 fields: Vec<(String, serde_json::Value)>,
204}
205
206impl JsonVisitor {
207 fn new() -> Self {
208 Self {
209 message: None,
210 fields: Vec::new(),
211 }
212 }
213}
214
215impl Visit for JsonVisitor {
216 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
217 let val = format!("{:?}", value);
218 if field.name() == "message" {
219 self.message = Some(val);
220 } else {
221 self.fields
227 .push((field.name().to_string(), serde_json::Value::String(val)));
228 }
229 }
230
231 fn record_str(&mut self, field: &Field, value: &str) {
232 if field.name() == "message" {
233 self.message = Some(value.to_string());
234 } else {
235 self.fields.push((
236 field.name().to_string(),
237 serde_json::Value::String(value.to_string()),
238 ));
239 }
240 }
241
242 fn record_i64(&mut self, field: &Field, value: i64) {
243 self.fields.push((
244 field.name().to_string(),
245 serde_json::Value::Number(value.into()),
246 ));
247 }
248
249 fn record_u64(&mut self, field: &Field, value: u64) {
250 self.fields.push((
251 field.name().to_string(),
252 serde_json::Value::Number(value.into()),
253 ));
254 }
255
256 fn record_f64(&mut self, field: &Field, value: f64) {
257 if let Some(n) = serde_json::Number::from_f64(value) {
258 self.fields
259 .push((field.name().to_string(), serde_json::Value::Number(n)));
260 } else {
261 self.fields.push((
262 field.name().to_string(),
263 serde_json::Value::String(value.to_string()),
264 ));
265 }
266 }
267
268 fn record_bool(&mut self, field: &Field, value: bool) {
269 self.fields
270 .push((field.name().to_string(), serde_json::Value::Bool(value)));
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use serde_json::json;
278
279 #[test]
285 fn code_field_is_accepted_by_log_builder() {
286 let result = crate::json_log(json!({"code": "cache_miss"})).build();
287 assert!(
288 result.is_ok(),
289 "builder should accept a tool-defined log code"
290 );
291 let Ok(value) = result else {
292 return;
293 };
294
295 assert_eq!(value["log"]["code"], "cache_miss");
296 }
297
298 #[test]
299 fn secret_named_field_is_redacted_at_emit() {
300 let line = crate::output_json(&json!({
301 "code": "info",
302 "api_key_secret": "sk-live-123",
303 }));
304 assert!(line.contains("\"api_key_secret\":\"***\""), "{line}");
305 assert!(!line.contains("sk-live-123"), "{line}");
306 }
307
308 #[test]
309 fn non_secret_field_whose_value_mentions_secret_is_not_redacted() {
310 let line = crate::output_json(&json!({
313 "code": "info",
314 "note": "see the api_key_secret field in docs",
315 }));
316 assert!(
317 line.contains("see the api_key_secret field in docs"),
318 "{line}"
319 );
320 }
321
322 #[test]
323 fn secret_typed_field_is_redacted_regardless_of_record_path() {
324 let line = crate::output_json(&json!({
327 "code": "warn",
328 "db_password_secret": 1234,
329 }));
330 assert!(line.contains("\"db_password_secret\":\"***\""), "{line}");
331 }
332
333 #[test]
334 fn legacy_secret_names_are_redacted_when_layer_has_options() {
335 #[allow(clippy::expect_used)]
336 let value = crate::json_log(json!({
337 "level": "info",
338 "message": "authorization appears in message but is not name-redacted",
339 "timestamp_epoch_ms": 1,
340 "authorization": "Bearer legacy",
341 "request_url": "https://example.test/path?authorization=legacy&ok=1",
342 }))
343 .build()
344 .expect("builder failed");
345 let redactor = crate::Redactor::new().secret_names(vec!["authorization".to_string()]);
346
347 for format in [LogFormat::Json, LogFormat::Plain, LogFormat::Yaml] {
348 let layer = AfdataLayer {
349 format,
350 redactor: redactor.clone(),
351 };
352 let line = layer.format_value(&value);
353 assert!(line.contains("***"), "{line}");
354 assert!(
355 !line.contains("Bearer legacy"),
356 "legacy field value should be redacted: {line}"
357 );
358 assert!(
359 !line.contains("authorization=legacy"),
360 "legacy URL query parameter should be redacted: {line}"
361 );
362 assert!(
363 line.contains("authorization appears in message"),
364 "message is free-form and should remain readable: {line}"
365 );
366 }
367 }
368
369 #[test]
370 fn legacy_secret_names_are_visible_without_layer_options() {
371 #[allow(clippy::expect_used)]
372 let value = crate::json_log(json!({
373 "level": "info",
374 "message": "ready",
375 "timestamp_epoch_ms": 1,
376 "authorization": "Bearer visible",
377 }))
378 .build()
379 .expect("builder failed");
380 let layer = AfdataLayer {
381 format: LogFormat::Json,
382 redactor: crate::Redactor::new(),
383 };
384
385 let line = layer.format_value(&value);
386 assert!(
387 line.contains("\"authorization\":\"Bearer visible\""),
388 "{line}"
389 );
390 }
391}