tracing_durations_export/
lib.rs1use fs::File;
50use once_cell::sync::Lazy;
51use serde::Serialize;
52use std::collections::hash_map::RandomState;
53use std::collections::HashMap;
54use std::fmt::Debug;
55use std::io::{BufWriter, Write};
56use std::marker::PhantomData;
57use std::path::PathBuf;
58use std::sync::atomic::{AtomicU64, Ordering};
59use std::sync::{Arc, Mutex};
60use std::time::{Duration, Instant};
61use std::{io, iter};
62use tracing::field::Field;
63use tracing::{span, Subscriber};
64use tracing_subscriber::layer::Context;
65use tracing_subscriber::registry::LookupSpan;
66use tracing_subscriber::Layer;
67
68#[cfg(feature = "plot")]
69pub mod plot;
70
71static START: Lazy<Instant> = Lazy::new(Instant::now);
73
74#[derive(Serialize)]
76#[serde(bound(serialize = ""))]
78pub struct SpanInfo<'a, RS = RandomState> {
79 pub id: u64,
80 pub name: &'static str,
81 pub start: Duration,
82 pub end: Duration,
83 pub parents: Option<&'a [u64]>,
84 pub is_main_thread: bool,
85 pub fields: Option<&'a HashMap<&'static str, String, RS>>,
86}
87
88pub struct DurationsLayerBuilder {
89 with_fields: bool,
91 with_parents: bool,
93 durations_file: Option<PathBuf>,
95 #[cfg(feature = "plot")]
97 plot_file: Option<PathBuf>,
98 #[cfg(feature = "plot")]
99 plot_config: plot::PlotConfig,
100 #[cfg(feature = "plot")]
101 plot_layout: plot::PlotLayout,
102}
103
104impl Default for DurationsLayerBuilder {
105 fn default() -> Self {
106 Self {
107 with_fields: true,
108 with_parents: true,
109 durations_file: None,
110 #[cfg(feature = "plot")]
111 plot_file: None,
112 #[cfg(feature = "plot")]
113 plot_config: plot::PlotConfig::default(),
114 #[cfg(feature = "plot")]
115 plot_layout: plot::PlotLayout::default(),
116 }
117 }
118}
119
120impl DurationsLayerBuilder {
121 pub fn build<S>(self) -> io::Result<(DurationsLayer<S>, DurationsLayerDropGuard)> {
123 let out = self
124 .durations_file
125 .map(|file| File::create(file).map(BufWriter::new))
126 .transpose()?;
127 let layer = DurationsLayer {
128 main_thead_id: std::thread::current().id(),
129 start_index: Mutex::default(),
130 fields: Mutex::default(),
131 is_main_thread: Mutex::new(Default::default()),
132 export_ids: Mutex::default(),
133 next_export_id: AtomicU64::new(1),
134 out: Arc::new(Mutex::new(out)),
135 #[cfg(feature = "plot")]
136 plot_data: Arc::new(Mutex::default()),
137 #[cfg(feature = "plot")]
138 plot_file: self.plot_file,
139 with_fields: self.with_fields,
140 with_parents: self.with_parents,
141 #[cfg(feature = "plot")]
142 plot_config: self.plot_config,
143 #[cfg(feature = "plot")]
144 plot_layout: self.plot_layout,
145 _inner: PhantomData,
146 };
147 let guard = layer.drop_guard();
148 Ok((layer, guard))
149 }
150
151 pub fn with_fields(self, enabled: bool) -> Self {
171 Self {
172 with_fields: enabled,
173 ..self
174 }
175 }
176
177 pub fn with_parents(self, enabled: bool) -> Self {
197 Self {
198 with_parents: enabled,
199 ..self
200 }
201 }
202
203 pub fn durations_file(self, file: impl Into<PathBuf>) -> Self {
213 Self {
214 durations_file: Some(file.into()),
215 ..self
216 }
217 }
218
219 #[cfg(feature = "plot")]
225 pub fn plot_file(self, file: impl Into<PathBuf>) -> Self {
226 Self {
227 plot_file: Some(file.into()),
228 ..self
229 }
230 }
231
232 #[cfg(feature = "plot")]
233 pub fn plot_config(self, plot_config: plot::PlotConfig) -> Self {
234 Self {
235 plot_config,
236 ..self
237 }
238 }
239}
240
241type CollectedFields<RS> = HashMap<&'static str, String, RS>;
242
243#[derive(Default)]
244struct FieldsCollector<RS = RandomState>(CollectedFields<RS>);
245
246impl tracing::field::Visit for FieldsCollector {
247 fn record_str(&mut self, field: &Field, value: &str) {
248 self.0.insert(field.name(), value.to_string());
249 }
250
251 fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
252 self.0.insert(field.name(), format!("{value:?}"));
253 }
254}
255
256pub struct DurationsLayerDropGuard {
258 out: Arc<Mutex<Option<BufWriter<File>>>>,
259 #[cfg(feature = "plot")]
260 plot_file: Option<PathBuf>,
261 #[cfg(feature = "plot")]
262 plot_data: Arc<Mutex<Vec<plot::OwnedSpanInfo>>>,
263 #[cfg(feature = "plot")]
264 plot_config: plot::PlotConfig,
265 #[cfg(feature = "plot")]
266 plot_layout: plot::PlotLayout,
267}
268
269impl Drop for DurationsLayerDropGuard {
270 fn drop(&mut self) {
271 if let Some(out) = self.out.lock().expect("There was a prior panic").as_mut() {
272 if let Err(err) = out.flush() {
273 eprintln!("`DurationLayer` failed to flush out file: {err}");
274 }
275 }
276
277 #[cfg(feature = "plot")]
278 {
279 if let Some(plot_file) = &self.plot_file {
280 let end = self
281 .plot_data
282 .lock()
283 .unwrap()
284 .iter()
285 .map(|span| span.end)
286 .max();
287 if let Some(end) = end {
289 let svg = plot::plot(
290 &self.plot_data.lock().expect("There was a prior panic"),
291 end,
292 &self.plot_config,
293 &self.plot_layout,
294 );
295 if let Err(err) = svg::save(plot_file, &svg) {
296 eprintln!("`DurationLayer` failed to write plot: {err}");
297 }
298 }
299 }
300 }
301 }
302}
303
304pub struct DurationsLayer<S, RS = RandomState> {
306 main_thead_id: std::thread::ThreadId,
307 start_index: Mutex<HashMap<span::Id, Duration, RS>>,
311 fields: Mutex<HashMap<span::Id, CollectedFields<RS>>>,
313 is_main_thread: Mutex<HashMap<span::Id, bool>>,
315 export_ids: Mutex<HashMap<span::Id, u64, RS>>,
318 next_export_id: AtomicU64,
320 out: Arc<Mutex<Option<BufWriter<File>>>>,
321 #[cfg(feature = "plot")]
322 plot_data: Arc<Mutex<Vec<plot::OwnedSpanInfo>>>,
323 #[cfg(feature = "plot")]
324 plot_file: Option<PathBuf>,
325 with_fields: bool,
326 with_parents: bool,
327 #[cfg(feature = "plot")]
328 plot_config: plot::PlotConfig,
329 #[cfg(feature = "plot")]
330 plot_layout: plot::PlotLayout,
331 _inner: PhantomData<S>,
332}
333
334impl<S> DurationsLayer<S> {
335 fn drop_guard(&self) -> DurationsLayerDropGuard {
336 DurationsLayerDropGuard {
337 out: self.out.clone(),
338 #[cfg(feature = "plot")]
339 plot_file: self.plot_file.clone(),
340 #[cfg(feature = "plot")]
341 plot_data: self.plot_data.clone(),
342 #[cfg(feature = "plot")]
343 plot_config: self.plot_config.clone(),
344 #[cfg(feature = "plot")]
345 plot_layout: self.plot_layout.clone(),
346 }
347 }
348}
349
350impl<S> Layer<S> for DurationsLayer<S>
351where
352 S: Subscriber + for<'span> LookupSpan<'span>,
353{
354 fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, _ctx: Context<'_, S>) {
356 self.export_ids
357 .lock()
358 .expect("There was a prior panic")
359 .insert(
360 id.clone(),
361 self.next_export_id.fetch_add(1, Ordering::Relaxed),
362 );
363
364 if self.with_fields {
366 let mut visitor = FieldsCollector::default();
367 attrs.record(&mut visitor);
368 self.fields
369 .lock()
370 .expect("There was a prior panic")
371 .insert(id.clone(), visitor.0);
372 }
373 self.is_main_thread
374 .lock()
375 .expect("There was a prior panic")
376 .insert(
377 id.clone(),
378 self.main_thead_id == std::thread::current().id(),
379 );
380 }
381
382 fn on_enter(&self, id: &span::Id, _ctx: Context<'_, S>) {
384 self.start_index
385 .lock()
386 .unwrap()
387 .insert(id.clone(), START.elapsed());
388 }
389
390 fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) {
392 let span = ctx.span(id).unwrap();
393 let export_ids = self.export_ids.lock().expect("There was a prior panic");
394 let export_id = export_ids.get(id).copied().unwrap_or_else(|| id.into_u64());
395 let parents = if self.with_parents {
396 let parents = iter::successors(span.parent(), |span| span.parent())
397 .map(|span| {
398 let id = span.id();
399 export_ids
400 .get(&id)
401 .copied()
402 .unwrap_or_else(|| id.into_u64())
403 })
404 .collect::<Vec<_>>();
405 Some(parents)
406 } else {
407 None
408 };
409 drop(export_ids);
410 let attributes = self.fields.lock().expect("There was a prior panic");
411 let fields = attributes.get(id);
412 debug_assert!(
413 !self.with_fields || fields.is_some(),
414 "Expected fields to be record for span {} {}",
415 span.name(),
416 id.into_u64()
417 );
418
419 let is_main_thread = self.main_thead_id == std::thread::current().id();
420 let span_info = SpanInfo {
421 id: export_id,
422 name: span.name(),
423 start: self.start_index.lock().expect("There was a prior panic")[id],
424 end: START.elapsed(),
425 parents: parents.as_deref(),
426 is_main_thread,
427 fields,
428 };
429 #[allow(clippy::needless_borrows_for_generic_args)]
431 if let Some(mut writer) = self.out.lock().expect("There was a prior panic").as_mut() {
432 serde_json::to_writer(&mut writer, &span_info).unwrap();
434 writeln!(&mut writer).unwrap();
435 }
436
437 #[cfg(feature = "plot")]
438 {
439 if self.plot_file.is_some() {
440 self.plot_data
441 .lock()
442 .expect("There was a prior panic")
443 .push(plot::OwnedSpanInfo {
444 id: export_id,
445 name: span.name().to_string(),
446 start: self.start_index.lock().expect("There was a prior panic")[id],
447 end: START.elapsed(),
448 parents,
449 is_main_thread,
450 fields: fields.map(|fields| {
451 fields
452 .iter()
453 .map(|(key, value)| (key.to_string(), value.to_string()))
454 .collect()
455 }),
456 })
457 }
458 }
459 }
460
461 fn on_close(&self, id: span::Id, _ctx: Context<'_, S>) {
462 self.start_index
463 .lock()
464 .expect("There was a prior panic")
465 .remove(&id);
466 self.fields
467 .lock()
468 .expect("There was a prior panic")
469 .remove(&id);
470 self.is_main_thread
471 .lock()
472 .expect("There was a prior panic")
473 .remove(&id);
474 self.export_ids
475 .lock()
476 .expect("There was a prior panic")
477 .remove(&id);
478 }
479}