1use std::{
13 sync::{
14 OnceLock,
15 atomic::{AtomicU64, Ordering},
16 },
17 thread,
18 time::Duration,
19};
20
21use dashmap::DashMap;
22use myko::wire::MykoMessage;
23use opentelemetry::{KeyValue, metrics::Counter};
24
25const WINDOW_MS: u64 = 250;
26
27fn per_client_counter() -> &'static Counter<u64> {
28 static COUNTER: OnceLock<Counter<u64>> = OnceLock::new();
29 COUNTER.get_or_init(|| {
30 opentelemetry::global::meter("myko-server")
31 .u64_counter("myko.ws.message.count")
32 .with_description("WS messages by direction, kind, and client")
33 .build()
34 })
35}
36
37fn in_counts() -> &'static DashMap<&'static str, AtomicU64> {
38 static C: OnceLock<DashMap<&'static str, AtomicU64>> = OnceLock::new();
39 C.get_or_init(DashMap::new)
40}
41
42fn out_counts() -> &'static DashMap<&'static str, AtomicU64> {
43 static C: OnceLock<DashMap<&'static str, AtomicU64>> = OnceLock::new();
44 C.get_or_init(DashMap::new)
45}
46
47pub fn record_inbound(kind: &'static str) {
50 in_counts()
51 .entry(kind)
52 .or_insert_with(|| AtomicU64::new(0))
53 .fetch_add(1, Ordering::Relaxed);
54}
55
56pub fn record_outbound(kind: &'static str) {
58 out_counts()
59 .entry(kind)
60 .or_insert_with(|| AtomicU64::new(0))
61 .fetch_add(1, Ordering::Relaxed);
62}
63
64pub fn record_inbound_for_client(kind: &'static str, client_id: &str, tag: Option<&str>) {
74 record_inbound(kind);
75 let mut attrs = vec![
76 KeyValue::new("direction", "in"),
77 KeyValue::new("kind", kind),
78 KeyValue::new("client_id", client_id.to_string()),
79 ];
80 if let Some(tag) = tag {
81 attrs.push(KeyValue::new("tag", tag.to_string()));
82 }
83 per_client_counter().add(1, &attrs);
84}
85
86pub fn record_outbound_for_client(kind: &'static str, client_id: &str, tag: Option<&str>) {
89 record_outbound(kind);
90 let mut attrs = vec![
91 KeyValue::new("direction", "out"),
92 KeyValue::new("kind", kind),
93 KeyValue::new("client_id", client_id.to_string()),
94 ];
95 if let Some(tag) = tag {
96 attrs.push(KeyValue::new("tag", tag.to_string()));
97 }
98 per_client_counter().add(1, &attrs);
99}
100
101pub fn message_tag(msg: &MykoMessage) -> Option<&str> {
108 match msg {
109 MykoMessage::Query(w) => Some(&w.query_id),
110 MykoMessage::QueryError(e) => Some(&e.query_id),
111 MykoMessage::View(w) => Some(&w.view_id),
112 MykoMessage::ViewError(e) => Some(&e.view_id),
113 MykoMessage::Report(w) => Some(&w.report_id),
114 MykoMessage::ReportError(e) => Some(&e.report_id),
115 MykoMessage::Command(w) => Some(&w.command_id),
116 MykoMessage::CommandError(e) => Some(&e.command_id),
117 _ => None,
118 }
119}
120
121pub fn message_kind(msg: &MykoMessage) -> &'static str {
124 match msg {
125 MykoMessage::Query(_) => "Query",
126 MykoMessage::QueryResponse(_) => "QueryResponse",
127 MykoMessage::QueryCancel(_) => "QueryCancel",
128 MykoMessage::QueryWindow(_) => "QueryWindow",
129 MykoMessage::QueryError(_) => "QueryError",
130 MykoMessage::View(_) => "View",
131 MykoMessage::ViewResponse(_) => "ViewResponse",
132 MykoMessage::ViewCancel(_) => "ViewCancel",
133 MykoMessage::ViewWindow(_) => "ViewWindow",
134 MykoMessage::ViewError(_) => "ViewError",
135 MykoMessage::Report(_) => "Report",
136 MykoMessage::ReportResponse(_) => "ReportResponse",
137 MykoMessage::ReportCancel(_) => "ReportCancel",
138 MykoMessage::ReportError(_) => "ReportError",
139 MykoMessage::Event(_) => "Event",
140 MykoMessage::EventBatch(_) => "EventBatch",
141 MykoMessage::Command(_) => "Command",
142 MykoMessage::CommandResponse(_) => "CommandResponse",
143 MykoMessage::CommandError(_) => "CommandError",
144 MykoMessage::Ping(_) => "Ping",
145 MykoMessage::Benchmark(_) => "Benchmark",
146 }
147}
148
149pub fn start_periodic_logger() {
152 static STARTED: OnceLock<()> = OnceLock::new();
153 if STARTED.set(()).is_err() {
154 return;
155 }
156
157 let _ = thread::Builder::new()
158 .name("myko-ws-timing".to_string())
159 .spawn(run_logger_loop)
160 .map_err(|e| {
161 tracing::warn!(
162 target: "myko_server::ws_timing",
163 "Failed to spawn ws_timing thread: {}", e
164 )
165 });
166}
167
168fn run_logger_loop() {
169 loop {
170 thread::sleep(Duration::from_millis(WINDOW_MS));
171 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(emit_window));
172 }
173}
174
175fn emit_window() {
176 let in_snap = drain_counts(in_counts());
177 let out_snap = drain_counts(out_counts());
178 if in_snap.is_empty() && out_snap.is_empty() {
179 return;
180 }
181 let in_total: u64 = in_snap.iter().map(|(_, n)| *n).sum();
182 let out_total: u64 = out_snap.iter().map(|(_, n)| *n).sum();
183 tracing::info!(
184 target: "myko_server::ws_timing",
185 "[ws_timing window={}ms] in={} [{}] out={} [{}]",
186 WINDOW_MS,
187 in_total,
188 format_kinds(&in_snap),
189 out_total,
190 format_kinds(&out_snap),
191 );
192}
193
194fn drain_counts(counts: &DashMap<&'static str, AtomicU64>) -> Vec<(&'static str, u64)> {
195 let mut out: Vec<(&'static str, u64)> = counts
196 .iter()
197 .filter_map(|e| {
198 let n = e.value().swap(0, Ordering::Relaxed);
199 if n == 0 { None } else { Some((*e.key(), n)) }
200 })
201 .collect();
202 out.sort_by_key(|b| std::cmp::Reverse(b.1));
203 out
204}
205
206fn format_kinds(snap: &[(&'static str, u64)]) -> String {
207 snap.iter()
208 .map(|(k, n)| format!("{}={}", k, n))
209 .collect::<Vec<_>>()
210 .join(", ")
211}