1use std::fmt::Write as _;
37use std::sync::atomic::{AtomicU64, Ordering};
38use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TrySendError};
39use std::sync::{Mutex, OnceLock};
40
41use tracing::field::{Field, Visit};
42use tracing::{Event, Level, Subscriber};
43use tracing_subscriber::layer::{Context, Layer};
44use tracing_subscriber::registry::LookupSpan;
45
46pub const CAPACITY: usize = 256;
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct LogLine {
53 pub level: Level,
54 pub target: &'static str,
57 pub message: String,
59 pub fields: Vec<(&'static str, String)>,
62 pub span_fields: Vec<(&'static str, String)>,
65}
66
67impl LogLine {
68 pub fn field(&self, name: &str) -> Option<&str> {
70 self.fields
71 .iter()
72 .find(|(k, _)| *k == name)
73 .map(|(_, v)| v.as_str())
74 }
75
76 pub fn get(&self, name: &str) -> Option<&str> {
80 self.field(name).or_else(|| {
81 self.span_fields
82 .iter()
83 .rev()
84 .find(|(k, _)| *k == name)
85 .map(|(_, v)| v.as_str())
86 })
87 }
88
89 pub fn event(&self) -> Option<&str> {
91 self.field("event")
92 }
93}
94
95struct Tap {
96 tx: SyncSender<LogLine>,
97}
98
99static TAP: OnceLock<Tap> = OnceLock::new();
100static RECEIVER: OnceLock<Mutex<Option<Receiver<LogLine>>>> = OnceLock::new();
101static DROPPED: AtomicU64 = AtomicU64::new(0);
102
103pub struct TapLayer;
106
107impl TapLayer {
108 pub fn new() -> Self {
109 TAP.get_or_init(|| {
110 let (tx, rx) = sync_channel(CAPACITY);
111 let _ = RECEIVER.set(Mutex::new(Some(rx)));
112 Tap { tx }
113 });
114 TapLayer
115 }
116}
117
118impl Default for TapLayer {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124pub fn take_receiver() -> Option<Receiver<LogLine>> {
127 RECEIVER.get()?.lock().ok()?.take()
128}
129
130pub fn dropped() -> u64 {
132 DROPPED.load(Ordering::Relaxed)
133}
134
135pub fn push(line: LogLine) {
138 if let Some(tap) = TAP.get() {
139 offer(&tap.tx, line);
140 }
141}
142
143fn offer(tx: &SyncSender<LogLine>, line: LogLine) {
144 match tx.try_send(line) {
145 Ok(()) => {}
146 Err(TrySendError::Full(_)) | Err(TrySendError::Disconnected(_)) => {
147 DROPPED.fetch_add(1, Ordering::Relaxed);
148 }
149 }
150}
151
152#[derive(Default)]
155struct SpanFields(Vec<(&'static str, String)>);
156
157impl SpanFields {
158 fn put(&mut self, name: &'static str, value: String) {
159 if let Some(slot) = self.0.iter_mut().find(|(k, _)| *k == name) {
160 slot.1 = value;
161 } else {
162 self.0.push((name, value));
163 }
164 }
165}
166
167struct Collect<'a> {
170 message: &'a mut String,
171 fields: &'a mut Vec<(&'static str, String)>,
172}
173
174impl Visit for Collect<'_> {
175 fn record_str(&mut self, field: &Field, value: &str) {
176 if field.name() == "message" {
177 self.message.push_str(value);
178 } else {
179 self.fields.push((field.name(), value.to_owned()));
180 }
181 }
182
183 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
184 if field.name() == "message" {
185 let _ = write!(self.message, "{value:?}");
186 } else {
187 self.fields.push((field.name(), format!("{value:?}")));
188 }
189 }
190}
191
192impl<S> Layer<S> for TapLayer
193where
194 S: Subscriber + for<'a> LookupSpan<'a>,
195{
196 fn on_new_span(
197 &self,
198 attrs: &tracing::span::Attributes<'_>,
199 id: &tracing::span::Id,
200 ctx: Context<'_, S>,
201 ) {
202 let Some(span) = ctx.span(id) else { return };
203 let mut fields = SpanFields::default();
204 let mut message = String::new();
205 attrs.record(&mut Collect {
206 message: &mut message,
207 fields: &mut fields.0,
208 });
209 span.extensions_mut().insert(fields);
210 }
211
212 fn on_record(
213 &self,
214 id: &tracing::span::Id,
215 values: &tracing::span::Record<'_>,
216 ctx: Context<'_, S>,
217 ) {
218 let Some(span) = ctx.span(id) else { return };
219 let mut fresh = Vec::new();
220 let mut message = String::new();
221 values.record(&mut Collect {
222 message: &mut message,
223 fields: &mut fresh,
224 });
225 let mut ext = span.extensions_mut();
226 let fields = ext.get_mut::<SpanFields>();
227 match fields {
228 Some(fields) => fresh.into_iter().for_each(|(k, v)| fields.put(k, v)),
229 None => ext.insert(SpanFields(fresh)),
230 }
231 }
232
233 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
234 let Some(tap) = TAP.get() else { return };
235 if *event.metadata().level() > Level::INFO {
238 return;
239 }
240 let mut message = String::new();
241 let mut fields = Vec::new();
242 event.record(&mut Collect {
243 message: &mut message,
244 fields: &mut fields,
245 });
246 let mut span_fields: Vec<(&'static str, String)> = Vec::new();
247 if let Some(scope) = ctx.event_scope(event) {
248 for span in scope.from_root() {
249 if let Some(sf) = span.extensions().get::<SpanFields>() {
250 for (k, v) in &sf.0 {
251 if let Some(slot) = span_fields.iter_mut().find(|(n, _)| n == k) {
252 slot.1 = v.clone();
253 } else {
254 span_fields.push((k, v.clone()));
255 }
256 }
257 }
258 }
259 }
260 offer(
261 &tap.tx,
262 LogLine {
263 level: *event.metadata().level(),
264 target: event.metadata().target(),
265 message,
266 fields,
267 span_fields,
268 },
269 );
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use tracing_subscriber::layer::SubscriberExt as _;
277
278 static ONE_AT_A_TIME: std::sync::Mutex<()> = std::sync::Mutex::new(());
283
284 fn with_tap<R>(f: impl FnOnce() -> R) -> R {
285 let _serial = ONE_AT_A_TIME.lock().unwrap_or_else(|e| e.into_inner());
286 let subscriber = tracing_subscriber::registry().with(TapLayer::new());
287 tracing::subscriber::with_default(subscriber, f)
288 }
289
290 #[test]
291 fn an_event_arrives_with_its_fields_and_the_span_fields_around_it() {
292 let rx = with_tap(|| {
293 let rx = take_receiver();
294 if let Some(rx) = &rx {
297 for _ in rx.try_iter() {}
298 }
299 let span = tracing::info_span!(
300 "ssh.connection",
301 repo = "ns/r",
302 principal = tracing::field::Empty
303 );
304 span.record("principal", "alice");
305 let _e = span.enter();
306 tracing::info!(target: "tap-test", event = "ssh.auth.accepted", fingerprint = "SHA256:x", "publickey accepted");
307 tracing::debug!(target: "tap-test", "never forwarded");
308 rx.map(|rx| rx.try_iter().collect::<Vec<_>>())
309 });
310 let Some(lines) = rx else { return };
311 let rx = lines.into_iter();
312 let mut rx = rx;
313 let line = rx
314 .by_ref()
315 .find(|l| l.target == "tap-test")
316 .expect("the info line was tapped");
317 assert_eq!(line.level, Level::INFO);
318 assert_eq!(line.message, "publickey accepted");
319 assert_eq!(line.event(), Some("ssh.auth.accepted"));
320 assert_eq!(line.field("fingerprint"), Some("SHA256:x"));
321 assert_eq!(line.get("repo"), Some("ns/r"));
322 assert_eq!(
323 line.get("principal"),
324 Some("alice"),
325 "a recorded-later span field is seen"
326 );
327 assert!(
328 rx.all(|l| l.target != "tap-test" || l.message != "never forwarded"),
329 "DEBUG is refused before the channel"
330 );
331 }
332
333 #[test]
334 fn a_full_buffer_drops_and_counts_rather_than_blocking() {
335 let before = dropped();
336 with_tap(|| {
339 for i in 0..(CAPACITY * 2) {
340 push(LogLine {
341 level: Level::INFO,
342 target: "tap-flood",
343 message: format!("{i}"),
344 fields: vec![],
345 span_fields: vec![],
346 });
347 }
348 });
349 assert!(
350 dropped() >= before + CAPACITY as u64,
351 "the surplus was counted, not waited for"
352 );
353 }
354}