use alloc::string::String;
use alloc::vec::Vec;
use crate::observability::Attribute;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TraceId(pub [u8; 16]);
impl TraceId {
pub const INVALID: Self = Self([0u8; 16]);
#[must_use]
pub fn is_valid(&self) -> bool {
self.0.iter().any(|b| *b != 0)
}
#[must_use]
pub fn to_hex(&self) -> String {
bytes_to_hex(&self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SpanId(pub [u8; 8]);
impl SpanId {
pub const INVALID: Self = Self([0u8; 8]);
#[must_use]
pub fn is_valid(&self) -> bool {
self.0.iter().any(|b| *b != 0)
}
#[must_use]
pub fn to_hex(&self) -> String {
bytes_to_hex(&self.0)
}
}
fn bytes_to_hex(b: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(b.len() * 2);
for &x in b {
out.push(HEX[(x >> 4) as usize] as char);
out.push(HEX[(x & 0x0f) as usize] as char);
}
out
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SpanContext {
pub trace_id: TraceId,
pub span_id: SpanId,
pub parent_span_id: Option<SpanId>,
}
impl SpanContext {
#[must_use]
pub fn new_root(trace_id: TraceId, span_id: SpanId) -> Self {
Self {
trace_id,
span_id,
parent_span_id: None,
}
}
#[must_use]
pub fn child_of(parent: &SpanContext, span_id: SpanId) -> Self {
Self {
trace_id: parent.trace_id,
span_id,
parent_span_id: Some(parent.span_id),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SpanStatus {
Unset,
Ok,
Error,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SpanKind {
Internal,
Server,
Client,
}
#[derive(Clone, Debug)]
pub struct Span {
pub context: SpanContext,
pub name: String,
pub kind: SpanKind,
pub start_unix_ns: u64,
pub end_unix_ns: u64,
pub status: SpanStatus,
pub status_description: Option<String>,
pub attributes: Vec<Attribute>,
}
impl Span {
#[must_use]
pub fn duration_ns(&self) -> u64 {
self.end_unix_ns.saturating_sub(self.start_unix_ns)
}
}
#[derive(Clone, Debug)]
pub struct Histogram {
pub name: String,
pub count: u64,
pub sum_ns: u64,
pub min_ns: u64,
pub max_ns: u64,
pub buckets: [u64; 11],
}
impl Histogram {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
count: 0,
sum_ns: 0,
min_ns: u64::MAX,
max_ns: 0,
buckets: [0; 11],
}
}
pub fn record_ns(&mut self, ns: u64) {
self.count = self.count.saturating_add(1);
self.sum_ns = self.sum_ns.saturating_add(ns);
if ns < self.min_ns {
self.min_ns = ns;
}
if ns > self.max_ns {
self.max_ns = ns;
}
let idx = bucket_index(ns);
self.buckets[idx] = self.buckets[idx].saturating_add(1);
}
#[must_use]
pub fn mean_ns(&self) -> u64 {
if self.count == 0 {
0
} else {
self.sum_ns / self.count
}
}
#[must_use]
pub fn bucket_bounds() -> [u64; 11] {
[
1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000, 10_000_000_000, ]
}
pub fn reset(&mut self) {
self.count = 0;
self.sum_ns = 0;
self.min_ns = u64::MAX;
self.max_ns = 0;
self.buckets = [0; 11];
}
}
fn bucket_index(ns: u64) -> usize {
let bounds = Histogram::bucket_bounds();
for (i, b) in bounds.iter().enumerate() {
if ns <= *b {
return i;
}
}
bounds.len() - 1
}
pub mod metric_name {
pub const DDS_WRITE_LATENCY: &str = "dds.write.latency";
pub const DDS_READ_LATENCY: &str = "dds.read.latency";
pub const DDS_HEARTBEAT_RTT: &str = "dds.heartbeat.rtt";
pub const DDS_DISCOVERY_MATCH_DURATION: &str = "dds.discovery.match.duration";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trace_id_invalid_is_zero() {
assert!(!TraceId::INVALID.is_valid());
assert!(TraceId([1u8; 16]).is_valid());
}
#[test]
fn span_id_hex_format() {
let id = SpanId([0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03, 0x04]);
assert_eq!(id.to_hex(), "deadbeef01020304");
}
#[test]
fn trace_id_hex_format() {
let id = TraceId([
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
0xee, 0xff,
]);
assert_eq!(id.to_hex(), "00112233445566778899aabbccddeeff");
}
#[test]
fn span_context_child_inherits_trace_id() {
let parent = SpanContext::new_root(TraceId([1u8; 16]), SpanId([2u8; 8]));
let child = SpanContext::child_of(&parent, SpanId([3u8; 8]));
assert_eq!(child.trace_id, parent.trace_id);
assert_eq!(child.parent_span_id, Some(parent.span_id));
}
#[test]
fn histogram_records_into_correct_bucket() {
let mut h = Histogram::new("test");
h.record_ns(500); h.record_ns(50_000); h.record_ns(2_000_000_000); assert_eq!(h.count, 3);
assert_eq!(h.buckets[3], 1);
assert_eq!(h.buckets[5], 1);
assert_eq!(h.buckets[10], 1);
}
#[test]
fn histogram_min_max_mean() {
let mut h = Histogram::new("test");
h.record_ns(100);
h.record_ns(200);
h.record_ns(300);
assert_eq!(h.min_ns, 100);
assert_eq!(h.max_ns, 300);
assert_eq!(h.mean_ns(), 200);
}
#[test]
fn histogram_reset_clears_state() {
let mut h = Histogram::new("test");
h.record_ns(50);
h.reset();
assert_eq!(h.count, 0);
assert_eq!(h.min_ns, u64::MAX);
assert_eq!(h.max_ns, 0);
assert_eq!(h.buckets, [0u64; 11]);
}
#[test]
fn histogram_clamps_to_top_bucket_for_huge_values() {
let mut h = Histogram::new("test");
h.record_ns(u64::MAX);
assert_eq!(h.buckets[10], 1);
}
#[test]
fn span_duration() {
let s = Span {
context: SpanContext::new_root(TraceId([1u8; 16]), SpanId([2u8; 8])),
name: "x".into(),
kind: SpanKind::Internal,
start_unix_ns: 1_000_000,
end_unix_ns: 1_500_000,
status: SpanStatus::Ok,
status_description: None,
attributes: Vec::new(),
};
assert_eq!(s.duration_ns(), 500_000);
}
}