zebra/zebrad/src/components/tracing/component.rs

372 lines
13 KiB
Rust

//! The Abscissa component for Zebra's `tracing` implementation.
use std::{
fs::{self, File},
io::Write,
};
use abscissa_core::{Component, FrameworkError, Shutdown};
use tracing_appender::non_blocking::{NonBlocking, NonBlockingBuilder, WorkerGuard};
use tracing_error::ErrorLayer;
use tracing_subscriber::{
fmt::{format, Formatter},
layer::SubscriberExt,
reload::Handle,
util::SubscriberInitExt,
EnvFilter,
};
use crate::{application::app_version, components::tracing::Config};
#[cfg(feature = "flamegraph")]
use super::flame;
/// A type-erased boxed writer that can be sent between threads safely.
pub type BoxWrite = Box<dyn Write + Send + Sync + 'static>;
/// Abscissa component for initializing the `tracing` subsystem
pub struct Tracing {
/// The installed filter reloading handle, if enabled.
//
// TODO: when fmt::Subscriber supports per-layer filtering, remove the Option
filter_handle: Option<
Handle<
EnvFilter,
Formatter<format::DefaultFields, format::Format<format::Full>, NonBlocking>,
>,
>,
/// The originally configured filter.
initial_filter: String,
/// The installed flame graph collector, if enabled.
#[cfg(feature = "flamegraph")]
flamegrapher: Option<flame::Grapher>,
/// Drop guard for worker thread of non-blocking logger,
/// responsible for flushing any remaining logs when the program terminates.
//
// Correctness: must be listed last in the struct, so it drops after other drops have logged.
_guard: Option<WorkerGuard>,
}
impl Tracing {
/// Try to create a new [`Tracing`] component with the given `filter`.
#[allow(clippy::print_stdout, clippy::print_stderr)]
pub fn new(config: Config) -> Result<Self, FrameworkError> {
let filter = config.filter.unwrap_or_default();
let flame_root = &config.flamegraph;
let writer = if let Some(log_file) = config.log_file.as_ref() {
println!("running zebra");
// Make sure the directory for the log file exists.
// If the log is configured in the current directory, it won't have a parent directory.
//
// # Security
//
// If the user is running Zebra with elevated permissions ("root"), they should
// create the log file directory before running Zebra, and make sure the Zebra user
// account has exclusive access to that directory, and other users can't modify
// its parent directories.
//
// This avoids a TOCTOU security issue in the Rust filesystem API.
let log_file_dir = log_file.parent();
if let Some(log_file_dir) = log_file_dir {
if !log_file_dir.exists() {
println!("directory for log file {log_file:?} does not exist, trying to create it...");
if let Err(create_dir_error) = fs::create_dir_all(log_file_dir) {
println!("failed to create directory for log file: {create_dir_error}");
println!("trying log file anyway...");
}
}
}
println!("sending logs to {log_file:?}...");
let log_file = File::options().append(true).create(true).open(log_file)?;
Box::new(log_file) as BoxWrite
} else {
let stdout = std::io::stdout();
Box::new(stdout) as BoxWrite
};
// Builds a lossy NonBlocking logger with a default line limit of 128_000 or an explicit buffer_limit.
// The write method queues lines down a bounded channel with this capacity to a worker thread that writes to stdout.
// Increments error_counter and drops lines when the buffer is full.
let (non_blocking, worker_guard) = NonBlockingBuilder::default()
.buffered_lines_limit(config.buffer_limit.max(100))
.finish(writer);
// Only use color if tracing output is being sent to a terminal or if it was explicitly
// forced to.
let use_color =
config.force_use_color || (config.use_color && atty::is(atty::Stream::Stdout));
// Construct a format subscriber with the supplied global logging filter,
// and optionally enable reloading.
//
// TODO: when fmt::Subscriber supports per-layer filtering, always enable this code
#[cfg(not(all(feature = "tokio-console", tokio_unstable)))]
let (subscriber, filter_handle) = {
use tracing_subscriber::FmtSubscriber;
let logger = FmtSubscriber::builder()
.with_ansi(use_color)
.with_writer(non_blocking)
.with_env_filter(&filter);
// Enable reloading if that feature is selected.
#[cfg(feature = "filter-reload")]
let (filter_handle, logger) = {
let logger = logger.with_filter_reloading();
(Some(logger.reload_handle()), logger)
};
#[cfg(not(feature = "filter-reload"))]
let filter_handle = None;
let subscriber = logger.finish().with(ErrorLayer::default());
(subscriber, filter_handle)
};
// Construct a tracing registry with the supplied per-layer logging filter,
// and disable filter reloading.
//
// TODO: when fmt::Subscriber supports per-layer filtering,
// remove this registry code, and layer tokio-console on top of fmt::Subscriber
#[cfg(all(feature = "tokio-console", tokio_unstable))]
let (subscriber, filter_handle) = {
use tracing_subscriber::{fmt, Layer};
let subscriber = tracing_subscriber::registry();
// TODO: find out why crawl_and_dial and try_to_sync evade this filter,
// and why they also don't get the global net/commit span.
// Note: this might have been fixed by tracing 0.3.15, or by recent Zebra refactors.
//
// Using `registry` as the base subscriber, the logs from most other functions get filtered.
// Using `FmtSubscriber` as the base subscriber, all the logs get filtered.
let logger = fmt::Layer::new()
.with_ansi(use_color)
.with_writer(non_blocking)
.with_filter(EnvFilter::from(&filter));
let subscriber = subscriber.with(logger);
let span_logger = ErrorLayer::default().with_filter(EnvFilter::from(&filter));
let subscriber = subscriber.with(span_logger);
(subscriber, None)
};
// Add optional layers based on dynamic and compile-time configs
// Add a flamegraph
#[cfg(feature = "flamegraph")]
let (flamelayer, flamegrapher) = if let Some(path) = flame_root {
let (flamelayer, flamegrapher) = flame::layer(path);
(Some(flamelayer), Some(flamegrapher))
} else {
(None, None)
};
#[cfg(feature = "flamegraph")]
let subscriber = subscriber.with(flamelayer);
#[cfg(feature = "journald")]
let journaldlayer = if config.use_journald {
use abscissa_core::FrameworkErrorKind;
let layer = tracing_journald::layer()
.map_err(|e| FrameworkErrorKind::ComponentError.context(e))?;
// If the global filter can't be used, add a per-layer filter instead.
// TODO: when fmt::Subscriber supports per-layer filtering, always enable this code
#[cfg(all(feature = "tokio-console", tokio_unstable))]
let layer = {
use tracing_subscriber::Layer;
layer.with_filter(EnvFilter::from(&filter))
};
Some(layer)
} else {
None
};
#[cfg(feature = "journald")]
let subscriber = subscriber.with(journaldlayer);
#[cfg(feature = "sentry")]
let subscriber = subscriber.with(sentry::integrations::tracing::layer());
// spawn the console server in the background, and apply the console layer
// TODO: set Builder::poll_duration_histogram_max() if needed
#[cfg(all(feature = "tokio-console", tokio_unstable))]
let subscriber = subscriber.with(console_subscriber::spawn());
// Initialise the global tracing subscriber
subscriber.init();
// Log the tracing stack we just created
tracing::info!(
?filter,
TRACING_STATIC_MAX_LEVEL = ?tracing::level_filters::STATIC_MAX_LEVEL,
LOG_STATIC_MAX_LEVEL = ?log::STATIC_MAX_LEVEL,
"started tracing component",
);
if flame_root.is_some() {
if cfg!(feature = "flamegraph") {
info!(flamegraph = ?flame_root, "installed flamegraph tracing layer");
} else {
warn!(
flamegraph = ?flame_root,
"unable to activate configured flamegraph: \
enable the 'flamegraph' feature when compiling zebrad",
);
}
}
if config.use_journald {
if cfg!(feature = "journald") {
info!("installed journald tracing layer");
} else {
warn!(
"unable to activate configured journald tracing: \
enable the 'journald' feature when compiling zebrad",
);
}
}
#[cfg(feature = "sentry")]
info!("installed sentry tracing layer");
#[cfg(all(feature = "tokio-console", tokio_unstable))]
info!(
TRACING_STATIC_MAX_LEVEL = ?tracing::level_filters::STATIC_MAX_LEVEL,
LOG_STATIC_MAX_LEVEL = ?log::STATIC_MAX_LEVEL,
"installed tokio-console tracing layer",
);
// Write any progress reports sent by other tasks to the terminal
//
// TODO: move this to its own module?
#[cfg(feature = "progress-bar")]
{
use howudoin::consumers::TermLine;
use std::time::Duration;
// Stops flickering during the initial sync.
const PROGRESS_BAR_DEBOUNCE: Duration = Duration::from_secs(2);
let terminal_consumer = TermLine::with_debounce(PROGRESS_BAR_DEBOUNCE);
howudoin::init(terminal_consumer);
info!("activated progress bar");
if config.log_file.is_some() {
eprintln!("waiting for initial progress reports...");
}
}
Ok(Self {
filter_handle,
initial_filter: filter,
#[cfg(feature = "flamegraph")]
flamegrapher,
_guard: Some(worker_guard),
})
}
/// Drops guard for worker thread of non-blocking logger,
/// to flush any remaining logs when the program terminates.
pub fn shutdown(&mut self) {
self.filter_handle.take();
#[cfg(feature = "flamegraph")]
self.flamegrapher.take();
self._guard.take();
}
/// Return the currently-active tracing filter.
pub fn filter(&self) -> String {
if let Some(filter_handle) = self.filter_handle.as_ref() {
filter_handle
.with_current(|filter| filter.to_string())
.expect("the subscriber is not dropped before the component is")
} else {
self.initial_filter.clone()
}
}
/// Reload the currently-active filter with the supplied value.
///
/// This can be used to provide a dynamic tracing filter endpoint.
pub fn reload_filter(&self, filter: impl Into<EnvFilter>) {
let filter = filter.into();
if let Some(filter_handle) = self.filter_handle.as_ref() {
tracing::info!(
?filter,
TRACING_STATIC_MAX_LEVEL = ?tracing::level_filters::STATIC_MAX_LEVEL,
LOG_STATIC_MAX_LEVEL = ?log::STATIC_MAX_LEVEL,
"reloading tracing filter",
);
filter_handle
.reload(filter)
.expect("the subscriber is not dropped before the component is");
} else {
tracing::warn!(
?filter,
TRACING_STATIC_MAX_LEVEL = ?tracing::level_filters::STATIC_MAX_LEVEL,
LOG_STATIC_MAX_LEVEL = ?log::STATIC_MAX_LEVEL,
"attempted to reload tracing filter, but filter reloading is disabled",
);
}
}
}
impl std::fmt::Debug for Tracing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Tracing").finish()
}
}
impl<A: abscissa_core::Application> Component<A> for Tracing {
fn id(&self) -> abscissa_core::component::Id {
abscissa_core::component::Id::new("zebrad::components::tracing::component::Tracing")
}
fn version(&self) -> abscissa_core::Version {
app_version()
}
fn before_shutdown(&self, _kind: Shutdown) -> Result<(), FrameworkError> {
#[cfg(feature = "flamegraph")]
if let Some(ref grapher) = self.flamegrapher {
use abscissa_core::FrameworkErrorKind;
info!("writing flamegraph");
grapher
.write_flamegraph()
.map_err(|e| FrameworkErrorKind::ComponentError.context(e))?
}
#[cfg(feature = "progress-bar")]
howudoin::disable();
Ok(())
}
}
impl Drop for Tracing {
fn drop(&mut self) {
#[cfg(feature = "progress-bar")]
howudoin::disable();
}
}