use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::OnceLock;
use std::time::Duration;
use super::otel_instruments::{instruments, Instruments};
use opentelemetry::metrics::{Counter, Gauge, Histogram, Meter};
use opentelemetry::trace::TracerProvider as _;
use opentelemetry::{global, KeyValue};
use opentelemetry_otlp::{MetricExporter, Protocol, SpanExporter, WithExportConfig};
use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
use opentelemetry_sdk::Resource;
use opentelemetry_semantic_conventions as semconv;
use crate::error::{Error, Result};
use crate::observability::catalog;
use crate::observability::config::{ObservabilityConfig, OtelProtocol};
const SCOPE: &str = "cqlite";
static TRACER_PROVIDER: OnceLock<SdkTracerProvider> = OnceLock::new();
static METRICS_ACTIVE: AtomicBool = AtomicBool::new(false);
fn build_resource(cfg: &ObservabilityConfig) -> Resource {
Resource::builder()
.with_service_name(cfg.service_name.clone())
.with_attribute(KeyValue::new(
semconv::attribute::SERVICE_VERSION,
cfg.service_version.clone(),
))
.build()
}
fn build_sampler(ratio: f64) -> Sampler {
Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(ratio.clamp(0.0, 1.0))))
}
fn otlp_protocol(p: OtelProtocol) -> Protocol {
match p {
OtelProtocol::Grpc => Protocol::Grpc,
OtelProtocol::Http => Protocol::HttpBinary,
}
}
fn build_span_exporter(cfg: &ObservabilityConfig) -> Result<SpanExporter> {
let builder = match cfg.protocol {
OtelProtocol::Grpc => SpanExporter::builder()
.with_tonic()
.with_endpoint(cfg.endpoint.clone())
.with_protocol(otlp_protocol(cfg.protocol))
.with_timeout(cfg.timeout)
.build(),
OtelProtocol::Http => SpanExporter::builder()
.with_http()
.with_endpoint(cfg.endpoint.clone())
.with_protocol(otlp_protocol(cfg.protocol))
.with_timeout(cfg.timeout)
.build(),
};
builder.map_err(|e| Error::configuration(format!("OTLP span exporter init failed: {e}")))
}
fn build_metric_exporter(cfg: &ObservabilityConfig) -> Result<MetricExporter> {
let builder = match cfg.protocol {
OtelProtocol::Grpc => MetricExporter::builder()
.with_tonic()
.with_endpoint(cfg.endpoint.clone())
.with_protocol(otlp_protocol(cfg.protocol))
.with_timeout(cfg.timeout)
.build(),
OtelProtocol::Http => MetricExporter::builder()
.with_http()
.with_endpoint(cfg.endpoint.clone())
.with_protocol(otlp_protocol(cfg.protocol))
.with_timeout(cfg.timeout)
.build(),
};
builder.map_err(|e| Error::configuration(format!("OTLP metric exporter init failed: {e}")))
}
pub struct ObservabilityGuard {
tracer_provider: Option<SdkTracerProvider>,
meter_provider: Option<SdkMeterProvider>,
}
impl std::fmt::Debug for ObservabilityGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ObservabilityGuard")
.field("active", &self.tracer_provider.is_some())
.finish()
}
}
impl ObservabilityGuard {
pub(crate) fn inert() -> Self {
Self {
tracer_provider: None,
meter_provider: None,
}
}
pub fn is_active(&self) -> bool {
self.tracer_provider.is_some() || self.meter_provider.is_some()
}
pub fn force_flush(&self) {
if let Some(tp) = &self.tracer_provider {
let _ = tp.force_flush();
}
if let Some(mp) = &self.meter_provider {
let _ = mp.force_flush();
}
}
}
impl Drop for ObservabilityGuard {
fn drop(&mut self) {
if let Some(tp) = &self.tracer_provider {
let _ = tp.force_flush();
let _ = tp.shutdown();
}
if let Some(mp) = &self.meter_provider {
let _ = mp.force_flush();
let _ = mp.shutdown();
METRICS_ACTIVE.store(false, Ordering::Relaxed);
}
}
}
pub fn init(cfg: ObservabilityConfig) -> Result<ObservabilityGuard> {
crate::storage::sstable::reader::presence_verification::apply_config(
cfg.verify_presence_oracle,
);
if !cfg.enabled {
return Ok(ObservabilityGuard::inert());
}
let resource = build_resource(&cfg);
let span_exporter = build_span_exporter(&cfg)?;
let tracer_provider = SdkTracerProvider::builder()
.with_batch_exporter(span_exporter)
.with_sampler(build_sampler(cfg.sampling_ratio))
.with_resource(resource.clone())
.build();
let _ = TRACER_PROVIDER.set(tracer_provider.clone());
global::set_tracer_provider(tracer_provider.clone());
let metric_exporter = build_metric_exporter(&cfg)?;
let reader = PeriodicReader::builder(metric_exporter).build();
let meter_provider = SdkMeterProvider::builder()
.with_reader(reader)
.with_resource(resource)
.build();
global::set_meter_provider(meter_provider.clone());
METRICS_ACTIVE.store(true, Ordering::Relaxed);
register_baseline_instruments();
Ok(ObservabilityGuard {
tracer_provider: Some(tracer_provider),
meter_provider: Some(meter_provider),
})
}
#[inline]
pub(crate) fn metrics_active() -> bool {
METRICS_ACTIVE.load(Ordering::Relaxed)
}
pub(crate) fn register_baseline_instruments() {
add_counter(catalog::ERRORS_TOTAL, 0, &[]);
}
#[cfg(feature = "observability-testing")]
pub(crate) fn set_metrics_active_for_testing() {
METRICS_ACTIVE.store(true, Ordering::Relaxed);
}
pub fn tracing_layer<S>() -> Option<impl tracing_subscriber::Layer<S>>
where
S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
{
let provider = TRACER_PROVIDER.get()?;
let tracer = provider.tracer(SCOPE);
Some(tracing_opentelemetry::layer().with_tracer(tracer))
}
pub(super) fn meter() -> &'static Meter {
static METER: OnceLock<Meter> = OnceLock::new();
METER.get_or_init(|| global::meter(SCOPE))
}
pub(super) fn counter_for<'a>(i: &'a Instruments, name: &str) -> Option<&'a Counter<u64>> {
i.counters.get(name)
}
fn stats_only_refuses_instrument(name: &str) -> bool {
let refused = catalog::STATS_ONLY_METRICS.iter().any(|m| m.name == name);
if refused {
tracing::debug!(
metric = name,
"stats-only metric emitted through the OTel path; no instrument created"
);
}
refused
}
pub(crate) fn add_counter(name: &'static str, value: u64, attributes: &[KeyValue]) {
add_counter_with(instruments(), meter(), name, value, attributes)
}
pub(super) fn add_counter_with(
i: &Instruments,
meter: &Meter,
name: &'static str,
value: u64,
attributes: &[KeyValue],
) {
match counter_for(i, name) {
Some(counter) => counter.add(value, attributes),
None => {
if stats_only_refuses_instrument(name) {
return;
}
meter.u64_counter(name).build().add(value, attributes)
}
}
}
pub(super) fn histogram_for<'a>(i: &'a Instruments, name: &str) -> Option<&'a Histogram<f64>> {
i.histograms.get(name)
}
pub(crate) fn record_histogram(name: &'static str, value: f64, attributes: &[KeyValue]) {
record_histogram_with(instruments(), meter(), name, value, attributes)
}
pub(super) fn record_histogram_with(
i: &Instruments,
meter: &Meter,
name: &'static str,
value: f64,
attributes: &[KeyValue],
) {
match histogram_for(i, name) {
Some(hist) => hist.record(value, attributes),
None => {
if stats_only_refuses_instrument(name) {
return;
}
meter.f64_histogram(name).build().record(value, attributes)
}
}
}
pub(super) fn gauge_for<'a>(i: &'a Instruments, name: &str) -> Option<&'a Gauge<i64>> {
i.gauges.get(name)
}
pub(crate) fn record_gauge(name: &'static str, value: i64, attributes: &[KeyValue]) {
record_gauge_with(instruments(), meter(), name, value, attributes)
}
pub(super) fn record_gauge_with(
i: &Instruments,
meter: &Meter,
name: &'static str,
value: i64,
attributes: &[KeyValue],
) {
match gauge_for(i, name) {
Some(gauge) => gauge.record(value, attributes),
None => {
if stats_only_refuses_instrument(name) {
return;
}
meter.i64_gauge(name).build().record(value, attributes)
}
}
}
pub(crate) fn mark_span_error(category: crate::observability::ObsErrorCategory) {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let span = tracing::Span::current();
span.set_attribute("otel.status_code", "ERROR");
span.set_attribute(catalog::attr::ERROR_CATEGORY, category.as_str());
}
pub(crate) fn set_span_parent_from_traceparent(span: &tracing::Span, traceparent: Option<&str>) {
use opentelemetry::propagation::{Extractor, TextMapPropagator};
use opentelemetry::trace::TraceContextExt;
use opentelemetry_sdk::propagation::TraceContextPropagator;
use tracing_opentelemetry::OpenTelemetrySpanExt;
let header = match traceparent {
Some(h) if !h.trim().is_empty() => h,
_ => return,
};
struct TraceParentCarrier<'a>(&'a str);
impl Extractor for TraceParentCarrier<'_> {
fn get(&self, key: &str) -> Option<&str> {
if key.eq_ignore_ascii_case("traceparent") {
Some(self.0)
} else {
None
}
}
fn keys(&self) -> Vec<&str> {
vec!["traceparent"]
}
}
let propagator = TraceContextPropagator::new();
let cx = propagator.extract(&TraceParentCarrier(header));
if cx.span().span_context().is_valid() {
span.set_parent(cx);
}
}
#[allow(dead_code)]
pub(crate) const DEFAULT_FLUSH: Duration = Duration::from_secs(1);
#[cfg(test)]
#[path = "otel_tests.rs"]
mod tests;