135 lines
4.6 KiB
Rust
135 lines
4.6 KiB
Rust
use std::{
|
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
|
time::Duration,
|
|
};
|
|
|
|
use crank_metrics::{
|
|
CacheOutcome, ConfirmationOutcome, DbPoolState, Exporter, HttpMethod, HttpRoute,
|
|
HttpStatusClass, IdempotencyOutcome, InFlightGuard, InvocationSource, LimitStage, McpMethod,
|
|
McpOutcome, McpResponseMode, SignalType, ToolErrorKind, ToolOutcome, UpstreamOperationKind,
|
|
UpstreamOutcome, record_cache_outcome, record_confirmation_outcome, record_export_failure,
|
|
record_http_request, record_idempotency_outcome, record_invocation_history_lost,
|
|
record_limit_rejection, record_mcp_request, record_tool_invocation, record_upstream_request,
|
|
set_catalog, set_db_pool_connections, set_mcp_active_sessions,
|
|
};
|
|
use crank_observability::{MetricsConfig, ObservabilityConfig, RedactionLimits, ServiceIdentity};
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
|
|
#[tokio::test]
|
|
async fn typed_product_signals_are_exposed_by_a_real_scrape() {
|
|
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), 0),
|
|
None,
|
|
)
|
|
.expect("loopback metrics config");
|
|
let server = lifecycle
|
|
.metrics_surface(config)
|
|
.bind()
|
|
.await
|
|
.expect("metrics listener");
|
|
let address = server.local_addr().expect("listener address");
|
|
let task = tokio::spawn(server.serve());
|
|
|
|
record_http_request(
|
|
HttpRoute::from_matched_path("/api/auth/session"),
|
|
HttpMethod::Get,
|
|
HttpStatusClass::Success,
|
|
Duration::from_millis(7),
|
|
);
|
|
record_mcp_request(
|
|
McpMethod::ToolsCall,
|
|
McpResponseMode::Json,
|
|
McpOutcome::ToolError,
|
|
Duration::from_millis(1),
|
|
);
|
|
set_mcp_active_sessions(2);
|
|
let _stream = InFlightGuard::mcp_stream();
|
|
let _runtime = InFlightGuard::runtime();
|
|
record_tool_invocation(
|
|
InvocationSource::AgentToolCall,
|
|
ToolOutcome::Error,
|
|
ToolErrorKind::Mapping,
|
|
Duration::from_millis(11),
|
|
);
|
|
record_upstream_request(
|
|
UpstreamOperationKind::Rest,
|
|
UpstreamOutcome::Timeout,
|
|
Duration::from_millis(13),
|
|
);
|
|
record_limit_rejection(LimitStage::Concurrency);
|
|
record_cache_outcome(CacheOutcome::Hit);
|
|
record_idempotency_outcome(IdempotencyOutcome::Replay);
|
|
record_confirmation_outcome(ConfirmationOutcome::Required);
|
|
set_db_pool_connections(DbPoolState::Idle, 3);
|
|
set_db_pool_connections(DbPoolState::Used, 1);
|
|
set_catalog(5, 1200, 1);
|
|
record_invocation_history_lost();
|
|
record_export_failure(SignalType::Trace, Exporter::Otlp);
|
|
|
|
let response = raw_get(address, "/metrics").await;
|
|
task.abort();
|
|
let _ = task.await;
|
|
|
|
assert!(response.starts_with("HTTP/1.1 200 OK"), "{response}");
|
|
for metric in [
|
|
"crank_http_requests_total",
|
|
"crank_mcp_requests_total",
|
|
"crank_mcp_active_sessions",
|
|
"crank_mcp_active_streams",
|
|
"crank_tool_invocations_total",
|
|
"crank_upstream_requests_total",
|
|
"crank_runtime_inflight",
|
|
"crank_runtime_limit_rejections_total",
|
|
"crank_runtime_cache_total",
|
|
"crank_idempotency_total",
|
|
"crank_confirmation_total",
|
|
"crank_db_pool_connections",
|
|
"crank_catalog_tools",
|
|
"crank_invocation_history_lost_total",
|
|
"crank_telemetry_export_failures_total",
|
|
] {
|
|
assert!(response.contains(metric), "missing metric {metric}");
|
|
}
|
|
for expected_label in [
|
|
"outcome=\"tool_error\"",
|
|
"outcome=\"timeout\"",
|
|
"outcome=\"hit\"",
|
|
"outcome=\"replay\"",
|
|
"outcome=\"required\"",
|
|
"error_kind=\"mapping\"",
|
|
] {
|
|
assert!(
|
|
response.contains(expected_label),
|
|
"missing label {expected_label}"
|
|
);
|
|
}
|
|
assert!(!response.contains("customer-secret"));
|
|
}
|
|
|
|
async fn raw_get(address: SocketAddr, path: &str) -> String {
|
|
let mut stream = tokio::net::TcpStream::connect(address)
|
|
.await
|
|
.expect("metrics listener connection");
|
|
stream
|
|
.write_all(
|
|
format!("GET {path} HTTP/1.1\r\nHost: {address}\r\nConnection: close\r\n\r\n")
|
|
.as_bytes(),
|
|
)
|
|
.await
|
|
.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")
|
|
}
|