Skip to main content

featherbit/metrics/
mod.rs

1//! Prometheus metrics for the gateway: per-route request counters and
2//! latency histograms plus per-node execution metrics, rendered in the
3//! Prometheus text format at the Admin API's `/metrics` endpoint.
4
5use prometheus::{
6    Encoder, HistogramOpts, HistogramVec, IntCounterVec, Opts, Registry, TextEncoder,
7};
8
9/// Global metrics registry for the gateway.
10///
11/// Owns a dedicated Prometheus [`Registry`] with all collectors
12/// pre-registered. All collectors are internally thread-safe, so a single
13/// instance is shared (behind an `Arc`) between the data-plane and the
14/// Admin API.
15pub struct GatewayMetrics {
16    /// Registry holding every collector below; gathered by [`Self::render`].
17    pub registry: Registry,
18    /// `gateway_requests_total` — requests by `route`, `method`, `status`.
19    pub request_count: IntCounterVec,
20    /// `gateway_request_duration_seconds` — end-to-end request latency
21    /// histogram per `route` (buckets 1ms to 5s).
22    pub request_duration: HistogramVec,
23    /// `gateway_request_errors_total` — one increment per **error record** in
24    /// `Context.errors` at the end of the request, by `route` and `error_code`.
25    ///
26    /// This counts genuine node failures only. Deliberate outcome exits
27    /// (`denied`, `limited`, `broken`, `abort`, `redirect`, `preflight`,
28    /// `routed`, `hit`) append no error record and therefore never increment
29    /// it — track those through `gateway_requests_total`'s `status` label.
30    pub request_errors: IntCounterVec,
31    /// `gateway_node_executions_total` — graph node executions by `policy`,
32    /// `node_id`, `node_type`.
33    pub node_execution_count: IntCounterVec,
34    /// `gateway_node_duration_seconds` — per-node execution latency
35    /// histogram by `policy` and `node_id` (buckets 0.1ms to 500ms).
36    pub node_execution_duration: HistogramVec,
37    /// `gateway_node_errors_total` — node failures by `policy`, `node_id`,
38    /// `error_code`.
39    pub node_errors: IntCounterVec,
40    /// `gateway_consumer_requests_total` — requests attributed to an
41    /// authenticated `consumer`, by `route` (parity with APISIX's prometheus
42    /// per-consumer counter). Recorded by the `prometheus` node, which must be
43    /// placed after the auth node that attaches the consumer.
44    pub consumer_requests: IntCounterVec,
45    /// Counter-store (stores:) backend errors, per named store.
46    // Only incremented by `RedisCounterStore` (`redis-store` feature); a
47    // headless build registers the collector but never reads the field.
48    #[cfg_attr(not(feature = "redis-store"), allow(dead_code))]
49    pub counter_store_errors: IntCounterVec,
50    /// Response-cache outcomes, per backend, store, and event.
51    ///
52    /// `store` is the named `stores:` entry for `policy: redis`, and the
53    /// empty string for `policy: local` (which has no store to name) — the
54    /// same convention `counter_store_errors`/`session_store_errors` would
55    /// use if they had a backend without one.
56    ///
57    /// `error` is the one that matters: a cache degraded to always-miss keeps
58    /// serving correct responses, just slower and with more upstream load, so
59    /// it is invisible in every other signal.
60    pub cache_events: IntCounterVec,
61    /// Session-store (stores:) backend errors, per named store.
62    // Only incremented by `RedisSessionStore` (`redis-store` feature); a
63    // headless build registers the collector but never reads the field.
64    #[cfg_attr(not(feature = "redis-store"), allow(dead_code))]
65    pub session_store_errors: IntCounterVec,
66}
67
68impl GatewayMetrics {
69    /// Creates a fresh registry with all gateway collectors registered.
70    ///
71    /// Panics only if a collector cannot be built or registered, which is
72    /// impossible with these fixed names/labels — treated as a programmer
73    /// error at startup.
74    pub fn new() -> Self {
75        let registry = Registry::new();
76
77        let request_count = IntCounterVec::new(
78            Opts::new("gateway_requests_total", "Total number of requests"),
79            &["route", "method", "status"],
80        )
81        .unwrap();
82
83        let request_duration = HistogramVec::new(
84            HistogramOpts::new(
85                "gateway_request_duration_seconds",
86                "Request duration in seconds",
87            )
88            .buckets(vec![
89                0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 5.0,
90            ]),
91            &["route"],
92        )
93        .unwrap();
94
95        let request_errors = IntCounterVec::new(
96            Opts::new(
97                "gateway_request_errors_total",
98                "Total number of request errors",
99            ),
100            &["route", "error_code"],
101        )
102        .unwrap();
103
104        let node_execution_count = IntCounterVec::new(
105            Opts::new("gateway_node_executions_total", "Total node executions"),
106            &["policy", "node_id", "node_type"],
107        )
108        .unwrap();
109
110        let node_execution_duration = HistogramVec::new(
111            HistogramOpts::new("gateway_node_duration_seconds", "Node execution duration")
112                .buckets(vec![0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5]),
113            &["policy", "node_id"],
114        )
115        .unwrap();
116
117        let node_errors = IntCounterVec::new(
118            Opts::new("gateway_node_errors_total", "Total node errors"),
119            &["policy", "node_id", "error_code"],
120        )
121        .unwrap();
122
123        let consumer_requests = IntCounterVec::new(
124            Opts::new(
125                "gateway_consumer_requests_total",
126                "Total requests per consumer",
127            ),
128            &["consumer", "route"],
129        )
130        .unwrap();
131
132        let counter_store_errors = IntCounterVec::new(
133            Opts::new(
134                "gateway_counter_store_errors_total",
135                "Total counter-store backend errors per named store",
136            ),
137            &["store"],
138        )
139        .unwrap();
140
141        let cache_events = IntCounterVec::new(
142            Opts::new(
143                "gateway_cache_events_total",
144                "Response-cache outcomes per backend, store (empty for policy: local), and event. hit and miss partition every lookup, so the hit rate is hits/(hits+misses); error is an overlapping diagnostic counted alongside the miss it caused, not a fourth bucket",
145            ),
146            &["backend", "store", "event"],
147        )
148        .unwrap();
149
150        let session_store_errors = IntCounterVec::new(
151            Opts::new(
152                "gateway_session_store_errors_total",
153                "Total session-store backend errors per named store",
154            ),
155            &["store"],
156        )
157        .unwrap();
158
159        registry.register(Box::new(request_count.clone())).unwrap();
160        registry
161            .register(Box::new(request_duration.clone()))
162            .unwrap();
163        registry.register(Box::new(request_errors.clone())).unwrap();
164        registry
165            .register(Box::new(node_execution_count.clone()))
166            .unwrap();
167        registry
168            .register(Box::new(node_execution_duration.clone()))
169            .unwrap();
170        registry.register(Box::new(node_errors.clone())).unwrap();
171        registry
172            .register(Box::new(consumer_requests.clone()))
173            .unwrap();
174        registry
175            .register(Box::new(counter_store_errors.clone()))
176            .unwrap();
177        registry.register(Box::new(cache_events.clone())).unwrap();
178        registry
179            .register(Box::new(session_store_errors.clone()))
180            .unwrap();
181
182        Self {
183            registry,
184            request_count,
185            request_duration,
186            request_errors,
187            node_execution_count,
188            node_execution_duration,
189            node_errors,
190            consumer_requests,
191            counter_store_errors,
192            cache_events,
193            session_store_errors,
194        }
195    }
196
197    /// Renders all registered metrics in the Prometheus text exposition
198    /// format, as served by the Admin API's `/metrics` endpoint.
199    pub fn render(&self) -> String {
200        let encoder = TextEncoder::new();
201        let metric_families = self.registry.gather();
202        let mut buffer = Vec::new();
203        encoder.encode(&metric_families, &mut buffer).unwrap();
204        String::from_utf8(buffer).unwrap()
205    }
206}