use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TraceId(String);
impl TraceId {
pub fn new() -> Self {
Self(uuid::Uuid::new_v4().to_string().replace('-', ""))
}
pub fn from_string(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for TraceId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for TraceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SpanId(String);
impl SpanId {
pub fn new() -> Self {
Self(uuid::Uuid::new_v4().to_string().replace('-', "")[..16].to_string())
}
pub fn from_string(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for SpanId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for SpanId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanContext {
pub trace_id: TraceId,
pub span_id: SpanId,
pub parent_span_id: Option<SpanId>,
pub trace_flags: u8,
}
impl SpanContext {
pub fn new_root() -> Self {
Self {
trace_id: TraceId::new(),
span_id: SpanId::new(),
parent_span_id: None,
trace_flags: 0x01, }
}
pub fn child(&self) -> Self {
Self {
trace_id: self.trace_id.clone(),
span_id: SpanId::new(),
parent_span_id: Some(self.span_id.clone()),
trace_flags: self.trace_flags,
}
}
pub fn is_sampled(&self) -> bool {
self.trace_flags & 0x01 != 0
}
pub fn to_traceparent(&self) -> String {
format!(
"00-{}-{}-{:02x}",
self.trace_id, self.span_id, self.trace_flags
)
}
pub fn from_traceparent(traceparent: &str) -> Option<Self> {
let parts: Vec<&str> = traceparent.split('-').collect();
if parts.len() != 4 || parts[0] != "00" {
return None;
}
let trace_id = TraceId::from_string(parts[1]);
let span_id = SpanId::from_string(parts[2]);
let trace_flags = u8::from_str_radix(parts[3], 16).ok()?;
Some(Self {
trace_id,
span_id,
parent_span_id: None,
trace_flags,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SpanKind {
#[default]
Internal,
Server,
Client,
Producer,
Consumer,
}
impl std::fmt::Display for SpanKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Internal => write!(f, "internal"),
Self::Server => write!(f, "server"),
Self::Client => write!(f, "client"),
Self::Producer => write!(f, "producer"),
Self::Consumer => write!(f, "consumer"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SpanStatus {
#[default]
Unset,
Ok,
Error,
}
impl std::fmt::Display for SpanStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unset => write!(f, "unset"),
Self::Ok => write!(f, "ok"),
Self::Error => write!(f, "error"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Span {
pub context: SpanContext,
pub name: String,
pub kind: SpanKind,
pub status: SpanStatus,
pub status_message: Option<String>,
pub start_time: chrono::DateTime<chrono::Utc>,
pub end_time: Option<chrono::DateTime<chrono::Utc>>,
pub attributes: HashMap<String, serde_json::Value>,
pub events: Vec<SpanEvent>,
pub node_id: Option<uuid::Uuid>,
}
impl Span {
pub fn new(name: impl Into<String>) -> Self {
Self {
context: SpanContext::new_root(),
name: name.into(),
kind: SpanKind::Internal,
status: SpanStatus::Unset,
status_message: None,
start_time: chrono::Utc::now(),
end_time: None,
attributes: HashMap::new(),
events: Vec::new(),
node_id: None,
}
}
pub fn child(&self, name: impl Into<String>) -> Self {
Self {
context: self.context.child(),
name: name.into(),
kind: SpanKind::Internal,
status: SpanStatus::Unset,
status_message: None,
start_time: chrono::Utc::now(),
end_time: None,
attributes: HashMap::new(),
events: Vec::new(),
node_id: self.node_id,
}
}
pub fn with_kind(mut self, kind: SpanKind) -> Self {
self.kind = kind;
self
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
if let Ok(v) = serde_json::to_value(value) {
self.attributes.insert(key.into(), v);
}
self
}
pub fn with_node_id(mut self, node_id: uuid::Uuid) -> Self {
self.node_id = Some(node_id);
self
}
pub fn with_parent(mut self, parent: &SpanContext) -> Self {
self.context = parent.child();
self
}
pub fn add_event(&mut self, name: impl Into<String>) {
self.events.push(SpanEvent {
name: name.into(),
timestamp: chrono::Utc::now(),
attributes: HashMap::new(),
});
}
pub fn end_ok(&mut self) {
self.status = SpanStatus::Ok;
self.end_time = Some(chrono::Utc::now());
}
pub fn end_error(&mut self, message: impl Into<String>) {
self.status = SpanStatus::Error;
self.status_message = Some(message.into());
self.end_time = Some(chrono::Utc::now());
}
pub fn duration_ms(&self) -> Option<f64> {
self.end_time
.map(|end| (end - self.start_time).num_milliseconds() as f64)
}
pub fn is_complete(&self) -> bool {
self.end_time.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanEvent {
pub name: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub attributes: HashMap<String, serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trace_id_generation() {
let id1 = TraceId::new();
let id2 = TraceId::new();
assert_ne!(id1.as_str(), id2.as_str());
assert!(!id1.as_str().contains('-'));
}
#[test]
fn test_span_context_root() {
let ctx = SpanContext::new_root();
assert!(ctx.parent_span_id.is_none());
assert!(ctx.is_sampled());
}
#[test]
fn test_span_context_child() {
let parent = SpanContext::new_root();
let child = parent.child();
assert_eq!(child.trace_id, parent.trace_id);
assert_ne!(child.span_id, parent.span_id);
assert_eq!(child.parent_span_id, Some(parent.span_id));
}
#[test]
fn test_traceparent_roundtrip() {
let ctx = SpanContext::new_root();
let header = ctx.to_traceparent();
let parsed = SpanContext::from_traceparent(&header).unwrap();
assert_eq!(parsed.trace_id, ctx.trace_id);
assert_eq!(parsed.span_id, ctx.span_id);
assert_eq!(parsed.trace_flags, ctx.trace_flags);
}
#[test]
fn test_span_lifecycle() {
let mut span = Span::new("test_operation")
.with_kind(SpanKind::Server)
.with_attribute("http.method", "GET");
assert!(!span.is_complete());
assert!(span.duration_ms().is_none());
span.add_event("started processing");
span.end_ok();
assert!(span.is_complete());
assert!(span.duration_ms().is_some());
assert_eq!(span.status, SpanStatus::Ok);
}
#[test]
fn test_span_error() {
let mut span = Span::new("failing_operation");
span.end_error("Something went wrong");
assert_eq!(span.status, SpanStatus::Error);
assert_eq!(
span.status_message,
Some("Something went wrong".to_string())
);
}
#[test]
fn test_child_span() {
let parent = Span::new("parent");
let child = parent.child("child");
assert_eq!(child.context.trace_id, parent.context.trace_id);
assert_eq!(
child.context.parent_span_id,
Some(parent.context.span_id.clone())
);
}
}