1use std::fmt::{self, Write};
4
5use crate::stats::codec::{StatsMetricType, POOL_CODEC, SERVER_CODEC};
6use crate::stats::failure::FailureSnapshot;
7use crate::stats::histogram::Histogram;
8
9#[derive(Clone, Debug, Default)]
24pub struct ServiceInfo {
25 pub source: String,
27 pub version: String,
29 pub rack: String,
31 pub dc: String,
33}
34
35#[derive(Clone, Copy, Debug, Default)]
45pub struct HistogramSummary {
46 pub max: u64,
48 pub p999: u64,
50 pub p99: u64,
52 pub p95: u64,
54 pub mean: u64,
56}
57
58impl HistogramSummary {
59 pub fn from_histogram(h: &Histogram) -> Self {
75 if h.is_overflowing() {
76 return Self::default();
77 }
78 let mean_f = h.mean();
79 let mean = if mean_f.is_finite() && mean_f > 0.0 {
80 ceil_f64_to_u64(mean_f)
82 } else {
83 0
84 };
85 Self {
86 max: h.max(),
87 p999: h.percentile(0.999),
88 p99: h.percentile(0.99),
89 p95: h.percentile(0.95),
90 mean,
91 }
92 }
93}
94
95fn ceil_f64_to_u64(x: f64) -> u64 {
98 if !x.is_finite() || x <= 0.0 {
99 return 0;
100 }
101 let ceil = x.ceil();
102 let bits = ceil.to_bits();
103 let exp = u32::try_from((bits >> 52) & 0x7FF).expect("11-bit field");
104 let mant = bits & ((1u64 << 52) - 1);
105 if exp < 1023 {
106 return 1;
108 }
109 let unbiased = exp - 1023;
110 if unbiased >= 64 {
111 return u64::MAX;
112 }
113 let m = (1u64 << 52) | mant;
114 if unbiased >= 52 {
115 let shift = unbiased - 52;
116 m.checked_shl(shift).unwrap_or(u64::MAX)
117 } else {
118 m >> (52 - unbiased)
119 }
120}
121
122#[derive(Clone, Debug)]
133pub struct PoolStats {
134 pub name: String,
136 pub metrics: Vec<i64>,
138}
139
140impl Default for PoolStats {
141 fn default() -> Self {
142 Self::new(String::new())
143 }
144}
145
146impl PoolStats {
147 pub fn new(name: impl Into<String>) -> Self {
157 Self {
158 name: name.into(),
159 metrics: vec![0; POOL_CODEC.len()],
160 }
161 }
162}
163
164#[derive(Clone, Debug)]
174pub struct ServerStats {
175 pub name: String,
177 pub metrics: Vec<i64>,
179}
180
181impl Default for ServerStats {
182 fn default() -> Self {
183 Self::new(String::new())
184 }
185}
186
187impl ServerStats {
188 pub fn new(name: impl Into<String>) -> Self {
198 Self {
199 name: name.into(),
200 metrics: vec![0; SERVER_CODEC.len()],
201 }
202 }
203}
204
205#[derive(Clone, Debug)]
215pub struct PeerStats {
216 pub name: String,
218 pub metrics: Vec<i64>,
220}
221
222impl PeerStats {
223 pub fn new(name: impl Into<String>) -> Self {
233 Self {
234 name: name.into(),
235 metrics: vec![0; SERVER_CODEC.len()],
236 }
237 }
238}
239
240#[derive(Clone, Debug, Default)]
245pub struct Snapshot {
246 pub info: ServiceInfo,
248 pub uptime: i64,
250 pub timestamp: i64,
252 pub latency: HistogramSummary,
254 pub payload_size: HistogramSummary,
256 pub cross_region_latency: HistogramSummary,
258 pub cross_zone_latency: HistogramSummary,
260 pub server_latency: HistogramSummary,
262 pub cross_region_queue_wait: HistogramSummary,
264 pub cross_zone_queue_wait: HistogramSummary,
266 pub server_queue_wait: HistogramSummary,
268 pub client_out_queue_p99: u64,
270 pub server_in_queue_p99: u64,
272 pub server_out_queue_p99: u64,
274 pub dnode_client_out_queue_p99: u64,
276 pub peer_in_queue_p99: u64,
278 pub peer_out_queue_p99: u64,
280 pub remote_peer_in_queue_p99: u64,
282 pub remote_peer_out_queue_p99: u64,
284 pub alloc_msgs: i64,
286 pub free_msgs: i64,
288 pub alloc_mbufs: i64,
290 pub free_mbufs: i64,
292 pub dyn_memory: i64,
294 pub pool: PoolStats,
296 pub server: ServerStats,
298 pub failure: FailureSnapshot,
300}
301
302impl Snapshot {
303 pub fn to_json(&self) -> String {
322 let mut out = String::new();
323 self.write_json(&mut out)
324 .expect("writing to a String never fails");
325 out
326 }
327
328 pub fn write_json<W: Write>(&self, w: &mut W) -> fmt::Result {
340 w.write_char('{')?;
341 self.write_header(w)?;
342 self.write_pool(w)?;
343 w.write_char('}')?;
344 Ok(())
345 }
346
347 fn write_header<W: Write>(&self, w: &mut W) -> fmt::Result {
348 write_string(w, "service", "dynomite")?;
349 write_string(w, "source", &self.info.source)?;
350 write_string(w, "version", &self.info.version)?;
351 write_num(w, "uptime", self.uptime)?;
352 write_num(w, "timestamp", self.timestamp)?;
353 write_string(w, "rack", &self.info.rack)?;
354 write_string(w, "dc", &self.info.dc)?;
355
356 write_num_u64(w, "latency_max", self.latency.max)?;
357 write_num_u64(w, "latency_999th", self.latency.p999)?;
358 write_num_u64(w, "latency_99th", self.latency.p99)?;
359 write_num_u64(w, "latency_95th", self.latency.p95)?;
360 write_num_u64(w, "latency_mean", self.latency.mean)?;
361
362 write_num_u64(w, "payload_size_max", self.payload_size.max)?;
363 write_num_u64(w, "payload_size_999th", self.payload_size.p999)?;
364 write_num_u64(w, "payload_size_99th", self.payload_size.p99)?;
365 write_num_u64(w, "payload_size_95th", self.payload_size.p95)?;
366 write_num_u64(w, "payload_size_mean", self.payload_size.mean)?;
367
368 self.write_cross_region_latency(w)?;
369 self.write_queue_wait(w)?;
370 self.write_queue_p99s(w)?;
371 self.write_resource_usage(w)?;
372 Ok(())
373 }
374
375 fn write_cross_region_latency<W: Write>(&self, w: &mut W) -> fmt::Result {
376 write_num_u64(
377 w,
378 "average_cross_region_rtt",
379 self.cross_region_latency.mean,
380 )?;
381 write_num_u64(w, "99_cross_region_rtt", self.cross_region_latency.p99)?;
382 write_num_u64(
383 w,
384 "average_cross_zone_latency",
385 self.cross_zone_latency.mean,
386 )?;
387 write_num_u64(w, "99_cross_zone_latency", self.cross_zone_latency.p99)?;
388 write_num_u64(w, "average_server_latency", self.server_latency.mean)?;
389 write_num_u64(w, "99_server_latency", self.server_latency.p99)?;
390 Ok(())
391 }
392
393 fn write_queue_wait<W: Write>(&self, w: &mut W) -> fmt::Result {
394 write_num_u64(
395 w,
396 "average_cross_region_queue_wait",
397 self.cross_region_queue_wait.mean,
398 )?;
399 write_num_u64(
400 w,
401 "99_cross_region_queue_wait",
402 self.cross_region_queue_wait.p99,
403 )?;
404 write_num_u64(
405 w,
406 "average_cross_zone_queue_wait",
407 self.cross_zone_queue_wait.mean,
408 )?;
409 write_num_u64(
410 w,
411 "99_cross_zone_queue_wait",
412 self.cross_zone_queue_wait.p99,
413 )?;
414 write_num_u64(w, "average_server_queue_wait", self.server_queue_wait.mean)?;
415 write_num_u64(w, "99_server_queue_wait", self.server_queue_wait.p99)?;
416 Ok(())
417 }
418
419 fn write_queue_p99s<W: Write>(&self, w: &mut W) -> fmt::Result {
420 write_num_u64(w, "client_out_queue_99", self.client_out_queue_p99)?;
421 write_num_u64(w, "server_in_queue_99", self.server_in_queue_p99)?;
422 write_num_u64(w, "server_out_queue_99", self.server_out_queue_p99)?;
423 write_num_u64(
424 w,
425 "dnode_client_out_queue_99",
426 self.dnode_client_out_queue_p99,
427 )?;
428 write_num_u64(w, "peer_in_queue_99", self.peer_in_queue_p99)?;
429 write_num_u64(w, "peer_out_queue_99", self.peer_out_queue_p99)?;
430 write_num_u64(
431 w,
432 "remote_peer_out_queue_99",
433 self.remote_peer_out_queue_p99,
434 )?;
435 write_num_u64(w, "remote_peer_in_queue_99", self.remote_peer_in_queue_p99)?;
436 Ok(())
437 }
438
439 fn write_resource_usage<W: Write>(&self, w: &mut W) -> fmt::Result {
440 write_num(w, "alloc_msgs", self.alloc_msgs)?;
441 write_num(w, "free_msgs", self.free_msgs)?;
442 write_num(w, "alloc_mbufs", self.alloc_mbufs)?;
443 write_num(w, "free_mbufs", self.free_mbufs)?;
444 write_num(w, "dyn_memory", self.dyn_memory)?;
445 Ok(())
446 }
447
448 fn write_pool<W: Write>(&self, w: &mut W) -> fmt::Result {
449 write!(w, "\"{}\":{{", escape_str(&self.pool.name))?;
450 for (i, spec) in POOL_CODEC.iter().enumerate() {
451 if !is_visible_metric(spec.kind) {
452 continue;
453 }
454 let value = self.pool.metrics.get(i).copied().unwrap_or(0);
455 write_num(w, spec.name, value)?;
456 }
457 self.write_server(w)?;
458 w.write_str("}")?;
459 Ok(())
460 }
461
462 fn write_server<W: Write>(&self, w: &mut W) -> fmt::Result {
463 write!(w, "\"{}\":{{", escape_str(&self.server.name))?;
464 let server_visible: Vec<usize> = SERVER_CODEC
465 .iter()
466 .enumerate()
467 .filter(|(_, s)| is_visible_metric(s.kind))
468 .map(|(i, _)| i)
469 .collect();
470 for (j, idx) in server_visible.iter().copied().enumerate() {
471 let spec = &SERVER_CODEC[idx];
472 let value = self.server.metrics.get(idx).copied().unwrap_or(0);
473 if j + 1 == server_visible.len() {
474 write_num_no_comma(w, spec.name, value)?;
475 } else {
476 write_num(w, spec.name, value)?;
477 }
478 }
479 w.write_str("}")?;
480 Ok(())
481 }
482}
483
484fn is_visible_metric(kind: StatsMetricType) -> bool {
488 matches!(
489 kind,
490 StatsMetricType::Counter | StatsMetricType::Gauge | StatsMetricType::Timestamp
491 )
492}
493
494fn write_string<W: Write>(w: &mut W, key: &str, value: &str) -> fmt::Result {
495 write!(w, "\"{}\":\"{}\",", escape_str(key), escape_str(value))
496}
497
498fn write_num<W: Write>(w: &mut W, key: &str, value: i64) -> fmt::Result {
499 write!(w, "\"{}\":{value},", escape_str(key))
500}
501
502fn write_num_no_comma<W: Write>(w: &mut W, key: &str, value: i64) -> fmt::Result {
503 write!(w, "\"{}\":{value}", escape_str(key))
504}
505
506fn write_num_u64<W: Write>(w: &mut W, key: &str, value: u64) -> fmt::Result {
507 write!(w, "\"{}\":{value},", escape_str(key))
508}
509
510fn escape_str(s: &str) -> EscapedStr<'_> {
513 EscapedStr(s)
514}
515
516struct EscapedStr<'a>(&'a str);
517
518impl fmt::Display for EscapedStr<'_> {
519 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520 for ch in self.0.chars() {
521 match ch {
522 '\\' => f.write_str("\\\\")?,
523 '"' => f.write_str("\\\"")?,
524 '\n' => f.write_str("\\n")?,
525 '\r' => f.write_str("\\r")?,
526 '\t' => f.write_str("\\t")?,
527 c if (c as u32) < 0x20 => write!(f, "\\u{:04x}", c as u32)?,
528 c => f.write_char(c)?,
529 }
530 }
531 Ok(())
532 }
533}
534
535pub fn describe_stats() -> String {
546 let mut out = String::new();
547 out.push_str("pool stats:\n");
548 for spec in POOL_CODEC {
549 let _ = writeln!(out, " {:<20}\"{}\"", spec.name, spec.description);
550 }
551 out.push('\n');
552 out.push_str("server stats:\n");
553 for spec in SERVER_CODEC {
554 let _ = writeln!(out, " {:<20}\"{}\"", spec.name, spec.description);
555 }
556 out
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562
563 #[test]
564 fn ceil_helper_matches_known_values() {
565 assert_eq!(ceil_f64_to_u64(0.0), 0);
566 assert_eq!(ceil_f64_to_u64(1.0), 1);
567 assert_eq!(ceil_f64_to_u64(1.5), 2);
568 assert_eq!(ceil_f64_to_u64(2.0), 2);
569 assert_eq!(ceil_f64_to_u64(99.99), 100);
570 assert_eq!(ceil_f64_to_u64(f64::NAN), 0);
571 assert_eq!(ceil_f64_to_u64(f64::INFINITY), 0);
572 assert_eq!(ceil_f64_to_u64(-1.0), 0);
573 }
574
575 #[test]
576 fn empty_snapshot_renders_to_valid_json_skeleton() {
577 let snap = Snapshot {
578 pool: PoolStats::new("dyn_o_mite"),
579 server: ServerStats::new("redis"),
580 ..Snapshot::default()
581 };
582 let s = snap.to_json();
583 assert!(s.starts_with('{'));
584 assert!(s.ends_with('}'));
585 assert!(s.contains("\"service\":\"dynomite\""));
586 assert!(s.contains("\"dyn_o_mite\":{"));
587 assert!(s.contains("\"redis\":{"));
588 }
589
590 #[test]
591 fn describe_lists_every_metric() {
592 let text = describe_stats();
593 for spec in POOL_CODEC {
594 assert!(
595 text.contains(spec.name),
596 "pool metric {} missing",
597 spec.name
598 );
599 assert!(text.contains(spec.description));
600 }
601 for spec in SERVER_CODEC {
602 assert!(
603 text.contains(spec.name),
604 "server metric {} missing",
605 spec.name
606 );
607 }
608 }
609
610 #[test]
611 fn escape_handles_quotes_and_controls() {
612 let s = EscapedStr("a\"b\nc").to_string();
613 assert_eq!(s, r#"a\"b\nc"#);
614 }
615}