134 lines
4.4 KiB
Rust
134 lines
4.4 KiB
Rust
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
|
|
use axum::{
|
|
Router,
|
|
body::{Body, to_bytes},
|
|
http::{Request, StatusCode, header},
|
|
middleware,
|
|
response::IntoResponse,
|
|
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("admin-api", "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(
|
|
"/api/admin/workspaces/{workspace_id}/operations/{operation_id}",
|
|
get(|| async {
|
|
let mut response = StatusCode::NO_CONTENT.into_response();
|
|
response.extensions_mut().insert(
|
|
crank_metrics::ExemplarTraceId::parse("0123456789abcdef0123456789abcdef")
|
|
.unwrap(),
|
|
);
|
|
response
|
|
}),
|
|
)
|
|
.layer(middleware::from_fn(record_http_request));
|
|
|
|
let sensitive_path_segment = "customer-secret-operation-id";
|
|
for index in 0..100 {
|
|
let response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::get(format!(
|
|
"/api/admin/workspaces/customer-secret-workspace-{index}/operations/{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
|
|
.clone()
|
|
.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=\"/api/admin/workspaces/{workspace_id}/operations/{operation_id}\"")
|
|
);
|
|
assert!(body.contains("method=\"GET\""));
|
|
assert!(body.contains("status_class=\"2xx\""));
|
|
assert!(body.contains("crank_http_request_duration_seconds_bucket"));
|
|
assert!(!body.contains("trace_id"));
|
|
assert!(!body.contains(sensitive_path_segment));
|
|
assert_eq!(
|
|
body.lines()
|
|
.filter(|line| {
|
|
line.starts_with("crank_http_requests_total{")
|
|
&& line.contains(
|
|
"route=\"/api/admin/workspaces/{workspace_id}/operations/{operation_id}\"",
|
|
)
|
|
})
|
|
.count(),
|
|
1,
|
|
"different entity ids must not create additional series"
|
|
);
|
|
|
|
let openmetrics = metrics
|
|
.oneshot(
|
|
Request::get("/metrics")
|
|
.header(
|
|
header::ACCEPT,
|
|
"application/openmetrics-text; version=1.0.0",
|
|
)
|
|
.body(Body::empty())
|
|
.expect("request"),
|
|
)
|
|
.await
|
|
.expect("OpenMetrics response");
|
|
assert_eq!(openmetrics.status(), StatusCode::OK);
|
|
assert!(
|
|
openmetrics.headers()[header::CONTENT_TYPE]
|
|
.to_str()
|
|
.unwrap()
|
|
.starts_with("application/openmetrics-text")
|
|
);
|
|
let body = to_bytes(openmetrics.into_body(), 1024 * 1024)
|
|
.await
|
|
.unwrap();
|
|
let body = String::from_utf8(body.to_vec()).unwrap();
|
|
assert!(body.contains("# {trace_id=\"0123456789abcdef0123456789abcdef\"}"));
|
|
assert!(body.ends_with("# EOF\n"));
|
|
}
|