a2a_protocol_server/otel/
mod.rs1mod builder;
45mod pipeline;
46
47use std::sync::atomic::{AtomicU64, Ordering};
48use std::time::Duration;
49
50use opentelemetry::KeyValue;
51use opentelemetry::metrics::{Counter, Gauge, Histogram, Meter};
52
53use crate::metrics::{ConnectionPoolStats, Metrics, RpcCall};
54
55pub use builder::OtelMetricsBuilder;
56pub use pipeline::{init_otlp_pipeline, init_otlp_pipeline_with_endpoint};
57
58pub struct OtelMetrics {
85 rpc_call_histogram: Histogram<f64>,
86 request_counter: Counter<u64>,
87 response_counter: Counter<u64>,
88 error_counter: Counter<u64>,
89 latency_histogram: Histogram<f64>,
90 queue_depth_gauge: Gauge<u64>,
91 pool_active_gauge: Gauge<u64>,
92 pool_idle_gauge: Gauge<u64>,
93 pool_created_counter: Counter<u64>,
94 pool_closed_counter: Counter<u64>,
95 pool_created_seen: AtomicU64,
98 pool_closed_seen: AtomicU64,
99 persistence_error_counter: Counter<u64>,
100 push_delivery_counter: Counter<u64>,
101}
102
103impl std::fmt::Debug for OtelMetrics {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct("OtelMetrics").finish_non_exhaustive()
106 }
107}
108
109impl OtelMetrics {
110 #[must_use]
114 pub fn from_meter(meter: &Meter) -> Self {
115 let request_counter = meter
116 .u64_counter("a2a.server.requests")
117 .with_description("Total number of inbound A2A requests")
118 .with_unit("request")
119 .build();
120
121 let response_counter = meter
122 .u64_counter("a2a.server.responses")
123 .with_description("Total number of outbound A2A responses")
124 .with_unit("response")
125 .build();
126
127 let error_counter = meter
128 .u64_counter("a2a.server.errors")
129 .with_description("Total number of A2A request errors")
130 .with_unit("error")
131 .build();
132
133 let (rpc_call_histogram, latency_histogram) = Self::duration_instruments(meter);
134
135 let queue_depth_gauge = meter
136 .u64_gauge("a2a.server.queue_depth")
137 .with_description("Number of active event queues")
138 .with_unit("queue")
139 .build();
140
141 let pool_active_gauge = meter
142 .u64_gauge("a2a.server.pool.active")
143 .with_description("Number of active (in-use) HTTP connections")
144 .with_unit("connection")
145 .build();
146
147 let pool_idle_gauge = meter
148 .u64_gauge("a2a.server.pool.idle")
149 .with_description("Number of idle HTTP connections")
150 .with_unit("connection")
151 .build();
152
153 let pool_created_counter = meter
154 .u64_counter("a2a.server.pool.created")
155 .with_description("Total HTTP connections created since process start")
156 .with_unit("connection")
157 .build();
158
159 let pool_closed_counter = meter
160 .u64_counter("a2a.server.pool.closed")
161 .with_description("HTTP connections closed due to errors or timeouts")
162 .with_unit("connection")
163 .build();
164
165 let (persistence_error_counter, push_delivery_counter) = Self::failure_instruments(meter);
166
167 Self {
168 rpc_call_histogram,
169 request_counter,
170 response_counter,
171 error_counter,
172 latency_histogram,
173 queue_depth_gauge,
174 pool_active_gauge,
175 pool_idle_gauge,
176 pool_created_counter,
177 pool_closed_counter,
178 pool_created_seen: AtomicU64::new(0),
179 pool_closed_seen: AtomicU64::new(0),
180 persistence_error_counter,
181 push_delivery_counter,
182 }
183 }
184
185 fn duration_instruments(meter: &Meter) -> (Histogram<f64>, Histogram<f64>) {
187 let rpc_call_histogram = meter
188 .f64_histogram("rpc.server.call.duration")
189 .with_description("Measures the duration of an incoming Remote Procedure Call (RPC).")
190 .with_unit("s")
191 .with_boundaries(RPC_DURATION_BUCKETS.to_vec())
192 .build();
193
194 let latency_histogram = meter
198 .f64_histogram("a2a.server.latency")
199 .with_description("A2A request latency")
200 .with_unit("s")
201 .with_boundaries(RPC_DURATION_BUCKETS.to_vec())
202 .build();
203
204 (rpc_call_histogram, latency_histogram)
205 }
206
207 fn failure_instruments(meter: &Meter) -> (Counter<u64>, Counter<u64>) {
215 let persistence_error_counter = meter
216 .u64_counter("a2a.server.persistence_errors")
217 .with_description(
218 "Task writes the background processor could not persist. \
219 Non-zero means data loss: the streaming client already \
220 received the event.",
221 )
222 .with_unit("error")
223 .build();
224
225 let push_delivery_counter = meter
226 .u64_counter("a2a.server.push_deliveries")
227 .with_description(
228 "Push notification delivery attempts, by outcome \
229 (delivered / failed / timeout)",
230 )
231 .with_unit("delivery")
232 .build();
233
234 (persistence_error_counter, push_delivery_counter)
235 }
236}
237
238impl Metrics for OtelMetrics {
239 fn on_request(&self, method: &str) {
240 self.request_counter
241 .add(1, &[KeyValue::new("method", method.to_owned())]);
242 }
243
244 fn on_response(&self, method: &str) {
245 self.response_counter
246 .add(1, &[KeyValue::new("method", method.to_owned())]);
247 }
248
249 fn on_error(&self, method: &str, error: &str) {
250 self.error_counter.add(
251 1,
252 &[
253 KeyValue::new("method", method.to_owned()),
254 KeyValue::new("error", error.to_owned()),
255 ],
256 );
257 }
258
259 fn on_latency(&self, method: &str, duration: Duration) {
260 self.latency_histogram.record(
261 duration.as_secs_f64(),
262 &[KeyValue::new("method", method.to_owned())],
263 );
264 }
265
266 fn on_queue_depth_change(&self, active_queues: usize) {
267 #[allow(clippy::cast_possible_truncation)]
268 self.queue_depth_gauge.record(active_queues as u64, &[]);
269 }
270
271 fn on_persistence_error(&self, operation: &str, error_kind: &str) {
272 self.persistence_error_counter.add(
273 1,
274 &[
275 KeyValue::new("operation", operation.to_owned()),
276 KeyValue::new("error", error_kind.to_owned()),
277 ],
278 );
279 }
280
281 fn on_push_delivery(&self, outcome: &str) {
282 self.push_delivery_counter
283 .add(1, &[KeyValue::new("outcome", outcome.to_owned())]);
284 }
285
286 fn on_connection_pool_stats(&self, stats: &ConnectionPoolStats) {
287 self.pool_active_gauge
288 .record(u64::from(stats.active_connections), &[]);
289 self.pool_idle_gauge
290 .record(u64::from(stats.idle_connections), &[]);
291 let created = self
297 .pool_created_seen
298 .fetch_max(stats.total_connections_created, Ordering::Relaxed);
299 self.pool_created_counter
300 .add(stats.total_connections_created.saturating_sub(created), &[]);
301 let closed = self
302 .pool_closed_seen
303 .fetch_max(stats.connections_closed, Ordering::Relaxed);
304 self.pool_closed_counter
305 .add(stats.connections_closed.saturating_sub(closed), &[]);
306 }
307
308 fn on_rpc_call(&self, call: &RpcCall<'_>) {
309 let mut attributes = Vec::with_capacity(4);
310 attributes.push(KeyValue::new("rpc.system.name", call.system.to_owned()));
311 if let Some(method) = call.method {
312 attributes.push(KeyValue::new("rpc.method", method.to_owned()));
313 }
314 if let Some(status) = call.status_code {
315 attributes.push(KeyValue::new("rpc.status_code", status.to_owned()));
316 }
317 if let Some(error) = call.error_type {
318 attributes.push(KeyValue::new("error.type", error.to_owned()));
319 }
320 self.rpc_call_histogram
321 .record(call.duration.as_secs_f64(), &attributes);
322 }
323}
324
325pub const RPC_DURATION_BUCKETS: [f64; 14] = [
329 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,
330];
331
332#[cfg(test)]
335mod rpc_tests;
336#[cfg(test)]
337mod tests;