#![doc(
html_logo_url = "https://raw.githubusercontent.com/tokio-rs/tracing/master/assets/logo-type.png",
issue_tracker_base_url = "https://github.com/tokio-rs/tracing/issues/"
)]
#![cfg_attr(docsrs, deny(rustdoc::broken_intra_doc_links))]
#![warn(
missing_debug_implementations,
missing_docs,
rust_2018_idioms,
unreachable_pub,
bad_style,
const_err,
dead_code,
improper_ctypes,
non_shorthand_field_patterns,
no_mangle_generic_items,
overflowing_literals,
path_statements,
patterns_in_fns_without_body,
private_in_public,
unconditional_recursion,
unused,
unused_allocation,
unused_comparisons,
unused_parens,
while_true
)]
pub use error::Error;
use error::Kind;
use lazy_static::lazy_static;
use std::cell::Cell;
use std::fmt;
use std::fmt::Write as _;
use std::fs::File;
use std::io::BufWriter;
use std::io::Write;
use std::marker::PhantomData;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use tracing::span;
use tracing::Subscriber;
use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::registry::SpanRef;
use tracing_subscriber::Layer;
mod error;
lazy_static! {
static ref START: Instant = Instant::now();
}
thread_local! {
static LAST_EVENT: Cell<Instant> = Cell::new(*START);
static THREAD_NAME: String = {
let thread = std::thread::current();
let mut thread_name = format!("{:?}", thread.id());
if let Some(name) = thread.name() {
thread_name += "-";
thread_name += name;
}
thread_name
};
}
#[derive(Debug)]
pub struct FlameLayer<S, W> {
out: Arc<Mutex<W>>,
config: Config,
_inner: PhantomData<S>,
}
#[derive(Debug)]
struct Config {
empty_samples: bool,
threads_collapsed: bool,
module_path: bool,
file_and_line: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
empty_samples: true,
threads_collapsed: false,
module_path: true,
file_and_line: true,
}
}
}
#[must_use]
#[derive(Debug)]
pub struct FlushGuard<W>
where
W: Write + 'static,
{
out: Arc<Mutex<W>>,
}
impl<S, W> FlameLayer<S, W>
where
S: Subscriber + for<'span> LookupSpan<'span>,
W: Write + 'static,
{
pub fn new(writer: W) -> Self {
let _unused = *START;
Self {
out: Arc::new(Mutex::new(writer)),
config: Default::default(),
_inner: PhantomData,
}
}
pub fn flush_on_drop(&self) -> FlushGuard<W> {
FlushGuard {
out: self.out.clone(),
}
}
pub fn with_empty_samples(mut self, enabled: bool) -> Self {
self.config.empty_samples = enabled;
self
}
pub fn with_threads_collapsed(mut self, enabled: bool) -> Self {
self.config.threads_collapsed = enabled;
self
}
pub fn with_module_path(mut self, enabled: bool) -> Self {
self.config.module_path = enabled;
self
}
pub fn with_file_and_line(mut self, enabled: bool) -> Self {
self.config.file_and_line = enabled;
self
}
}
impl<W> FlushGuard<W>
where
W: Write + 'static,
{
pub fn flush(&self) -> Result<(), Error> {
let mut guard = match self.out.lock() {
Ok(guard) => guard,
Err(e) => {
if !std::thread::panicking() {
panic!("{}", e);
} else {
return Ok(());
}
}
};
guard.flush().map_err(Kind::FlushFile).map_err(Error)
}
}
impl<W> Drop for FlushGuard<W>
where
W: Write + 'static,
{
fn drop(&mut self) {
match self.flush() {
Ok(_) => (),
Err(e) => e.report(),
}
}
}
impl<S> FlameLayer<S, BufWriter<File>>
where
S: Subscriber + for<'span> LookupSpan<'span>,
{
pub fn with_file(path: impl AsRef<Path>) -> Result<(Self, FlushGuard<BufWriter<File>>), Error> {
let path = path.as_ref();
let file = File::create(path)
.map_err(|source| Kind::CreateFile {
path: path.into(),
source,
})
.map_err(Error)?;
let writer = BufWriter::new(file);
let layer = Self::new(writer);
let guard = layer.flush_on_drop();
Ok((layer, guard))
}
}
impl<S, W> Layer<S> for FlameLayer<S, W>
where
S: Subscriber + for<'span> LookupSpan<'span>,
W: Write + 'static,
{
fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) {
let samples = self.time_since_last_event();
let first = ctx.span(id).expect("expected: span id exists in registry");
if !self.config.empty_samples && first.parent().is_none() {
return;
}
let mut stack = String::new();
if !self.config.threads_collapsed {
THREAD_NAME.with(|name| stack += name.as_str());
} else {
stack += "all-threads";
}
if let Some(second) = first.parent() {
for parent in second.scope().from_root() {
stack += "; ";
write(&mut stack, parent, &self.config)
.expect("expected: write to String never fails");
}
}
write!(&mut stack, " {}", samples.as_nanos())
.expect("expected: write to String never fails");
let _ = writeln!(*self.out.lock().unwrap(), "{}", stack);
}
fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) {
let panicking = std::thread::panicking();
macro_rules! expect {
($e:expr, $msg:literal) => {
if panicking {
return;
} else {
$e.expect($msg)
}
};
($e:expr) => {
if panicking {
return;
} else {
$e.unwrap()
}
};
}
let samples = self.time_since_last_event();
let first = expect!(ctx.span(id), "expected: span id exists in registry");
let mut stack = String::new();
if !self.config.threads_collapsed {
THREAD_NAME.with(|name| stack += name.as_str());
} else {
stack += "all-threads";
}
for parent in first.scope().from_root() {
stack += "; ";
expect!(
write(&mut stack, parent, &self.config),
"expected: write to String never fails"
);
}
expect!(
write!(&mut stack, " {}", samples.as_nanos()),
"expected: write to String never fails"
);
let _ = writeln!(*expect!(self.out.lock()), "{}", stack);
}
}
impl<S, W> FlameLayer<S, W>
where
S: Subscriber + for<'span> LookupSpan<'span>,
W: Write + 'static,
{
fn time_since_last_event(&self) -> Duration {
let now = Instant::now();
let prev = LAST_EVENT.with(|e| {
let prev = e.get();
e.set(now);
prev
});
now - prev
}
}
fn write<S>(dest: &mut String, span: SpanRef<'_, S>, config: &Config) -> fmt::Result
where
S: Subscriber + for<'span> LookupSpan<'span>,
{
if config.module_path {
if let Some(module_path) = span.metadata().module_path() {
write!(dest, "{}::", module_path)?;
}
}
write!(dest, "{}", span.name())?;
if config.file_and_line {
if let Some(file) = span.metadata().file() {
write!(dest, ":{}", file)?;
}
if let Some(line) = span.metadata().line() {
write!(dest, ":{}", line)?;
}
}
Ok(())
}