feat: freeze typed metrics registry and bounded exemplars
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
use std::{collections::BTreeSet, time::Duration};
|
||||
|
||||
use crank_metrics::{
|
||||
HttpMethod, HttpRoute, HttpStatusClass, McpMethod, McpOutcome, McpResponseMode,
|
||||
record_http_request, record_mcp_request,
|
||||
};
|
||||
use metrics_util::debugging::DebuggingRecorder;
|
||||
|
||||
#[test]
|
||||
fn one_million_hostile_values_collapse_to_the_same_closed_series() {
|
||||
let recorder = DebuggingRecorder::new();
|
||||
let snapshotter = recorder.snapshotter();
|
||||
metrics::with_local_recorder(&recorder, || {
|
||||
for index in 0..1_000_000_u32 {
|
||||
let canary = format!("workspace-{index}-request-trace-url-secret");
|
||||
record_http_request(
|
||||
HttpRoute::from_matched_path(&canary),
|
||||
HttpMethod::classify(&canary),
|
||||
HttpStatusClass::from_status(999),
|
||||
Duration::ZERO,
|
||||
);
|
||||
record_mcp_request(
|
||||
McpMethod::Invalid,
|
||||
McpResponseMode::Unknown,
|
||||
McpOutcome::Other,
|
||||
Duration::ZERO,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let series = snapshotter
|
||||
.snapshot()
|
||||
.into_vec()
|
||||
.into_iter()
|
||||
.map(|(key, _, _, _)| format!("{:?}", key.key()))
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(series.len(), 4);
|
||||
assert!(series.iter().all(|value| !value.contains("workspace-")));
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use crank_metrics::{
|
||||
metric_schema, 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,
|
||||
set_catalog, set_db_pool_connections, set_mcp_active_sessions, set_mcp_session_metrics_fresh,
|
||||
};
|
||||
use metrics_util::debugging::DebuggingRecorder;
|
||||
|
||||
@@ -80,6 +80,26 @@ fn application_outcomes_are_distinct_from_transport_success() {
|
||||
assert_eq!(ConfirmationOutcome::Required.as_str(), "required");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn impossible_tool_outcome_pairs_are_normalized() {
|
||||
let recorder = DebuggingRecorder::new();
|
||||
let snapshotter = recorder.snapshotter();
|
||||
metrics::with_local_recorder(&recorder, || {
|
||||
record_tool_invocation(
|
||||
InvocationSource::AgentToolCall,
|
||||
ToolOutcome::Success,
|
||||
ToolErrorKind::Mapping,
|
||||
Duration::ZERO,
|
||||
);
|
||||
});
|
||||
let snapshot = snapshotter.snapshot().into_vec();
|
||||
assert!(snapshot.iter().any(|(key, _, _, _)| {
|
||||
key.key().name() == "crank_tool_invocations_total"
|
||||
&& has_label(key.key(), "outcome", "success")
|
||||
&& has_label(key.key(), "error_kind", "none")
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropped_product_guards_record_aborted_outcomes() {
|
||||
let recorder = DebuggingRecorder::new();
|
||||
@@ -128,6 +148,7 @@ fn diverse_calls_cannot_create_unbounded_series() {
|
||||
McpMethod::ToolsCall,
|
||||
McpResponseMode::Json,
|
||||
McpOutcome::ToolError,
|
||||
Duration::from_millis(1),
|
||||
);
|
||||
record_tool_invocation(
|
||||
InvocationSource::AgentToolCall,
|
||||
@@ -147,7 +168,7 @@ fn diverse_calls_cannot_create_unbounded_series() {
|
||||
.map(|(key, _, _, _)| format!("{:?}", key.key()))
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
assert_eq!(series.len(), 8);
|
||||
assert_eq!(series.len(), 9);
|
||||
assert!(
|
||||
series
|
||||
.iter()
|
||||
@@ -172,8 +193,10 @@ fn every_recording_api_matches_the_declared_schema() {
|
||||
McpMethod::ToolsCall,
|
||||
McpResponseMode::Json,
|
||||
McpOutcome::Success,
|
||||
Duration::from_millis(1),
|
||||
);
|
||||
set_mcp_active_sessions(1);
|
||||
set_mcp_session_metrics_fresh(true);
|
||||
let _stream = InFlightGuard::mcp_stream();
|
||||
let _runtime = InFlightGuard::runtime();
|
||||
record_tool_invocation(
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crank_metrics::{
|
||||
ExemplarTraceId, HttpMethod, HttpRoute, HttpStatusClass, exemplar_snapshot,
|
||||
record_http_request_with_exemplar, reset_exemplars_for_test,
|
||||
};
|
||||
use metrics_util::debugging::DebuggingRecorder;
|
||||
|
||||
#[test]
|
||||
fn trace_id_is_validated_and_never_becomes_an_ordinary_label() {
|
||||
assert!(ExemplarTraceId::parse("0123456789abcdef0123456789abcdef").is_some());
|
||||
assert!(ExemplarTraceId::parse("0123456789ABCDEF0123456789ABCDEF").is_none());
|
||||
assert!(ExemplarTraceId::parse("short").is_none());
|
||||
|
||||
reset_exemplars_for_test();
|
||||
let recorder = DebuggingRecorder::new();
|
||||
let snapshotter = recorder.snapshotter();
|
||||
metrics::with_local_recorder(&recorder, || {
|
||||
record_http_request_with_exemplar(
|
||||
HttpRoute::from_matched_path("/health"),
|
||||
HttpMethod::Get,
|
||||
HttpStatusClass::Success,
|
||||
Duration::from_millis(10),
|
||||
ExemplarTraceId::parse("0123456789abcdef0123456789abcdef"),
|
||||
);
|
||||
});
|
||||
|
||||
let aggregate = snapshotter.snapshot().into_vec();
|
||||
assert!(
|
||||
aggregate
|
||||
.iter()
|
||||
.all(|(key, _, _, _)| { key.key().labels().all(|label| label.key() != "trace_id") })
|
||||
);
|
||||
let exemplars = exemplar_snapshot();
|
||||
assert_eq!(exemplars.len(), 1);
|
||||
assert_eq!(
|
||||
exemplars[0].trace_id.as_str(),
|
||||
"0123456789abcdef0123456789abcdef"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use std::{
|
||||
hint::black_box,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crank_metrics::{CacheOutcome, record_cache_outcome};
|
||||
use metrics_util::debugging::DebuggingRecorder;
|
||||
|
||||
const SAMPLES: usize = 40;
|
||||
const WORK_UNITS: u64 = 500_000;
|
||||
|
||||
#[test]
|
||||
fn reproducible_foundation_profile_stays_inside_latency_and_cpu_proxy_budgets() {
|
||||
let recorder = DebuggingRecorder::new();
|
||||
let snapshotter = recorder.snapshotter();
|
||||
let mut baseline = Vec::with_capacity(SAMPLES);
|
||||
let mut enabled = Vec::with_capacity(SAMPLES);
|
||||
|
||||
metrics::with_local_recorder(&recorder, || {
|
||||
for index in 0..SAMPLES {
|
||||
if index % 2 == 0 {
|
||||
baseline.push(sample(false));
|
||||
enabled.push(sample(true));
|
||||
} else {
|
||||
enabled.push(sample(true));
|
||||
baseline.push(sample(false));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
baseline.sort_unstable();
|
||||
enabled.sort_unstable();
|
||||
let baseline_p95 = baseline[SAMPLES * 95 / 100];
|
||||
let enabled_p95 = enabled[SAMPLES * 95 / 100];
|
||||
let baseline_cpu: Duration = baseline.iter().copied().sum();
|
||||
let enabled_cpu: Duration = enabled.iter().copied().sum();
|
||||
let series = snapshotter.snapshot().into_vec().len();
|
||||
|
||||
println!(
|
||||
"metrics_profile_v1 samples={SAMPLES} work_units={WORK_UNITS} baseline_p95_ns={} enabled_p95_ns={} baseline_cpu_ns={} enabled_cpu_ns={} logical_series={series}",
|
||||
baseline_p95.as_nanos(),
|
||||
enabled_p95.as_nanos(),
|
||||
baseline_cpu.as_nanos(),
|
||||
enabled_cpu.as_nanos()
|
||||
);
|
||||
assert!(enabled_p95.as_nanos() * 100 <= baseline_p95.as_nanos() * 105);
|
||||
assert!(enabled_cpu.as_nanos() * 100 <= baseline_cpu.as_nanos() * 110);
|
||||
assert_eq!(series, 1);
|
||||
}
|
||||
|
||||
fn sample(with_metrics: bool) -> Duration {
|
||||
let started = Instant::now();
|
||||
let mut value = 0x9e37_79b9_u64;
|
||||
for index in 0..WORK_UNITS {
|
||||
value = value.rotate_left(7) ^ index.wrapping_mul(0x100_0000_01b3);
|
||||
}
|
||||
black_box(value);
|
||||
if with_metrics {
|
||||
record_cache_outcome(CacheOutcome::Hit);
|
||||
}
|
||||
started.elapsed()
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crank_metrics::{
|
||||
HTTP_ROUTE_DOMAIN, HttpRoute, MAX_LOGICAL_SERIES_PER_PROCESS, MAX_PRODUCT_LABELS_PER_FAMILY,
|
||||
MAX_RENDERED_SERIES_PER_PROCESS, METRIC_SCHEMA_VERSION, metric_schema,
|
||||
render_metric_schema_json, schema_budget, validate_budget,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn schema_v1_is_complete_deterministic_and_inside_the_frozen_budget() {
|
||||
assert_eq!(METRIC_SCHEMA_VERSION, 1);
|
||||
assert_eq!(metric_schema().len(), 23);
|
||||
|
||||
let budget = schema_budget().expect("canonical schema must be valid");
|
||||
assert!(budget.logical_series <= MAX_LOGICAL_SERIES_PER_PROCESS);
|
||||
assert!(budget.rendered_series <= MAX_RENDERED_SERIES_PER_PROCESS);
|
||||
|
||||
for definition in metric_schema() {
|
||||
assert!(!definition.description.trim().is_empty());
|
||||
assert!(definition.labels.len() <= MAX_PRODUCT_LABELS_PER_FAMILY);
|
||||
assert!(!definition.processes.is_empty());
|
||||
assert_eq!(definition.labels.len(), definition.label_domains.len());
|
||||
assert!(definition.label_domains.iter().all(|domain| {
|
||||
!domain.values.is_empty()
|
||||
&& domain.values.iter().all(|value| !value.is_empty())
|
||||
&& domain.values.iter().copied().collect::<BTreeSet<_>>().len()
|
||||
== domain.values.len()
|
||||
}));
|
||||
if definition.kind.as_str() == "histogram" {
|
||||
assert!(!definition.buckets.is_empty());
|
||||
assert!(definition.buckets.windows(2).all(|pair| pair[0] < pair[1]));
|
||||
} else {
|
||||
assert!(definition.buckets.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
let first = render_metric_schema_json().expect("schema renders");
|
||||
let second = render_metric_schema_json().expect("schema renders deterministically");
|
||||
assert_eq!(first, second);
|
||||
assert!(first.len() < 65_536);
|
||||
assert!(!first.contains("trace_id"));
|
||||
assert!(!first.contains("request_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_budget_edges_are_exact() {
|
||||
assert!(validate_budget(4_999, 14_999).is_ok());
|
||||
assert!(validate_budget(5_000, 15_000).is_ok());
|
||||
assert!(validate_budget(5_001, 15_000).is_err());
|
||||
assert!(validate_budget(5_000, 15_001).is_err());
|
||||
assert_eq!(crank_metrics::max_exemplar_slots(), 9_170);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_registered_route_round_trips_without_dynamic_segments() {
|
||||
assert_eq!(HTTP_ROUTE_DOMAIN.len(), 57);
|
||||
for route in HTTP_ROUTE_DOMAIN {
|
||||
assert_eq!(HttpRoute::from_matched_path(route).as_str(), *route);
|
||||
assert!(!route.contains("trace_id"));
|
||||
assert!(!route.contains("request_id"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_schema_validation_reports_its_category() {
|
||||
assert_eq!(
|
||||
schema_budget(),
|
||||
Ok(crank_metrics::SchemaBudget {
|
||||
logical_series: 4_513,
|
||||
rendered_series: 14_338
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_budget_math_counts_histogram_bucket_sum_and_count_series() {
|
||||
for definition in metric_schema() {
|
||||
let logical = definition
|
||||
.label_domains
|
||||
.iter()
|
||||
.map(|domain| domain.values.len())
|
||||
.product::<usize>();
|
||||
let logical = logical.max(1);
|
||||
let rendered = if definition.kind.as_str() == "histogram" {
|
||||
logical * (definition.buckets.len() + 3)
|
||||
} else {
|
||||
logical
|
||||
};
|
||||
assert_eq!(
|
||||
definition.max_logical_series, logical,
|
||||
"{}",
|
||||
definition.name
|
||||
);
|
||||
assert_eq!(
|
||||
definition.max_rendered_series, rendered,
|
||||
"{}",
|
||||
definition.name
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user