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 fn init_metrics_recorder() -> PrometheusHandle {
METRICS_HANDLE
.get_or_init(|| {
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()
}
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 increment_messages_parsed() {
metrics::counter!("hl7v2_messages_parsed_total").increment(1);
}
pub fn increment_messages_validated() {
metrics::counter!("hl7v2_messages_validated_total").increment(1);
}
pub fn increment_validation_errors() {
metrics::counter!("hl7v2_validation_errors_total").increment(1);
}
pub fn increment_parse_errors() {
metrics::counter!("hl7v2_parse_errors_total").increment(1);
}
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();
}
#[test]
fn test_record_message_size() {
record_message_size(1024);
}
}