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}");
}
}
}
}
#[must_use]
pub fn init(verbose: bool) -> TelemetryGuard {
let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok();
match endpoint.as_deref() {
None | Some("") => init_fmt(verbose),
Some("stderr") => init_stderr(),
#[cfg(feature = "otel")]
Some(_) => init_otlp(verbose),
#[cfg(not(feature = "otel"))]
Some(_) => {
eprintln!("warning: OTEL_EXPORTER_OTLP_ENDPOINT set but vessel built without 'otel' feature");
init_fmt(verbose)
}
}
}
fn init_fmt(verbose: bool) -> TelemetryGuard {
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
let default = if verbose { "vessel=debug" } else { "vessel=warn" };
let stderr_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(default));
if let Some(log_path) = std::env::var("VESSEL_LOG").ok().filter(|s| !s.is_empty()) {
match std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
{
Ok(file) => {
let filter = if verbose {
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("vessel=debug"))
} else {
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("vessel=info"))
};
let file = std::sync::Mutex::new(file);
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_ansi(false)
.with_writer(file)
.init();
}
Err(e) => {
eprintln!("warning: VESSEL_LOG={log_path}: {e}");
tracing_subscriber::fmt()
.with_env_filter(stderr_filter)
.with_writer(std::io::stderr)
.init();
}
}
} else {
tracing_subscriber::fmt()
.with_env_filter(stderr_filter)
.with_writer(std::io::stderr)
.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(verbose: bool) -> 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_fmt(verbose);
}
};
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_fmt(verbose);
}
};
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 default = if verbose { "vessel=debug" } else { "vessel=warn" };
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default));
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")]
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"))]
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()
}