use std::collections::VecDeque;
use std::sync::{Arc, Mutex, OnceLock};
use tracing::Subscriber;
use tracing_subscriber::EnvFilter;
pub struct TelemetryGuard {
#[cfg(feature = "otel")]
trace_provider: Option<opentelemetry_sdk::trace::SdkTracerProvider>,
#[cfg(feature = "otel")]
log_provider: Option<opentelemetry_sdk::logs::SdkLoggerProvider>,
}
impl Drop for TelemetryGuard {
fn drop(&mut self) {
#[cfg(feature = "otel")]
{
if let Some(provider) = self.trace_provider.take()
&& let Err(e) = provider.shutdown()
{
eprintln!("otel trace shutdown error: {e}");
}
if let Some(provider) = self.log_provider.take()
&& let Err(e) = provider.shutdown()
{
eprintln!("otel log shutdown error: {e}");
}
}
}
}
type TuiLogSink = Arc<Mutex<VecDeque<String>>>;
static TUI_LOG_SINK: OnceLock<TuiLogSink> = OnceLock::new();
pub fn tui_drain_errors() -> Vec<String> {
if let Some(sink) = TUI_LOG_SINK.get()
&& let Ok(mut s) = sink.lock()
{
return s.drain(..).collect();
}
vec![]
}
struct TuiLogLayer {
sink: TuiLogSink,
}
impl<S: Subscriber> tracing_subscriber::Layer<S> for TuiLogLayer {
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
if *event.metadata().level() > tracing::Level::ERROR {
return;
}
let mut visitor = MessageVisitor(String::new());
event.record(&mut visitor);
let msg = if visitor.0.is_empty() {
event.metadata().name().to_string()
} else {
visitor.0.clone()
};
if let Ok(mut s) = self.sink.lock() {
s.push_back(msg);
while s.len() > 20 {
s.pop_front();
}
}
}
}
struct MessageVisitor(String);
impl tracing::field::Visit for MessageVisitor {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "message" {
self.0 = value.to_string();
}
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.0 = format!("{value:?}");
}
}
}
#[must_use]
pub fn init() -> TelemetryGuard {
let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok();
match endpoint.as_deref() {
None | Some("") => init_noop(),
Some("stderr") => init_stderr(),
#[cfg(feature = "otel")]
Some(_) => init_otlp(),
#[cfg(not(feature = "otel"))]
Some(_) => {
eprintln!(
"warning: OTEL_EXPORTER_OTLP_ENDPOINT set but bones built without 'otel' feature"
);
init_noop()
}
}
}
#[must_use]
pub fn init_for_tui() -> TelemetryGuard {
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
let sink: TuiLogSink = Arc::new(Mutex::new(VecDeque::new()));
let _ = TUI_LOG_SINK.set(Arc::clone(&sink));
let filter = EnvFilter::new("error");
let _ = tracing_subscriber::registry()
.with(filter)
.with(TuiLogLayer { sink })
.try_init();
TelemetryGuard {
#[cfg(feature = "otel")]
trace_provider: None,
#[cfg(feature = "otel")]
log_provider: None,
}
}
fn init_noop() -> TelemetryGuard {
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
let filter = EnvFilter::try_from_env("BONES_LOG").unwrap_or_else(|_| {
let debug = std::env::var("DEBUG").is_ok();
EnvFilter::new(if debug {
"bones=debug,info"
} else {
"bones=info,warn"
})
});
let format = std::env::var("BONES_LOG_FORMAT").unwrap_or_else(|_| "compact".to_string());
let _ = match format.as_str() {
"json" => tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer().json().with_ansi(false))
.try_init(),
_ => tracing_subscriber::registry()
.with(filter)
.with(
tracing_subscriber::fmt::layer()
.compact()
.without_time()
.with_target(false)
.with_thread_ids(false)
.with_thread_names(false)
.with_file(false)
.with_line_number(false),
)
.try_init(),
};
TelemetryGuard {
#[cfg(feature = "otel")]
trace_provider: None,
#[cfg(feature = "otel")]
log_provider: None,
}
}
fn init_stderr() -> TelemetryGuard {
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(filter)
.with(
tracing_subscriber::fmt::layer()
.json()
.with_writer(std::io::stderr)
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE),
)
.init();
TelemetryGuard {
#[cfg(feature = "otel")]
trace_provider: None,
#[cfg(feature = "otel")]
log_provider: None,
}
}
#[cfg(feature = "otel")]
fn init_otlp() -> TelemetryGuard {
use opentelemetry::trace::TracerProvider as _;
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
let span_exporter = match opentelemetry_otlp::SpanExporter::builder()
.with_http()
.build()
{
Ok(e) => e,
Err(e) => {
eprintln!("warning: failed to init OTLP span exporter: {e}");
return init_noop();
}
};
let resource = otel_resource();
let trace_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_simple_exporter(span_exporter)
.with_resource(resource.clone())
.build();
let tracer = trace_provider.tracer(env!("CARGO_PKG_NAME"));
let trace_layer = tracing_opentelemetry::layer().with_tracer(tracer);
let log_exporter = match opentelemetry_otlp::LogExporter::builder()
.with_http()
.build()
{
Ok(e) => e,
Err(e) => {
eprintln!("warning: failed to init OTLP log exporter: {e}");
return init_noop();
}
};
let log_provider = opentelemetry_sdk::logs::SdkLoggerProvider::builder()
.with_simple_exporter(log_exporter)
.with_resource(resource)
.build();
let log_layer =
opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(&log_provider);
install_parent_context();
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(filter)
.with(trace_layer)
.with(log_layer)
.init();
TelemetryGuard {
trace_provider: Some(trace_provider),
log_provider: Some(log_provider),
}
}
#[cfg(feature = "otel")]
#[allow(dead_code)]
pub fn current_traceparent() -> Option<String> {
use opentelemetry::propagation::TextMapPropagator as _;
use opentelemetry_sdk::propagation::TraceContextPropagator;
use std::collections::HashMap;
let propagator = TraceContextPropagator::new();
let mut carrier: HashMap<String, String> = HashMap::new();
propagator.inject(&mut carrier);
carrier.remove("traceparent")
}
#[cfg(not(feature = "otel"))]
#[allow(dead_code)]
pub fn current_traceparent() -> Option<String> {
None
}
#[cfg(feature = "otel")]
fn install_parent_context() {
use opentelemetry::propagation::TextMapPropagator as _;
use opentelemetry_sdk::propagation::TraceContextPropagator;
use std::collections::HashMap;
if let Ok(traceparent) = std::env::var("TRACEPARENT") {
let mut carrier: HashMap<String, String> = HashMap::new();
carrier.insert("traceparent".to_string(), traceparent);
let propagator = TraceContextPropagator::new();
let cx = propagator.extract(&carrier);
let _guard = cx.attach();
std::mem::forget(_guard);
}
}
#[cfg(feature = "otel")]
fn otel_resource() -> opentelemetry_sdk::Resource {
use opentelemetry::KeyValue;
opentelemetry_sdk::Resource::builder()
.with_attribute(KeyValue::new("service.name", env!("CARGO_PKG_NAME")))
.with_attribute(KeyValue::new("service.version", env!("CARGO_PKG_VERSION")))
.build()
}