use crate::telemetry::{
GatewayTelemetryExportError, GatewayTelemetryExporter, GatewayTelemetryRegistry,
GatewayTelemetrySnapshot, RouteOutcome, RouteSample,
};
use appcore_types::CapabilityName;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
const MESH_ROUTE_CAPABILITY: &str = "runtime.gateway.mesh";
#[derive(Debug, Default)]
pub struct GatewayMetrics {
active_workers: AtomicU64,
active_clients: AtomicU64,
messages_routed: AtomicU64,
routing_failures: AtomicU64,
telemetry: GatewayTelemetryRegistry,
}
impl GatewayMetrics {
pub fn new() -> Arc<Self> {
Arc::new(Self::default())
}
pub fn worker_connected(&self) {
self.active_workers.fetch_add(1, Ordering::Relaxed);
}
pub fn worker_disconnected(&self) {
saturating_decrement(&self.active_workers);
}
pub fn client_connected(&self) {
self.active_clients.fetch_add(1, Ordering::Relaxed);
}
pub fn client_disconnected(&self) {
saturating_decrement(&self.active_clients);
}
pub fn message_routed(&self) {
self.messages_routed.fetch_add(1, Ordering::Relaxed);
}
pub fn routing_failure(&self) {
self.routing_failures.fetch_add(1, Ordering::Relaxed);
}
pub fn active_workers(&self) -> u64 {
self.active_workers.load(Ordering::Relaxed)
}
pub fn active_clients(&self) -> u64 {
self.active_clients.load(Ordering::Relaxed)
}
pub fn messages_routed(&self) -> u64 {
self.messages_routed.load(Ordering::Relaxed)
}
pub fn routing_failures(&self) -> u64 {
self.routing_failures.load(Ordering::Relaxed)
}
pub fn telemetry_snapshot(&self) -> GatewayTelemetrySnapshot {
self.telemetry.snapshot()
}
pub fn export_telemetry(
&self,
exporter: &dyn GatewayTelemetryExporter,
) -> Result<(), GatewayTelemetryExportError> {
let snapshot = self.telemetry_snapshot();
exporter.export(&snapshot).inspect_err(|_| {
self.telemetry.export_failure();
})
}
pub fn route_retried(&self) {
self.telemetry.retry();
}
pub(crate) fn route_started(
self: &Arc<Self>,
capability: Option<&CapabilityName>,
payload_bytes: usize,
) -> RouteObservation {
self.telemetry.route_started();
RouteObservation {
metrics: Arc::clone(self),
capability: capability.map_or_else(
|| MESH_ROUTE_CAPABILITY.to_string(),
|value| value.as_str().to_string(),
),
started: Instant::now(),
dispatched: None,
lock_wait_ns: 0,
payload_bytes: u64::try_from(payload_bytes).unwrap_or(u64::MAX),
outcome: RouteOutcome::TransportFailure,
}
}
pub(crate) fn worker_reconnected(&self) {
self.telemetry.reconnect();
}
pub(crate) fn worker_route_admitted(&self, inflight: u64) {
self.telemetry.observe_worker_inflight(inflight);
}
pub(crate) fn authentication_failure(&self) {
self.telemetry.authentication_failure();
}
}
pub(crate) struct RouteObservation {
metrics: Arc<GatewayMetrics>,
capability: String,
started: Instant,
dispatched: Option<Instant>,
lock_wait_ns: u64,
payload_bytes: u64,
outcome: RouteOutcome,
}
impl RouteObservation {
pub(crate) fn set_capability(&mut self, capability: &CapabilityName) {
self.capability = capability.as_str().to_string();
}
pub(crate) fn record_lock_wait(&mut self, wait: Duration) {
self.lock_wait_ns = self.lock_wait_ns.saturating_add(duration_ns(wait));
}
pub(crate) fn observe_queue_depth(&self, depth: usize) {
self.metrics.telemetry.observe_queue_depth(depth);
}
pub(crate) fn dispatched(&mut self) {
self.dispatched = Some(Instant::now());
}
pub(crate) fn finish(&mut self, outcome: RouteOutcome) {
self.outcome = outcome;
}
}
impl Drop for RouteObservation {
fn drop(&mut self) {
self.metrics.telemetry.route_finished(RouteSample {
capability: self.capability.clone(),
outcome: self.outcome,
latency_ns: duration_ns(self.started.elapsed()),
worker_wait_ns: self
.dispatched
.map(|started| duration_ns(started.elapsed())),
lock_wait_ns: self.lock_wait_ns,
payload_bytes: self.payload_bytes,
});
}
}
fn saturating_decrement(counter: &AtomicU64) {
let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
Some(value.saturating_sub(1))
});
}
fn duration_ns(duration: Duration) -> u64 {
u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
}