a2a_protocol_server/otel/
mod.rs1mod builder;
45mod pipeline;
46
47use std::time::Duration;
48
49use opentelemetry::metrics::{Counter, Gauge, Histogram, Meter};
50use opentelemetry::KeyValue;
51
52use crate::metrics::{ConnectionPoolStats, Metrics};
53
54pub use builder::OtelMetricsBuilder;
55pub use pipeline::init_otlp_pipeline;
56
57pub struct OtelMetrics {
78 request_counter: Counter<u64>,
79 response_counter: Counter<u64>,
80 error_counter: Counter<u64>,
81 latency_histogram: Histogram<f64>,
82 queue_depth_gauge: Gauge<u64>,
83 pool_active_gauge: Gauge<u64>,
84 pool_idle_gauge: Gauge<u64>,
85 pool_created_counter: Counter<u64>,
86 pool_closed_counter: Counter<u64>,
87 persistence_error_counter: Counter<u64>,
88 push_delivery_counter: Counter<u64>,
89}
90
91impl std::fmt::Debug for OtelMetrics {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct("OtelMetrics").finish_non_exhaustive()
94 }
95}
96
97impl OtelMetrics {
98 #[must_use]
102 pub fn from_meter(meter: &Meter) -> Self {
103 let request_counter = meter
104 .u64_counter("a2a.server.requests")
105 .with_description("Total number of inbound A2A requests")
106 .with_unit("request")
107 .build();
108
109 let response_counter = meter
110 .u64_counter("a2a.server.responses")
111 .with_description("Total number of outbound A2A responses")
112 .with_unit("response")
113 .build();
114
115 let error_counter = meter
116 .u64_counter("a2a.server.errors")
117 .with_description("Total number of A2A request errors")
118 .with_unit("error")
119 .build();
120
121 let latency_histogram = meter
122 .f64_histogram("a2a.server.latency")
123 .with_description("A2A request latency")
124 .with_unit("s")
125 .build();
126
127 let queue_depth_gauge = meter
128 .u64_gauge("a2a.server.queue_depth")
129 .with_description("Number of active event queues")
130 .with_unit("queue")
131 .build();
132
133 let pool_active_gauge = meter
134 .u64_gauge("a2a.server.pool.active")
135 .with_description("Number of active (in-use) HTTP connections")
136 .with_unit("connection")
137 .build();
138
139 let pool_idle_gauge = meter
140 .u64_gauge("a2a.server.pool.idle")
141 .with_description("Number of idle HTTP connections")
142 .with_unit("connection")
143 .build();
144
145 let pool_created_counter = meter
146 .u64_counter("a2a.server.pool.created")
147 .with_description("Total HTTP connections created since process start")
148 .with_unit("connection")
149 .build();
150
151 let pool_closed_counter = meter
152 .u64_counter("a2a.server.pool.closed")
153 .with_description("HTTP connections closed due to errors or timeouts")
154 .with_unit("connection")
155 .build();
156
157 let (persistence_error_counter, push_delivery_counter) = Self::failure_instruments(meter);
158
159 Self {
160 request_counter,
161 response_counter,
162 error_counter,
163 latency_histogram,
164 queue_depth_gauge,
165 pool_active_gauge,
166 pool_idle_gauge,
167 pool_created_counter,
168 pool_closed_counter,
169 persistence_error_counter,
170 push_delivery_counter,
171 }
172 }
173
174 fn failure_instruments(meter: &Meter) -> (Counter<u64>, Counter<u64>) {
182 let persistence_error_counter = meter
183 .u64_counter("a2a.server.persistence_errors")
184 .with_description(
185 "Task writes the background processor could not persist. \
186 Non-zero means data loss: the streaming client already \
187 received the event.",
188 )
189 .with_unit("error")
190 .build();
191
192 let push_delivery_counter = meter
193 .u64_counter("a2a.server.push_deliveries")
194 .with_description(
195 "Push notification delivery attempts, by outcome \
196 (delivered / failed / timeout)",
197 )
198 .with_unit("delivery")
199 .build();
200
201 (persistence_error_counter, push_delivery_counter)
202 }
203}
204
205impl Metrics for OtelMetrics {
206 fn on_request(&self, method: &str) {
207 self.request_counter
208 .add(1, &[KeyValue::new("method", method.to_owned())]);
209 }
210
211 fn on_response(&self, method: &str) {
212 self.response_counter
213 .add(1, &[KeyValue::new("method", method.to_owned())]);
214 }
215
216 fn on_error(&self, method: &str, error: &str) {
217 self.error_counter.add(
218 1,
219 &[
220 KeyValue::new("method", method.to_owned()),
221 KeyValue::new("error", error.to_owned()),
222 ],
223 );
224 }
225
226 fn on_latency(&self, method: &str, duration: Duration) {
227 self.latency_histogram.record(
228 duration.as_secs_f64(),
229 &[KeyValue::new("method", method.to_owned())],
230 );
231 }
232
233 fn on_queue_depth_change(&self, active_queues: usize) {
234 #[allow(clippy::cast_possible_truncation)]
235 self.queue_depth_gauge.record(active_queues as u64, &[]);
236 }
237
238 fn on_persistence_error(&self, operation: &str, error_kind: &str) {
239 self.persistence_error_counter.add(
240 1,
241 &[
242 KeyValue::new("operation", operation.to_owned()),
243 KeyValue::new("error", error_kind.to_owned()),
244 ],
245 );
246 }
247
248 fn on_push_delivery(&self, outcome: &str) {
249 self.push_delivery_counter
250 .add(1, &[KeyValue::new("outcome", outcome.to_owned())]);
251 }
252
253 fn on_connection_pool_stats(&self, stats: &ConnectionPoolStats) {
254 self.pool_active_gauge
255 .record(u64::from(stats.active_connections), &[]);
256 self.pool_idle_gauge
257 .record(u64::from(stats.idle_connections), &[]);
258 self.pool_created_counter
259 .add(stats.total_connections_created, &[]);
260 self.pool_closed_counter.add(stats.connections_closed, &[]);
261 }
262}
263
264#[cfg(test)]
267mod tests;