1use alloc::string::String;
29use alloc::vec::Vec;
30
31use crate::observability::Attribute;
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36pub struct TraceId(pub [u8; 16]);
37
38impl TraceId {
39 pub const INVALID: Self = Self([0u8; 16]);
41
42 #[must_use]
44 pub fn is_valid(&self) -> bool {
45 self.0.iter().any(|b| *b != 0)
46 }
47
48 #[must_use]
50 pub fn to_hex(&self) -> String {
51 bytes_to_hex(&self.0)
52 }
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57pub struct SpanId(pub [u8; 8]);
58
59impl SpanId {
60 pub const INVALID: Self = Self([0u8; 8]);
62
63 #[must_use]
65 pub fn is_valid(&self) -> bool {
66 self.0.iter().any(|b| *b != 0)
67 }
68
69 #[must_use]
71 pub fn to_hex(&self) -> String {
72 bytes_to_hex(&self.0)
73 }
74}
75
76fn bytes_to_hex(b: &[u8]) -> String {
77 const HEX: &[u8; 16] = b"0123456789abcdef";
78 let mut out = String::with_capacity(b.len() * 2);
79 for &x in b {
80 out.push(HEX[(x >> 4) as usize] as char);
81 out.push(HEX[(x & 0x0f) as usize] as char);
82 }
83 out
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub struct SpanContext {
90 pub trace_id: TraceId,
92 pub span_id: SpanId,
94 pub parent_span_id: Option<SpanId>,
96}
97
98impl SpanContext {
99 #[must_use]
102 pub fn new_root(trace_id: TraceId, span_id: SpanId) -> Self {
103 Self {
104 trace_id,
105 span_id,
106 parent_span_id: None,
107 }
108 }
109
110 #[must_use]
112 pub fn child_of(parent: &SpanContext, span_id: SpanId) -> Self {
113 Self {
114 trace_id: parent.trace_id,
115 span_id,
116 parent_span_id: Some(parent.span_id),
117 }
118 }
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub enum SpanStatus {
124 Unset,
126 Ok,
128 Error,
130}
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
134pub enum SpanKind {
135 Internal,
137 Server,
139 Client,
141}
142
143#[derive(Clone, Debug)]
149pub struct Span {
150 pub context: SpanContext,
152 pub name: String,
154 pub kind: SpanKind,
156 pub start_unix_ns: u64,
158 pub end_unix_ns: u64,
160 pub status: SpanStatus,
162 pub status_description: Option<String>,
164 pub attributes: Vec<Attribute>,
166}
167
168impl Span {
169 #[must_use]
171 pub fn duration_ns(&self) -> u64 {
172 self.end_unix_ns.saturating_sub(self.start_unix_ns)
173 }
174}
175
176#[derive(Clone, Debug)]
184pub struct Histogram {
185 pub name: String,
187 pub count: u64,
189 pub sum_ns: u64,
191 pub min_ns: u64,
193 pub max_ns: u64,
195 pub buckets: [u64; 11],
198}
199
200impl Histogram {
201 #[must_use]
203 pub fn new(name: impl Into<String>) -> Self {
204 Self {
205 name: name.into(),
206 count: 0,
207 sum_ns: 0,
208 min_ns: u64::MAX,
209 max_ns: 0,
210 buckets: [0; 11],
211 }
212 }
213
214 pub fn record_ns(&mut self, ns: u64) {
216 self.count = self.count.saturating_add(1);
217 self.sum_ns = self.sum_ns.saturating_add(ns);
218 if ns < self.min_ns {
219 self.min_ns = ns;
220 }
221 if ns > self.max_ns {
222 self.max_ns = ns;
223 }
224 let idx = bucket_index(ns);
225 self.buckets[idx] = self.buckets[idx].saturating_add(1);
226 }
227
228 #[must_use]
230 pub fn mean_ns(&self) -> u64 {
231 if self.count == 0 {
232 0
233 } else {
234 self.sum_ns / self.count
235 }
236 }
237
238 #[must_use]
241 pub fn bucket_bounds() -> [u64; 11] {
242 [
243 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, ]
255 }
256
257 pub fn reset(&mut self) {
259 self.count = 0;
260 self.sum_ns = 0;
261 self.min_ns = u64::MAX;
262 self.max_ns = 0;
263 self.buckets = [0; 11];
264 }
265}
266
267fn bucket_index(ns: u64) -> usize {
268 let bounds = Histogram::bucket_bounds();
269 for (i, b) in bounds.iter().enumerate() {
270 if ns <= *b {
271 return i;
272 }
273 }
274 bounds.len() - 1
275}
276
277pub mod metric_name {
279 pub const DDS_WRITE_LATENCY: &str = "dds.write.latency";
281 pub const DDS_READ_LATENCY: &str = "dds.read.latency";
283 pub const DDS_HEARTBEAT_RTT: &str = "dds.heartbeat.rtt";
285 pub const DDS_DISCOVERY_MATCH_DURATION: &str = "dds.discovery.match.duration";
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn trace_id_invalid_is_zero() {
295 assert!(!TraceId::INVALID.is_valid());
296 assert!(TraceId([1u8; 16]).is_valid());
297 }
298
299 #[test]
300 fn span_id_hex_format() {
301 let id = SpanId([0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03, 0x04]);
302 assert_eq!(id.to_hex(), "deadbeef01020304");
303 }
304
305 #[test]
306 fn trace_id_hex_format() {
307 let id = TraceId([
308 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
309 0xee, 0xff,
310 ]);
311 assert_eq!(id.to_hex(), "00112233445566778899aabbccddeeff");
312 }
313
314 #[test]
315 fn span_context_child_inherits_trace_id() {
316 let parent = SpanContext::new_root(TraceId([1u8; 16]), SpanId([2u8; 8]));
317 let child = SpanContext::child_of(&parent, SpanId([3u8; 8]));
318 assert_eq!(child.trace_id, parent.trace_id);
319 assert_eq!(child.parent_span_id, Some(parent.span_id));
320 }
321
322 #[test]
323 fn histogram_records_into_correct_bucket() {
324 let mut h = Histogram::new("test");
325 h.record_ns(500); h.record_ns(50_000); h.record_ns(2_000_000_000); assert_eq!(h.count, 3);
329 assert_eq!(h.buckets[3], 1);
330 assert_eq!(h.buckets[5], 1);
331 assert_eq!(h.buckets[10], 1);
332 }
333
334 #[test]
335 fn histogram_min_max_mean() {
336 let mut h = Histogram::new("test");
337 h.record_ns(100);
338 h.record_ns(200);
339 h.record_ns(300);
340 assert_eq!(h.min_ns, 100);
341 assert_eq!(h.max_ns, 300);
342 assert_eq!(h.mean_ns(), 200);
343 }
344
345 #[test]
346 fn histogram_reset_clears_state() {
347 let mut h = Histogram::new("test");
348 h.record_ns(50);
349 h.reset();
350 assert_eq!(h.count, 0);
351 assert_eq!(h.min_ns, u64::MAX);
352 assert_eq!(h.max_ns, 0);
353 assert_eq!(h.buckets, [0u64; 11]);
354 }
355
356 #[test]
357 fn histogram_clamps_to_top_bucket_for_huge_values() {
358 let mut h = Histogram::new("test");
359 h.record_ns(u64::MAX);
360 assert_eq!(h.buckets[10], 1);
361 }
362
363 #[test]
364 fn span_duration() {
365 let s = Span {
366 context: SpanContext::new_root(TraceId([1u8; 16]), SpanId([2u8; 8])),
367 name: "x".into(),
368 kind: SpanKind::Internal,
369 start_unix_ns: 1_000_000,
370 end_unix_ns: 1_500_000,
371 status: SpanStatus::Ok,
372 status_description: None,
373 attributes: Vec::new(),
374 };
375 assert_eq!(s.duration_ns(), 500_000);
376 }
377}