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}