veecle_telemetry/
protocol.rs

1//! Telemetry protocol types and message definitions.
2//!
3//! This module defines the core data structures used for telemetry message exchange.
4//! It includes message types for logging, tracing, and time synchronization, as well
5//! as supporting types for execution tracking and attribute management.
6//!
7//! # Message Types
8//!
9//! The protocol supports several categories of telemetry messages:
10//!
11//! - **Log Messages** - Structured logging with severity levels and attributes
12//! - **Tracing Messages** - Distributed tracing with spans, events, and links
13//! - **Time Sync Messages** - Time synchronization between systems
14//!
15//! # Execution Tracking
16//!
17//! Each message is associated with an [`ExecutionId`] that uniquely identifies
18//! the execution context.
19//! This allows telemetry data from multiple executions to be correlated and analyzed separately.
20//!
21//! # Serialization
22//!
23//! All protocol types implement [`serde::Serialize`] and optionally [`serde::Deserialize`]
24//! (when the `alloc` feature is enabled) for easy serialization to various formats.
25
26#[cfg(feature = "alloc")]
27use alloc::vec::Vec;
28use core::ops::Deref;
29
30use serde::{Deserialize, Serialize};
31
32use crate::SpanContext;
33use crate::id::{SpanId, TraceId};
34#[cfg(feature = "alloc")]
35use crate::to_static::ToStatic;
36use crate::types::{ListType, StringType, list_from_slice};
37use crate::value::KeyValue;
38
39/// A specialised form of [`list_from_slice`] for attributes.
40pub fn attribute_list_from_slice<'a>(slice: &'a [KeyValue<'a>]) -> AttributeListType<'a> {
41    list_from_slice::<KeyValue<'a>>(slice)
42}
43
44/// Type alias for a list of attributes.
45pub type AttributeListType<'a> = ListType<'a, KeyValue<'a>>;
46
47#[cfg(feature = "alloc")]
48impl ToStatic for AttributeListType<'_> {
49    type Static = AttributeListType<'static>;
50
51    fn to_static(&self) -> Self::Static {
52        self.iter()
53            .map(|item| item.to_static())
54            .collect::<Vec<_>>()
55            .into()
56    }
57}
58
59/// An Id uniquely identifying an execution.
60///
61/// An [`ExecutionId`] should never be re-used as it's used to collect metadata about the execution and to generate
62/// [`TraceId`]s which need to be globally unique.
63#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
64pub struct ExecutionId(u128);
65
66impl ExecutionId {
67    /// Uses a random generator to generate the [`ExecutionId`].
68    pub fn random(rng: &mut impl rand::Rng) -> Self {
69        Self(rng.random())
70    }
71
72    /// Creates an [`ExecutionId`] from a raw value, extra care needs to be taken that this is not a constant value or
73    /// re-used in any way.
74    ///
75    /// When possible prefer using [`ExecutionId::random`].
76    pub const fn from_raw(raw: u128) -> Self {
77        Self(raw)
78    }
79}
80
81impl Deref for ExecutionId {
82    type Target = u128;
83
84    fn deref(&self) -> &Self::Target {
85        &self.0
86    }
87}
88
89/// A telemetry message associated with a specific execution.
90///
91/// This structure wraps a telemetry message with its execution context,
92/// allowing messages from different executions to be properly correlated.
93#[derive(Clone, Debug, Serialize)]
94#[cfg_attr(feature = "alloc", derive(Deserialize))]
95pub struct InstanceMessage<'a> {
96    /// The execution ID this message belongs to
97    pub execution: ExecutionId,
98
99    /// The telemetry message content
100    #[serde(borrow)]
101    pub message: TelemetryMessage<'a>,
102}
103
104#[cfg(feature = "alloc")]
105impl ToStatic for InstanceMessage<'_> {
106    type Static = InstanceMessage<'static>;
107
108    fn to_static(&self) -> Self::Static {
109        InstanceMessage {
110            execution: self.execution,
111            message: self.message.to_static(),
112        }
113    }
114}
115
116/// An enumeration of all possible telemetry message types.
117///
118/// This enum represents the different categories of telemetry data that can be
119/// collected and exported by the system.
120#[derive(Clone, Debug, Serialize)]
121#[cfg_attr(feature = "alloc", derive(Deserialize))]
122pub enum TelemetryMessage<'a> {
123    /// A structured log message with severity and attributes
124    Log(#[serde(borrow)] LogMessage<'a>),
125    /// A time synchronization message for clock coordination
126    TimeSync(TimeSyncMessage),
127    /// A distributed tracing message (spans, events, links)
128    Tracing(#[serde(borrow)] TracingMessage<'a>),
129}
130
131#[cfg(feature = "alloc")]
132impl ToStatic for TelemetryMessage<'_> {
133    type Static = TelemetryMessage<'static>;
134
135    fn to_static(&self) -> Self::Static {
136        match self {
137            TelemetryMessage::Log(msg) => TelemetryMessage::Log(msg.to_static()),
138            TelemetryMessage::TimeSync(msg) => TelemetryMessage::TimeSync(msg.clone()),
139            TelemetryMessage::Tracing(msg) => TelemetryMessage::Tracing(msg.to_static()),
140        }
141    }
142}
143
144/// Log message severity levels.
145///
146/// These levels follow standard logging conventions, ordered from most verbose
147/// to most critical.
148#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
149pub enum Severity {
150    /// The "trace" level.
151    ///
152    /// Designates very low priority, often extremely verbose, information.
153    Trace,
154    /// The "debug" level.
155    ///
156    /// Designates lower priority information.
157    Debug,
158    /// The "info" level.
159    ///
160    /// Designates useful information.
161    Info,
162    /// The "warn" level.
163    ///
164    /// Designates hazardous situations.
165    Warn,
166    /// The "error" level.
167    ///
168    /// Designates very serious errors.
169    Error,
170    /// The "fatal" level.
171    ///
172    /// Designates critical failures that might crash the program.
173    Fatal,
174}
175
176/// A structured log message with severity, timestamp, and attributes.
177///
178/// Log messages can be optionally correlated with traces by including trace and span IDs when available.
179#[derive(Clone, Debug, Serialize)]
180#[cfg_attr(feature = "alloc", derive(Deserialize))]
181pub struct LogMessage<'a> {
182    /// Timestamp in nanoseconds since Unix epoch (or system start)
183    pub time_unix_nano: u64,
184    /// The severity level of this log message
185    pub severity: Severity,
186
187    /// The message body
188    #[serde(borrow)]
189    pub body: StringType<'a>,
190
191    /// Key-value attributes providing additional context
192    #[serde(borrow)]
193    pub attributes: AttributeListType<'a>,
194
195    /// Optional trace ID for correlation with traces
196    pub trace_id: Option<TraceId>,
197    /// Optional span ID for correlation with traces
198    pub span_id: Option<SpanId>,
199}
200
201#[cfg(feature = "alloc")]
202impl ToStatic for LogMessage<'_> {
203    type Static = LogMessage<'static>;
204
205    fn to_static(&self) -> Self::Static {
206        LogMessage {
207            time_unix_nano: self.time_unix_nano,
208            severity: self.severity,
209            body: self.body.to_static(),
210            attributes: self.attributes.to_static(),
211            trace_id: self.trace_id,
212            span_id: self.span_id,
213        }
214    }
215}
216
217/// A time synchronization message for coordinating clocks between systems.
218///
219/// This message contains both local time and absolute time information,
220/// allowing downstream systems to correlate local timestamps with real-world time.
221#[derive(Clone, Debug, Serialize, Deserialize)]
222pub struct TimeSyncMessage {
223    /// Local timestamp in system-specific units
224    pub local_timestamp: u64,
225    /// Time since Unix epoch in nanoseconds
226    pub since_epoch: u64,
227}
228
229/// Messages related to distributed tracing operations.
230///
231/// This enum encompasses all the different types of tracing messages that can be
232/// generated during span lifecycle management and tracing operations.
233#[derive(Clone, Debug, Serialize)]
234#[cfg_attr(feature = "alloc", derive(Deserialize))]
235pub enum TracingMessage<'a> {
236    /// A new span has been created
237    CreateSpan(#[serde(borrow)] SpanCreateMessage<'a>),
238    /// A span has been entered (made current)
239    EnterSpan(SpanEnterMessage),
240    /// A span has been exited (no longer current)
241    ExitSpan(SpanExitMessage),
242    /// A span has been closed (completed)
243    CloseSpan(SpanCloseMessage),
244    /// An event has been added to a span
245    AddEvent(#[serde(borrow)] SpanAddEventMessage<'a>),
246    /// A link has been added to a span
247    AddLink(SpanAddLinkMessage),
248    /// An attribute has been set on a span
249    SetAttribute(#[serde(borrow)] SpanSetAttributeMessage<'a>),
250}
251
252#[cfg(feature = "alloc")]
253impl ToStatic for TracingMessage<'_> {
254    type Static = TracingMessage<'static>;
255
256    fn to_static(&self) -> Self::Static {
257        match self {
258            TracingMessage::CreateSpan(msg) => TracingMessage::CreateSpan(msg.to_static()),
259            TracingMessage::EnterSpan(msg) => TracingMessage::EnterSpan(*msg),
260            TracingMessage::ExitSpan(msg) => TracingMessage::ExitSpan(*msg),
261            TracingMessage::CloseSpan(msg) => TracingMessage::CloseSpan(*msg),
262            TracingMessage::AddEvent(msg) => TracingMessage::AddEvent(msg.to_static()),
263            TracingMessage::AddLink(msg) => TracingMessage::AddLink(*msg),
264            TracingMessage::SetAttribute(msg) => TracingMessage::SetAttribute(msg.to_static()),
265        }
266    }
267}
268
269/// Message indicating the creation of a new span.
270///
271/// This message provides all the information needed to create a new span
272/// in the trace, including its identity, timing, and initial attributes.
273#[derive(Clone, Debug, Serialize)]
274#[cfg_attr(feature = "alloc", derive(Deserialize))]
275pub struct SpanCreateMessage<'a> {
276    /// The trace this span belongs to
277    pub trace_id: TraceId,
278    /// The unique identifier for this span
279    pub span_id: SpanId,
280    /// The parent span ID, if this is a child span
281    pub parent_span_id: Option<SpanId>,
282
283    /// The name of the span
284    #[serde(borrow)]
285    pub name: StringType<'a>,
286
287    /// Timestamp when the span was started
288    pub start_time_unix_nano: u64,
289
290    /// Initial attributes attached to the span
291    #[serde(borrow)]
292    pub attributes: AttributeListType<'a>,
293}
294
295#[cfg(feature = "alloc")]
296impl ToStatic for SpanCreateMessage<'_> {
297    type Static = SpanCreateMessage<'static>;
298
299    fn to_static(&self) -> Self::Static {
300        SpanCreateMessage {
301            trace_id: self.trace_id,
302            span_id: self.span_id,
303            parent_span_id: self.parent_span_id,
304            name: self.name.to_static(),
305            start_time_unix_nano: self.start_time_unix_nano,
306            attributes: self.attributes.to_static(),
307        }
308    }
309}
310
311/// Message indicating a span has been entered.
312#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
313pub struct SpanEnterMessage {
314    /// The trace this span belongs to
315    pub trace_id: TraceId,
316    /// The span being entered
317    pub span_id: SpanId,
318
319    /// Timestamp when the span was entered
320    pub time_unix_nano: u64,
321}
322
323/// Message indicating a span has been exited.
324#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
325pub struct SpanExitMessage {
326    /// The trace this span belongs to
327    pub trace_id: TraceId,
328    /// The span being exited
329    pub span_id: SpanId,
330
331    /// Timestamp when the span was exited
332    pub time_unix_nano: u64,
333}
334
335/// Message indicating a span has been closed (completed).
336#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
337pub struct SpanCloseMessage {
338    /// The trace this span belongs to
339    pub trace_id: TraceId,
340    /// The span being closed
341    pub span_id: SpanId,
342
343    /// Timestamp when the span was closed
344    pub end_time_unix_nano: u64,
345}
346
347/// Message indicating an attribute has been set on a span.
348#[derive(Clone, Debug, Serialize, Deserialize)]
349pub struct SpanSetAttributeMessage<'a> {
350    /// The trace this span belongs to
351    pub trace_id: TraceId,
352    /// The span the attribute is being set on
353    pub span_id: SpanId,
354
355    /// The attribute being set
356    #[serde(borrow)]
357    pub attribute: KeyValue<'a>,
358}
359
360#[cfg(feature = "alloc")]
361impl ToStatic for SpanSetAttributeMessage<'_> {
362    type Static = SpanSetAttributeMessage<'static>;
363
364    fn to_static(&self) -> Self::Static {
365        SpanSetAttributeMessage {
366            trace_id: self.trace_id,
367            span_id: self.span_id,
368            attribute: self.attribute.to_static(),
369        }
370    }
371}
372
373/// Message indicating an event has been added to a span.
374#[derive(Clone, Debug, Serialize)]
375#[cfg_attr(feature = "alloc", derive(Deserialize))]
376pub struct SpanAddEventMessage<'a> {
377    /// The trace this span belongs to
378    pub trace_id: TraceId,
379    /// The span the event is being added to
380    pub span_id: SpanId,
381
382    /// The name of the event
383    #[serde(borrow)]
384    pub name: StringType<'a>,
385    /// Timestamp when the event occurred
386    pub time_unix_nano: u64,
387
388    /// Attributes providing additional context for the event
389    #[serde(borrow)]
390    pub attributes: AttributeListType<'a>,
391}
392
393#[cfg(feature = "alloc")]
394impl ToStatic for SpanAddEventMessage<'_> {
395    type Static = SpanAddEventMessage<'static>;
396
397    fn to_static(&self) -> Self::Static {
398        SpanAddEventMessage {
399            trace_id: self.trace_id,
400            span_id: self.span_id,
401            name: self.name.to_static(),
402            time_unix_nano: self.time_unix_nano,
403            attributes: self.attributes.to_static(),
404        }
405    }
406}
407
408/// Message indicating a link has been added to a span.
409///
410/// Links connect spans across different traces, representing relationships
411/// that are not parent-child hierarchies.
412#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
413pub struct SpanAddLinkMessage {
414    /// The trace this span belongs to
415    pub trace_id: TraceId,
416    /// The span the link is being added to
417    pub span_id: SpanId,
418
419    /// The span context being linked to
420    pub link: SpanContext,
421}
422
423#[cfg(test)]
424#[cfg_attr(coverage_nightly, coverage(off))]
425mod tests {
426    #[cfg(feature = "alloc")]
427    use alloc::string::String;
428
429    use super::*;
430
431    #[test]
432    fn string_type_conversions() {
433        let static_str: StringType<'static> = "static".into();
434
435        let _event = SpanAddEventMessage {
436            trace_id: TraceId(0),
437            span_id: SpanId(0),
438            name: static_str,
439            time_unix_nano: 0,
440            attributes: attribute_list_from_slice(&[]),
441        };
442
443        let borrowed_str: StringType = "borrowed".into();
444
445        let _event = SpanAddEventMessage {
446            trace_id: TraceId(0),
447            span_id: SpanId(0),
448            name: borrowed_str,
449            time_unix_nano: 0,
450            attributes: attribute_list_from_slice(&[]),
451        };
452    }
453
454    #[cfg(any(feature = "std", feature = "alloc"))]
455    #[test]
456    fn string_type_with_owned_strings() {
457        let string = String::from("owned");
458        let owned: StringType<'static> = StringType::from(string);
459
460        let _event = SpanAddEventMessage {
461            trace_id: TraceId(0),
462            span_id: SpanId(0),
463            name: owned,
464            time_unix_nano: 0,
465            attributes: attribute_list_from_slice(&[]),
466        };
467    }
468
469    #[cfg(feature = "alloc")]
470    #[test]
471    fn to_static_conversion() {
472        use alloc::string::String;
473
474        use crate::value::Value;
475
476        // Create some data with non-static lifetime
477        let borrowed_name_str = "test_span";
478        let borrowed_name: StringType = borrowed_name_str.into();
479
480        let owned_key = String::from("test_key");
481        let owned_value = String::from("test_value");
482        let attribute = KeyValue {
483            key: owned_key.as_str().into(),
484            value: Value::String(owned_value.as_str().into()),
485        };
486
487        let attributes = [attribute];
488        let span_event = SpanAddEventMessage {
489            trace_id: TraceId(0),
490            span_id: SpanId(0),
491            name: borrowed_name,
492            time_unix_nano: 0,
493            attributes: attribute_list_from_slice(&attributes),
494        };
495
496        let tracing_message = TracingMessage::AddEvent(span_event);
497        let telemetry_message = TelemetryMessage::Tracing(tracing_message);
498        let instance_message = InstanceMessage {
499            execution: ExecutionId(999),
500            message: telemetry_message,
501        };
502
503        let static_message: InstanceMessage<'static> = instance_message.to_static();
504
505        // Verify the conversion worked - the static message should have the same data
506        if let TelemetryMessage::Tracing(TracingMessage::AddEvent(span_event)) =
507            &static_message.message
508        {
509            assert_eq!(span_event.name.as_ref(), "test_span");
510        } else {
511            panic!("Expected CreateSpan message");
512        }
513    }
514}