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