Skip to main content

futu_server/metrics/
prometheus.rs

1use std::sync::Arc;
2use std::sync::atomic::Ordering;
3
4use super::{GatewayMetrics, HourBreakdown};
5
6/// v1.4.90 P1-B: 把 24 小时 hour breakdown 渲染成 Prometheus 行 (with `hour` label).
7///
8/// 输出形如 `<metric_name>{hour="00"} 7\n<metric_name>{hour="01"} 0\n...`
9/// 每个 metric 24 行 (UTC hour 0..23).
10fn 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    /// v1.4.90 P1-B: 输出 Prometheus text exposition 格式, 涵盖:
21    ///
22    /// - 连接 / 请求 / 响应 / 后端连接基础 counter
23    /// - per-cmd_id push counter (cmd_6212_quote / cmd_4716 / cmd_14716 /
24    ///   cmd_5300 / cmd_other) — v1.4.83/84 telnet 已暴露, 此处补 Prometheus
25    /// - per-cmd_id × UTC-hour 24-bucket breakdown (v1.4.84 §14)
26    /// - 订阅 / KeepAlive 计数
27    /// - 延迟 p50/p95/p99 gauge (取最近 1000 样本)
28    ///
29    /// 注册到 [`futu_auth::metrics::Registry`] extension renderer 后, 在
30    /// `/metrics` HTTP 输出末尾自动追加. 见
31    /// [`install_prometheus_extension`].
32    #[must_use]
33    pub fn render_prometheus(&self) -> String {
34        let mut s = String::with_capacity(8192);
35
36        // ===== 连接 =====
37        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        // ===== 请求 =====
66        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        // ===== 后端 =====
89        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        // ===== 推送总数 =====
112        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        // ===== per-cmd push counters (v1.4.83 §14) =====
179        // 这是 P1-B 修复的核心: telnet 已有但 /metrics 之前没暴露
180        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        // ===== per-cmd × UTC hour breakdown (v1.4.84 §14) =====
205        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        // ===== 订阅 =====
235        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        // v1.4.106 codex 0631 F5: dual counter — attempts 触发 vs applied_keys 真生效.
257        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        // v1.4.110 codex audit Round2 P3 #19: cold-cache wait 监控.
273        // hit/total 比看 backend push 延迟健康度, timeout/total 比看常超时.
274        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        // ===== 延迟 (gauge, 取 ring 当前 stats) =====
297        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
322/// v1.4.90 P1-B: 把 [`GatewayMetrics`] 注册为 [`futu_auth::metrics::Registry`]
323/// 的 extension renderer, 让 `/metrics` HTTP 端点自动包含 per-cmd / per-hour
324/// counter.
325///
326/// 调用方 (futu-opend `main.rs`) 在创建 `GatewayMetrics` Arc 之后调一次:
327///
328/// ```ignore
329/// futu_auth::metrics::install(Arc::new(MetricsRegistry::default()));
330/// // ... bridge / server 初始化, 都共享同一份 Arc<GatewayMetrics> ...
331/// futu_server::metrics::install_prometheus_extension(server.metrics().clone());
332/// ```
333///
334/// 多次调用会注册多个 renderer (无害但重复输出); 实际上只该调用一次.
335pub fn install_prometheus_extension(metrics: Arc<GatewayMetrics>) {
336    futu_auth::metrics::register_global_renderer(move || metrics.render_prometheus());
337}