use fs::File;
use once_cell::sync::Lazy;
use serde::Serialize;
use std::collections::hash_map::RandomState;
use std::collections::HashMap;
use std::fmt::Debug;
use std::io::{BufWriter, Write};
use std::marker::PhantomData;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use std::{io, iter};
use tracing::field::Field;
use tracing::{span, Subscriber};
use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::Layer;
#[cfg(feature = "plot")]
pub mod plot;
static START: Lazy<Instant> = Lazy::new(Instant::now);
#[derive(Serialize)]
#[serde(bound(serialize = ""))]
pub struct SpanInfo<'a, RS = RandomState> {
pub id: u64,
pub name: &'static str,
pub start: Duration,
pub end: Duration,
pub parents: Option<&'a [u64]>,
pub is_main_thread: bool,
pub fields: Option<&'a HashMap<&'static str, String, RS>>,
}
pub struct DurationsLayerBuilder {
with_fields: bool,
with_parents: bool,
durations_file: Option<PathBuf>,
#[cfg(feature = "plot")]
plot_file: Option<PathBuf>,
#[cfg(feature = "plot")]
plot_config: plot::PlotConfig,
#[cfg(feature = "plot")]
plot_layout: plot::PlotLayout,
}
impl Default for DurationsLayerBuilder {
fn default() -> Self {
Self {
with_fields: true,
with_parents: true,
durations_file: None,
#[cfg(feature = "plot")]
plot_file: None,
#[cfg(feature = "plot")]
plot_config: plot::PlotConfig::default(),
#[cfg(feature = "plot")]
plot_layout: plot::PlotLayout::default(),
}
}
}
impl DurationsLayerBuilder {
pub fn build<S>(self) -> io::Result<(DurationsLayer<S>, DurationsLayerDropGuard)> {
let out = self
.durations_file
.map(|file| File::create(file).map(BufWriter::new))
.transpose()?;
let layer = DurationsLayer {
main_thead_id: std::thread::current().id(),
start_index: Mutex::default(),
fields: Mutex::default(),
is_main_thread: Mutex::new(Default::default()),
export_ids: Mutex::default(),
next_export_id: AtomicU64::new(1),
out: Arc::new(Mutex::new(out)),
#[cfg(feature = "plot")]
plot_data: Arc::new(Mutex::default()),
#[cfg(feature = "plot")]
plot_file: self.plot_file,
with_fields: self.with_fields,
with_parents: self.with_parents,
#[cfg(feature = "plot")]
plot_config: self.plot_config,
#[cfg(feature = "plot")]
plot_layout: self.plot_layout,
_inner: PhantomData,
};
let guard = layer.drop_guard();
Ok((layer, guard))
}
pub fn with_fields(self, enabled: bool) -> Self {
Self {
with_fields: enabled,
..self
}
}
pub fn with_parents(self, enabled: bool) -> Self {
Self {
with_parents: enabled,
..self
}
}
pub fn durations_file(self, file: impl Into<PathBuf>) -> Self {
Self {
durations_file: Some(file.into()),
..self
}
}
#[cfg(feature = "plot")]
pub fn plot_file(self, file: impl Into<PathBuf>) -> Self {
Self {
plot_file: Some(file.into()),
..self
}
}
#[cfg(feature = "plot")]
pub fn plot_config(self, plot_config: plot::PlotConfig) -> Self {
Self {
plot_config,
..self
}
}
}
type CollectedFields<RS> = HashMap<&'static str, String, RS>;
#[derive(Default)]
struct FieldsCollector<RS = RandomState>(CollectedFields<RS>);
impl tracing::field::Visit for FieldsCollector {
fn record_str(&mut self, field: &Field, value: &str) {
self.0.insert(field.name(), value.to_string());
}
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
self.0.insert(field.name(), format!("{value:?}"));
}
}
pub struct DurationsLayerDropGuard {
out: Arc<Mutex<Option<BufWriter<File>>>>,
#[cfg(feature = "plot")]
plot_file: Option<PathBuf>,
#[cfg(feature = "plot")]
plot_data: Arc<Mutex<Vec<plot::OwnedSpanInfo>>>,
#[cfg(feature = "plot")]
plot_config: plot::PlotConfig,
#[cfg(feature = "plot")]
plot_layout: plot::PlotLayout,
}
impl Drop for DurationsLayerDropGuard {
fn drop(&mut self) {
if let Some(out) = self.out.lock().expect("There was a prior panic").as_mut() {
if let Err(err) = out.flush() {
eprintln!("`DurationLayer` failed to flush out file: {err}");
}
}
#[cfg(feature = "plot")]
{
if let Some(plot_file) = &self.plot_file {
let end = self
.plot_data
.lock()
.unwrap()
.iter()
.map(|span| span.end)
.max();
if let Some(end) = end {
let svg = plot::plot(
&self.plot_data.lock().expect("There was a prior panic"),
end,
&self.plot_config,
&self.plot_layout,
);
if let Err(err) = svg::save(plot_file, &svg) {
eprintln!("`DurationLayer` failed to write plot: {err}");
}
}
}
}
}
}
pub struct DurationsLayer<S, RS = RandomState> {
main_thead_id: std::thread::ThreadId,
start_index: Mutex<HashMap<span::Id, Duration, RS>>,
fields: Mutex<HashMap<span::Id, CollectedFields<RS>>>,
is_main_thread: Mutex<HashMap<span::Id, bool>>,
export_ids: Mutex<HashMap<span::Id, u64, RS>>,
next_export_id: AtomicU64,
out: Arc<Mutex<Option<BufWriter<File>>>>,
#[cfg(feature = "plot")]
plot_data: Arc<Mutex<Vec<plot::OwnedSpanInfo>>>,
#[cfg(feature = "plot")]
plot_file: Option<PathBuf>,
with_fields: bool,
with_parents: bool,
#[cfg(feature = "plot")]
plot_config: plot::PlotConfig,
#[cfg(feature = "plot")]
plot_layout: plot::PlotLayout,
_inner: PhantomData<S>,
}
impl<S> DurationsLayer<S> {
fn drop_guard(&self) -> DurationsLayerDropGuard {
DurationsLayerDropGuard {
out: self.out.clone(),
#[cfg(feature = "plot")]
plot_file: self.plot_file.clone(),
#[cfg(feature = "plot")]
plot_data: self.plot_data.clone(),
#[cfg(feature = "plot")]
plot_config: self.plot_config.clone(),
#[cfg(feature = "plot")]
plot_layout: self.plot_layout.clone(),
}
}
}
impl<S> Layer<S> for DurationsLayer<S>
where
S: Subscriber + for<'span> LookupSpan<'span>,
{
fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, _ctx: Context<'_, S>) {
self.export_ids
.lock()
.expect("There was a prior panic")
.insert(
id.clone(),
self.next_export_id.fetch_add(1, Ordering::Relaxed),
);
if self.with_fields {
let mut visitor = FieldsCollector::default();
attrs.record(&mut visitor);
self.fields
.lock()
.expect("There was a prior panic")
.insert(id.clone(), visitor.0);
}
self.is_main_thread
.lock()
.expect("There was a prior panic")
.insert(
id.clone(),
self.main_thead_id == std::thread::current().id(),
);
}
fn on_enter(&self, id: &span::Id, _ctx: Context<'_, S>) {
self.start_index
.lock()
.unwrap()
.insert(id.clone(), START.elapsed());
}
fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) {
let span = ctx.span(id).unwrap();
let export_ids = self.export_ids.lock().expect("There was a prior panic");
let export_id = export_ids.get(id).copied().unwrap_or_else(|| id.into_u64());
let parents = if self.with_parents {
let parents = iter::successors(span.parent(), |span| span.parent())
.map(|span| {
let id = span.id();
export_ids
.get(&id)
.copied()
.unwrap_or_else(|| id.into_u64())
})
.collect::<Vec<_>>();
Some(parents)
} else {
None
};
drop(export_ids);
let attributes = self.fields.lock().expect("There was a prior panic");
let fields = attributes.get(id);
debug_assert!(
!self.with_fields || fields.is_some(),
"Expected fields to be record for span {} {}",
span.name(),
id.into_u64()
);
let is_main_thread = self.main_thead_id == std::thread::current().id();
let span_info = SpanInfo {
id: export_id,
name: span.name(),
start: self.start_index.lock().expect("There was a prior panic")[id],
end: START.elapsed(),
parents: parents.as_deref(),
is_main_thread,
fields,
};
#[allow(clippy::needless_borrows_for_generic_args)]
if let Some(mut writer) = self.out.lock().expect("There was a prior panic").as_mut() {
serde_json::to_writer(&mut writer, &span_info).unwrap();
writeln!(&mut writer).unwrap();
}
#[cfg(feature = "plot")]
{
if self.plot_file.is_some() {
self.plot_data
.lock()
.expect("There was a prior panic")
.push(plot::OwnedSpanInfo {
id: export_id,
name: span.name().to_string(),
start: self.start_index.lock().expect("There was a prior panic")[id],
end: START.elapsed(),
parents,
is_main_thread,
fields: fields.map(|fields| {
fields
.iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect()
}),
})
}
}
}
fn on_close(&self, id: span::Id, _ctx: Context<'_, S>) {
self.start_index
.lock()
.expect("There was a prior panic")
.remove(&id);
self.fields
.lock()
.expect("There was a prior panic")
.remove(&id);
self.is_main_thread
.lock()
.expect("There was a prior panic")
.remove(&id);
self.export_ids
.lock()
.expect("There was a prior panic")
.remove(&id);
}
}