fix: harden typed metrics review findings

This commit is contained in:
2026-08-14 13:50:04 +03:00
parent 996a5461de
commit b7face0e94
30 changed files with 722 additions and 382 deletions
+9 -2
View File
@@ -718,13 +718,20 @@ async fn mcp_post(
let message = match payload {
Ok(Json(message)) => message,
Err(rejection) => {
let mut request_metrics = McpRequestMetrics::invalid();
let mut request_metrics = McpRequestMetrics::invalid(
request_context.started_at(),
request_context.exemplar(),
);
let response = rejection.into_response();
request_metrics.complete(&response);
return with_request_id_header(response, request_context.request_id());
}
};
let mut request_metrics = McpRequestMetrics::new(&message);
let mut request_metrics = McpRequestMetrics::new(
&message,
request_context.started_at(),
request_context.exemplar(),
);
let transport_correlation = request_context.correlation;
let transport_request_id = transport_correlation.request_id().to_string();
info!(
+10 -11
View File
@@ -19,32 +19,31 @@ pub(super) struct McpRequestMetrics {
}
impl McpRequestMetrics {
pub(super) fn invalid() -> Self {
pub(super) fn invalid(started_at: Instant, exemplar: Option<ExemplarTraceId>) -> Self {
Self {
method: McpMethod::Invalid,
response_mode: McpResponseMode::Unknown,
outcome: McpOutcome::Aborted,
started_at: Instant::now(),
exemplar: None,
started_at,
exemplar,
}
}
pub(super) fn new(message: &Value) -> Self {
pub(super) fn new(
message: &Value,
started_at: Instant,
exemplar: Option<ExemplarTraceId>,
) -> Self {
Self {
method: normalized_mcp_method(message),
response_mode: McpResponseMode::Unknown,
outcome: McpOutcome::Aborted,
started_at: Instant::now(),
exemplar: None,
started_at,
exemplar,
}
}
pub(super) fn complete(&mut self, response: &Response) {
self.exemplar = response
.headers()
.get("x-trace-id")
.and_then(|value| value.to_str().ok())
.and_then(ExemplarTraceId::parse);
self.response_mode = response
.headers()
.get(CONTENT_TYPE)
+5 -5
View File
@@ -28,7 +28,7 @@ use crate::transport::transport_response;
#[tokio::test]
async fn tool_error_response_includes_structured_context() {
let message = json!({"jsonrpc": "2.0", "id": "req-1", "method": "tools/call"});
let mut request_metrics = McpRequestMetrics::new(&message);
let mut request_metrics = McpRequestMetrics::new(&message, std::time::Instant::now(), None);
let response = tool_error_response(
&message,
ResponseMode::Json,
@@ -69,7 +69,7 @@ async fn tool_error_response_includes_structured_context() {
#[test]
fn jsonrpc_error_over_http_200_is_not_counted_as_success() {
let message = json!({"jsonrpc": "2.0", "id": 1, "method": "unsupported"});
let mut request_metrics = McpRequestMetrics::new(&message);
let mut request_metrics = McpRequestMetrics::new(&message, std::time::Instant::now(), None);
let response = transport_response(
axum::http::StatusCode::OK,
jsonrpc_error(json!(1), -32601, "unsupported"),
@@ -86,7 +86,7 @@ fn jsonrpc_error_over_http_200_is_not_counted_as_success() {
#[test]
fn metric_uses_actual_json_response_when_sse_request_falls_back() {
let message = json!({"jsonrpc": "2.0", "id": 1, "method": "unsupported"});
let mut request_metrics = McpRequestMetrics::new(&message);
let mut request_metrics = McpRequestMetrics::new(&message, std::time::Instant::now(), None);
let response = transport_response(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
jsonrpc_error(json!(1), -32603, "internal error"),
@@ -104,7 +104,7 @@ fn metric_uses_actual_json_response_when_sse_request_falls_back() {
#[test]
fn transport_failure_takes_priority_over_jsonrpc_payload() {
let message = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call"});
let mut request_metrics = McpRequestMetrics::new(&message);
let mut request_metrics = McpRequestMetrics::new(&message, std::time::Instant::now(), None);
let response = transport_response(
axum::http::StatusCode::TOO_MANY_REQUESTS,
jsonrpc_error(json!(1), -32000, "rate limited"),
@@ -121,7 +121,7 @@ fn transport_failure_takes_priority_over_jsonrpc_payload() {
#[test]
fn unfinished_request_is_classified_as_aborted() {
let message = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call"});
let request_metrics = McpRequestMetrics::new(&message);
let request_metrics = McpRequestMetrics::new(&message, std::time::Instant::now(), None);
assert_eq!(request_metrics.response_mode(), McpResponseMode::Unknown);
assert_eq!(request_metrics.outcome(), McpOutcome::Aborted);
@@ -1,5 +1,8 @@
use std::time::Instant;
use axum::{extract::Request, http::HeaderValue, middleware::Next, response::Response};
use crank_core::{CorrelationContext, RequestId, TraceContext};
use crank_metrics::ExemplarTraceId;
use crank_observability::{set_remote_trace_parent, with_request_correlation};
use tracing::{Instrument, info_span};
@@ -10,12 +13,25 @@ const HEADER_X_TRACE_ID: axum::http::HeaderName = axum::http::HeaderName::from_s
#[derive(Clone, Debug)]
pub(super) struct RequestContext {
pub(super) correlation: CorrelationContext,
started_at: Instant,
}
impl RequestContext {
pub(super) fn request_id(&self) -> &str {
self.correlation.request_id().as_str()
}
pub(super) fn started_at(&self) -> Instant {
self.started_at
}
pub(super) fn exemplar(&self) -> Option<ExemplarTraceId> {
self.correlation
.trace_context()
.is_sampled()
.then(|| ExemplarTraceId::parse(self.correlation.trace_id().as_str()))
.flatten()
}
}
pub(super) async fn apply_request_context(mut request: Request, next: Next) -> Response {
@@ -37,6 +53,7 @@ pub(super) async fn apply_request_context(mut request: Request, next: Next) -> R
span.record("trace_id", trace_context.trace_id().as_str());
let context = RequestContext {
correlation: CorrelationContext::new(request_id, trace_context),
started_at: Instant::now(),
};
request.extensions_mut().insert(context.clone());
@@ -51,6 +68,9 @@ pub(super) async fn apply_request_context(mut request: Request, next: Next) -> R
if let Ok(value) = HeaderValue::from_str(context.correlation.trace_id().as_str()) {
response.headers_mut().insert(HEADER_X_TRACE_ID, value);
}
if let Some(exemplar) = context.exemplar() {
response.extensions_mut().insert(exemplar);
}
response
},
)