Copied from api_gateway, removed REST handlers (port 8080), added tonic-web + CORS for grpc-web browser access. Binary renamed: api-gateway → api Changes: - Package name: api-gateway → api - Deleted src/handlers/ (REST ML endpoints on port 8080) - Added tonic-web 0.13 + tower-http CORS layer - Server::builder().accept_http1(true) for grpc-web - CORS_ORIGINS env var (default http://localhost:5173) - Metrics server on port 9091 (axum) preserved - All 95 lib tests pass, 0 clippy warnings - Added services/api to workspace members Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
137 lines
4.2 KiB
Rust
137 lines
4.2 KiB
Rust
//! Prometheus Metrics Exporter
|
|
//!
|
|
//! Provides HTTP endpoint for Prometheus scraping at /metrics
|
|
|
|
use prometheus::{Encoder, Registry, TextEncoder};
|
|
use std::sync::Arc;
|
|
use tonic::{Response, Status};
|
|
|
|
/// Prometheus exporter for metrics collection
|
|
#[derive(Clone)]
|
|
pub struct PrometheusExporter {
|
|
registry: Arc<Registry>,
|
|
}
|
|
|
|
impl PrometheusExporter {
|
|
/// Create new exporter with registry
|
|
pub fn new(registry: Arc<Registry>) -> Self {
|
|
Self { registry }
|
|
}
|
|
|
|
/// Gather and encode metrics in Prometheus text format
|
|
pub fn gather_metrics(&self) -> Result<String, prometheus::Error> {
|
|
let encoder = TextEncoder::new();
|
|
let metric_families = self.registry.gather();
|
|
let mut buffer = vec![];
|
|
encoder.encode(&metric_families, &mut buffer)?;
|
|
String::from_utf8(buffer).map_err(|e| {
|
|
prometheus::Error::Msg(format!("Failed to convert metrics to UTF-8: {}", e))
|
|
})
|
|
}
|
|
|
|
/// Export metrics as `HTTP` response (for Axum/Hyper integration)
|
|
pub fn export_http(&self) -> Result<Vec<u8>, prometheus::Error> {
|
|
let encoder = TextEncoder::new();
|
|
let metric_families = self.registry.gather();
|
|
let mut buffer = vec![];
|
|
encoder.encode(&metric_families, &mut buffer)?;
|
|
Ok(buffer)
|
|
}
|
|
}
|
|
|
|
/// Create Prometheus metrics endpoint (for standalone `HTTP` server)
|
|
///
|
|
/// Usage with Axum:
|
|
/// ```rust,ignore
|
|
/// use axum::{Router, routing::get};
|
|
/// use api::metrics::{GatewayMetrics, metrics_router};
|
|
///
|
|
/// let metrics = GatewayMetrics::new()?;
|
|
/// let router = metrics_router(metrics.registry());
|
|
/// ```
|
|
pub fn metrics_router(registry: Arc<Registry>) -> axum::Router {
|
|
use axum::{routing::get, Router};
|
|
|
|
let exporter = PrometheusExporter::new(registry);
|
|
|
|
Router::new().route(
|
|
"/metrics",
|
|
get(move || {
|
|
let exporter = exporter.clone();
|
|
async move {
|
|
match exporter.gather_metrics() {
|
|
Ok(metrics) => Ok(metrics),
|
|
Err(e) => Err(format!("Failed to gather metrics: {}", e)),
|
|
}
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
|
|
/// Create combined metrics and health router
|
|
///
|
|
/// This combines Prometheus metrics with health/resilience endpoints
|
|
pub fn combined_router(registry: Arc<Registry>) -> axum::Router {
|
|
use crate::health_router::{health_router, HealthState};
|
|
use axum::Router;
|
|
|
|
let metrics_routes = metrics_router(registry);
|
|
let health_state = HealthState::new();
|
|
let health_routes = health_router(health_state);
|
|
|
|
Router::new().merge(metrics_routes).merge(health_routes)
|
|
}
|
|
|
|
/// Create Prometheus endpoint as gRPC health check extension
|
|
///
|
|
/// This allows exposing metrics through the same gRPC server
|
|
pub async fn serve_metrics_grpc(registry: Arc<Registry>) -> Result<Response<String>, Status> {
|
|
let exporter = PrometheusExporter::new(registry);
|
|
|
|
exporter
|
|
.gather_metrics()
|
|
.map(Response::new)
|
|
.map_err(|e| Status::internal(format!("Failed to gather metrics: {}", e)))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use prometheus::{Counter, Opts};
|
|
|
|
#[test]
|
|
fn test_prometheus_exporter() {
|
|
let registry = Arc::new(Registry::new());
|
|
|
|
// Register test counter
|
|
let counter = Counter::with_opts(Opts::new("test_counter", "Test counter")).unwrap();
|
|
registry.register(Box::new(counter.clone())).unwrap();
|
|
|
|
counter.inc();
|
|
counter.inc();
|
|
|
|
let exporter = PrometheusExporter::new(registry);
|
|
let metrics = exporter.gather_metrics().unwrap();
|
|
|
|
assert!(metrics.contains("test_counter"));
|
|
assert!(metrics.contains("# HELP test_counter Test counter"));
|
|
assert!(metrics.contains("test_counter 2"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_http_export() {
|
|
let registry = Arc::new(Registry::new());
|
|
let counter = Counter::with_opts(Opts::new("http_test", "HTTP test")).unwrap();
|
|
registry.register(Box::new(counter.clone())).unwrap();
|
|
|
|
counter.inc_by(42.0);
|
|
|
|
let exporter = PrometheusExporter::new(registry);
|
|
let buffer = exporter.export_http().unwrap();
|
|
let metrics = String::from_utf8(buffer).expect("INVARIANT: Valid UTF-8 bytes");
|
|
|
|
assert!(metrics.contains("http_test"));
|
|
assert!(metrics.contains("42"));
|
|
}
|
|
}
|