cratefield_runtime_native/
tracing_setup.rs1use std::fmt;
15use std::sync::OnceLock;
16
17use serde_json::{Map, Value, json};
18use tracing::field::Visit;
19use tracing::{Event, Subscriber};
20use tracing_subscriber::Registry;
21use tracing_subscriber::fmt as ts_fmt;
22use tracing_subscriber::fmt::format::{DefaultFields, Writer};
23use tracing_subscriber::fmt::{FmtContext, FormatEvent};
24use tracing_subscriber::layer::SubscriberExt;
25use tracing_subscriber::registry::LookupSpan;
26
27static INSTALLED: OnceLock<()> = OnceLock::new();
28
29pub fn install_tracing() {
35 INSTALLED.get_or_init(|| {
36 let filter = tracing_subscriber::EnvFilter::try_from_default_env()
37 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
38 let fmt_layer = ts_fmt::Layer::default()
39 .event_format(RedactingJson)
40 .with_writer(std::io::stdout);
41 let subscriber = Registry::default().with(filter).with(fmt_layer);
42 let _ = tracing::subscriber::set_global_default(subscriber);
43 });
44}
45
46struct RedactingJson;
50
51impl<S> FormatEvent<S, DefaultFields> for RedactingJson
52where
53 S: Subscriber + for<'a> LookupSpan<'a>,
54{
55 fn format_event(
56 &self,
57 _ctx: &FmtContext<'_, S, DefaultFields>,
58 mut writer: Writer<'_>,
59 event: &Event<'_>,
60 ) -> fmt::Result {
61 let mut visitor = RedactingVisitor::default();
62 event.record(&mut visitor);
63
64 let metadata = event.metadata();
65 let mut fields = visitor.fields;
66 fields.insert("level".into(), json!(metadata.level().as_str()));
67 fields.insert("target".into(), json!(metadata.target()));
68 let timestamp = time::OffsetDateTime::now_utc()
69 .format(&time::format_description::well_known::Rfc3339)
70 .unwrap_or_else(|_| "unknown".to_owned());
71 fields.insert("timestamp".into(), json!(timestamp));
72
73 writer.write_str(&Value::Object(fields).to_string())?;
74 writer.write_char('\n')
75 }
76}
77
78#[derive(Default)]
81struct RedactingVisitor {
82 fields: Map<String, Value>,
83}
84
85impl RedactingVisitor {
86 fn record_field(&mut self, name: &str, value: &str) {
87 let redacted = cratefield_core::redacted_value(name, value);
88 if redacted == "[redacted]" {
89 self.fields.insert(name.to_owned(), json!("[redacted]"));
90 } else if let Some(hash) = redacted.strip_prefix("subject_hash:") {
91 self.fields
92 .insert(format!("{name}_hash"), json!(format!("sha256:{hash}")));
93 } else {
94 self.fields.insert(name.to_owned(), json!(redacted));
98 }
99 }
100}
101
102impl Visit for RedactingVisitor {
103 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
104 self.record_field(field.name(), value);
105 }
106
107 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn fmt::Debug) {
108 self.record_field(field.name(), &format!("{value:?}"));
109 }
110
111 fn record_error(
112 &mut self,
113 field: &tracing::field::Field,
114 value: &(dyn std::error::Error + 'static),
115 ) {
116 self.record_field(field.name(), &value.to_string());
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn visitor_redacts_like_core() {
126 let mut visitor = RedactingVisitor::default();
127 visitor.record_field("authorization", "Bearer x");
128 visitor.record_field("email", "nick@example.com");
129 visitor.record_field("outcome", "sent");
130 assert_eq!(visitor.fields["authorization"], "[redacted]");
131 assert!(
132 visitor.fields["email_hash"]
133 .as_str()
134 .is_some_and(|hash| hash.starts_with("sha256:"))
135 );
136 assert_eq!(visitor.fields["outcome"], "sent");
137
138 let mut visitor = RedactingVisitor::default();
142 visitor.record_field(
143 "error",
144 "DETAIL: Key (email)=(nick@example.com) exists; see /v1/waitlist/status?token=abc",
145 );
146 let error = visitor.fields["error"].as_str().expect("error field");
147 assert!(!error.contains('@'), "{error}");
148 assert!(!error.contains("token=abc"), "{error}");
149 assert!(error.contains("subject_hash:"), "{error}");
150 }
151}