use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use axum::{ Router, body::{Body, to_bytes}, http::{Request, StatusCode}, middleware, routing::get, }; use crank_observability::{ MetricsConfig, ObservabilityConfig, RedactionLimits, ServiceIdentity, record_http_request, }; use tower::ServiceExt; #[tokio::test] async fn http_metrics_use_matched_routes_and_closed_labels() { let identity = ServiceIdentity::try_new("metrics-test", "0.3.1", "test").expect("valid identity"); let lifecycle = crank_observability::init(ObservabilityConfig::new( identity, "off", RedactionLimits::default(), )) .expect("observability lifecycle"); let config = MetricsConfig::new( true, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9464), None, ) .expect("loopback metrics"); let metrics = lifecycle.metrics_surface(config).router(); let app = Router::new() .route( "/documents/{document_id}", get(|| async { StatusCode::NO_CONTENT }), ) .layer(middleware::from_fn(record_http_request)); let sensitive_path_segment = "customer-secret-document-id"; for index in 0..100 { let response = app .clone() .oneshot( Request::get(format!("/documents/{sensitive_path_segment}-{index}")) .body(Body::empty()) .expect("request"), ) .await .expect("response"); assert_eq!(response.status(), StatusCode::NO_CONTENT); } let response = app .oneshot( Request::get("/unknown/customer-controlled-path") .body(Body::empty()) .expect("request"), ) .await .expect("response"); assert_eq!(response.status(), StatusCode::NOT_FOUND); let response = metrics .oneshot( Request::get("/metrics") .body(Body::empty()) .expect("request"), ) .await .expect("metrics response"); let body = to_bytes(response.into_body(), 1024 * 1024) .await .expect("bounded metrics body"); let body = String::from_utf8(body.to_vec()).expect("utf-8 metrics"); assert!(body.contains("crank_http_requests_total")); assert!(body.contains("route=\"/documents/{document_id}\"")); assert!(body.contains("method=\"GET\"")); assert!(body.contains("status_class=\"2xx\"")); assert!(body.contains("crank_http_request_duration_seconds_bucket")); assert!(!body.contains(sensitive_path_segment)); assert_eq!( body.lines() .filter(|line| { line.starts_with("crank_http_requests_total{") && line.contains("route=\"/documents/{document_id}\"") }) .count(), 1, "different entity ids must not create additional series" ); }