use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::trace::SdkTracerProvider;
use opentelemetry_sdk::Resource;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
pub fn otel_endpoint() -> Option<String> {
std::env::var("GHOSTLINK_OTEL_EXPORTER_ENDPOINT")
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
}
fn service_name() -> String {
std::env::var("GHOSTLINK_OTEL_SERVICE_NAME")
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.unwrap_or_else(|| "ghost-link".to_string())
}
pub fn init_tracing() {
let fmt_layer = tracing_subscriber::fmt::layer();
let Some(endpoint) = otel_endpoint() else {
tracing_subscriber::registry().with(fmt_layer).init();
return;
};
match build_tracer(&endpoint) {
Ok(tracer) => {
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
tracing_subscriber::registry()
.with(fmt_layer)
.with(otel_layer)
.init();
tracing::info!("otel: exporting traces to {endpoint}");
}
Err(err) => {
tracing_subscriber::registry().with(fmt_layer).init();
tracing::warn!(
"otel: failed to initialize OTLP export to {endpoint}: {err} — continuing with \
console logging only"
);
}
}
}
fn build_tracer(endpoint: &str) -> Result<opentelemetry_sdk::trace::SdkTracer, String> {
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_endpoint(endpoint)
.build()
.map_err(|err| err.to_string())?;
let resource = Resource::builder()
.with_service_name(service_name())
.with_attribute(opentelemetry::KeyValue::new(
"service.version",
env!("CARGO_PKG_VERSION"),
))
.build();
let provider = SdkTracerProvider::builder()
.with_simple_exporter(exporter)
.with_resource(resource)
.build();
Ok(provider.tracer("ghost-link"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, OnceLock};
fn env_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
#[test]
fn otel_endpoint_is_none_when_unset_or_whitespace_only() {
let _guard = env_lock().lock().expect("env lock poisoned");
std::env::remove_var("GHOSTLINK_OTEL_EXPORTER_ENDPOINT");
assert_eq!(otel_endpoint(), None);
std::env::set_var("GHOSTLINK_OTEL_EXPORTER_ENDPOINT", " ");
assert_eq!(
otel_endpoint(),
None,
"whitespace-only must count as unset, not a literal ' ' endpoint"
);
std::env::remove_var("GHOSTLINK_OTEL_EXPORTER_ENDPOINT");
}
#[test]
fn otel_endpoint_returns_trimmed_value_when_set() {
let _guard = env_lock().lock().expect("env lock poisoned");
std::env::set_var(
"GHOSTLINK_OTEL_EXPORTER_ENDPOINT",
" http://localhost:4318 ",
);
assert_eq!(otel_endpoint(), Some("http://localhost:4318".to_string()));
std::env::remove_var("GHOSTLINK_OTEL_EXPORTER_ENDPOINT");
}
#[test]
fn service_name_defaults_and_honors_override() {
let _guard = env_lock().lock().expect("env lock poisoned");
std::env::remove_var("GHOSTLINK_OTEL_SERVICE_NAME");
assert_eq!(service_name(), "ghost-link");
std::env::set_var("GHOSTLINK_OTEL_SERVICE_NAME", "my-service");
assert_eq!(service_name(), "my-service");
std::env::remove_var("GHOSTLINK_OTEL_SERVICE_NAME");
}
}