use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SpanRecord {
pub trace_id: [u8; 16],
pub span_id: [u8; 8],
#[serde(skip_serializing_if = "Option::is_none", default)]
pub parent_span_id: Option<[u8; 8]>,
pub span_name: String,
pub span_kind: SpanKind,
pub start_time_nanos: u64,
pub end_time_nanos: u64,
pub duration_nanos: u64,
pub logical_clock: u64,
pub status_code: StatusCode,
pub status_message: String,
pub attributes_json: String,
pub resource_json: String,
pub process_id: u32,
pub thread_id: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[repr(u8)]
pub enum SpanKind {
#[default]
Internal = 0,
Server = 1,
Client = 2,
Producer = 3,
Consumer = 4,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[repr(u8)]
pub enum StatusCode {
#[default]
Unset = 0,
Ok = 1,
Error = 2,
}
impl SpanRecord {
#[allow(clippy::too_many_arguments)]
pub fn new(
trace_id: [u8; 16],
span_id: [u8; 8],
parent_span_id: Option<[u8; 8]>,
span_name: String,
span_kind: SpanKind,
start_time_nanos: u64,
end_time_nanos: u64,
logical_clock: u64,
status_code: StatusCode,
status_message: String,
attributes: HashMap<String, String>,
resource: HashMap<String, String>,
process_id: u32,
thread_id: u64,
) -> Self {
let duration_nanos = end_time_nanos.saturating_sub(start_time_nanos);
let attributes_json =
serde_json::to_string(&attributes).unwrap_or_else(|_| "{}".to_string());
let resource_json = serde_json::to_string(&resource).unwrap_or_else(|_| "{}".to_string());
Self {
trace_id,
span_id,
parent_span_id,
span_name,
span_kind,
start_time_nanos,
end_time_nanos,
duration_nanos,
logical_clock,
status_code,
status_message,
attributes_json,
resource_json,
process_id,
thread_id,
}
}
pub fn parse_attributes(&self) -> HashMap<String, String> {
serde_json::from_str(&self.attributes_json).unwrap_or_default()
}
pub fn parse_resource(&self) -> HashMap<String, String> {
serde_json::from_str(&self.resource_json).unwrap_or_default()
}
pub fn trace_id_hex(&self) -> String {
hex::encode(self.trace_id)
}
pub fn span_id_hex(&self) -> String {
hex::encode(self.span_id)
}
pub fn parent_span_id_hex(&self) -> Option<String> {
self.parent_span_id.map(hex::encode)
}
pub fn is_root(&self) -> bool {
self.parent_span_id.is_none()
}
pub fn is_error(&self) -> bool {
self.status_code == StatusCode::Error
}
}
static_assertions::assert_impl_all!(SpanRecord: Send, Sync);
static_assertions::assert_impl_all!(SpanKind: Send, Sync);
static_assertions::assert_impl_all!(StatusCode: Send, Sync);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_span_record_creation() {
let mut attributes = HashMap::new();
attributes.insert("syscall.name".to_string(), "read".to_string());
attributes.insert("syscall.fd".to_string(), "3".to_string());
let mut resource = HashMap::new();
resource.insert("service.name".to_string(), "renacer".to_string());
let span = SpanRecord::new(
[1; 16],
[2; 8],
None,
"read".to_string(),
SpanKind::Internal,
1000,
2000,
42,
StatusCode::Ok,
String::new(),
attributes,
resource,
1234,
5678,
);
assert_eq!(span.trace_id, [1; 16]);
assert_eq!(span.span_id, [2; 8]);
assert_eq!(span.parent_span_id, None);
assert_eq!(span.span_name, "read");
assert_eq!(span.span_kind, SpanKind::Internal);
assert_eq!(span.start_time_nanos, 1000);
assert_eq!(span.end_time_nanos, 2000);
assert_eq!(span.duration_nanos, 1000);
assert_eq!(span.logical_clock, 42);
assert_eq!(span.status_code, StatusCode::Ok);
assert_eq!(span.process_id, 1234);
assert_eq!(span.thread_id, 5678);
}
#[test]
fn test_duration_computation() {
let span = SpanRecord::new(
[1; 16],
[2; 8],
None,
"test".to_string(),
SpanKind::Internal,
1000,
3500,
42,
StatusCode::Ok,
String::new(),
HashMap::new(),
HashMap::new(),
0,
0,
);
assert_eq!(span.duration_nanos, 2500);
}
#[test]
fn test_attributes_serialization() {
let mut attributes = HashMap::new();
attributes.insert("key1".to_string(), "value1".to_string());
attributes.insert("key2".to_string(), "value2".to_string());
let span = SpanRecord::new(
[1; 16],
[2; 8],
None,
"test".to_string(),
SpanKind::Internal,
0,
0,
0,
StatusCode::Ok,
String::new(),
attributes.clone(),
HashMap::new(),
0,
0,
);
let parsed = span.parse_attributes();
assert_eq!(parsed.get("key1"), Some(&"value1".to_string()));
assert_eq!(parsed.get("key2"), Some(&"value2".to_string()));
}
#[test]
fn test_trace_id_hex() {
let span = SpanRecord::new(
[
0x4b, 0xf9, 0x2f, 0x3c, 0x7b, 0x64, 0x4b, 0xf9, 0x2f, 0x3c, 0x7b, 0x64, 0x4b, 0xf9,
0x2f, 0x3c,
],
[0x00, 0xf0, 0x67, 0xaa, 0x0b, 0xa9, 0x02, 0xb7],
None,
"test".to_string(),
SpanKind::Internal,
0,
0,
0,
StatusCode::Ok,
String::new(),
HashMap::new(),
HashMap::new(),
0,
0,
);
assert_eq!(span.trace_id_hex(), "4bf92f3c7b644bf92f3c7b644bf92f3c");
assert_eq!(span.span_id_hex(), "00f067aa0ba902b7");
}
#[test]
fn test_is_root() {
let root_span = SpanRecord::new(
[1; 16],
[2; 8],
None,
"root".to_string(),
SpanKind::Internal,
0,
0,
0,
StatusCode::Ok,
String::new(),
HashMap::new(),
HashMap::new(),
0,
0,
);
let child_span = SpanRecord::new(
[1; 16],
[3; 8],
Some([2; 8]),
"child".to_string(),
SpanKind::Internal,
0,
0,
1,
StatusCode::Ok,
String::new(),
HashMap::new(),
HashMap::new(),
0,
0,
);
assert!(root_span.is_root());
assert!(!child_span.is_root());
}
#[test]
fn test_is_error() {
let ok_span = SpanRecord::new(
[1; 16],
[2; 8],
None,
"ok".to_string(),
SpanKind::Internal,
0,
0,
0,
StatusCode::Ok,
String::new(),
HashMap::new(),
HashMap::new(),
0,
0,
);
let error_span = SpanRecord::new(
[1; 16],
[3; 8],
None,
"error".to_string(),
SpanKind::Internal,
0,
0,
1,
StatusCode::Error,
"Something went wrong".to_string(),
HashMap::new(),
HashMap::new(),
0,
0,
);
assert!(!ok_span.is_error());
assert!(error_span.is_error());
}
#[test]
fn test_span_kind_default() {
assert_eq!(SpanKind::default(), SpanKind::Internal);
}
#[test]
fn test_status_code_default() {
assert_eq!(StatusCode::default(), StatusCode::Unset);
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
#[kani::proof]
fn proof_duration_consistency() {
let start: u64 = kani::any();
let end: u64 = kani::any();
kani::assume(end >= start);
let duration = end - start;
kani::assert(start + duration == end, "duration must equal end - start");
}
#[kani::proof]
fn proof_span_kind_exhaustive() {
let kind: u8 = kani::any();
kani::assume(kind <= 4);
let _result = match kind {
0 => SpanKind::Internal,
1 => SpanKind::Server,
2 => SpanKind::Client,
3 => SpanKind::Producer,
_ => SpanKind::Consumer,
};
}
#[kani::proof]
fn proof_status_code_exhaustive() {
let code: u8 = kani::any();
kani::assume(code <= 2);
let _result = match code {
0 => StatusCode::Unset,
1 => StatusCode::Ok,
_ => StatusCode::Error,
};
}
}