use opentelemetry::trace::TracerProvider as _;
use opentelemetry_sdk::trace::SdkTracerProvider;
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
#[derive(Debug, thiserror::Error)]
pub enum MetricsError {
#[error("OTLP exporter build failed: {0}")]
OtlpBuild(String),
#[error("subscriber init failed: {0}")]
Subscriber(String),
}
pub enum TracerExporter {
Stdout,
NoOp,
InMemory(opentelemetry_sdk::trace::InMemorySpanExporter),
}
pub struct MetricsConfig {
pub service_name: String,
pub exporter: TracerExporter,
pub log_filter: Option<String>,
pub otlp_endpoint: Option<String>,
pub deployment_target: Option<String>,
}
impl MetricsConfig {
pub fn resolved_deployment_target(&self) -> String {
self.deployment_target
.clone()
.or_else(|| std::env::var("KRISHIV_DEPLOYMENT_TARGET").ok())
.unwrap_or_else(|| "unknown".to_string())
}
}
impl Default for MetricsConfig {
fn default() -> Self {
Self {
service_name: "krishiv".to_string(),
exporter: TracerExporter::NoOp,
log_filter: None,
otlp_endpoint: None,
deployment_target: None,
}
}
}
pub struct MetricsHandle {
tracer_provider: SdkTracerProvider,
}
impl MetricsHandle {
pub fn noop() -> Self {
Self {
tracer_provider: SdkTracerProvider::builder().build(),
}
}
pub fn shutdown(self) {
}
#[cfg(test)]
pub(crate) fn tracer_provider(&self) -> &SdkTracerProvider {
&self.tracer_provider
}
}
impl Drop for MetricsHandle {
fn drop(&mut self) {
if let Err(error) = self.tracer_provider.shutdown() {
tracing::debug!(error = %error, "metrics tracer provider shutdown failed");
}
}
}
pub fn init(config: MetricsConfig) -> Result<MetricsHandle, MetricsError> {
let filter_str = config.log_filter.as_deref().unwrap_or("info").to_string();
let filter = tracing_subscriber::EnvFilter::new(&filter_str);
let deployment_target = config.resolved_deployment_target();
let resource = opentelemetry_sdk::Resource::builder()
.with_attribute(opentelemetry::KeyValue::new(
"service.name",
config.service_name.clone(),
))
.with_attribute(opentelemetry::KeyValue::new(
"deployment.target",
deployment_target,
))
.build();
let tracer_provider = if let Some(endpoint) = config.otlp_endpoint {
use opentelemetry_otlp::{SpanExporter, WithExportConfig as _};
let exporter = SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()
.map_err(|e| MetricsError::OtlpBuild(format!("{e}")))?;
SdkTracerProvider::builder()
.with_resource(resource)
.with_batch_exporter(exporter)
.build()
} else {
match config.exporter {
TracerExporter::Stdout => SdkTracerProvider::builder()
.with_resource(resource)
.with_simple_exporter(opentelemetry_stdout::SpanExporter::default())
.build(),
TracerExporter::NoOp => SdkTracerProvider::builder().build(),
TracerExporter::InMemory(exporter) => SdkTracerProvider::builder()
.with_resource(resource)
.with_simple_exporter(exporter)
.build(),
}
};
let tracer = tracer_provider.tracer(config.service_name.clone());
opentelemetry::global::set_tracer_provider(tracer_provider.clone());
let _ = tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer().json())
.with(tracing_opentelemetry::layer().with_tracer(tracer))
.try_init();
Ok(MetricsHandle { tracer_provider })
}
pub fn current_traceparent() -> Option<String> {
use opentelemetry::trace::TraceContextExt as _;
use tracing_opentelemetry::OpenTelemetrySpanExt as _;
let ctx = tracing::Span::current().context();
let span_ref = ctx.span();
let span_ctx = span_ref.span_context();
if span_ctx.is_valid() {
Some(format!(
"00-{}-{}-01",
span_ctx.trace_id(),
span_ctx.span_id()
))
} else {
None
}
}