use std::sync::OnceLock;
use opentelemetry::trace::TracerProvider as _;
use opentelemetry::Value;
use opentelemetry_sdk::metrics::data::AggregatedMetrics;
use opentelemetry_sdk::metrics::data::MetricData;
use opentelemetry_sdk::metrics::{
InMemoryMetricExporter, InMemoryMetricExporterBuilder, PeriodicReader, SdkMeterProvider,
Temporality,
};
use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider, SpanData};
use tracing_subscriber::prelude::*;
const SCOPE: &str = "cqlite";
#[derive(Debug, Clone)]
pub struct CapturedSpan {
pub name: String,
pub data: SpanData,
}
impl CapturedSpan {
pub fn span_id(&self) -> opentelemetry::trace::SpanId {
self.data.span_context.span_id()
}
pub fn parent_span_id(&self) -> opentelemetry::trace::SpanId {
self.data.parent_span_id
}
pub fn trace_id(&self) -> opentelemetry::trace::TraceId {
self.data.span_context.trace_id()
}
pub fn attribute(&self, key: &str) -> Option<String> {
self.data
.attributes
.iter()
.find(|kv| kv.key.as_str() == key)
.map(|kv| kv.value.as_str().into_owned())
}
pub fn is_error(&self) -> bool {
matches!(self.data.status, opentelemetry::trace::Status::Error { .. })
|| self.attribute("otel.status_code").as_deref() == Some("ERROR")
}
}
#[derive(Debug, Clone, Default)]
pub struct CapturedSpans {
spans: Vec<CapturedSpan>,
}
impl CapturedSpans {
pub fn from_raw(raw: Vec<(String, SpanData)>) -> Self {
Self {
spans: raw
.into_iter()
.map(|(name, data)| CapturedSpan { name, data })
.collect(),
}
}
pub fn all(&self) -> &[CapturedSpan] {
&self.spans
}
pub fn iter(&self) -> std::slice::Iter<'_, CapturedSpan> {
self.spans.iter()
}
pub fn find(&self, name: &str) -> Option<&CapturedSpan> {
self.spans.iter().find(|s| s.name == name)
}
pub fn contains(&self, name: &str) -> bool {
self.find(name).is_some()
}
pub fn is_parent_of(&self, parent: &str, child: &str) -> bool {
let parents: Vec<_> = self.spans.iter().filter(|s| s.name == parent).collect();
self.spans.iter().filter(|s| s.name == child).any(|c| {
parents
.iter()
.any(|p| p.span_id() == c.parent_span_id() && p.trace_id() == c.trace_id())
})
}
}
pub fn capture_spans<F, R>(flow: F) -> CapturedSpans
where
F: FnOnce() -> R,
{
let exporter = InMemorySpanExporter::default();
let provider = SdkTracerProvider::builder()
.with_simple_exporter(exporter.clone())
.build();
let tracer = provider.tracer(SCOPE);
let layer = tracing_opentelemetry::layer().with_tracer(tracer);
let subscriber = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(subscriber, flow);
let _ = provider.force_flush();
let spans = exporter
.get_finished_spans()
.expect("in-memory span exporter must yield finished spans")
.into_iter()
.map(|data| CapturedSpan {
name: data.name.to_string(),
data,
})
.collect();
let _ = provider.shutdown();
CapturedSpans { spans }
}
#[derive(Clone)]
pub struct MetricsCapture {
exporter: InMemoryMetricExporter,
provider: SdkMeterProvider,
}
#[derive(Debug, Default)]
pub struct CapturedMetrics {
entries: Vec<MetricEntry>,
}
#[derive(Debug, Clone)]
pub struct MetricEntry {
pub name: String,
pub unit: String,
pub points: Vec<MetricPoint>,
}
#[derive(Debug, Clone)]
pub struct MetricPoint {
pub value: f64,
pub attributes: Vec<(String, String)>,
}
impl CapturedMetrics {
pub fn entries(&self) -> &[MetricEntry] {
&self.entries
}
pub fn find(&self, name: &str) -> Option<&MetricEntry> {
self.entries.iter().find(|m| m.name == name)
}
pub fn contains(&self, name: &str) -> bool {
self.find(name).is_some()
}
pub fn unit(&self, name: &str) -> Option<&str> {
self.find(name).map(|m| m.unit.as_str())
}
pub fn counter_sum(&self, name: &str) -> f64 {
self.find(name)
.map(|m| m.points.iter().map(|p| p.value).sum())
.unwrap_or(0.0)
}
pub fn has_point_with_empty_attrs_at(&self, name: &str, value: f64) -> bool {
self.find(name).is_some_and(|m| {
m.points
.iter()
.any(|p| p.attributes.is_empty() && (p.value - value).abs() < f64::EPSILON)
})
}
pub fn sum_where(&self, name: &str, required: &[(&str, &str)]) -> f64 {
let Some(m) = self.find(name) else {
return 0.0;
};
m.points
.iter()
.filter(|p| {
required
.iter()
.all(|(k, v)| p.attributes.iter().any(|(pk, pv)| pk == k && pv == v))
})
.map(|p| p.value)
.sum()
}
}
impl MetricsCapture {
pub fn reset(&self) {
let _ = self.provider.force_flush();
self.exporter.reset();
}
pub fn seed_baseline(&self) {
super::otel::register_baseline_instruments();
}
pub fn flush_and_collect(&self) -> CapturedMetrics {
let _ = self.provider.force_flush();
let resource_metrics = self
.exporter
.get_finished_metrics()
.expect("in-memory metric exporter must yield collected metrics");
let mut entries = Vec::new();
for rm in &resource_metrics {
for sm in rm.scope_metrics() {
for metric in sm.metrics() {
entries.push(metric_to_entry(metric));
}
}
}
CapturedMetrics { entries }
}
}
pub fn metrics_capture() -> MetricsCapture {
static CAPTURE: OnceLock<MetricsCapture> = OnceLock::new();
CAPTURE
.get_or_init(|| {
let exporter = InMemoryMetricExporterBuilder::new()
.with_temporality(Temporality::Delta)
.build();
let reader = PeriodicReader::builder(exporter.clone()).build();
let provider = SdkMeterProvider::builder().with_reader(reader).build();
opentelemetry::global::set_meter_provider(provider.clone());
super::otel::set_metrics_active_for_testing();
super::otel::register_baseline_instruments();
MetricsCapture { exporter, provider }
})
.clone()
}
fn value_to_string(v: &Value) -> String {
match v {
Value::String(s) => s.to_string(),
Value::Bool(b) => b.to_string(),
Value::I64(i) => i.to_string(),
Value::F64(f) => f.to_string(),
other => format!("{other:?}"),
}
}
fn metric_to_entry(metric: &opentelemetry_sdk::metrics::data::Metric) -> MetricEntry {
let mut points = Vec::new();
match metric.data() {
AggregatedMetrics::F64(data) => collect_points_f64(data, &mut points),
AggregatedMetrics::U64(data) => collect_points_u64(data, &mut points),
AggregatedMetrics::I64(data) => collect_points_i64(data, &mut points),
}
MetricEntry {
name: metric.name().to_string(),
unit: metric.unit().to_string(),
points,
}
}
macro_rules! collect_points_impl {
($fn_name:ident, $ty:ty, $to_f64:expr) => {
fn $fn_name(data: &MetricData<$ty>, out: &mut Vec<MetricPoint>) {
let to_f64: fn($ty) -> f64 = $to_f64;
match data {
MetricData::Sum(sum) => {
for dp in sum.data_points() {
out.push(MetricPoint {
value: to_f64(dp.value()),
attributes: attrs(dp.attributes()),
});
}
}
MetricData::Gauge(gauge) => {
for dp in gauge.data_points() {
out.push(MetricPoint {
value: to_f64(dp.value()),
attributes: attrs(dp.attributes()),
});
}
}
MetricData::Histogram(hist) => {
for dp in hist.data_points() {
out.push(MetricPoint {
value: to_f64(dp.sum()),
attributes: attrs(dp.attributes()),
});
}
}
MetricData::ExponentialHistogram(hist) => {
for dp in hist.data_points() {
out.push(MetricPoint {
value: to_f64(dp.sum()),
attributes: attrs(dp.attributes()),
});
}
}
}
}
};
}
fn attrs<'a>(it: impl Iterator<Item = &'a opentelemetry::KeyValue>) -> Vec<(String, String)> {
it.map(|kv| (kv.key.as_str().to_string(), value_to_string(&kv.value)))
.collect()
}
collect_points_impl!(collect_points_f64, f64, |v| v);
collect_points_impl!(collect_points_u64, u64, |v| v as f64);
collect_points_impl!(collect_points_i64, i64, |v| v as f64);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capture_spans_records_tree_and_attributes() {
let spans = capture_spans(|| {
let parent = tracing::info_span!("parent.span");
let _g = parent.enter();
let child = tracing::info_span!("child.span", cqlite.result = "hit");
child.in_scope(|| {});
});
assert!(spans.contains("parent.span"));
assert!(spans.contains("child.span"));
assert!(
spans.is_parent_of("parent.span", "child.span"),
"child.span must nest under parent.span; saw {:?}",
spans.iter().map(|s| s.name.clone()).collect::<Vec<_>>()
);
let child = spans.find("child.span").expect("child present");
assert_eq!(child.attribute("cqlite.result").as_deref(), Some("hit"));
}
#[test]
fn captured_spans_from_raw_roundtrips_empty() {
let empty = CapturedSpans::from_raw(Vec::new());
assert!(empty.all().is_empty());
assert!(!empty.contains("anything"));
}
}