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 #[allow(clippy::disallowed_methods)]
120 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
121 let meta = event.metadata();
122
123 let mut visitor = JsonVisitor::new();
125 event.record(&mut visitor);
126
127 let mut map = serde_json::Map::with_capacity(4 + visitor.fields.len());
129
130 let level = match *meta.level() {
131 Level::TRACE => "trace",
132 Level::DEBUG => "debug",
133 Level::INFO => "info",
134 Level::WARN => "warn",
135 Level::ERROR => "error",
136 };
137
138 map.insert(
139 "timestamp_epoch_ms".into(),
140 serde_json::Value::Number(chrono::Utc::now().timestamp_millis().into()),
141 );
142
143 if let Some(msg) = visitor.message.take() {
145 map.insert("message".into(), serde_json::Value::String(msg));
146 }
147
148 if let Some(scope) = ctx.event_scope(event) {
150 for span in scope.from_root() {
151 let extensions = span.extensions();
152 if let Some(fields) = extensions.get::<SpanFields>() {
153 for (k, v) in &fields.0 {
154 map.insert(k.clone(), v.clone());
155 }
156 }
157 }
158 }
159
160 map.insert("level".into(), serde_json::Value::String(level.to_string()));
161
162 for (k, v) in visitor.fields {
166 map.insert(k, v);
167 }
168
169 let message = map
172 .remove("message")
173 .and_then(|v| v.as_str().map(|s| s.to_string()))
174 .unwrap_or_else(|| "(no message)".to_string());
175 let level_str = map
176 .remove("level")
177 .and_then(|v| v.as_str().map(|s| s.to_string()))
178 .unwrap_or_else(|| "info".to_string());
179 let level = match level_str.as_str() {
180 "debug" => crate::LogLevel::Debug,
181 "info" => crate::LogLevel::Info,
182 "warn" => crate::LogLevel::Warn,
183 "error" => crate::LogLevel::Error,
184 _ => crate::LogLevel::Info,
185 };
186
187 map.insert(
188 "level".to_string(),
189 serde_json::Value::String(level.as_str().to_string()),
190 );
191 map.insert("message".to_string(), serde_json::Value::String(message));
192 let builder = crate::json_log(serde_json::Value::Object(map));
193 let value = builder.build();
194
195 let line = self.format_value(value.as_value());
197
198 let mut out = io::stderr().lock();
199 let _ = out.write_all(line.as_bytes());
200 let _ = out.write_all(b"\n");
201 }
202}
203
204struct JsonVisitor {
206 message: Option<String>,
207 fields: Vec<(String, serde_json::Value)>,
208}
209
210impl JsonVisitor {
211 fn new() -> Self {
212 Self {
213 message: None,
214 fields: Vec::new(),
215 }
216 }
217}
218
219impl Visit for JsonVisitor {
220 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
221 let val = format!("{:?}", value);
222 if field.name() == "message" {
223 self.message = Some(val);
224 } else {
225 self.fields
231 .push((field.name().to_string(), serde_json::Value::String(val)));
232 }
233 }
234
235 fn record_str(&mut self, field: &Field, value: &str) {
236 if field.name() == "message" {
237 self.message = Some(value.to_string());
238 } else {
239 self.fields.push((
240 field.name().to_string(),
241 serde_json::Value::String(value.to_string()),
242 ));
243 }
244 }
245
246 fn record_i64(&mut self, field: &Field, value: i64) {
247 self.fields.push((
248 field.name().to_string(),
249 serde_json::Value::Number(value.into()),
250 ));
251 }
252
253 fn record_u64(&mut self, field: &Field, value: u64) {
254 self.fields.push((
255 field.name().to_string(),
256 serde_json::Value::Number(value.into()),
257 ));
258 }
259
260 fn record_f64(&mut self, field: &Field, value: f64) {
261 if let Some(n) = serde_json::Number::from_f64(value) {
262 self.fields
263 .push((field.name().to_string(), serde_json::Value::Number(n)));
264 } else {
265 self.fields.push((
266 field.name().to_string(),
267 serde_json::Value::String(value.to_string()),
268 ));
269 }
270 }
271
272 fn record_bool(&mut self, field: &Field, value: bool) {
273 self.fields
274 .push((field.name().to_string(), serde_json::Value::Bool(value)));
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use serde_json::json;
282
283 #[test]
289 fn code_field_is_accepted_by_log_builder() {
290 let value = crate::json_log(json!({"code": "cache_miss"})).build();
291 assert_eq!(value.as_value()["log"]["code"], "cache_miss");
292 }
293
294 #[test]
295 fn secret_named_field_is_redacted_at_emit() {
296 let line = crate::render(
297 &json!({
298 "code": "info",
299 "api_key_secret": "sk-live-123",
300 }),
301 crate::OutputFormat::Json,
302 &crate::OutputOptions::default(),
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::render(
313 &json!({
314 "code": "info",
315 "note": "see the api_key_secret field in docs",
316 }),
317 crate::OutputFormat::Json,
318 &crate::OutputOptions::default(),
319 );
320 assert!(
321 line.contains("see the api_key_secret field in docs"),
322 "{line}"
323 );
324 }
325
326 #[test]
327 fn secret_typed_field_is_redacted_regardless_of_record_path() {
328 let line = crate::render(
331 &json!({
332 "code": "warn",
333 "db_password_secret": 1234,
334 }),
335 crate::OutputFormat::Json,
336 &crate::OutputOptions::default(),
337 );
338 assert!(line.contains("\"db_password_secret\":\"***\""), "{line}");
339 }
340
341 #[test]
342 fn legacy_secret_names_are_redacted_when_layer_has_options() {
343 let value = crate::json_log(json!({
344 "level": "info",
345 "message": "authorization appears in message but is not name-redacted",
346 "timestamp_epoch_ms": 1,
347 "authorization": "Bearer legacy",
348 "request_url": "https://example.test/path?authorization=legacy&ok=1",
349 }))
350 .build();
351 let redactor = crate::Redactor::new().secret_names(vec!["authorization".to_string()]);
352
353 let formats = [LogFormat::Json, LogFormat::Plain, LogFormat::Yaml];
354
355 for format in formats {
356 let layer = AfdataLayer {
357 format,
358 redactor: redactor.clone(),
359 };
360 let line = layer.format_value(value.as_value());
361 assert!(line.contains("***"), "{line}");
362 assert!(
363 !line.contains("Bearer legacy"),
364 "legacy field value should be redacted: {line}"
365 );
366 assert!(
367 !line.contains("authorization=legacy"),
368 "legacy URL query parameter should be redacted: {line}"
369 );
370 assert!(
371 line.contains("authorization appears in message"),
372 "message is free-form and should remain readable: {line}"
373 );
374 }
375 }
376
377 #[test]
378 fn legacy_secret_names_are_visible_without_layer_options() {
379 let value = crate::json_log(json!({
380 "level": "info",
381 "message": "ready",
382 "timestamp_epoch_ms": 1,
383 "authorization": "Bearer visible",
384 }))
385 .build();
386 let layer = AfdataLayer {
387 format: LogFormat::Json,
388 redactor: crate::Redactor::new(),
389 };
390
391 let line = layer.format_value(value.as_value());
392 assert!(
393 line.contains("\"authorization\":\"Bearer visible\""),
394 "{line}"
395 );
396 }
397}