наблюдаемость: ввести безопасный контракт метрик
CI / Rust Checks (pull_request) Successful in 6m15s
CI / UI Checks (pull_request) Successful in 5s
CI / Community Image Smoke (pull_request) Successful in 4m25s
CI / Frontend E2E (pull_request) Successful in 5m17s
CI / Deploy (pull_request) Has been skipped
CI / Rust Checks (push) Successful in 6m9s
CI / UI Checks (push) Successful in 5s
CI / Community Image Smoke (push) Successful in 1m3s
CI / Frontend E2E (push) Successful in 3m47s
CI / Deploy (push) Failing after 3s
CI / Rust Checks (pull_request) Successful in 6m15s
CI / UI Checks (pull_request) Successful in 5s
CI / Community Image Smoke (pull_request) Successful in 4m25s
CI / Frontend E2E (pull_request) Successful in 5m17s
CI / Deploy (pull_request) Has been skipped
CI / Rust Checks (push) Successful in 6m9s
CI / UI Checks (push) Successful in 5s
CI / Community Image Smoke (push) Successful in 1m3s
CI / Frontend E2E (push) Successful in 3m47s
CI / Deploy (push) Failing after 3s
This commit is contained in:
@@ -8,6 +8,7 @@ use crank_observability::{
|
||||
DURATION_BUCKETS_SECONDS, MetricsConfig, MetricsConfigError, MetricsSurface, ServiceIdentity,
|
||||
metric_schema,
|
||||
};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tower::ServiceExt;
|
||||
|
||||
fn identity() -> ServiceIdentity {
|
||||
@@ -27,6 +28,33 @@ fn loopback_is_allowed_without_a_token() {
|
||||
assert!(!config.requires_authentication());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_service_ports_bind_and_serve_real_metrics_listeners() {
|
||||
for (service, port) in [("admin-api", 9464), ("mcp-server", 9465)] {
|
||||
let config = MetricsConfig::new(
|
||||
true,
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port),
|
||||
None,
|
||||
)
|
||||
.expect("default loopback metrics config");
|
||||
let identity =
|
||||
ServiceIdentity::try_new(service, "0.3.1", "test").expect("valid service identity");
|
||||
let server = MetricsSurface::for_test(config, identity)
|
||||
.expect("metrics surface")
|
||||
.bind()
|
||||
.await
|
||||
.expect("default metrics port must bind");
|
||||
assert_eq!(server.local_addr().expect("local address").port(), port);
|
||||
|
||||
let task = tokio::spawn(server.serve());
|
||||
let response = raw_get(port, "/metrics").await;
|
||||
assert!(response.starts_with("HTTP/1.1 200 OK"), "{response}");
|
||||
assert!(response.contains("content-type: text/plain"));
|
||||
task.abort();
|
||||
let _ = task.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_loopback_without_a_token_is_rejected_without_secret_data() {
|
||||
let error = MetricsConfig::new(
|
||||
@@ -51,8 +79,13 @@ fn schema_is_closed_and_uses_fixed_duration_buckets() {
|
||||
assert!(names.contains(&"crank_http_requests_total"));
|
||||
assert!(names.contains(&"crank_http_request_duration_seconds"));
|
||||
assert!(names.contains(&"crank_mcp_requests_total"));
|
||||
assert!(names.contains(&"crank_mcp_active_sessions"));
|
||||
assert!(names.contains(&"crank_mcp_active_streams"));
|
||||
assert!(names.contains(&"crank_tool_invocations_total"));
|
||||
assert!(names.contains(&"crank_runtime_inflight"));
|
||||
assert!(names.contains(&"crank_runtime_cache_total"));
|
||||
assert!(names.contains(&"crank_idempotency_total"));
|
||||
assert!(names.contains(&"crank_confirmation_total"));
|
||||
assert!(names.contains(&"crank_db_pool_connections"));
|
||||
assert!(names.contains(&"crank_catalog_tools"));
|
||||
assert!(names.contains(&"crank_invocation_history_lost_total"));
|
||||
@@ -108,7 +141,7 @@ async fn external_surface_protects_both_routes_and_exposes_nothing_else() {
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::get(path)
|
||||
.header(header::AUTHORIZATION, format!("Bearer {token}"))
|
||||
.header(header::AUTHORIZATION, format!("bEaReR {token}"))
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
@@ -121,14 +154,35 @@ async fn external_surface_protects_both_routes_and_exposes_nothing_else() {
|
||||
assert!(!String::from_utf8_lossy(&body).contains(token));
|
||||
}
|
||||
|
||||
let absent = app
|
||||
.oneshot(
|
||||
Request::get("/api/operations")
|
||||
.header(header::AUTHORIZATION, format!("Bearer {token}"))
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
for authorization in [None, Some(format!("Bearer {token}"))] {
|
||||
let mut request = Request::get("/api/operations");
|
||||
if let Some(authorization) = authorization {
|
||||
request = request.header(header::AUTHORIZATION, authorization);
|
||||
}
|
||||
let absent = app
|
||||
.clone()
|
||||
.oneshot(request.body(Body::empty()).expect("request"))
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(absent.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
async fn raw_get(port: u16, path: &str) -> String {
|
||||
let mut stream = tokio::net::TcpStream::connect((Ipv4Addr::LOCALHOST, port))
|
||||
.await
|
||||
.expect("metrics listener connection");
|
||||
stream
|
||||
.write_all(
|
||||
format!("GET {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\n\r\n")
|
||||
.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(absent.status(), StatusCode::NOT_FOUND);
|
||||
.expect("metrics request");
|
||||
let mut response = Vec::new();
|
||||
stream
|
||||
.read_to_end(&mut response)
|
||||
.await
|
||||
.expect("metrics response");
|
||||
String::from_utf8(response).expect("utf-8 response")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user