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>
347 lines
12 KiB
Rust
347 lines
12 KiB
Rust
//! Backend Proxy Metrics
|
|
//!
|
|
//! Tracks performance and health for backend gRPC services:
|
|
//! - Trading Service
|
|
//! - Backtesting Service
|
|
//! - ML Training Service
|
|
|
|
use prometheus::{CounterVec, Histogram, HistogramOpts, HistogramVec, IntGaugeVec, Opts, Registry};
|
|
|
|
/// Backend proxy and routing metrics
|
|
pub struct ProxyMetrics {
|
|
// === Backend Request Metrics ===
|
|
/// Total requests to each backend service
|
|
pub backend_requests_total: CounterVec,
|
|
/// Successful backend requests
|
|
pub backend_requests_success: CounterVec,
|
|
/// Failed backend requests
|
|
pub backend_requests_failure: CounterVec,
|
|
|
|
// === Backend Latencies (milliseconds) ===
|
|
/// Request latency to backend services
|
|
pub backend_request_duration_ms: HistogramVec,
|
|
/// gRPC connection establishment time
|
|
pub backend_connection_duration_ms: HistogramVec,
|
|
|
|
// === Circuit Breaker State ===
|
|
/// Circuit breaker state per service (0=closed, 1=half-open, 2=open)
|
|
pub circuit_breaker_state: IntGaugeVec,
|
|
/// Circuit breaker trips (transitions to open)
|
|
pub circuit_breaker_trips: CounterVec,
|
|
/// Circuit breaker recoveries (transitions to closed)
|
|
pub circuit_breaker_recoveries: CounterVec,
|
|
|
|
// === Connection Pool ===
|
|
/// Active connections per backend
|
|
pub connection_pool_active: IntGaugeVec,
|
|
/// Idle connections per backend
|
|
pub connection_pool_idle: IntGaugeVec,
|
|
/// Connection pool max size
|
|
pub connection_pool_max: IntGaugeVec,
|
|
/// Connection pool wait time (when exhausted)
|
|
pub connection_pool_wait_duration_ms: HistogramVec,
|
|
|
|
// === Health Checks ===
|
|
/// Health check successes per service
|
|
pub health_check_success: CounterVec,
|
|
/// Health check failures per service
|
|
pub health_check_failure: CounterVec,
|
|
/// Health check latency
|
|
pub health_check_duration_ms: HistogramVec,
|
|
/// Current health status (0=unhealthy, 1=healthy)
|
|
pub health_status: IntGaugeVec,
|
|
|
|
// === gRPC Error Breakdown ===
|
|
/// gRPC errors by status code and service
|
|
pub grpc_errors: CounterVec,
|
|
/// gRPC timeouts per service
|
|
pub grpc_timeouts: CounterVec,
|
|
|
|
// === Request Routing ===
|
|
/// Requests routed to each method
|
|
pub requests_by_method: CounterVec,
|
|
/// Routing latency (method resolution)
|
|
pub routing_duration_us: Histogram,
|
|
}
|
|
|
|
impl ProxyMetrics {
|
|
/// Create new proxy metrics and register with Prometheus
|
|
pub fn new(registry: &Registry) -> Result<Self, prometheus::Error> {
|
|
// === Backend Request Metrics ===
|
|
let backend_requests_total = CounterVec::new(
|
|
Opts::new(
|
|
"api_backend_requests_total",
|
|
"Total requests to backend services",
|
|
),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(backend_requests_total.clone()))?;
|
|
|
|
let backend_requests_success = CounterVec::new(
|
|
Opts::new(
|
|
"api_backend_requests_success",
|
|
"Successful backend requests",
|
|
),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(backend_requests_success.clone()))?;
|
|
|
|
let backend_requests_failure = CounterVec::new(
|
|
Opts::new(
|
|
"api_backend_requests_failure",
|
|
"Failed backend requests",
|
|
),
|
|
&["service", "error_type"],
|
|
)?;
|
|
registry.register(Box::new(backend_requests_failure.clone()))?;
|
|
|
|
// === Backend Latencies (milliseconds) ===
|
|
let backend_request_duration_ms = HistogramVec::new(
|
|
HistogramOpts::new(
|
|
"api_backend_request_duration_milliseconds",
|
|
"Backend request latency in milliseconds",
|
|
)
|
|
.buckets(vec![
|
|
1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0,
|
|
]),
|
|
&["service", "method"],
|
|
)?;
|
|
registry.register(Box::new(backend_request_duration_ms.clone()))?;
|
|
|
|
let backend_connection_duration_ms = HistogramVec::new(
|
|
HistogramOpts::new(
|
|
"api_backend_connection_duration_milliseconds",
|
|
"gRPC connection establishment time in milliseconds",
|
|
)
|
|
.buckets(vec![1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0]),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(backend_connection_duration_ms.clone()))?;
|
|
|
|
// === Circuit Breaker State ===
|
|
let circuit_breaker_state = IntGaugeVec::new(
|
|
Opts::new(
|
|
"api_circuit_breaker_state",
|
|
"Circuit breaker state (0=closed, 1=half-open, 2=open)",
|
|
),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(circuit_breaker_state.clone()))?;
|
|
|
|
let circuit_breaker_trips = CounterVec::new(
|
|
Opts::new(
|
|
"api_circuit_breaker_trips",
|
|
"Circuit breaker trips to open state",
|
|
),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(circuit_breaker_trips.clone()))?;
|
|
|
|
let circuit_breaker_recoveries = CounterVec::new(
|
|
Opts::new(
|
|
"api_circuit_breaker_recoveries",
|
|
"Circuit breaker recoveries to closed state",
|
|
),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(circuit_breaker_recoveries.clone()))?;
|
|
|
|
// === Connection Pool ===
|
|
let connection_pool_active = IntGaugeVec::new(
|
|
Opts::new(
|
|
"api_connection_pool_active",
|
|
"Active connections per backend",
|
|
),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(connection_pool_active.clone()))?;
|
|
|
|
let connection_pool_idle = IntGaugeVec::new(
|
|
Opts::new(
|
|
"api_connection_pool_idle",
|
|
"Idle connections per backend",
|
|
),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(connection_pool_idle.clone()))?;
|
|
|
|
let connection_pool_max = IntGaugeVec::new(
|
|
Opts::new(
|
|
"api_connection_pool_max",
|
|
"Connection pool max size per backend",
|
|
),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(connection_pool_max.clone()))?;
|
|
|
|
let connection_pool_wait_duration_ms = HistogramVec::new(
|
|
HistogramOpts::new(
|
|
"api_connection_pool_wait_duration_milliseconds",
|
|
"Connection pool wait time when exhausted",
|
|
)
|
|
.buckets(vec![1.0, 5.0, 10.0, 25.0, 50.0, 100.0]),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(connection_pool_wait_duration_ms.clone()))?;
|
|
|
|
// === Health Checks ===
|
|
let health_check_success = CounterVec::new(
|
|
Opts::new(
|
|
"api_health_check_success",
|
|
"Health check successes per service",
|
|
),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(health_check_success.clone()))?;
|
|
|
|
let health_check_failure = CounterVec::new(
|
|
Opts::new(
|
|
"api_health_check_failure",
|
|
"Health check failures per service",
|
|
),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(health_check_failure.clone()))?;
|
|
|
|
let health_check_duration_ms = HistogramVec::new(
|
|
HistogramOpts::new(
|
|
"api_health_check_duration_milliseconds",
|
|
"Health check latency in milliseconds",
|
|
)
|
|
.buckets(vec![10.0, 50.0, 100.0, 250.0, 500.0, 1000.0]),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(health_check_duration_ms.clone()))?;
|
|
|
|
let health_status = IntGaugeVec::new(
|
|
Opts::new(
|
|
"api_health_status",
|
|
"Current health status (0=unhealthy, 1=healthy)",
|
|
),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(health_status.clone()))?;
|
|
|
|
// === gRPC Error Breakdown ===
|
|
let grpc_errors = CounterVec::new(
|
|
Opts::new("api_grpc_errors", "gRPC errors by status code"),
|
|
&["service", "status_code"],
|
|
)?;
|
|
registry.register(Box::new(grpc_errors.clone()))?;
|
|
|
|
let grpc_timeouts = CounterVec::new(
|
|
Opts::new("api_grpc_timeouts", "gRPC timeouts per service"),
|
|
&["service"],
|
|
)?;
|
|
registry.register(Box::new(grpc_timeouts.clone()))?;
|
|
|
|
// === Request Routing ===
|
|
let requests_by_method = CounterVec::new(
|
|
Opts::new("api_requests_by_method", "Requests by gRPC method"),
|
|
&["service", "method"],
|
|
)?;
|
|
registry.register(Box::new(requests_by_method.clone()))?;
|
|
|
|
let routing_duration_us = Histogram::with_opts(
|
|
HistogramOpts::new(
|
|
"api_routing_duration_microseconds",
|
|
"Routing decision latency in microseconds",
|
|
)
|
|
.buckets(vec![0.1, 0.5, 1.0, 2.0, 5.0, 10.0]),
|
|
)?;
|
|
registry.register(Box::new(routing_duration_us.clone()))?;
|
|
|
|
Ok(Self {
|
|
backend_requests_total,
|
|
backend_requests_success,
|
|
backend_requests_failure,
|
|
backend_request_duration_ms,
|
|
backend_connection_duration_ms,
|
|
circuit_breaker_state,
|
|
circuit_breaker_trips,
|
|
circuit_breaker_recoveries,
|
|
connection_pool_active,
|
|
connection_pool_idle,
|
|
connection_pool_max,
|
|
connection_pool_wait_duration_ms,
|
|
health_check_success,
|
|
health_check_failure,
|
|
health_check_duration_ms,
|
|
health_status,
|
|
grpc_errors,
|
|
grpc_timeouts,
|
|
requests_by_method,
|
|
routing_duration_us,
|
|
})
|
|
}
|
|
|
|
/// Record successful backend request
|
|
pub fn record_backend_success(&self, service: &str, method: &str, duration_ms: f64) {
|
|
self.backend_requests_total
|
|
.with_label_values(&[service])
|
|
.inc();
|
|
self.backend_requests_success
|
|
.with_label_values(&[service])
|
|
.inc();
|
|
self.backend_request_duration_ms
|
|
.with_label_values(&[service, method])
|
|
.observe(duration_ms);
|
|
self.requests_by_method
|
|
.with_label_values(&[service, method])
|
|
.inc();
|
|
}
|
|
|
|
/// Record failed backend request
|
|
pub fn record_backend_failure(&self, service: &str, error_type: &str) {
|
|
self.backend_requests_total
|
|
.with_label_values(&[service])
|
|
.inc();
|
|
self.backend_requests_failure
|
|
.with_label_values(&[service, error_type])
|
|
.inc();
|
|
}
|
|
|
|
/// Update circuit breaker state (0=closed, 1=half-open, 2=open)
|
|
pub fn update_circuit_breaker_state(&self, service: &str, state: i64) {
|
|
self.circuit_breaker_state
|
|
.with_label_values(&[service])
|
|
.set(state);
|
|
}
|
|
|
|
/// Record circuit breaker trip
|
|
pub fn record_circuit_breaker_trip(&self, service: &str) {
|
|
self.circuit_breaker_trips
|
|
.with_label_values(&[service])
|
|
.inc();
|
|
self.update_circuit_breaker_state(service, 2); // 2 = open
|
|
}
|
|
|
|
/// Record circuit breaker recovery
|
|
pub fn record_circuit_breaker_recovery(&self, service: &str) {
|
|
self.circuit_breaker_recoveries
|
|
.with_label_values(&[service])
|
|
.inc();
|
|
self.update_circuit_breaker_state(service, 0); // 0 = closed
|
|
}
|
|
|
|
/// Update connection pool stats
|
|
pub fn update_connection_pool(&self, service: &str, active: i64, idle: i64, max: i64) {
|
|
self.connection_pool_active
|
|
.with_label_values(&[service])
|
|
.set(active);
|
|
self.connection_pool_idle
|
|
.with_label_values(&[service])
|
|
.set(idle);
|
|
self.connection_pool_max
|
|
.with_label_values(&[service])
|
|
.set(max);
|
|
}
|
|
|
|
/// Update health status (0=unhealthy, 1=healthy)
|
|
pub fn update_health_status(&self, service: &str, healthy: bool) {
|
|
self.health_status
|
|
.with_label_values(&[service])
|
|
.set(if healthy { 1 } else { 0 });
|
|
}
|
|
}
|