Skip to main content

futu_server/
metrics.rs

1// 网关运行时监控指标
2//
3// 使用原子计数器,零开销采集,无需外部依赖。
4// 通过 telnet `show_metrics` 命令查看。
5// v1.4.90 P1-B: 也通过 [`GatewayMetrics::render_prometheus`] 暴露到
6// `/metrics` HTTP 端点 (经 [`futu_auth::metrics::Registry`] extension renderer
7// 注册). 之前 v1.4.83/84 声称这些 counter 在 Prometheus 但仅在 telnet 输出.
8
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::Instant;
11
12use chrono::{Timelike, Utc};
13use parking_lot::RwLock;
14
15mod prometheus;
16pub use prometheus::install_prometheus_extension;
17
18/// v1.4.84 §14: per-cmd_id per-UTC-hour breakdown for monitoring.
19///
20/// 让 tester CI 长窗口 job 能做时段异常检测 (CMD14716 UTC 15-18 window).
21/// 单独结构, 各 cmd counter 独立持有一组 24 bucket.
22#[derive(Debug)]
23pub struct HourBreakdown {
24    counters: [AtomicU64; 24],
25}
26
27impl HourBreakdown {
28    pub const fn new() -> Self {
29        Self {
30            counters: [const { AtomicU64::new(0) }; 24],
31        }
32    }
33
34    /// Bump the counter for current UTC hour (0..23).
35    pub fn bump_now(&self) {
36        let hour = Utc::now().hour() as usize;
37        if hour < 24 {
38            self.counters[hour].fetch_add(1, Ordering::Relaxed);
39        }
40    }
41
42    /// Read counter for specific hour (0..23). Out-of-range returns 0.
43    pub fn get(&self, hour: usize) -> u64 {
44        self.counters
45            .get(hour)
46            .map(|a| a.load(Ordering::Relaxed))
47            .unwrap_or(0)
48    }
49
50    /// Return snapshot of all 24 hours as array.
51    pub fn snapshot(&self) -> [u64; 24] {
52        let mut out = [0u64; 24];
53        for (i, c) in self.counters.iter().enumerate() {
54            out[i] = c.load(Ordering::Relaxed);
55        }
56        out
57    }
58}
59
60impl Default for HourBreakdown {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66/// 网关运行时监控指标
67pub struct GatewayMetrics {
68    /// 网关启动时间
69    pub start_time: Instant,
70
71    // ===== 连接指标 =====
72    /// 累计接受的客户端连接数
73    pub total_connections: AtomicU64,
74    /// 累计客户端断开数
75    pub total_disconnections: AtomicU64,
76    /// 被拒绝的连接数(超过上限)
77    pub rejected_connections: AtomicU64,
78
79    // ===== 请求指标 =====
80    /// 累计处理的请求数
81    pub total_requests: AtomicU64,
82    /// 累计请求错误数(handler 返回 None 或解密失败)
83    pub total_request_errors: AtomicU64,
84    /// 累计响应字节数
85    pub total_response_bytes: AtomicU64,
86
87    // ===== 后端指标 =====
88    /// 后端重连次数
89    pub backend_reconnects: AtomicU64,
90    /// 后端重连失败次数
91    pub backend_reconnect_failures: AtomicU64,
92    /// 最近一次重连时间 (Unix 毫秒, 0=未重连过)
93    pub last_reconnect_ms: AtomicU64,
94    /// 后端是否在线 (1=online, 0=offline)
95    pub backend_online: AtomicU64,
96
97    // ===== 推送指标 =====
98    /// 后端收到的推送数 (CMD 6212 / 4716 / 5300 等)
99    pub backend_pushes_received: AtomicU64,
100    /// 向客户端发送的推送数
101    pub client_pushes_sent: AtomicU64,
102    /// 向客户端推送时 client channel 已关闭导致发送失败的次数
103    pub client_push_send_failures: AtomicU64,
104    /// Ordinary push 队列饱和后首次请求关闭慢客户端的次数。
105    ///
106    /// 同一连接从 open 进入 closing 只计一次;与 QOT 的逐帧 drop 指标分离。
107    pub ordinary_client_push_backpressure_disconnects: AtomicU64,
108    /// 行情推送 fanout 时 client channel 已满、仅丢该慢客户端本帧的次数。
109    ///
110    /// 注意: `qot_push_dropped_total` 统计 backend → dispatcher 队列 drop;
111    /// 本 counter 统计 dispatcher → client channel backpressure,语义不同。
112    pub qot_client_push_backpressure_drops: AtomicU64,
113    /// 按 SubType 拆分的 dispatcher → client channel backpressure drop 计数。
114    /// 桶 0 = 未知 / 首推未携带 SubType;桶 1..=17 = 对应 Qot_Common.SubType。
115    pub qot_client_push_backpressure_drops_by_sub_type: [AtomicU64; 18],
116    // v1.4.83 §14 Phase 4: per-cmd_id push 细分计数 (monitoring)
117    /// CMD 6212 quote push 计数
118    pub backend_pushes_cmd_quote: AtomicU64,
119    /// CMD 4716 trade notify (legacy channel) 计数
120    pub backend_pushes_cmd_trade_legacy: AtomicU64,
121    /// CMD 14716 trade notify (v1.4.41 new channel) 计数 — tester §14 追踪
122    pub backend_pushes_cmd_trade_new: AtomicU64,
123    /// CMD 5300 msg-center push 计数
124    pub backend_pushes_cmd_msg_center: AtomicU64,
125    /// 其他未路由 CMD push 计数
126    pub backend_pushes_cmd_other: AtomicU64,
127    /// Backend lifecycle control events that could not enter the dispatcher
128    /// queue. This is distinct from dropping a QOT payload frame.
129    pub backend_control_event_drops: AtomicU64,
130    /// CMD5300 category=6 async option-event work that could not enter the
131    /// dispatcher queue. Kept separate from quote payload and lifecycle drops.
132    pub message_center_async_dropped_total: AtomicU64,
133
134    // v1.4.84 §14: per-cmd_id × UTC-hour 分桶 (监控深化 — tester CI 长窗口
135    // job 用于 "CMD14716 UTC 15-18 异常时段" 检测). cmd_other 不分时段.
136    /// CMD 6212 quote push per-UTC-hour 计数
137    pub backend_pushes_cmd_quote_by_hour: HourBreakdown,
138    /// CMD 4716 trade notify (legacy) per-UTC-hour 计数
139    pub backend_pushes_cmd_trade_legacy_by_hour: HourBreakdown,
140    /// CMD 14716 trade notify (new) per-UTC-hour 计数 — tester §14 主角
141    pub backend_pushes_cmd_trade_new_by_hour: HourBreakdown,
142    /// CMD 5300 msg-center push per-UTC-hour 计数
143    pub backend_pushes_cmd_msg_center_by_hour: HourBreakdown,
144
145    // ===== 订阅指标 =====
146    /// 行情订阅操作次数
147    pub qot_subscribe_ops: AtomicU64,
148    /// 行情退订操作次数
149    pub qot_unsubscribe_ops: AtomicU64,
150
151    // ===== v1.4.110 codex audit Round2 P3 #19: cold-cache wait 监控 =====
152    //
153    // GetBasicQot / GetOrderBook cache miss + 已订阅 → cold-cache wait 路径
154    // (Pull_SubData 主动拉 + 最多 3s 等 push). ops 用 hit/total 比看 backend
155    // push 延迟健康度, timeout/total 比看 cold-cache wait 是否常超时.
156    /// cold-cache wait 进入次数 (cache miss + IsSub, 触发 wait)
157    pub cold_cache_wait_total: AtomicU64,
158    /// cold-cache wait 命中次数 (3s 内 push 写 cache → re-read 拿到值)
159    pub cold_cache_wait_hit: AtomicU64,
160    /// cold-cache wait 超时次数 (3s timeout 仍 cache miss)
161    pub cold_cache_wait_timeout: AtomicU64,
162    /// 重连后重新订阅次数 (legacy, == applied_keys 累加; v1.4.106 codex 0631
163    /// F5 起仍 bump 向后兼容旧 dashboard, 等价新 `resubscribe_applied_keys`).
164    pub resubscribe_ops: AtomicU64,
165
166    // ===== v1.4.106 codex 0631 F5 [P3]: dual resubscribe counter =====
167    //
168    // 老 `resubscribe_ops_total` 把"触发数"和"真生效 keys 数"混一桶, 看不出
169    // partial / cache miss 的 silent loss. 拆 dual:
170    //   - resubscribe_attempts_total: 触发次数 (本 reconnect / staleness 触
171    //     发了 N 次 resubscribe, 不论结果).
172    //   - resubscribe_applied_keys_total: 真生效 keys 数 (cache resolve OK +
173    //     backend ack OK 的 (sec_key, sub_type) 对). 部分失败时 < attempts.
174    //
175    // ratio applied/attempts < 1.0 显著 → ghost subs / cache miss / backend
176    // partial reject 信号. legacy resubscribe_ops_total 仍 bump (向后兼容).
177    /// **v1.4.106 codex 0631 F5 [P3]**: resubscribe 触发次数 (每次 reconnect /
178    /// staleness loop 触发 += 1). 与 applied_keys 对比 ratio 看 partial 程度.
179    pub resubscribe_attempts: AtomicU64,
180    /// **v1.4.106 codex 0631 F5 [P3]**: resubscribe 真生效 keys 数 (累积).
181    /// applied < attempts × global_keys → 部分 partial.
182    pub resubscribe_applied_keys: AtomicU64,
183
184    // ===== v1.4.106 codex 1140 F8: 行情 push 投递失败计数 =====
185    //
186    // 之前 `bridge::push_parser` 用 `let _ = push_tx.try_send(event)` 静默吞错,
187    // cache 已更新但 subscriber 收不到 push (audit Finding 8). 加 metric +
188    // warn log 让 channel full / closed 立即可观测.
189    /// CMD 6212 行情 push (BasicQot/OrderBook/Ticker/RT/KL/Broker/...) 因
190    /// `push_tx` 队列满或关闭被 drop 的总次数 (累积).
191    pub qot_push_dropped_total: AtomicU64,
192    /// 按 SubType 拆分的 drop 计数 (proto Qot_Common.SubType: 0..=17, 共 18 桶).
193    /// 桶 0 = "未知 / 不属于任何已知 SubType" (兜底, e.g. 拼错的 sub_type).
194    /// 桶 1..=17 = 对应 SubType. v1.4.106 codex 1140 F8 加.
195    pub qot_push_dropped_by_sub_type: [AtomicU64; 18],
196
197    // ===== KeepAlive 指标 =====
198    /// KeepAlive 超时断开数
199    pub keepalive_timeouts: AtomicU64,
200
201    // ===== 延迟采样 =====
202    /// 最近 N 个请求延迟的环形缓冲 (纳秒)
203    latency_ring: RwLock<LatencyRing>,
204}
205
206/// 延迟环形缓冲 — 保留最近 1000 个采样
207struct LatencyRing {
208    buf: Vec<u64>,
209    pos: usize,
210    count: u64,
211    total_ns: u64,
212}
213
214const LATENCY_RING_SIZE: usize = 1000;
215
216impl LatencyRing {
217    fn new() -> Self {
218        Self {
219            buf: vec![0u64; LATENCY_RING_SIZE],
220            pos: 0,
221            count: 0,
222            total_ns: 0,
223        }
224    }
225
226    fn push(&mut self, ns: u64) {
227        // 减去被覆盖的旧值
228        if self.count >= LATENCY_RING_SIZE as u64 {
229            self.total_ns = self.total_ns.saturating_sub(self.buf[self.pos]);
230        }
231        self.buf[self.pos] = ns;
232        self.total_ns += ns;
233        self.pos = (self.pos + 1) % LATENCY_RING_SIZE;
234        self.count += 1;
235    }
236
237    fn stats(&self) -> LatencyStats {
238        let n = self.count.min(LATENCY_RING_SIZE as u64) as usize;
239        if n == 0 {
240            return LatencyStats::default();
241        }
242
243        let mut samples: Vec<u64> = if self.count >= LATENCY_RING_SIZE as u64 {
244            self.buf.clone()
245        } else {
246            self.buf[..n].to_vec()
247        };
248        samples.sort_unstable();
249
250        LatencyStats {
251            count: self.count,
252            avg_us: (self.total_ns / n as u64) / 1000,
253            p50_us: samples[n / 2] / 1000,
254            p95_us: samples[(n as f64 * 0.95) as usize] / 1000,
255            p99_us: samples[(n as f64 * 0.99).min((n - 1) as f64) as usize] / 1000,
256            max_us: samples[n - 1] / 1000,
257        }
258    }
259}
260
261/// 延迟统计摘要 (微秒)
262#[derive(Default)]
263pub struct LatencyStats {
264    /// 总采样数
265    pub count: u64,
266    /// 平均延迟 (微秒)
267    pub avg_us: u64,
268    /// P50 延迟
269    pub p50_us: u64,
270    /// P95 延迟
271    pub p95_us: u64,
272    /// P99 延迟
273    pub p99_us: u64,
274    /// 最大延迟
275    pub max_us: u64,
276}
277
278/// v1.4.84 §14: 把 24 小时 counters snapshot 格式化为空格分隔的单行.
279///
280/// 输出格式: `"h00=N h01=N ... h23=N"` — 便于人眼 scan 时段异常,
281/// 同时保持 parseable (awk / grep / Prometheus textfile).
282fn format_hour_row(hb: &HourBreakdown) -> String {
283    let snap = hb.snapshot();
284    let mut out = String::with_capacity(24 * 10);
285    for (i, v) in snap.iter().enumerate() {
286        if i > 0 {
287            out.push(' ');
288        }
289        out.push_str(&format!("h{:02}={}", i, v));
290    }
291    out
292}
293
294fn qot_sub_type_bucket(sub_type: i32) -> usize {
295    if (0..18).contains(&sub_type) {
296        sub_type as usize
297    } else {
298        0
299    }
300}
301
302impl GatewayMetrics {
303    pub fn new() -> Self {
304        Self {
305            start_time: Instant::now(),
306            total_connections: AtomicU64::new(0),
307            total_disconnections: AtomicU64::new(0),
308            rejected_connections: AtomicU64::new(0),
309            total_requests: AtomicU64::new(0),
310            total_request_errors: AtomicU64::new(0),
311            total_response_bytes: AtomicU64::new(0),
312            backend_reconnects: AtomicU64::new(0),
313            backend_reconnect_failures: AtomicU64::new(0),
314            last_reconnect_ms: AtomicU64::new(0),
315            backend_online: AtomicU64::new(1),
316            backend_pushes_received: AtomicU64::new(0),
317            client_pushes_sent: AtomicU64::new(0),
318            client_push_send_failures: AtomicU64::new(0),
319            ordinary_client_push_backpressure_disconnects: AtomicU64::new(0),
320            qot_client_push_backpressure_drops: AtomicU64::new(0),
321            qot_client_push_backpressure_drops_by_sub_type: [const { AtomicU64::new(0) }; 18],
322            backend_pushes_cmd_quote: AtomicU64::new(0),
323            backend_pushes_cmd_trade_legacy: AtomicU64::new(0),
324            backend_pushes_cmd_trade_new: AtomicU64::new(0),
325            backend_pushes_cmd_msg_center: AtomicU64::new(0),
326            backend_pushes_cmd_other: AtomicU64::new(0),
327            backend_control_event_drops: AtomicU64::new(0),
328            message_center_async_dropped_total: AtomicU64::new(0),
329            backend_pushes_cmd_quote_by_hour: HourBreakdown::new(),
330            backend_pushes_cmd_trade_legacy_by_hour: HourBreakdown::new(),
331            backend_pushes_cmd_trade_new_by_hour: HourBreakdown::new(),
332            backend_pushes_cmd_msg_center_by_hour: HourBreakdown::new(),
333            qot_subscribe_ops: AtomicU64::new(0),
334            qot_unsubscribe_ops: AtomicU64::new(0),
335            cold_cache_wait_total: AtomicU64::new(0),
336            cold_cache_wait_hit: AtomicU64::new(0),
337            cold_cache_wait_timeout: AtomicU64::new(0),
338            resubscribe_ops: AtomicU64::new(0),
339            resubscribe_attempts: AtomicU64::new(0),
340            resubscribe_applied_keys: AtomicU64::new(0),
341            // v1.4.106 codex 1140 F8: qot push drop counter init.
342            qot_push_dropped_total: AtomicU64::new(0),
343            qot_push_dropped_by_sub_type: [const { AtomicU64::new(0) }; 18],
344            keepalive_timeouts: AtomicU64::new(0),
345            latency_ring: RwLock::new(LatencyRing::new()),
346        }
347    }
348
349    /// 记录一次请求延迟 (纳秒)
350    pub fn record_latency_ns(&self, ns: u64) {
351        self.latency_ring.write().push(ns);
352    }
353
354    /// v1.4.106 codex 1140 F8: 记录一次 qot push 被 drop (channel full / closed).
355    ///
356    /// `sub_type` 范围 0..=17 (proto Qot_Common.SubType). 越界值归桶 0
357    /// (未知). 同时 bump 总计数 + per-sub-type 桶, 保证 dashboard 可分维度.
358    pub fn record_qot_push_dropped(&self, sub_type: i32) {
359        self.qot_push_dropped_total.fetch_add(1, Ordering::Relaxed);
360        let bucket = qot_sub_type_bucket(sub_type);
361        self.qot_push_dropped_by_sub_type[bucket].fetch_add(1, Ordering::Relaxed);
362    }
363
364    /// 记录一次 QOT fanout 因单个客户端 channel 满而丢给该客户端的帧。
365    pub fn record_qot_client_push_backpressure_drop(&self, sub_type: i32) {
366        self.qot_client_push_backpressure_drops
367            .fetch_add(1, Ordering::Relaxed);
368        let bucket = qot_sub_type_bucket(sub_type);
369        self.qot_client_push_backpressure_drops_by_sub_type[bucket].fetch_add(1, Ordering::Relaxed);
370    }
371
372    /// v1.4.106 codex 1140 F8: 读取每个 sub_type 桶的 drop 计数 (snapshot).
373    /// 用于 metrics endpoint render.
374    pub fn qot_push_dropped_per_sub_type(&self) -> [u64; 18] {
375        let mut out = [0u64; 18];
376        for (i, slot) in self.qot_push_dropped_by_sub_type.iter().enumerate() {
377            out[i] = slot.load(Ordering::Relaxed);
378        }
379        out
380    }
381
382    pub fn qot_client_push_backpressure_drops_per_sub_type(&self) -> [u64; 18] {
383        let mut out = [0u64; 18];
384        for (i, slot) in self
385            .qot_client_push_backpressure_drops_by_sub_type
386            .iter()
387            .enumerate()
388        {
389            out[i] = slot.load(Ordering::Relaxed);
390        }
391        out
392    }
393
394    /// 获取延迟统计
395    pub fn latency_stats(&self) -> LatencyStats {
396        self.latency_ring.read().stats()
397    }
398
399    /// 格式化运行时间
400    pub fn uptime_str(&self) -> String {
401        let elapsed = self.start_time.elapsed();
402        let secs = elapsed.as_secs();
403        let days = secs / 86400;
404        let hours = (secs % 86400) / 3600;
405        let mins = (secs % 3600) / 60;
406        let s = secs % 60;
407        if days > 0 {
408            format!("{days}d {hours}h {mins}m {s}s")
409        } else if hours > 0 {
410            format!("{hours}h {mins}m {s}s")
411        } else {
412            format!("{mins}m {s}s")
413        }
414    }
415
416    /// 生成 telnet 可展示的指标报告
417    pub fn report(&self) -> String {
418        let lat = self.latency_stats();
419        let backend_status = if self.backend_online.load(Ordering::Relaxed) == 1 {
420            "ONLINE"
421        } else {
422            "OFFLINE"
423        };
424
425        let total_req = self.total_requests.load(Ordering::Relaxed);
426        let uptime_secs = self.start_time.elapsed().as_secs_f64();
427        let avg_rps = if uptime_secs > 0.0 {
428            total_req as f64 / uptime_secs
429        } else {
430            0.0
431        };
432
433        format!(
434            "=== Gateway Metrics ===\r\n\
435             Uptime: {uptime}\r\n\
436             \r\n\
437             [Connections]\r\n\
438             total_accepted: {total_conn}\r\n\
439             total_disconnected: {total_disconn}\r\n\
440             rejected (limit): {rejected}\r\n\
441             keepalive_timeouts: {ka_timeout}\r\n\
442             \r\n\
443             [Requests]\r\n\
444             total_requests: {total_req}\r\n\
445             total_errors: {total_err}\r\n\
446             avg_rps: {avg_rps:.1}\r\n\
447             response_bytes: {resp_bytes}\r\n\
448             \r\n\
449             [Latency (recent {lat_count} samples)]\r\n\
450             avg: {lat_avg}us  p50: {lat_p50}us  p95: {lat_p95}us  p99: {lat_p99}us  max: {lat_max}us\r\n\
451             \r\n\
452             [Backend]\r\n\
453             status: {backend_status}\r\n\
454             reconnects: {reconnects}\r\n\
455             reconnect_failures: {reconnect_fail}\r\n\
456             pushes_received: {push_recv}\r\n\
457             pushes_sent_to_clients: {push_sent}\r\n\
458             push_send_failures_to_clients: {push_send_failures}\r\n\
459             ordinary_client_push_backpressure_disconnects: {ordinary_client_backpressure_disconnects}\r\n\
460             qot_client_push_backpressure_drops: {qot_client_backpressure_drops}\r\n\
461             backend_control_event_drops: {backend_control_event_drops}\r\n\
462             message_center_async_drops: {message_center_async_drops}\r\n\
463             \r\n\
464             [Pushes by CMD (v1.4.83 §14)]\r\n\
465             cmd_6212_quote: {push_cmd_quote}\r\n\
466             cmd_4716_trade_legacy: {push_cmd_trade_legacy}\r\n\
467             cmd_14716_trade_new: {push_cmd_trade_new}\r\n\
468             cmd_5300_msg_center: {push_cmd_msg_center}\r\n\
469             cmd_other: {push_cmd_other}\r\n\
470             \r\n\
471             [Pushes by CMD × UTC hour (v1.4.84 §14)]\r\n\
472             cmd_14716_trade_new_hour_0..23: {hour_trade_new}\r\n\
473             cmd_6212_quote_hour_0..23: {hour_quote}\r\n\
474             cmd_4716_trade_legacy_hour_0..23: {hour_trade_legacy}\r\n\
475             cmd_5300_msg_center_hour_0..23: {hour_msg_center}\r\n\
476             \r\n\
477             [Subscriptions]\r\n\
478             subscribe_ops: {sub_ops}\r\n\
479             unsubscribe_ops: {unsub_ops}\r\n\
480             resubscribe_ops: {resub_ops}\r\n\
481             \r\n\
482             [Cold-cache wait (v1.4.110 §P3 #19)]\r\n\
483             total: {cc_total}  hit: {cc_hit}  timeout: {cc_timeout}\r\n",
484            uptime = self.uptime_str(),
485            total_conn = self.total_connections.load(Ordering::Relaxed),
486            total_disconn = self.total_disconnections.load(Ordering::Relaxed),
487            rejected = self.rejected_connections.load(Ordering::Relaxed),
488            ka_timeout = self.keepalive_timeouts.load(Ordering::Relaxed),
489            total_req = total_req,
490            total_err = self.total_request_errors.load(Ordering::Relaxed),
491            resp_bytes = self.total_response_bytes.load(Ordering::Relaxed),
492            lat_count = lat.count.min(LATENCY_RING_SIZE as u64),
493            lat_avg = lat.avg_us,
494            lat_p50 = lat.p50_us,
495            lat_p95 = lat.p95_us,
496            lat_p99 = lat.p99_us,
497            lat_max = lat.max_us,
498            reconnects = self.backend_reconnects.load(Ordering::Relaxed),
499            reconnect_fail = self.backend_reconnect_failures.load(Ordering::Relaxed),
500            push_recv = self.backend_pushes_received.load(Ordering::Relaxed),
501            push_sent = self.client_pushes_sent.load(Ordering::Relaxed),
502            push_send_failures = self.client_push_send_failures.load(Ordering::Relaxed),
503            ordinary_client_backpressure_disconnects = self
504                .ordinary_client_push_backpressure_disconnects
505                .load(Ordering::Relaxed),
506            qot_client_backpressure_drops = self
507                .qot_client_push_backpressure_drops
508                .load(Ordering::Relaxed),
509            backend_control_event_drops = self.backend_control_event_drops.load(Ordering::Relaxed),
510            message_center_async_drops = self
511                .message_center_async_dropped_total
512                .load(Ordering::Relaxed),
513            push_cmd_quote = self.backend_pushes_cmd_quote.load(Ordering::Relaxed),
514            push_cmd_trade_legacy = self.backend_pushes_cmd_trade_legacy.load(Ordering::Relaxed),
515            push_cmd_trade_new = self.backend_pushes_cmd_trade_new.load(Ordering::Relaxed),
516            push_cmd_msg_center = self.backend_pushes_cmd_msg_center.load(Ordering::Relaxed),
517            push_cmd_other = self.backend_pushes_cmd_other.load(Ordering::Relaxed),
518            hour_trade_new = format_hour_row(&self.backend_pushes_cmd_trade_new_by_hour),
519            hour_quote = format_hour_row(&self.backend_pushes_cmd_quote_by_hour),
520            hour_trade_legacy = format_hour_row(&self.backend_pushes_cmd_trade_legacy_by_hour),
521            hour_msg_center = format_hour_row(&self.backend_pushes_cmd_msg_center_by_hour),
522            sub_ops = self.qot_subscribe_ops.load(Ordering::Relaxed),
523            unsub_ops = self.qot_unsubscribe_ops.load(Ordering::Relaxed),
524            resub_ops = self.resubscribe_ops.load(Ordering::Relaxed),
525            cc_total = self.cold_cache_wait_total.load(Ordering::Relaxed),
526            cc_hit = self.cold_cache_wait_hit.load(Ordering::Relaxed),
527            cc_timeout = self.cold_cache_wait_timeout.load(Ordering::Relaxed),
528        )
529    }
530}
531
532impl Default for GatewayMetrics {
533    fn default() -> Self {
534        Self::new()
535    }
536}
537
538#[cfg(test)]
539mod tests;