1use std::{
2 collections::HashMap,
3 future::Future,
4 pin::Pin,
5 sync::{Arc, Mutex, OnceLock},
6 task::{ready, Context, Poll},
7};
8
9use http::{uri::Authority, Request, Response, StatusCode, Uri, Version};
10use http_body::Body;
11use metrics::Counter;
12use pin_project_lite::pin_project;
13use saluki_metrics::MetricsBuilder;
14use stringtheory::MetaString;
15use tower::{Layer, Service};
16
17pub type EndpointNameFn = dyn Fn(&Uri) -> Option<MetaString> + Send + Sync;
18
19const ERROR_TYPE_CLIENT: &str = "client_error";
20pub(super) const ERROR_TYPE_CONNECTION: &str = "connection_error";
21pub(super) const ERROR_TYPE_DNS: &str = "dns_error";
22pub(super) const ERROR_TYPE_TLS: &str = "tls_error";
23pub(super) const ERROR_TYPE_WROTE_REQUEST: &str = "wrote_request_error";
24const ERROR_TYPE_CANT_SEND: &str = "cant_send";
25const ERROR_TYPE_GT_400: &str = "gt_400";
26const ERROR_SCOPE_PHASE: &str = "phase";
27const ERROR_SCOPE_TRANSACTION: &str = "transaction";
28
29#[derive(Clone)]
31pub(crate) struct HttpTransactionErrorTelemetry {
32 dns_errors: Counter,
33 connection_errors: Counter,
34 tls_errors: Counter,
35 wrote_request_errors: Counter,
36 send_errors: Counter,
37 http_errors: Counter,
38}
39
40impl HttpTransactionErrorTelemetry {
41 pub(crate) fn from_builder(builder: &MetricsBuilder) -> Self {
43 Self {
46 dns_errors: register_scoped_error(builder, ERROR_TYPE_DNS, ERROR_SCOPE_PHASE),
47 connection_errors: register_scoped_error(builder, ERROR_TYPE_CONNECTION, ERROR_SCOPE_PHASE),
48 tls_errors: register_scoped_error(builder, ERROR_TYPE_TLS, ERROR_SCOPE_PHASE),
49 wrote_request_errors: register_scoped_error(builder, ERROR_TYPE_WROTE_REQUEST, ERROR_SCOPE_PHASE),
50 send_errors: register_scoped_error(builder, ERROR_TYPE_CANT_SEND, ERROR_SCOPE_TRANSACTION),
51 http_errors: register_scoped_error(builder, ERROR_TYPE_GT_400, ERROR_SCOPE_TRANSACTION),
52 }
53 }
54
55 pub(crate) fn dns_errors(&self) -> Counter {
56 self.dns_errors.clone()
57 }
58
59 pub(crate) fn increment_connection_error(&self) {
60 self.connection_errors.increment(1);
61 }
62
63 pub(crate) fn increment_tls_error(&self) {
64 self.tls_errors.increment(1);
65 }
66
67 pub(crate) fn increment_wrote_request_error(&self) {
68 self.wrote_request_errors.increment(1);
69 }
70
71 fn increment_send_error(&self) {
72 self.send_errors.increment(1);
73 }
74
75 fn increment_http_error(&self) {
76 self.http_errors.increment(1);
77 }
78}
79
80fn register_scoped_error(builder: &MetricsBuilder, error_type: &'static str, error_scope: &'static str) -> Counter {
81 builder.register_counter_with_tags(
82 "network_http_requests_errors_total",
83 [("error_type", error_type), ("error_scope", error_scope)],
84 )
85}
86
87#[derive(Clone, Default)]
128pub struct EndpointTelemetryLayer {
129 builder: MetricsBuilder,
130 endpoint_name_fn: Option<Arc<EndpointNameFn>>,
131 error_telemetry: Option<HttpTransactionErrorTelemetry>,
132}
133
134impl EndpointTelemetryLayer {
135 pub fn with_metrics_builder(mut self, builder: MetricsBuilder) -> Self {
140 self.builder = builder;
141 self
142 }
143
144 pub(super) fn with_error_telemetry(mut self, error_telemetry: HttpTransactionErrorTelemetry) -> Self {
145 self.error_telemetry = Some(error_telemetry);
146 self
147 }
148
149 pub fn with_endpoint_name_fn<F>(mut self, endpoint_name_fn: F) -> Self
159 where
160 F: Fn(&Uri) -> Option<MetaString> + Send + Sync + 'static,
161 {
162 self.endpoint_name_fn = Some(Arc::new(endpoint_name_fn));
163 self
164 }
165}
166
167impl<S> Layer<S> for EndpointTelemetryLayer {
168 type Service = EndpointTelemetry<S>;
169
170 fn layer(&self, service: S) -> Self::Service {
171 EndpointTelemetry {
172 service,
173 builder: self.builder.clone(),
174 endpoint_name_fn: self.endpoint_name_fn.clone(),
175 error_telemetry: self.error_telemetry.clone(),
176 domains: HashMap::new(),
177 endpoint_name_cache: HashMap::new(),
178 }
179 }
180}
181
182#[derive(Default)]
183struct SuccessCounters {
184 http_09: OnceLock<Counter>,
185 http_10: OnceLock<Counter>,
186 http_11: OnceLock<Counter>,
187 http_2: OnceLock<Counter>,
188 http_3: OnceLock<Counter>,
189 fallback: OnceLock<Counter>,
190}
191
192impl SuccessCounters {
193 fn slot_for_version(&self, version: Version) -> (&OnceLock<Counter>, &'static str) {
194 match version {
195 Version::HTTP_09 => (&self.http_09, "HTTP/0.9"),
196 Version::HTTP_10 => (&self.http_10, "HTTP/1.0"),
197 Version::HTTP_11 => (&self.http_11, "HTTP/1.1"),
198 Version::HTTP_2 => (&self.http_2, "HTTP/2.0"),
199 Version::HTTP_3 => (&self.http_3, "HTTP/3.0"),
200 _ => (&self.fallback, "unknown"),
201 }
202 }
203}
204
205struct PerEndpointTelemetry {
206 builder: MetricsBuilder,
207 dropped: Counter,
208 success: SuccessCounters,
209 success_bytes: Counter,
210 http_errors_map: Mutex<HashMap<StatusCode, Counter>>,
211}
212
213impl PerEndpointTelemetry {
214 fn new(builder: MetricsBuilder, uri: &Uri, endpoint_name: &str) -> Self {
215 let mut domain = format!("{}://{}", uri.scheme_str().unwrap(), uri.host().unwrap());
217 if let Some(port) = uri.port() {
218 domain.push(':');
219 domain.push_str(port.as_str());
220 }
221
222 let builder = builder
223 .add_default_tag(("domain", domain))
224 .add_default_tag(("endpoint", endpoint_name.to_string()));
225
226 let dropped = builder.register_counter("network_http_requests_failed_total");
227 let success = SuccessCounters::default();
228 let success_bytes = builder.register_counter("network_http_requests_success_sent_bytes_total");
229 let http_errors_map = Mutex::new(HashMap::new());
230
231 Self {
232 builder,
233 dropped,
234 success,
235 success_bytes,
236 http_errors_map,
237 }
238 }
239
240 fn increment_dropped(&self) {
241 self.dropped.increment(1);
242 }
243
244 fn increment_success(&self, version: Version) {
245 let (slot, proto_version) = self.success.slot_for_version(version);
246 slot.get_or_init(|| {
247 self.builder.register_counter_with_tags(
248 "network_http_requests_success_total",
249 [("proto_version", proto_version)],
250 )
251 })
252 .increment(1);
253 }
254
255 fn increment_success_bytes(&self, len: u64) {
256 self.success_bytes.increment(len);
257 }
258
259 fn increment_http_error(&self, status: StatusCode) {
260 let mut http_errors_map = self.http_errors_map.lock().unwrap();
261 let counter = http_errors_map.entry(status).or_insert_with(move || {
262 self.builder.register_counter_with_tags(
263 "network_http_requests_errors_total",
264 [
265 ("error_type", ERROR_TYPE_CLIENT.to_string()),
266 ("code", status.as_str().to_string()),
267 ],
268 )
269 });
270 counter.increment(1);
271 }
272}
273
274#[derive(Clone)]
276pub struct EndpointTelemetry<S> {
277 service: S,
278 builder: MetricsBuilder,
279 endpoint_name_fn: Option<Arc<EndpointNameFn>>,
280 error_telemetry: Option<HttpTransactionErrorTelemetry>,
281 domains: HashMap<Authority, HashMap<MetaString, Arc<PerEndpointTelemetry>>>,
282 endpoint_name_cache: HashMap<Uri, MetaString>,
283}
284
285impl<S> EndpointTelemetry<S> {
286 fn get_telemetry_handle<B>(&mut self, req: &Request<B>) -> Option<Arc<PerEndpointTelemetry>>
287 where
288 B: Body,
289 {
290 if req.uri().scheme().is_none() || req.uri().host().is_none() {
292 return None;
293 }
294
295 let authority = req.uri().authority()?.clone();
297 let domain = self.domains.entry(authority).or_default();
298
299 let endpoint_telemetry = match self.endpoint_name_cache.get(req.uri()) {
304 Some(endpoint_name) => domain
305 .get(endpoint_name)
306 .expect("per-endpoint telemetry must exist if name is cached"),
307 None => {
308 let endpoint_name = self
310 .endpoint_name_fn
311 .as_ref()
312 .and_then(|f| f(req.uri()))
313 .map(sanitize_endpoint_name)
314 .unwrap_or_else(|| sanitize_endpoint_name(req.uri().path().into()));
315
316 self.endpoint_name_cache
317 .insert(req.uri().clone(), endpoint_name.clone());
318
319 domain.entry(endpoint_name).or_insert_with_key(|endpoint_name| {
321 Arc::new(PerEndpointTelemetry::new(
322 self.builder.clone(),
323 req.uri(),
324 endpoint_name,
325 ))
326 })
327 }
328 };
329
330 Some(Arc::clone(endpoint_telemetry))
331 }
332}
333
334impl<B, B2, S> Service<Request<B>> for EndpointTelemetry<S>
335where
336 S: Service<Request<B>, Response = http::Response<B2>>,
337 B: Body,
338 B2: Body,
339{
340 type Response = S::Response;
341 type Error = S::Error;
342 type Future = EndpointTelemetryFuture<S::Future>;
343
344 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
345 self.service.poll_ready(cx)
346 }
347
348 fn call(&mut self, req: Request<B>) -> Self::Future {
349 let maybe_body_len = req.body().size_hint().exact();
350 let per_endpoint = self.get_telemetry_handle(&req);
351 let fut = self.service.call(req);
352
353 EndpointTelemetryFuture {
354 per_endpoint,
355 error_telemetry: self.error_telemetry.clone(),
356 maybe_body_len,
357 fut,
358 }
359 }
360}
361
362pin_project! {
363 pub struct EndpointTelemetryFuture<F> {
365 per_endpoint: Option<Arc<PerEndpointTelemetry>>,
366 error_telemetry: Option<HttpTransactionErrorTelemetry>,
367 maybe_body_len: Option<u64>,
368
369 #[pin]
370 fut: F,
371 }
372}
373
374impl<F, B, E> Future for EndpointTelemetryFuture<F>
375where
376 F: Future<Output = Result<Response<B>, E>>,
377 B: Body,
378{
379 type Output = Result<Response<B>, E>;
380
381 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
382 let this = self.project();
383 match ready!(this.fut.poll(cx)) {
384 Ok(response) => {
385 if let Some(per_endpoint) = this.per_endpoint.as_ref() {
386 let status = response.status();
387 if status.is_client_error() || status.is_server_error() {
388 per_endpoint.increment_http_error(status);
390
391 let status_code = status.as_u16();
392 if status_code == 400 || status_code == 403 || status_code == 413 {
393 per_endpoint.increment_dropped()
396 } else if let Some(error_telemetry) = this.error_telemetry.as_ref() {
397 error_telemetry.increment_http_error();
398 }
399 } else {
400 per_endpoint.increment_success(response.version());
401 if let Some(body_len) = this.maybe_body_len {
402 per_endpoint.increment_success_bytes(*body_len);
403 }
404 }
405 }
406
407 Poll::Ready(Ok(response))
408 }
409 Err(e) => {
410 if let Some(error_telemetry) = this.error_telemetry.as_ref() {
411 error_telemetry.increment_send_error();
412 }
413
414 Poll::Ready(Err(e))
415 }
416 }
417 }
418}
419
420fn is_sanitized_endpoint_name(s: &str) -> bool {
421 s.chars().all(|c| {
422 c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-' || c == '/' || c == '\\' || c == '.'
423 })
424}
425
426fn sanitize_endpoint_name(endpoint_name: MetaString) -> MetaString {
427 if is_sanitized_endpoint_name(&endpoint_name) {
429 return endpoint_name;
430 }
431
432 endpoint_name
433 .chars()
434 .map(|c| {
435 if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '/' || c == '\\' || c == '.' {
436 c.to_ascii_lowercase()
437 } else {
438 '_'
439 }
440 })
441 .collect::<String>()
442 .into()
443}
444
445#[cfg(test)]
446mod tests {
447 use std::convert::Infallible;
448
449 use bytes::Bytes;
450 use http_body_util::{Empty, Full};
451 use metrics::{Key, Label};
452 use metrics_util::{
453 debugging::{DebugValue, DebuggingRecorder},
454 CompositeKey, MetricKind,
455 };
456 use proptest::prelude::*;
457
458 use super::*;
459
460 #[test]
461 fn success_counter_slots_cover_supported_http_versions() {
462 let counters = SuccessCounters::default();
463 let cases = [
464 (Version::HTTP_09, &counters.http_09, "HTTP/0.9"),
465 (Version::HTTP_10, &counters.http_10, "HTTP/1.0"),
466 (Version::HTTP_11, &counters.http_11, "HTTP/1.1"),
467 (Version::HTTP_2, &counters.http_2, "HTTP/2.0"),
468 (Version::HTTP_3, &counters.http_3, "HTTP/3.0"),
469 ];
470
471 for (version, expected_slot, expected_label) in cases {
472 let (slot, label) = counters.slot_for_version(version);
473 assert!(std::ptr::eq(slot, expected_slot));
474 assert_eq!(label, expected_label);
475 }
476 }
477
478 #[test]
479 fn successful_requests_are_counted_by_response_protocol_without_splitting_bytes() {
480 let recorder = DebuggingRecorder::new();
481 let snapshotter = recorder.snapshotter();
482 let mut response_versions = [http::Version::HTTP_11, http::Version::HTTP_2].into_iter();
483 let inner = tower::service_fn(move |_request: Request<Full<Bytes>>| {
484 let version = response_versions.next().expect("response version should be available");
485 async move {
486 Ok::<_, Infallible>(
487 Response::builder()
488 .version(version)
489 .body(Empty::<Bytes>::new())
490 .expect("response should be valid"),
491 )
492 }
493 });
494 let mut service = EndpointTelemetryLayer::default().layer(inner);
495
496 let first_request = Request::builder()
497 .uri("https://example.com/api/v1/series")
498 .version(http::Version::HTTP_10)
499 .body(Full::new(Bytes::from_static(b"hello")))
500 .expect("request should be valid");
501 metrics::with_local_recorder(&recorder, || {
502 tokio_test::block_on(service.call(first_request)).expect("request should succeed")
503 });
504
505 let second_request = Request::builder()
506 .uri("https://example.com/api/v1/series")
507 .version(http::Version::HTTP_10)
508 .body(Full::new(Bytes::from_static(b"goodbye")))
509 .expect("request should be valid");
510 metrics::with_local_recorder(&recorder, || {
511 tokio_test::block_on(service.call(second_request)).expect("request should succeed")
512 });
513
514 let snapshot = snapshotter.snapshot().into_hashmap();
515 let success_keys = snapshot
516 .keys()
517 .filter(|key| key.key().name() == "network_http_requests_success_total")
518 .count();
519 assert_eq!(success_keys, 2);
520 for proto_version in ["HTTP/1.1", "HTTP/2.0"] {
521 let key = CompositeKey::new(
522 MetricKind::Counter,
523 Key::from_parts(
524 "network_http_requests_success_total",
525 vec![
526 Label::new("domain", "https://example.com"),
527 Label::new("endpoint", "/api/v1/series"),
528 Label::new("proto_version", proto_version),
529 ],
530 ),
531 );
532 let (_, _, value) = snapshot
533 .get(&key)
534 .expect("protocol-specific success counter should exist");
535 assert_eq!(value, &DebugValue::Counter(1));
536 }
537
538 let success_bytes_keys = snapshot
539 .keys()
540 .filter(|key| key.key().name() == "network_http_requests_success_sent_bytes_total")
541 .count();
542 assert_eq!(success_bytes_keys, 1);
543 let success_bytes_key = CompositeKey::new(
544 MetricKind::Counter,
545 Key::from_parts(
546 "network_http_requests_success_sent_bytes_total",
547 vec![
548 Label::new("domain", "https://example.com"),
549 Label::new("endpoint", "/api/v1/series"),
550 ],
551 ),
552 );
553 let (_, _, value) = snapshot
554 .get(&success_bytes_key)
555 .expect("protocol-agnostic success bytes counter should exist");
556 assert_eq!(value, &DebugValue::Counter(12));
557 }
558
559 proptest! {
560 #[test]
561 fn property_test_sanitize_endpoint_name(input in ".*") {
562 let sanitized = sanitize_endpoint_name(input.into());
563 prop_assert!(is_sanitized_endpoint_name(&sanitized));
564 }
565 }
566}