use span_latency_prom::PrometheusSpanLatencyLayer;
use std::{
env,
io::{stderr, Write},
str::FromStr,
};
use tracing::metadata::LevelFilter;
use tracing::Level;
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
use tracing_subscriber::{
filter::{self, FilterExt},
fmt::{self, format::FmtSpan},
layer::{Filter, SubscriberExt},
reload,
util::SubscriberInitExt,
EnvFilter, Layer, Registry,
};
use crossterm::tty::IsTty;
pub mod span_latency_prom;
pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
#[derive(Default)]
pub struct TelemetryConfig {
pub service_name: String,
pub enable_jaeger: bool,
pub tokio_console: bool,
pub json_log_output: bool,
pub span_log_output: bool,
pub chrome_trace_output: bool,
pub log_file: Option<String>,
pub log_string: Option<String>,
pub span_level: Option<Level>,
pub panic_hook: bool,
pub crash_on_panic: bool,
pub prom_registry: Option<prometheus::Registry>,
pub custom_layers: Vec<Box<dyn Layer<Registry> + Send + Sync + 'static>>,
pub custom_filter: Option<Box<dyn Filter<Registry> + Send + Sync>>,
}
#[must_use]
#[allow(dead_code)]
pub struct TelemetryGuards {
worker_guard: WorkerGuard,
#[cfg(feature = "chrome")]
chrome_guard: Option<tracing_chrome::FlushGuard>,
}
#[derive(Clone, Debug)]
pub struct FilterHandle(reload::Handle<EnvFilter, Registry>);
impl FilterHandle {
pub fn update<S: AsRef<str>>(&self, directives: S) -> Result<(), BoxError> {
let filter = EnvFilter::try_new(directives)?;
self.0.reload(filter)?;
Ok(())
}
pub fn get(&self) -> Result<String, BoxError> {
self.0
.with_current(|filter| filter.to_string())
.map_err(Into::into)
}
}
fn get_output(log_file: Option<String>) -> (NonBlocking, WorkerGuard) {
if let Some(logfile_prefix) = log_file {
let file_appender = tracing_appender::rolling::daily("", logfile_prefix);
tracing_appender::non_blocking(file_appender)
} else {
tracing_appender::non_blocking(stderr())
}
}
fn set_panic_hook(crash_on_panic: bool) {
let default_panic_handler = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic| {
if let Some(location) = panic.location() {
tracing::error!(
message = %panic,
panic.file = location.file(),
panic.line = location.line(),
panic.column = location.column(),
);
} else {
tracing::error!(message = %panic);
}
default_panic_handler(panic);
let _ = std::io::stderr().flush();
let _ = std::io::stdout().flush();
if crash_on_panic {
std::process::exit(12);
}
}));
}
impl TelemetryConfig {
pub fn new(service_name: &str) -> Self {
Self {
service_name: service_name.to_owned(),
enable_jaeger: false,
tokio_console: false,
json_log_output: false,
span_log_output: false,
chrome_trace_output: false,
log_file: None,
log_string: None,
span_level: None,
panic_hook: true,
crash_on_panic: false,
prom_registry: None,
custom_layers: Vec::new(),
custom_filter: None,
}
}
pub fn with_log_level(mut self, log_string: &str) -> Self {
self.log_string = Some(log_string.to_owned());
self
}
pub fn with_span_level(mut self, span_level: Level) -> Self {
self.span_level = Some(span_level);
self
}
pub fn with_log_file(mut self, filename: &str) -> Self {
self.log_file = Some(filename.to_owned());
self
}
pub fn with_prom_registry(mut self, registry: &prometheus::Registry) -> Self {
self.prom_registry = Some(registry.clone());
self
}
pub fn with_layer<L>(mut self, layer: L) -> Self
where
L: Layer<Registry> + Send + Sync + 'static,
{
self.custom_layers.push(Box::new(layer));
self
}
pub fn with_json_logs(mut self) -> Self {
self.json_log_output = true;
self
}
pub fn with_custom_filter<F>(mut self, filter: F) -> Self
where
F: Filter<Registry> + Send + Sync + 'static,
{
self.custom_filter = Some(Box::new(filter));
self
}
pub fn with_env(mut self) -> Self {
if env::var("CRASH_ON_PANIC").is_ok() {
self.crash_on_panic = true
}
if env::var("TOKIO_JAEGER").is_ok() {
self.enable_jaeger = true
}
if env::var("TOKIO_CHROME").is_ok() {
self.chrome_trace_output = true;
}
if env::var("ENABLE_JSON_LOGS").is_ok() {
self.json_log_output = true;
}
if env::var("ENABLE_SPAN_LOGS").is_ok() {
self.span_log_output = true;
}
if env::var("TOKIO_CONSOLE").is_ok() {
self.tokio_console = true;
}
if let Ok(span_level) = env::var("TOKIO_SPAN_LEVEL") {
self.span_level =
Some(Level::from_str(&span_level).expect("Cannot parse TOKIO_SPAN_LEVEL"));
}
if let Ok(filepath) = env::var("RUST_LOG_FILE") {
self.log_file = Some(filepath);
}
self
}
pub fn init(self) -> (TelemetryGuards, FilterHandle) {
let config = self;
let log_level = config.log_string.unwrap_or_else(|| "info".into());
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(log_level));
let (log_filter, reload_handle) = reload::Layer::new(env_filter);
let filter_handle = FilterHandle(reload_handle);
let span_level = config.span_level.unwrap_or(Level::INFO);
let span_filter = filter::filter_fn(move |metadata| {
metadata.is_span() && *metadata.level() <= span_level
});
let mut layers = Vec::new();
#[cfg(feature = "tokio-console")]
if config.tokio_console {
layers.push(console_subscriber::spawn().boxed());
}
#[cfg(feature = "chrome")]
let chrome_guard = if config.chrome_trace_output {
let (chrome_layer, guard) = tracing_chrome::ChromeLayerBuilder::new().build();
layers.push(chrome_layer.boxed());
Some(guard)
} else {
None
};
if let Some(registry) = config.prom_registry {
let span_lat_layer = PrometheusSpanLatencyLayer::try_new(®istry, 15)
.expect("Could not initialize span latency layer");
layers.push(span_lat_layer.with_filter(span_filter.clone()).boxed());
}
#[cfg(feature = "jaeger")]
if config.enable_jaeger {
let tracer = opentelemetry_jaeger::new_agent_pipeline()
.with_service_name(&config.service_name)
.with_max_packet_size(9216) .with_auto_split_batch(true) .install_batch(opentelemetry::runtime::Tokio)
.expect("Could not create async Tracer");
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
opentelemetry::global::set_text_map_propagator(
opentelemetry::sdk::propagation::TraceContextPropagator::new(),
);
layers.push(telemetry.with_filter(span_filter.clone()).boxed());
}
let (nb_output, worker_guard) = get_output(config.log_file.clone());
let fmt_layer = fmt::layer().with_writer(nb_output);
let fmt_layer = if config.json_log_output {
let layer = fmt_layer.json().flatten_event(true);
if let Some(custom_filter) = config.custom_filter {
layer.with_filter(log_filter.and(custom_filter)).boxed()
} else {
layer.with_filter(log_filter).boxed()
}
} else {
let span_events = if config.span_log_output {
FmtSpan::NEW | FmtSpan::CLOSE
} else {
FmtSpan::NONE
};
fmt_layer
.with_ansi(config.log_file.is_none() && stderr().is_tty())
.with_span_events(span_events)
.with_filter(log_filter)
.boxed()
};
layers.push(fmt_layer);
for layer in config.custom_layers {
layers.push(layer);
}
tracing_subscriber::registry().with(layers).init();
if config.panic_hook {
set_panic_hook(config.crash_on_panic);
}
let guards = TelemetryGuards {
worker_guard,
#[cfg(feature = "chrome")]
chrome_guard,
};
(guards, filter_handle)
}
}
pub fn init_for_testing() {
use once_cell::sync::Lazy;
static LOGGER: Lazy<()> = Lazy::new(|| {
let subscriber = ::tracing_subscriber::FmtSubscriber::builder()
.with_env_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.with_file(true)
.with_line_number(true)
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
.with_test_writer()
.finish();
::tracing::subscriber::set_global_default(subscriber)
.expect("unable to initialize logging for tests");
});
Lazy::force(&LOGGER);
}
#[cfg(test)]
mod tests {
use super::*;
use prometheus::proto::MetricType;
use std::time::Duration;
use tracing::{debug, debug_span, info, trace_span, warn};
#[test]
#[should_panic]
fn test_telemetry_init() {
let registry = prometheus::Registry::new();
let config = TelemetryConfig::new("my_app")
.with_span_level(Level::DEBUG)
.with_prom_registry(®istry);
let _guard = config.init();
info!(a = 1, "This will be INFO.");
debug_span!("yo span yo").in_scope(|| {
debug!(a = 2, "This will be DEBUG.");
std::thread::sleep(Duration::from_millis(100));
warn!(a = 3, "This will be WARNING.");
});
trace_span!("this span should not be created").in_scope(|| {
info!("This log appears, but surrounding span is not created");
std::thread::sleep(Duration::from_millis(100));
});
let metrics = registry.gather();
assert_eq!(metrics.len(), 1);
assert_eq!(metrics[0].get_name(), "tracing_span_latencies");
assert_eq!(metrics[0].get_field_type(), MetricType::HISTOGRAM);
let inner = metrics[0].get_metric();
assert_eq!(inner.len(), 1);
let labels = inner[0].get_label();
assert_eq!(labels.len(), 1);
assert_eq!(labels[0].get_name(), "span_name");
assert_eq!(labels[0].get_value(), "yo span yo");
panic!("This should cause error logs to be printed out!");
}
#[test]
fn testing_logger_1() {
init_for_testing();
}
#[test]
fn testing_logger_2() {
init_for_testing();
}
}