use axum::{
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
};
use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle};
use std::sync::{Arc, OnceLock};
static METRICS_HANDLE: OnceLock<PrometheusHandle> = OnceLock::new();
pub mod operation {
pub const ACK: &str = "ack";
pub const ACK_POLICY: &str = "ack_policy";
pub const BUNDLE: &str = "bundle";
pub const CORPUS_DIFF: &str = "corpus_diff";
pub const NORMALIZE: &str = "normalize";
pub const PARSE: &str = "parse";
pub const REPLAY: &str = "replay";
pub const VALIDATE: &str = "validate";
pub const VALIDATE_REDACTED: &str = "validate_redacted";
}
pub fn init_metrics_recorder() -> PrometheusHandle {
METRICS_HANDLE
.get_or_init(|| {
describe_metrics();
PrometheusBuilder::new()
.set_buckets_for_metric(
Matcher::Full("hl7v2_request_duration_seconds".to_string()),
&[
0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
],
)
.expect("Failed to set histogram buckets")
.install_recorder()
.expect("Failed to install Prometheus recorder")
})
.clone()
}
fn describe_metrics() {
metrics::describe_counter!(
"hl7v2_requests_total",
"Total HTTP requests by endpoint and HTTP status code."
);
metrics::describe_histogram!(
"hl7v2_request_duration_seconds",
"HTTP request latency in seconds by endpoint."
);
metrics::describe_counter!(
"hl7v2_messages_parsed_total",
"HL7 messages parsed successfully by server evidence workflows."
);
metrics::describe_counter!(
"hl7v2_messages_validated_total",
"HL7 messages validated by server evidence workflows."
);
metrics::describe_histogram!(
"hl7v2_message_size_bytes",
"Input HL7 message size in bytes."
);
metrics::describe_counter!(
"hl7v2_parse_failures_total",
"Parse failures by bounded server operation label."
);
metrics::describe_counter!(
"hl7v2_validation_failures_total",
"Validation failures by bounded server operation label."
);
metrics::describe_counter!(
"hl7v2_redaction_failures_total",
"Redaction failures by bounded server operation label."
);
metrics::describe_counter!(
"hl7v2_bundles_created_total",
"Evidence bundles created by the server."
);
metrics::describe_counter!("hl7v2_replays_total", "Evidence replay attempts.");
metrics::describe_counter!(
"hl7v2_replay_failures_total",
"Evidence replay attempts that did not reproduce."
);
metrics::describe_counter!(
"hl7v2_corpus_diffs_total",
"Inline corpus diff requests completed by the server."
);
metrics::describe_counter!(
"hl7v2_parse_errors_total",
"Compatibility counter for parse errors."
);
metrics::describe_counter!(
"hl7v2_validation_errors_total",
"Compatibility counter for validation errors."
);
}
pub fn record_request(endpoint: &str, status: &str, duration_seconds: f64) {
metrics::counter!("hl7v2_requests_total", "endpoint" => endpoint.to_string(), "status" => status.to_string())
.increment(1);
metrics::histogram!(
"hl7v2_request_duration_seconds",
"endpoint" => endpoint.to_string()
)
.record(duration_seconds);
}
pub fn record_parse_success(operation: &'static str, size_bytes: usize) {
metrics::counter!("hl7v2_messages_parsed_total", "operation" => operation).increment(1);
record_message_size(size_bytes);
}
pub fn record_parse_failure(operation: &'static str) {
metrics::counter!("hl7v2_parse_failures_total", "operation" => operation).increment(1);
metrics::counter!("hl7v2_parse_errors_total").increment(1);
}
pub fn record_validation_result(operation: &'static str, valid: bool) {
metrics::counter!("hl7v2_messages_validated_total", "operation" => operation).increment(1);
if !valid {
metrics::counter!("hl7v2_validation_failures_total", "operation" => operation).increment(1);
metrics::counter!("hl7v2_validation_errors_total").increment(1);
}
}
pub fn record_redaction_failure(operation: &'static str) {
metrics::counter!("hl7v2_redaction_failures_total", "operation" => operation).increment(1);
}
pub fn record_bundle_created() {
metrics::counter!("hl7v2_bundles_created_total").increment(1);
}
pub fn record_replay_result(reproduced: bool) {
metrics::counter!("hl7v2_replays_total").increment(1);
if !reproduced {
metrics::counter!("hl7v2_replay_failures_total").increment(1);
}
}
pub fn record_corpus_diff() {
metrics::counter!("hl7v2_corpus_diffs_total").increment(1);
}
pub fn increment_messages_parsed() {
metrics::counter!("hl7v2_messages_parsed_total", "operation" => "unspecified").increment(1);
}
pub fn increment_messages_validated() {
metrics::counter!("hl7v2_messages_validated_total", "operation" => "unspecified").increment(1);
}
pub fn increment_validation_errors() {
record_validation_result("unspecified", false);
}
pub fn increment_parse_errors() {
record_parse_failure("unspecified");
}
pub fn record_message_size(size_bytes: usize) {
metrics::histogram!("hl7v2_message_size_bytes").record(size_bytes as f64);
}
pub async fn metrics_handler(
State(state): State<Arc<crate::server::AppState>>,
) -> impl IntoResponse {
let metrics = state.metrics_handle.render();
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
.body(metrics)
.expect("Failed to build metrics response")
}
pub mod middleware {
use super::*;
use axum::{extract::Request, middleware::Next, response::Response};
use std::time::Instant;
pub async fn metrics_middleware(request: Request, next: Next) -> Response {
let start = Instant::now();
let path = request.uri().path().to_string();
let response = next.run(request).await;
let duration = start.elapsed();
let status = response.status().as_u16().to_string();
record_request(&path, &status, duration.as_secs_f64());
response
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_recorder_init() {
let handle = init_metrics_recorder();
record_request("/test", "200", 0.01);
let output = handle.render();
assert!(output.contains("hl7v2_requests_total"));
}
#[test]
fn test_record_request() {
record_request("/hl7/parse", "200", 0.05);
}
#[test]
fn test_increment_counters() {
increment_messages_parsed();
increment_messages_validated();
increment_validation_errors();
increment_parse_errors();
record_redaction_failure(operation::VALIDATE_REDACTED);
record_bundle_created();
record_replay_result(false);
record_corpus_diff();
}
#[test]
fn test_record_message_size() {
record_message_size(1024);
}
#[test]
fn test_evidence_contract_metrics_render() {
let handle = init_metrics_recorder();
record_request("/hl7/validate", "200", 0.015);
record_parse_success(operation::PARSE, 256);
record_parse_failure(operation::VALIDATE);
record_validation_result(operation::VALIDATE, false);
record_redaction_failure(operation::BUNDLE);
record_bundle_created();
record_replay_result(false);
record_corpus_diff();
let output = handle.render();
for metric_name in [
"hl7v2_requests_total",
"hl7v2_request_duration_seconds",
"hl7v2_messages_parsed_total",
"hl7v2_messages_validated_total",
"hl7v2_message_size_bytes",
"hl7v2_parse_failures_total",
"hl7v2_validation_failures_total",
"hl7v2_redaction_failures_total",
"hl7v2_bundles_created_total",
"hl7v2_replays_total",
"hl7v2_replay_failures_total",
"hl7v2_corpus_diffs_total",
] {
assert!(
output.contains(metric_name),
"Prometheus output should include {metric_name}; output was: {output}"
);
}
}
}