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_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        // ===== per-cmd push counters (v1.4.83 §14) =====
156        // 这是 P1-B 修复的核心: telnet 已有但 /metrics 之前没暴露
157        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        // ===== per-cmd × UTC hour breakdown (v1.4.84 §14) =====
182        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        // ===== 订阅 =====
212        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        // v1.4.106 codex 0631 F5: dual counter — attempts 触发 vs applied_keys 真生效.
234        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        // v1.4.110 codex audit Round2 P3 #19: cold-cache wait 监控.
250        // hit/total 比看 backend push 延迟健康度, timeout/total 比看常超时.
251        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        // ===== 延迟 (gauge, 取 ring 当前 stats) =====
274        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
299/// v1.4.90 P1-B: 把 [`GatewayMetrics`] 注册为 [`futu_auth::metrics::Registry`]
300/// 的 extension renderer, 让 `/metrics` HTTP 端点自动包含 per-cmd / per-hour
301/// counter.
302///
303/// 调用方 (futu-opend `main.rs`) 在创建 `GatewayMetrics` Arc 之后调一次:
304///
305/// ```ignore
306/// futu_auth::metrics::install(Arc::new(MetricsRegistry::default()));
307/// // ... bridge / server 初始化, 都共享同一份 Arc<GatewayMetrics> ...
308/// futu_server::metrics::install_prometheus_extension(server.metrics().clone());
309/// ```
310///
311/// 多次调用会注册多个 renderer (无害但重复输出); 实际上只该调用一次.
312pub fn install_prometheus_extension(metrics: Arc<GatewayMetrics>) {
313    futu_auth::metrics::register_global_renderer(move || metrics.render_prometheus());
314}