1use std::sync::Arc;
2use std::sync::atomic::Ordering;
3
4use super::{GatewayMetrics, HourBreakdown};
5
6fn render_hour_breakdown_prom(metric_name: &str, hb: &HourBreakdown) -> String {
11 let snap = hb.snapshot();
12 let mut out = String::with_capacity(24 * 60);
13 for (h, v) in snap.iter().enumerate() {
14 out.push_str(&format!("{}{{hour=\"{:02}\"}} {}\n", metric_name, h, v));
15 }
16 out
17}
18
19impl GatewayMetrics {
20 #[must_use]
33 pub fn render_prometheus(&self) -> String {
34 let mut s = String::with_capacity(8192);
35
36 s.push_str("# HELP futu_gateway_connections_total Total accepted client connections\n");
38 s.push_str("# TYPE futu_gateway_connections_total counter\n");
39 s.push_str(&format!(
40 "futu_gateway_connections_total {}\n",
41 self.total_connections.load(Ordering::Relaxed)
42 ));
43 s.push_str(
44 "# HELP futu_gateway_disconnections_total Total client disconnections\n# TYPE futu_gateway_disconnections_total counter\n",
45 );
46 s.push_str(&format!(
47 "futu_gateway_disconnections_total {}\n",
48 self.total_disconnections.load(Ordering::Relaxed)
49 ));
50 s.push_str(
51 "# HELP futu_gateway_rejected_connections_total Connections rejected (limit hit)\n# TYPE futu_gateway_rejected_connections_total counter\n",
52 );
53 s.push_str(&format!(
54 "futu_gateway_rejected_connections_total {}\n",
55 self.rejected_connections.load(Ordering::Relaxed)
56 ));
57 s.push_str(
58 "# HELP futu_gateway_keepalive_timeouts_total KeepAlive timeout disconnects\n# TYPE futu_gateway_keepalive_timeouts_total counter\n",
59 );
60 s.push_str(&format!(
61 "futu_gateway_keepalive_timeouts_total {}\n",
62 self.keepalive_timeouts.load(Ordering::Relaxed)
63 ));
64
65 s.push_str(
67 "# HELP futu_gateway_requests_total Total handled client requests\n# TYPE futu_gateway_requests_total counter\n",
68 );
69 s.push_str(&format!(
70 "futu_gateway_requests_total {}\n",
71 self.total_requests.load(Ordering::Relaxed)
72 ));
73 s.push_str(
74 "# HELP futu_gateway_request_errors_total Handler-returned-None or decryption errors\n# TYPE futu_gateway_request_errors_total counter\n",
75 );
76 s.push_str(&format!(
77 "futu_gateway_request_errors_total {}\n",
78 self.total_request_errors.load(Ordering::Relaxed)
79 ));
80 s.push_str(
81 "# HELP futu_gateway_response_bytes_total Cumulative response payload bytes\n# TYPE futu_gateway_response_bytes_total counter\n",
82 );
83 s.push_str(&format!(
84 "futu_gateway_response_bytes_total {}\n",
85 self.total_response_bytes.load(Ordering::Relaxed)
86 ));
87
88 s.push_str(
90 "# HELP futu_gateway_backend_online Backend connection state (1=online,0=offline)\n# TYPE futu_gateway_backend_online gauge\n",
91 );
92 s.push_str(&format!(
93 "futu_gateway_backend_online {}\n",
94 self.backend_online.load(Ordering::Relaxed)
95 ));
96 s.push_str(
97 "# HELP futu_gateway_backend_reconnects_total Backend reconnect attempts\n# TYPE futu_gateway_backend_reconnects_total counter\n",
98 );
99 s.push_str(&format!(
100 "futu_gateway_backend_reconnects_total {}\n",
101 self.backend_reconnects.load(Ordering::Relaxed)
102 ));
103 s.push_str(
104 "# HELP futu_gateway_backend_reconnect_failures_total Backend reconnect failures\n# TYPE futu_gateway_backend_reconnect_failures_total counter\n",
105 );
106 s.push_str(&format!(
107 "futu_gateway_backend_reconnect_failures_total {}\n",
108 self.backend_reconnect_failures.load(Ordering::Relaxed)
109 ));
110
111 s.push_str(
113 "# HELP futu_gateway_backend_pushes_received_total Pushes received from backend\n# TYPE futu_gateway_backend_pushes_received_total counter\n",
114 );
115 s.push_str(&format!(
116 "futu_gateway_backend_pushes_received_total {}\n",
117 self.backend_pushes_received.load(Ordering::Relaxed)
118 ));
119 s.push_str(
120 "# HELP futu_gateway_backend_control_event_drops_total Backend lifecycle control events dropped before dispatcher processing\n# TYPE futu_gateway_backend_control_event_drops_total counter\n",
121 );
122 s.push_str(&format!(
123 "futu_gateway_backend_control_event_drops_total {}\n",
124 self.backend_control_event_drops.load(Ordering::Relaxed)
125 ));
126 s.push_str(
127 "# HELP futu_gateway_message_center_async_dropped_total CMD5300 option-event work dropped before async dispatcher processing\n# TYPE futu_gateway_message_center_async_dropped_total counter\n",
128 );
129 s.push_str(&format!(
130 "futu_gateway_message_center_async_dropped_total {}\n",
131 self.message_center_async_dropped_total
132 .load(Ordering::Relaxed)
133 ));
134 s.push_str(
135 "# HELP futu_gateway_client_pushes_sent_total Pushes forwarded to clients\n# TYPE futu_gateway_client_pushes_sent_total counter\n",
136 );
137 s.push_str(&format!(
138 "futu_gateway_client_pushes_sent_total {}\n",
139 self.client_pushes_sent.load(Ordering::Relaxed)
140 ));
141 s.push_str(
142 "# HELP futu_gateway_client_push_send_failures_total Client push send failures because the downstream channel was closed\n# TYPE futu_gateway_client_push_send_failures_total counter\n",
143 );
144 s.push_str(&format!(
145 "futu_gateway_client_push_send_failures_total {}\n",
146 self.client_push_send_failures.load(Ordering::Relaxed)
147 ));
148 s.push_str(
149 "# HELP futu_gateway_ordinary_client_push_backpressure_disconnects_total Slow ordinary-push clients closed after their bounded queue filled\n# TYPE futu_gateway_ordinary_client_push_backpressure_disconnects_total counter\n",
150 );
151 s.push_str(&format!(
152 "futu_gateway_ordinary_client_push_backpressure_disconnects_total {}\n",
153 self.ordinary_client_push_backpressure_disconnects
154 .load(Ordering::Relaxed)
155 ));
156 s.push_str(
157 "# HELP futu_gateway_qot_client_push_backpressure_drops_total Quote push frames dropped for clients whose downstream channel is full\n# TYPE futu_gateway_qot_client_push_backpressure_drops_total counter\n",
158 );
159 s.push_str(&format!(
160 "futu_gateway_qot_client_push_backpressure_drops_total {}\n",
161 self.qot_client_push_backpressure_drops
162 .load(Ordering::Relaxed)
163 ));
164 s.push_str(
165 "# HELP futu_gateway_qot_client_push_backpressure_drops_by_sub_type_total Quote push frames dropped by full client channel, grouped by Qot_Common.SubType\n# TYPE futu_gateway_qot_client_push_backpressure_drops_by_sub_type_total counter\n",
166 );
167 for (sub_type, count) in self
168 .qot_client_push_backpressure_drops_per_sub_type()
169 .iter()
170 .enumerate()
171 {
172 s.push_str(&format!(
173 "futu_gateway_qot_client_push_backpressure_drops_by_sub_type_total{{sub_type=\"{}\"}} {}\n",
174 sub_type, count
175 ));
176 }
177
178 s.push_str(
181 "# HELP futu_gateway_backend_pushes_cmd_total Backend pushes by cmd_id (v1.4.83 §14)\n# TYPE futu_gateway_backend_pushes_cmd_total counter\n",
182 );
183 s.push_str(&format!(
184 "futu_gateway_backend_pushes_cmd_total{{cmd=\"6212_quote\"}} {}\n",
185 self.backend_pushes_cmd_quote.load(Ordering::Relaxed)
186 ));
187 s.push_str(&format!(
188 "futu_gateway_backend_pushes_cmd_total{{cmd=\"4716_trade_legacy\"}} {}\n",
189 self.backend_pushes_cmd_trade_legacy.load(Ordering::Relaxed)
190 ));
191 s.push_str(&format!(
192 "futu_gateway_backend_pushes_cmd_total{{cmd=\"14716_trade_new\"}} {}\n",
193 self.backend_pushes_cmd_trade_new.load(Ordering::Relaxed)
194 ));
195 s.push_str(&format!(
196 "futu_gateway_backend_pushes_cmd_total{{cmd=\"5300_msg_center\"}} {}\n",
197 self.backend_pushes_cmd_msg_center.load(Ordering::Relaxed)
198 ));
199 s.push_str(&format!(
200 "futu_gateway_backend_pushes_cmd_total{{cmd=\"other\"}} {}\n",
201 self.backend_pushes_cmd_other.load(Ordering::Relaxed)
202 ));
203
204 s.push_str(
206 "# HELP futu_gateway_backend_pushes_cmd_quote_by_hour Cmd 6212 quote pushes per UTC hour\n# TYPE futu_gateway_backend_pushes_cmd_quote_by_hour counter\n",
207 );
208 s.push_str(&render_hour_breakdown_prom(
209 "futu_gateway_backend_pushes_cmd_quote_by_hour",
210 &self.backend_pushes_cmd_quote_by_hour,
211 ));
212 s.push_str(
213 "# HELP futu_gateway_backend_pushes_cmd_trade_legacy_by_hour Cmd 4716 trade-legacy pushes per UTC hour\n# TYPE futu_gateway_backend_pushes_cmd_trade_legacy_by_hour counter\n",
214 );
215 s.push_str(&render_hour_breakdown_prom(
216 "futu_gateway_backend_pushes_cmd_trade_legacy_by_hour",
217 &self.backend_pushes_cmd_trade_legacy_by_hour,
218 ));
219 s.push_str(
220 "# HELP futu_gateway_backend_pushes_cmd_trade_new_by_hour Cmd 14716 trade-new pushes per UTC hour (v1.4.84 §14 tester subject)\n# TYPE futu_gateway_backend_pushes_cmd_trade_new_by_hour counter\n",
221 );
222 s.push_str(&render_hour_breakdown_prom(
223 "futu_gateway_backend_pushes_cmd_trade_new_by_hour",
224 &self.backend_pushes_cmd_trade_new_by_hour,
225 ));
226 s.push_str(
227 "# HELP futu_gateway_backend_pushes_cmd_msg_center_by_hour Cmd 5300 msg-center pushes per UTC hour\n# TYPE futu_gateway_backend_pushes_cmd_msg_center_by_hour counter\n",
228 );
229 s.push_str(&render_hour_breakdown_prom(
230 "futu_gateway_backend_pushes_cmd_msg_center_by_hour",
231 &self.backend_pushes_cmd_msg_center_by_hour,
232 ));
233
234 s.push_str(
236 "# HELP futu_gateway_qot_subscribe_ops_total Quote subscribe operations\n# TYPE futu_gateway_qot_subscribe_ops_total counter\n",
237 );
238 s.push_str(&format!(
239 "futu_gateway_qot_subscribe_ops_total {}\n",
240 self.qot_subscribe_ops.load(Ordering::Relaxed)
241 ));
242 s.push_str(
243 "# HELP futu_gateway_qot_unsubscribe_ops_total Quote unsubscribe operations\n# TYPE futu_gateway_qot_unsubscribe_ops_total counter\n",
244 );
245 s.push_str(&format!(
246 "futu_gateway_qot_unsubscribe_ops_total {}\n",
247 self.qot_unsubscribe_ops.load(Ordering::Relaxed)
248 ));
249 s.push_str(
250 "# HELP futu_gateway_resubscribe_ops_total Re-subscribe ops after reconnect (legacy, == resubscribe_applied_keys)\n# TYPE futu_gateway_resubscribe_ops_total counter\n",
251 );
252 s.push_str(&format!(
253 "futu_gateway_resubscribe_ops_total {}\n",
254 self.resubscribe_ops.load(Ordering::Relaxed)
255 ));
256 s.push_str(
258 "# HELP futu_gateway_resubscribe_attempts_total Re-subscribe trigger count (each reconnect/staleness loop +=1)\n# TYPE futu_gateway_resubscribe_attempts_total counter\n",
259 );
260 s.push_str(&format!(
261 "futu_gateway_resubscribe_attempts_total {}\n",
262 self.resubscribe_attempts.load(Ordering::Relaxed)
263 ));
264 s.push_str(
265 "# HELP futu_gateway_resubscribe_applied_keys_total Re-subscribe applied keys total (cache resolve OK + backend ack OK)\n# TYPE futu_gateway_resubscribe_applied_keys_total counter\n",
266 );
267 s.push_str(&format!(
268 "futu_gateway_resubscribe_applied_keys_total {}\n",
269 self.resubscribe_applied_keys.load(Ordering::Relaxed)
270 ));
271
272 s.push_str(
275 "# HELP futu_gateway_cold_cache_wait_total Cold-cache wait entries (cache miss + IsSub)\n# TYPE futu_gateway_cold_cache_wait_total counter\n",
276 );
277 s.push_str(&format!(
278 "futu_gateway_cold_cache_wait_total {}\n",
279 self.cold_cache_wait_total.load(Ordering::Relaxed)
280 ));
281 s.push_str(
282 "# HELP futu_gateway_cold_cache_wait_hit_total Cold-cache wait hits (push filled cache within timeout)\n# TYPE futu_gateway_cold_cache_wait_hit_total counter\n",
283 );
284 s.push_str(&format!(
285 "futu_gateway_cold_cache_wait_hit_total {}\n",
286 self.cold_cache_wait_hit.load(Ordering::Relaxed)
287 ));
288 s.push_str(
289 "# HELP futu_gateway_cold_cache_wait_timeout_total Cold-cache wait timeouts (3s elapsed, cache still miss)\n# TYPE futu_gateway_cold_cache_wait_timeout_total counter\n",
290 );
291 s.push_str(&format!(
292 "futu_gateway_cold_cache_wait_timeout_total {}\n",
293 self.cold_cache_wait_timeout.load(Ordering::Relaxed)
294 ));
295
296 let lat = self.latency_stats();
298 s.push_str(
299 "# HELP futu_gateway_request_latency_us Request latency percentiles (microseconds, recent ring)\n# TYPE futu_gateway_request_latency_us gauge\n",
300 );
301 s.push_str(&format!(
302 "futu_gateway_request_latency_us{{quantile=\"p50\"}} {}\n",
303 lat.p50_us
304 ));
305 s.push_str(&format!(
306 "futu_gateway_request_latency_us{{quantile=\"p95\"}} {}\n",
307 lat.p95_us
308 ));
309 s.push_str(&format!(
310 "futu_gateway_request_latency_us{{quantile=\"p99\"}} {}\n",
311 lat.p99_us
312 ));
313 s.push_str(&format!(
314 "futu_gateway_request_latency_us{{quantile=\"max\"}} {}\n",
315 lat.max_us
316 ));
317
318 s
319 }
320}
321
322pub fn install_prometheus_extension(metrics: Arc<GatewayMetrics>) {
336 futu_auth::metrics::register_global_renderer(move || metrics.render_prometheus());
337}