futu_server/metrics/
prometheus.rs1use 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_client_pushes_sent_total Pushes forwarded to clients\n# TYPE futu_gateway_client_pushes_sent_total counter\n",
121 );
122 s.push_str(&format!(
123 "futu_gateway_client_pushes_sent_total {}\n",
124 self.client_pushes_sent.load(Ordering::Relaxed)
125 ));
126 s.push_str(
127 "# 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",
128 );
129 s.push_str(&format!(
130 "futu_gateway_client_push_send_failures_total {}\n",
131 self.client_push_send_failures.load(Ordering::Relaxed)
132 ));
133 s.push_str(
134 "# 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",
135 );
136 s.push_str(&format!(
137 "futu_gateway_qot_client_push_backpressure_drops_total {}\n",
138 self.qot_client_push_backpressure_drops
139 .load(Ordering::Relaxed)
140 ));
141 s.push_str(
142 "# 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",
143 );
144 for (sub_type, count) in self
145 .qot_client_push_backpressure_drops_per_sub_type()
146 .iter()
147 .enumerate()
148 {
149 s.push_str(&format!(
150 "futu_gateway_qot_client_push_backpressure_drops_by_sub_type_total{{sub_type=\"{}\"}} {}\n",
151 sub_type, count
152 ));
153 }
154
155 s.push_str(
158 "# 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",
159 );
160 s.push_str(&format!(
161 "futu_gateway_backend_pushes_cmd_total{{cmd=\"6212_quote\"}} {}\n",
162 self.backend_pushes_cmd_quote.load(Ordering::Relaxed)
163 ));
164 s.push_str(&format!(
165 "futu_gateway_backend_pushes_cmd_total{{cmd=\"4716_trade_legacy\"}} {}\n",
166 self.backend_pushes_cmd_trade_legacy.load(Ordering::Relaxed)
167 ));
168 s.push_str(&format!(
169 "futu_gateway_backend_pushes_cmd_total{{cmd=\"14716_trade_new\"}} {}\n",
170 self.backend_pushes_cmd_trade_new.load(Ordering::Relaxed)
171 ));
172 s.push_str(&format!(
173 "futu_gateway_backend_pushes_cmd_total{{cmd=\"5300_msg_center\"}} {}\n",
174 self.backend_pushes_cmd_msg_center.load(Ordering::Relaxed)
175 ));
176 s.push_str(&format!(
177 "futu_gateway_backend_pushes_cmd_total{{cmd=\"other\"}} {}\n",
178 self.backend_pushes_cmd_other.load(Ordering::Relaxed)
179 ));
180
181 s.push_str(
183 "# 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",
184 );
185 s.push_str(&render_hour_breakdown_prom(
186 "futu_gateway_backend_pushes_cmd_quote_by_hour",
187 &self.backend_pushes_cmd_quote_by_hour,
188 ));
189 s.push_str(
190 "# 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",
191 );
192 s.push_str(&render_hour_breakdown_prom(
193 "futu_gateway_backend_pushes_cmd_trade_legacy_by_hour",
194 &self.backend_pushes_cmd_trade_legacy_by_hour,
195 ));
196 s.push_str(
197 "# 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",
198 );
199 s.push_str(&render_hour_breakdown_prom(
200 "futu_gateway_backend_pushes_cmd_trade_new_by_hour",
201 &self.backend_pushes_cmd_trade_new_by_hour,
202 ));
203 s.push_str(
204 "# 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",
205 );
206 s.push_str(&render_hour_breakdown_prom(
207 "futu_gateway_backend_pushes_cmd_msg_center_by_hour",
208 &self.backend_pushes_cmd_msg_center_by_hour,
209 ));
210
211 s.push_str(
213 "# HELP futu_gateway_qot_subscribe_ops_total Quote subscribe operations\n# TYPE futu_gateway_qot_subscribe_ops_total counter\n",
214 );
215 s.push_str(&format!(
216 "futu_gateway_qot_subscribe_ops_total {}\n",
217 self.qot_subscribe_ops.load(Ordering::Relaxed)
218 ));
219 s.push_str(
220 "# HELP futu_gateway_qot_unsubscribe_ops_total Quote unsubscribe operations\n# TYPE futu_gateway_qot_unsubscribe_ops_total counter\n",
221 );
222 s.push_str(&format!(
223 "futu_gateway_qot_unsubscribe_ops_total {}\n",
224 self.qot_unsubscribe_ops.load(Ordering::Relaxed)
225 ));
226 s.push_str(
227 "# HELP futu_gateway_resubscribe_ops_total Re-subscribe ops after reconnect (legacy, == resubscribe_applied_keys)\n# TYPE futu_gateway_resubscribe_ops_total counter\n",
228 );
229 s.push_str(&format!(
230 "futu_gateway_resubscribe_ops_total {}\n",
231 self.resubscribe_ops.load(Ordering::Relaxed)
232 ));
233 s.push_str(
235 "# HELP futu_gateway_resubscribe_attempts_total Re-subscribe trigger count (each reconnect/staleness loop +=1)\n# TYPE futu_gateway_resubscribe_attempts_total counter\n",
236 );
237 s.push_str(&format!(
238 "futu_gateway_resubscribe_attempts_total {}\n",
239 self.resubscribe_attempts.load(Ordering::Relaxed)
240 ));
241 s.push_str(
242 "# 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",
243 );
244 s.push_str(&format!(
245 "futu_gateway_resubscribe_applied_keys_total {}\n",
246 self.resubscribe_applied_keys.load(Ordering::Relaxed)
247 ));
248
249 s.push_str(
252 "# HELP futu_gateway_cold_cache_wait_total Cold-cache wait entries (cache miss + IsSub)\n# TYPE futu_gateway_cold_cache_wait_total counter\n",
253 );
254 s.push_str(&format!(
255 "futu_gateway_cold_cache_wait_total {}\n",
256 self.cold_cache_wait_total.load(Ordering::Relaxed)
257 ));
258 s.push_str(
259 "# 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",
260 );
261 s.push_str(&format!(
262 "futu_gateway_cold_cache_wait_hit_total {}\n",
263 self.cold_cache_wait_hit.load(Ordering::Relaxed)
264 ));
265 s.push_str(
266 "# 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",
267 );
268 s.push_str(&format!(
269 "futu_gateway_cold_cache_wait_timeout_total {}\n",
270 self.cold_cache_wait_timeout.load(Ordering::Relaxed)
271 ));
272
273 let lat = self.latency_stats();
275 s.push_str(
276 "# HELP futu_gateway_request_latency_us Request latency percentiles (microseconds, recent ring)\n# TYPE futu_gateway_request_latency_us gauge\n",
277 );
278 s.push_str(&format!(
279 "futu_gateway_request_latency_us{{quantile=\"p50\"}} {}\n",
280 lat.p50_us
281 ));
282 s.push_str(&format!(
283 "futu_gateway_request_latency_us{{quantile=\"p95\"}} {}\n",
284 lat.p95_us
285 ));
286 s.push_str(&format!(
287 "futu_gateway_request_latency_us{{quantile=\"p99\"}} {}\n",
288 lat.p99_us
289 ));
290 s.push_str(&format!(
291 "futu_gateway_request_latency_us{{quantile=\"max\"}} {}\n",
292 lat.max_us
293 ));
294
295 s
296 }
297}
298
299pub fn install_prometheus_extension(metrics: Arc<GatewayMetrics>) {
313 futu_auth::metrics::register_global_renderer(move || metrics.render_prometheus());
314}