feat(ml-service): add Prometheus metrics and request telemetry instrumentation
This commit is contained in:
@@ -3,6 +3,7 @@ mod error;
|
||||
mod image;
|
||||
mod model;
|
||||
mod routes;
|
||||
mod telemetry;
|
||||
|
||||
use anyhow::Result;
|
||||
use config::Config;
|
||||
@@ -32,6 +33,9 @@ async fn main() -> Result<()> {
|
||||
"Model service initialized"
|
||||
);
|
||||
|
||||
// Set model load status metric
|
||||
telemetry::model_load_status().set(if model.is_loaded() { 1.0 } else { 0.0 });
|
||||
|
||||
// Create AppState
|
||||
let state = AppState { model };
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ use crate::config::{LABELS, SERVICE_NAME, SERVICE_VERSION};
|
||||
use crate::model::{ModelService, Prediction};
|
||||
use crate::error::ServiceError;
|
||||
use crate::image::preprocess_image;
|
||||
use crate::telemetry;
|
||||
use crate::telemetry::RequestMetricsGuard;
|
||||
use axum::{
|
||||
extract::{State, Multipart},
|
||||
routing::{get, post},
|
||||
@@ -64,22 +66,34 @@ pub fn prediction_response(prediction: Prediction) -> PredictionResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn metrics() -> (axum::http::StatusCode, String) {
|
||||
(axum::http::StatusCode::OK, telemetry::encode_metrics())
|
||||
}
|
||||
|
||||
pub async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
|
||||
Json(health_response(state.model.is_loaded()))
|
||||
let _guard = RequestMetricsGuard::new();
|
||||
let res = health_response(state.model.is_loaded());
|
||||
_guard.finish();
|
||||
Json(res)
|
||||
}
|
||||
|
||||
pub async fn metadata(State(state): State<AppState>) -> Json<MetadataResponse> {
|
||||
Json(metadata_response(
|
||||
let _guard = RequestMetricsGuard::new();
|
||||
let res = metadata_response(
|
||||
state.model.model_path().to_string_lossy().to_string(),
|
||||
state.model.is_loaded(),
|
||||
state.model.input_size(),
|
||||
))
|
||||
);
|
||||
_guard.finish();
|
||||
Json(res)
|
||||
}
|
||||
|
||||
pub async fn predict(
|
||||
State(state): State<AppState>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<PredictionResponse>, ServiceError> {
|
||||
let _guard = RequestMetricsGuard::new();
|
||||
|
||||
// Extract the file field from multipart
|
||||
let mut file_data = None;
|
||||
while let Ok(Some(field)) = multipart.next_field().await {
|
||||
@@ -121,6 +135,10 @@ pub async fn predict(
|
||||
// Run prediction
|
||||
let prediction = state.model.predict(input)?;
|
||||
|
||||
// Record business and request telemetry
|
||||
telemetry::predictions_total().inc();
|
||||
_guard.finish();
|
||||
|
||||
Ok(Json(prediction_response(prediction)))
|
||||
}
|
||||
|
||||
@@ -129,6 +147,7 @@ pub fn router(state: AppState) -> Router {
|
||||
.route("/health", get(health))
|
||||
.route("/metadata", get(metadata))
|
||||
.route("/predict", post(predict))
|
||||
.route("/metrics", get(metrics))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
use prometheus::{Counter, Gauge, Histogram, HistogramOpts, Registry, TextEncoder};
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Instant;
|
||||
|
||||
fn global_registry() -> &'static Registry {
|
||||
static REGISTRY: OnceLock<Registry> = OnceLock::new();
|
||||
REGISTRY.get_or_init(|| {
|
||||
Registry::new_custom(Some("zeavis_ml".to_string()), None).expect("create registry")
|
||||
})
|
||||
}
|
||||
|
||||
macro_rules! define_metric {
|
||||
($name:ident, $ty:ty, $init:expr) => {
|
||||
pub fn $name() -> &'static $ty {
|
||||
static METRIC: OnceLock<$ty> = OnceLock::new();
|
||||
METRIC.get_or_init(|| {
|
||||
let m = $init;
|
||||
global_registry()
|
||||
.register(Box::new(m.clone()))
|
||||
.expect(concat!("register ", stringify!($name)));
|
||||
m
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── HTTP Metrics ────────────────────────────────────────
|
||||
|
||||
define_metric!(
|
||||
http_requests_total,
|
||||
Counter,
|
||||
Counter::new("zeavis_ml_http_requests_total", "Total number of HTTP requests")
|
||||
.expect("create counter")
|
||||
);
|
||||
|
||||
define_metric!(
|
||||
http_request_duration_seconds,
|
||||
Histogram,
|
||||
Histogram::with_opts(
|
||||
HistogramOpts::new(
|
||||
"zeavis_ml_http_request_duration_seconds",
|
||||
"HTTP request duration in seconds",
|
||||
)
|
||||
.buckets(vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]),
|
||||
)
|
||||
.expect("create histogram")
|
||||
);
|
||||
|
||||
define_metric!(
|
||||
http_requests_active,
|
||||
Gauge,
|
||||
Gauge::new(
|
||||
"zeavis_ml_http_requests_active",
|
||||
"Number of active HTTP requests",
|
||||
)
|
||||
.expect("create gauge")
|
||||
);
|
||||
|
||||
// ── Business Metrics ────────────────────────────────────
|
||||
|
||||
define_metric!(
|
||||
predictions_total,
|
||||
Counter,
|
||||
Counter::new(
|
||||
"zeavis_ml_predictions_total",
|
||||
"Total number of prediction requests",
|
||||
)
|
||||
.expect("create counter")
|
||||
);
|
||||
|
||||
define_metric!(
|
||||
model_load_status,
|
||||
Gauge,
|
||||
Gauge::new(
|
||||
"zeavis_ml_model_load_status",
|
||||
"Model load status (1 = loaded, 0 = not loaded)",
|
||||
)
|
||||
.expect("create gauge")
|
||||
);
|
||||
|
||||
// ── Request Guard (Drop-based cleanup for active gauge) ─
|
||||
|
||||
pub struct RequestMetricsGuard {
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl RequestMetricsGuard {
|
||||
pub fn new() -> Self {
|
||||
http_requests_active().inc();
|
||||
Self {
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record duration and request count before the guard drops.
|
||||
pub fn finish(&self) {
|
||||
http_request_duration_seconds().observe(self.start.elapsed().as_secs_f64());
|
||||
http_requests_total().inc();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RequestMetricsGuard {
|
||||
fn drop(&mut self) {
|
||||
http_requests_active().dec();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Export ──────────────────────────────────────────────
|
||||
|
||||
pub fn encode_metrics() -> String {
|
||||
let encoder = TextEncoder::new();
|
||||
let mut buffer = String::new();
|
||||
let metric_families = global_registry().gather();
|
||||
encoder
|
||||
.encode_utf8(&metric_families, &mut buffer)
|
||||
.unwrap();
|
||||
buffer
|
||||
}
|
||||
Reference in New Issue
Block a user