use opentelemetry::{global, trace::Span, trace::Tracer, KeyValue};
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
use opentelemetry_sdk::{
trace::{SdkTracerProvider, SimpleSpanProcessor},
Resource,
};
use std::collections::HashMap;
const OTLP_ENDPOINT: &str = "https://otel.observability.app.launchdarkly.com:4318";
#[allow(dead_code)]
pub struct OtelSpan {
inner: opentelemetry::global::BoxedSpan,
}
#[allow(dead_code)]
impl OtelSpan {
pub fn str_attr(&mut self, key: &str, val: &str) {
self.inner
.set_attribute(KeyValue::new(key.to_string(), val.to_string()));
}
pub fn int_attr(&mut self, key: &str, val: i64) {
self.inner
.set_attribute(KeyValue::new(key.to_string(), val));
}
pub fn float_attr(&mut self, key: &str, val: f64) {
self.inner
.set_attribute(KeyValue::new(key.to_string(), val));
}
pub fn error(&mut self, msg: &str) {
self.inner
.set_status(opentelemetry::trace::Status::error(msg.to_string()));
}
}
impl Drop for OtelSpan {
fn drop(&mut self) {
self.inner.end();
}
}
pub struct Telemetry {
provider: Option<SdkTracerProvider>,
}
impl Telemetry {
pub fn new(sdk_key: Option<&str>) -> Self {
let sdk_key = match sdk_key {
Some(v) if !v.is_empty() => v,
_ => return Telemetry { provider: None },
};
match Self::build(sdk_key) {
Ok(provider) => {
global::set_tracer_provider(provider.clone());
log::info!("LD Observability: tracing enabled → {}", OTLP_ENDPOINT);
Telemetry {
provider: Some(provider),
}
}
Err(e) => {
log::warn!("LD Observability init error: {e}");
Telemetry { provider: None }
}
}
}
fn build(sdk_key: &str) -> anyhow::Result<SdkTracerProvider> {
let mut headers = HashMap::new();
headers.insert("Authorization".to_string(), format!("Bearer {}", sdk_key));
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_endpoint(format!("{}/v1/traces", OTLP_ENDPOINT))
.with_headers(headers)
.build()
.map_err(|e| anyhow::anyhow!("OTel exporter: {e}"))?;
let resource = Resource::builder()
.with_attributes([
KeyValue::new("service.name", "hvac"),
KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
])
.build();
let provider = SdkTracerProvider::builder()
.with_span_processor(SimpleSpanProcessor::new(exporter))
.with_resource(resource)
.build();
Ok(provider)
}
#[allow(dead_code)]
pub fn span(&self, name: &str) -> OtelSpan {
OtelSpan {
inner: global::tracer("hvac").start(name.to_string()),
}
}
pub fn shutdown(&mut self) {
if let Some(provider) = self.provider.take() {
let _ = provider.shutdown();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn telemetry_new_with_none_is_noop() {
let t = Telemetry::new(None);
assert!(t.provider.is_none());
}
#[test]
fn telemetry_new_with_empty_str_is_noop() {
let t = Telemetry::new(Some(""));
assert!(t.provider.is_none());
}
}