Skip to content

Commit 3333822

Browse files
committed
feat(telemetry): implement custom serialization for telemetry ids
Add custom `Display`, `FromStr`, `Serialize`, and `Deserialize` implementations for `ProcessId`, `ThreadId`, `ExecutionId`, and `SpanContext` types. These provide a consistent hex-encoded string format with colon separators for composite IDs (`process:thread` for `ExecutionId`, `process:span` for `SpanContext`). This makes telemetry ids more readable and provides a unified format for logging and serialization. Signed-off-by: Wim Looman <[email protected]>
1 parent 534faf7 commit 3333822

File tree

5 files changed

+393
-14
lines changed

5 files changed

+393
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* **breaking** Removed `Span::root` method and `root_span!` macro; root spans should use `Span::new` and `span!` instead.
99
* **breaking** Replaced `SpanContext::from_span` with `Span::context` method.
1010
* **breaking** Telemetry execution id is replaced by separate thread and process ids to uniquely identify thread/task combinations.
11+
* Added custom serialization for telemetry ids with hex-encoded string format.
1112
* Added `ThreadAbstraction` trait to OSAL for querying current thread id.
1213
* Updated MSRV to 1.91.
1314
* Fixed `veecle_os::telemetry::instrument` macro to automatically resolve correct crate paths for the facade.

veecle-telemetry/src/id.rs

Lines changed: 221 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
use core::fmt;
1717
use core::str::FromStr;
1818

19-
use serde::{Deserialize, Serialize};
20-
2119
/// A globally-unique id identifying a process.
2220
///
2321
/// The primary purpose of this id is to provide a globally-unique context within which
@@ -26,9 +24,7 @@ use serde::{Deserialize, Serialize};
2624
/// most embedded setups it should be unique for each time the system is restarted.
2725
///
2826
/// [`ThreadId`]: crate::protocol::ThreadId
29-
#[derive(
30-
Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize,
31-
)]
27+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Default)]
3228
pub struct ProcessId(u128);
3329

3430
impl ProcessId {
@@ -52,6 +48,43 @@ impl ProcessId {
5248
}
5349
}
5450

51+
impl fmt::Display for ProcessId {
52+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53+
write!(f, "{:032x}", self.0)
54+
}
55+
}
56+
57+
impl FromStr for ProcessId {
58+
type Err = core::num::ParseIntError;
59+
60+
fn from_str(s: &str) -> Result<Self, Self::Err> {
61+
u128::from_str_radix(s, 16).map(ProcessId)
62+
}
63+
}
64+
65+
impl serde::Serialize for ProcessId {
66+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
67+
where
68+
S: serde::Serializer,
69+
{
70+
let mut hex_bytes = [0u8; size_of::<u128>() * 2];
71+
hex::encode_to_slice(self.0.to_le_bytes(), &mut hex_bytes).unwrap();
72+
73+
serializer.serialize_str(str::from_utf8(&hex_bytes).unwrap())
74+
}
75+
}
76+
77+
impl<'de> serde::Deserialize<'de> for ProcessId {
78+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
79+
where
80+
D: serde::Deserializer<'de>,
81+
{
82+
let bytes: [u8; size_of::<u128>()] = hex::serde::deserialize(deserializer)?;
83+
84+
Ok(ProcessId(u128::from_le_bytes(bytes)))
85+
}
86+
}
87+
5588
/// A process-unique id for a span.
5689
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
5790
pub struct SpanId(pub u64);
@@ -106,7 +139,7 @@ impl<'de> serde::Deserialize<'de> for SpanId {
106139
}
107140

108141
/// A struct representing the context of a span, including its [`ProcessId`] and [`SpanId`].
109-
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
142+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
110143
pub struct SpanContext {
111144
/// The id of the process this span belongs to.
112145
pub process_id: ProcessId,
@@ -160,6 +193,119 @@ impl SpanContext {
160193
}
161194
}
162195

196+
impl fmt::Display for SpanContext {
197+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198+
let Self {
199+
process_id,
200+
span_id,
201+
} = self;
202+
write!(f, "{process_id}:{span_id}")
203+
}
204+
}
205+
206+
/// Errors that can occur while parsing [`SpanContext`] from a string.
207+
#[derive(Clone, Debug)]
208+
pub enum ParseSpanContextError {
209+
/// The string is missing a `:` separator.
210+
MissingSeparator,
211+
212+
/// The embedded [`ProcessId`] failed to parse.
213+
InvalidProcessId(core::num::ParseIntError),
214+
215+
/// The embedded [`SpanId`] failed to parse.
216+
InvalidSpanId(core::num::ParseIntError),
217+
}
218+
219+
impl fmt::Display for ParseSpanContextError {
220+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221+
match self {
222+
Self::MissingSeparator => f.write_str("missing ':' separator"),
223+
Self::InvalidProcessId(_) => f.write_str("failed to parse process id"),
224+
Self::InvalidSpanId(_) => f.write_str("failed to parse span id"),
225+
}
226+
}
227+
}
228+
229+
impl core::error::Error for ParseSpanContextError {
230+
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
231+
match self {
232+
Self::MissingSeparator => None,
233+
Self::InvalidProcessId(error) => Some(error),
234+
Self::InvalidSpanId(error) => Some(error),
235+
}
236+
}
237+
}
238+
239+
impl FromStr for SpanContext {
240+
type Err = ParseSpanContextError;
241+
242+
fn from_str(s: &str) -> Result<Self, Self::Err> {
243+
let Some((process_id, span_id)) = s.split_once(":") else {
244+
return Err(ParseSpanContextError::MissingSeparator);
245+
};
246+
let process_id =
247+
ProcessId::from_str(process_id).map_err(ParseSpanContextError::InvalidProcessId)?;
248+
let span_id = SpanId::from_str(span_id).map_err(ParseSpanContextError::InvalidSpanId)?;
249+
Ok(Self {
250+
process_id,
251+
span_id,
252+
})
253+
}
254+
}
255+
256+
impl serde::Serialize for SpanContext {
257+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
258+
where
259+
S: serde::Serializer,
260+
{
261+
let mut bytes = [0u8; 49];
262+
263+
hex::encode_to_slice(self.process_id.to_raw().to_le_bytes(), &mut bytes[..32]).unwrap();
264+
bytes[32] = b':';
265+
hex::encode_to_slice(self.span_id.0.to_le_bytes(), &mut bytes[33..]).unwrap();
266+
267+
serializer.serialize_str(str::from_utf8(&bytes).unwrap())
268+
}
269+
}
270+
271+
impl<'de> serde::Deserialize<'de> for SpanContext {
272+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
273+
where
274+
D: serde::Deserializer<'de>,
275+
{
276+
use serde::de::Error;
277+
278+
let string = <&str>::deserialize(deserializer)?;
279+
280+
if string.len() != 49 {
281+
return Err(D::Error::invalid_length(
282+
string.len(),
283+
&"expected 49 byte string",
284+
));
285+
}
286+
287+
let bytes = string.as_bytes();
288+
289+
if bytes[32] != b':' {
290+
return Err(D::Error::invalid_value(
291+
serde::de::Unexpected::Str(string),
292+
&"expected : separator at byte 32",
293+
));
294+
}
295+
296+
let mut process = [0; 16];
297+
hex::decode_to_slice(&bytes[..32], &mut process).map_err(D::Error::custom)?;
298+
299+
let mut span = [0; 8];
300+
hex::decode_to_slice(&bytes[33..], &mut span).map_err(D::Error::custom)?;
301+
302+
Ok(Self {
303+
process_id: ProcessId::from_raw(u128::from_le_bytes(process)),
304+
span_id: SpanId(u64::from_le_bytes(span)),
305+
})
306+
}
307+
}
308+
163309
#[cfg(all(test, feature = "std"))]
164310
mod tests {
165311
use std::collections::HashSet;
@@ -308,6 +454,75 @@ mod tests {
308454
assert_eq!(context.span_id, span_id);
309455
}
310456

457+
#[test]
458+
fn process_id_format_from_str_roundtrip() {
459+
let test_cases = [
460+
0u128,
461+
1,
462+
0x123,
463+
0xFEDCBA9876543210,
464+
0x123456789ABCDEF0FEDCBA9876543210,
465+
u128::MAX,
466+
u128::MAX - 1,
467+
];
468+
469+
for value in test_cases {
470+
let process_id = ProcessId::from_raw(value);
471+
let formatted = format!("{process_id}");
472+
let parsed = formatted.parse::<ProcessId>().unwrap();
473+
assert_eq!(process_id, parsed, "Failed roundtrip for value {value:#x}");
474+
}
475+
}
476+
477+
#[test]
478+
fn process_id_serde_roundtrip() {
479+
let test_cases = [
480+
ProcessId::from_raw(0),
481+
ProcessId::from_raw(1),
482+
ProcessId::from_raw(0x123),
483+
ProcessId::from_raw(0xFEDCBA9876543210),
484+
ProcessId::from_raw(0x123456789ABCDEF0FEDCBA9876543210),
485+
ProcessId::from_raw(u128::MAX),
486+
ProcessId::from_raw(u128::MAX - 1),
487+
];
488+
489+
for original in test_cases {
490+
let json = serde_json::to_string(&original).unwrap();
491+
let deserialized: ProcessId = serde_json::from_str(&json).unwrap();
492+
assert_eq!(
493+
original,
494+
deserialized,
495+
"JSON roundtrip failed for {:#x}",
496+
original.to_raw()
497+
);
498+
}
499+
}
500+
501+
#[test]
502+
fn span_context_format_from_str_roundtrip() {
503+
let test_cases = [
504+
SpanContext::new(ProcessId::from_raw(0), SpanId(0)),
505+
SpanContext::new(
506+
ProcessId::from_raw(0x123456789ABCDEF0FEDCBA9876543210),
507+
SpanId(0xFEDCBA9876543210),
508+
),
509+
SpanContext::new(ProcessId::from_raw(u128::MAX), SpanId(u64::MAX)),
510+
SpanContext::new(ProcessId::from_raw(1), SpanId(1)),
511+
];
512+
513+
for context in test_cases {
514+
let formatted = format!("{context}");
515+
let parsed = formatted.parse::<SpanContext>().unwrap();
516+
assert_eq!(
517+
context,
518+
parsed,
519+
"Failed roundtrip for {:#x}:{:#x}",
520+
context.process_id.to_raw(),
521+
context.span_id.0
522+
);
523+
}
524+
}
525+
311526
#[test]
312527
fn span_id_next_id_produces_non_zero_values() {
313528
let ids: Vec<SpanId> = (0..100).map(|_| SpanId::next_id()).collect();

0 commit comments

Comments
 (0)