mod builder;
mod pipeline;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use opentelemetry::KeyValue;
use opentelemetry::metrics::{Counter, Gauge, Histogram, Meter};
use crate::metrics::{ConnectionPoolStats, Metrics, RpcCall};
pub use builder::OtelMetricsBuilder;
pub use pipeline::{init_otlp_pipeline, init_otlp_pipeline_with_endpoint};
pub struct OtelMetrics {
rpc_call_histogram: Histogram<f64>,
request_counter: Counter<u64>,
response_counter: Counter<u64>,
error_counter: Counter<u64>,
latency_histogram: Histogram<f64>,
queue_depth_gauge: Gauge<u64>,
pool_active_gauge: Gauge<u64>,
pool_idle_gauge: Gauge<u64>,
pool_created_counter: Counter<u64>,
pool_closed_counter: Counter<u64>,
pool_created_seen: AtomicU64,
pool_closed_seen: AtomicU64,
persistence_error_counter: Counter<u64>,
push_delivery_counter: Counter<u64>,
}
impl std::fmt::Debug for OtelMetrics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OtelMetrics").finish_non_exhaustive()
}
}
impl OtelMetrics {
#[must_use]
pub fn from_meter(meter: &Meter) -> Self {
let request_counter = meter
.u64_counter("a2a.server.requests")
.with_description("Total number of inbound A2A requests")
.with_unit("request")
.build();
let response_counter = meter
.u64_counter("a2a.server.responses")
.with_description("Total number of outbound A2A responses")
.with_unit("response")
.build();
let error_counter = meter
.u64_counter("a2a.server.errors")
.with_description("Total number of A2A request errors")
.with_unit("error")
.build();
let (rpc_call_histogram, latency_histogram) = Self::duration_instruments(meter);
let queue_depth_gauge = meter
.u64_gauge("a2a.server.queue_depth")
.with_description("Number of active event queues")
.with_unit("queue")
.build();
let pool_active_gauge = meter
.u64_gauge("a2a.server.pool.active")
.with_description("Number of active (in-use) HTTP connections")
.with_unit("connection")
.build();
let pool_idle_gauge = meter
.u64_gauge("a2a.server.pool.idle")
.with_description("Number of idle HTTP connections")
.with_unit("connection")
.build();
let pool_created_counter = meter
.u64_counter("a2a.server.pool.created")
.with_description("Total HTTP connections created since process start")
.with_unit("connection")
.build();
let pool_closed_counter = meter
.u64_counter("a2a.server.pool.closed")
.with_description("HTTP connections closed due to errors or timeouts")
.with_unit("connection")
.build();
let (persistence_error_counter, push_delivery_counter) = Self::failure_instruments(meter);
Self {
rpc_call_histogram,
request_counter,
response_counter,
error_counter,
latency_histogram,
queue_depth_gauge,
pool_active_gauge,
pool_idle_gauge,
pool_created_counter,
pool_closed_counter,
pool_created_seen: AtomicU64::new(0),
pool_closed_seen: AtomicU64::new(0),
persistence_error_counter,
push_delivery_counter,
}
}
fn duration_instruments(meter: &Meter) -> (Histogram<f64>, Histogram<f64>) {
let rpc_call_histogram = meter
.f64_histogram("rpc.server.call.duration")
.with_description("Measures the duration of an incoming Remote Procedure Call (RPC).")
.with_unit("s")
.with_boundaries(RPC_DURATION_BUCKETS.to_vec())
.build();
let latency_histogram = meter
.f64_histogram("a2a.server.latency")
.with_description("A2A request latency")
.with_unit("s")
.with_boundaries(RPC_DURATION_BUCKETS.to_vec())
.build();
(rpc_call_histogram, latency_histogram)
}
fn failure_instruments(meter: &Meter) -> (Counter<u64>, Counter<u64>) {
let persistence_error_counter = meter
.u64_counter("a2a.server.persistence_errors")
.with_description(
"Task writes the background processor could not persist. \
Non-zero means data loss: the streaming client already \
received the event.",
)
.with_unit("error")
.build();
let push_delivery_counter = meter
.u64_counter("a2a.server.push_deliveries")
.with_description(
"Push notification delivery attempts, by outcome \
(delivered / failed / timeout)",
)
.with_unit("delivery")
.build();
(persistence_error_counter, push_delivery_counter)
}
}
impl Metrics for OtelMetrics {
fn on_request(&self, method: &str) {
self.request_counter
.add(1, &[KeyValue::new("method", method.to_owned())]);
}
fn on_response(&self, method: &str) {
self.response_counter
.add(1, &[KeyValue::new("method", method.to_owned())]);
}
fn on_error(&self, method: &str, error: &str) {
self.error_counter.add(
1,
&[
KeyValue::new("method", method.to_owned()),
KeyValue::new("error", error.to_owned()),
],
);
}
fn on_latency(&self, method: &str, duration: Duration) {
self.latency_histogram.record(
duration.as_secs_f64(),
&[KeyValue::new("method", method.to_owned())],
);
}
fn on_queue_depth_change(&self, active_queues: usize) {
#[allow(clippy::cast_possible_truncation)]
self.queue_depth_gauge.record(active_queues as u64, &[]);
}
fn on_persistence_error(&self, operation: &str, error_kind: &str) {
self.persistence_error_counter.add(
1,
&[
KeyValue::new("operation", operation.to_owned()),
KeyValue::new("error", error_kind.to_owned()),
],
);
}
fn on_push_delivery(&self, outcome: &str) {
self.push_delivery_counter
.add(1, &[KeyValue::new("outcome", outcome.to_owned())]);
}
fn on_connection_pool_stats(&self, stats: &ConnectionPoolStats) {
self.pool_active_gauge
.record(u64::from(stats.active_connections), &[]);
self.pool_idle_gauge
.record(u64::from(stats.idle_connections), &[]);
let created = self
.pool_created_seen
.fetch_max(stats.total_connections_created, Ordering::Relaxed);
self.pool_created_counter
.add(stats.total_connections_created.saturating_sub(created), &[]);
let closed = self
.pool_closed_seen
.fetch_max(stats.connections_closed, Ordering::Relaxed);
self.pool_closed_counter
.add(stats.connections_closed.saturating_sub(closed), &[]);
}
fn on_rpc_call(&self, call: &RpcCall<'_>) {
let mut attributes = Vec::with_capacity(4);
attributes.push(KeyValue::new("rpc.system.name", call.system.to_owned()));
if let Some(method) = call.method {
attributes.push(KeyValue::new("rpc.method", method.to_owned()));
}
if let Some(status) = call.status_code {
attributes.push(KeyValue::new("rpc.status_code", status.to_owned()));
}
if let Some(error) = call.error_type {
attributes.push(KeyValue::new("error.type", error.to_owned()));
}
self.rpc_call_histogram
.record(call.duration.as_secs_f64(), &attributes);
}
}
pub const RPC_DURATION_BUCKETS: [f64; 14] = [
0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0,
];
#[cfg(test)]
mod rpc_tests;
#[cfg(test)]
mod tests;