use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Serialize, Serializer};
use crate::RunId;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TraceId([u8; 16]);
impl TraceId {
pub fn new() -> Self {
let mut bytes = [0u8; 16];
for byte in &mut bytes {
*byte = (rand_byte() % 256) as u8;
}
if bytes.iter().all(|b| *b == 0) {
bytes[0] = 1;
}
TraceId(bytes)
}
pub fn from_hex(hex: &str) -> Option<Self> {
if hex.len() != 32 {
return None;
}
let mut bytes = [0u8; 16];
for i in 0..16 {
let byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()?;
bytes[i] = byte;
}
if bytes.iter().all(|b| *b == 0) {
return None;
}
Some(TraceId(bytes))
}
pub fn to_hex(&self) -> String {
self.0.iter().map(|b| format!("{:02x}", b)).collect()
}
pub fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
}
impl Default for TraceId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for TraceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_hex())
}
}
impl Serialize for TraceId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_hex())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SpanId([u8; 8]);
impl SpanId {
pub fn new() -> Self {
let mut bytes = [0u8; 8];
for byte in &mut bytes {
*byte = (rand_byte() % 256) as u8;
}
if bytes.iter().all(|b| *b == 0) {
bytes[0] = 1;
}
SpanId(bytes)
}
pub fn from_hex(hex: &str) -> Option<Self> {
if hex.len() != 16 {
return None;
}
let mut bytes = [0u8; 8];
for i in 0..8 {
let byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()?;
bytes[i] = byte;
}
if bytes.iter().all(|b| *b == 0) {
return None;
}
Some(SpanId(bytes))
}
pub fn to_hex(&self) -> String {
self.0.iter().map(|b| format!("{:02x}", b)).collect()
}
pub fn as_bytes(&self) -> &[u8; 8] {
&self.0
}
}
impl Default for SpanId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for SpanId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_hex())
}
}
impl Serialize for SpanId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_hex())
}
}
fn rand_byte() -> u32 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
static COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
let count = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
((now.as_nanos() as u32) ^ count ^ 0xDEADBEEF) & 0xFF
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum SpanKind {
Internal,
Client,
Server,
Producer,
Consumer,
}
impl fmt::Display for SpanKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SpanKind::Internal => write!(f, "INTERNAL"),
SpanKind::Client => write!(f, "CLIENT"),
SpanKind::Server => write!(f, "SERVER"),
SpanKind::Producer => write!(f, "PRODUCER"),
SpanKind::Consumer => write!(f, "CONSUMER"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum SpanStatus {
Ok,
Error,
Cancelled,
}
impl fmt::Display for SpanStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SpanStatus::Ok => write!(f, "OK"),
SpanStatus::Error => write!(f, "ERROR"),
SpanStatus::Cancelled => write!(f, "CANCELLED"),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct SpanEvent {
pub name: String,
#[serde(rename = "timestampMs")]
pub timestamp_ms: u64,
#[serde(flatten)]
pub attributes: HashMap<String, serde_json::Value>,
}
impl SpanEvent {
pub fn new(name: impl Into<String>) -> Self {
SpanEvent {
name: name.into(),
timestamp_ms: current_time_ms(),
attributes: HashMap::new(),
}
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
if let Ok(json_value) = serde_json::to_value(value) {
self.attributes.insert(key.into(), json_value);
}
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct SpanAttributes(HashMap<String, serde_json::Value>);
impl SpanAttributes {
pub fn new() -> Self {
SpanAttributes(HashMap::new())
}
pub fn insert(&mut self, key: impl Into<String>, value: impl Serialize) {
if let Ok(json_value) = serde_json::to_value(value) {
self.0.insert(key.into(), json_value);
}
}
pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
self.0.get(key)
}
pub fn contains(&self, key: &str) -> bool {
self.0.contains_key(key)
}
pub fn as_map(&self) -> &HashMap<String, serde_json::Value> {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Span {
#[serde(rename = "traceId")]
pub trace_id: TraceId,
#[serde(rename = "spanId")]
pub span_id: SpanId,
#[serde(rename = "parentSpanId", skip_serializing_if = "Option::is_none")]
pub parent_span_id: Option<SpanId>,
pub name: String,
pub kind: SpanKind,
#[serde(rename = "startTimeMs")]
pub start_time_ms: u64,
#[serde(rename = "endTimeMs", skip_serializing_if = "Option::is_none")]
pub end_time_ms: Option<u64>,
pub status: SpanStatus,
#[serde(flatten)]
pub attributes: SpanAttributes,
#[serde(rename = "events", skip_serializing_if = "Vec::is_empty")]
pub events: Vec<SpanEvent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub run_id: Option<String>,
}
impl Span {
pub fn context(&self) -> TraceContext {
TraceContext {
trace_id: self.trace_id.clone(),
parent_id: self.span_id.clone(),
trace_flags: if self.status == SpanStatus::Ok {
TraceFlags::SAMPLED
} else {
TraceFlags::default()
},
}
}
pub fn end(&mut self) {
self.end_time_ms = Some(current_time_ms());
}
pub fn end_with_status(&mut self, status: SpanStatus) {
self.status = status;
self.end();
}
pub fn set_attribute(&mut self, key: impl Into<String>, value: impl Serialize) {
self.attributes.insert(key, value);
}
pub fn add_event(&mut self, name: impl Into<String>) {
self.events.push(SpanEvent::new(name));
}
pub fn add_event_with_attrs(&mut self, name: impl Into<String>, attrs: SpanAttributes) {
let mut event = SpanEvent::new(name);
for (key, value) in attrs.as_map().iter() {
event.attributes.insert(key.clone(), value.clone());
}
self.events.push(event);
}
pub fn set_status(&mut self, status: SpanStatus) {
self.status = status;
}
pub fn set_run_id(&mut self, run_id: &RunId) {
self.run_id = Some(run_id.as_str().to_string());
}
pub fn duration_ms(&self) -> Option<u64> {
self.end_time_ms.map(|end| end - self.start_time_ms)
}
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap_or_else(|_| {
format!(
r#"{{"traceId":"{}","spanId":"{}","name":"{}"}}"#,
self.trace_id, self.span_id, self.name
)
})
}
pub fn is_ended(&self) -> bool {
self.end_time_ms.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TraceFlags(u8);
impl TraceFlags {
pub const SAMPLED: TraceFlags = TraceFlags(0x01);
pub fn new(value: u8) -> Self {
TraceFlags(value)
}
pub fn is_sampled(&self) -> bool {
(self.0 & 0x01) != 0
}
pub fn value(&self) -> u8 {
self.0
}
pub fn to_hex(&self) -> String {
format!("{:02x}", self.0)
}
}
impl fmt::Display for TraceFlags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_hex())
}
}
impl Serialize for TraceFlags {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_hex())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct TraceContext {
pub trace_id: TraceId,
pub parent_id: SpanId,
pub trace_flags: TraceFlags,
}
impl TraceContext {
pub fn new() -> Self {
TraceContext {
trace_id: TraceId::new(),
parent_id: SpanId::new(),
trace_flags: TraceFlags::SAMPLED,
}
}
pub fn from_traceparent(traceparent: &str) -> Option<Self> {
let parts: Vec<&str> = traceparent.split('-').collect();
if parts.len() != 4 {
return None;
}
if parts[0] != "00" {
return None;
}
let trace_id = TraceId::from_hex(parts[1])?;
let parent_id = SpanId::from_hex(parts[2])?;
if parts[3].len() != 2 {
return None;
}
let flags = u8::from_str_radix(parts[3], 16).ok()?;
let trace_flags = TraceFlags::new(flags);
Some(TraceContext {
trace_id,
parent_id,
trace_flags,
})
}
pub fn traceparent(&self) -> String {
format!(
"00-{}-{}-{}",
self.trace_id.to_hex(),
self.parent_id.to_hex(),
self.trace_flags.to_hex()
)
}
pub fn child(&self) -> TraceContext {
TraceContext {
trace_id: self.trace_id.clone(),
parent_id: SpanId::new(),
trace_flags: self.trace_flags,
}
}
pub fn is_sampled(&self) -> bool {
self.trace_flags.is_sampled()
}
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap_or_else(|_| {
format!(
r#"{{"trace_id":"{}","parent_id":"{}","trace_flags":"{}"}}"#,
self.trace_id, self.parent_id, self.trace_flags
)
})
}
}
impl Default for TraceContext {
fn default() -> Self {
Self::new()
}
}
pub struct SpanBuilder {
name: String,
kind: SpanKind,
parent_context: Option<TraceContext>,
attributes: SpanAttributes,
run_id: Option<RunId>,
}
impl SpanBuilder {
pub fn new(name: impl Into<String>) -> Self {
SpanBuilder {
name: name.into(),
kind: SpanKind::Internal,
parent_context: None,
attributes: SpanAttributes::new(),
run_id: None,
}
}
pub fn with_kind(mut self, kind: SpanKind) -> Self {
self.kind = kind;
self
}
pub fn with_parent(mut self, parent: TraceContext) -> Self {
self.parent_context = Some(parent);
self
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
self.attributes.insert(key, value);
self
}
pub fn with_attributes(mut self, attrs: SpanAttributes) -> Self {
for (key, value) in attrs.as_map().iter() {
self.attributes.0.insert(key.clone(), value.clone());
}
self
}
pub fn with_run_id(mut self, run_id: RunId) -> Self {
self.run_id = Some(run_id);
self
}
pub fn start(self, tracer: &Tracer) -> Span {
tracer.start_span(self)
}
}
#[derive(Debug, Clone)]
pub struct Tracer {
service_name: String,
}
impl Tracer {
pub fn new() -> Self {
Tracer {
service_name: "bzzz".to_string(),
}
}
pub fn with_service_name(name: impl Into<String>) -> Self {
Tracer {
service_name: name.into(),
}
}
pub fn span_builder(&self, name: impl Into<String>) -> SpanBuilder {
SpanBuilder::new(name)
}
pub fn start_span(&self, builder: SpanBuilder) -> Span {
let (trace_id, span_id, parent_span_id) = match builder.parent_context {
Some(ctx) => (ctx.trace_id.clone(), SpanId::new(), Some(ctx.parent_id)),
None => (TraceId::new(), SpanId::new(), None),
};
Span {
trace_id,
span_id,
parent_span_id,
name: builder.name,
kind: builder.kind,
start_time_ms: current_time_ms(),
end_time_ms: None,
status: SpanStatus::Ok,
attributes: builder.attributes,
events: Vec::new(),
run_id: builder.run_id.map(|r| r.as_str().to_string()),
}
}
pub fn root_span(&self, name: impl Into<String>) -> Span {
self.span_builder(name).start(self)
}
pub fn child_span(&self, name: impl Into<String>, parent: &TraceContext) -> Span {
self.span_builder(name)
.with_parent(parent.clone())
.start(self)
}
pub fn service_name(&self) -> &str {
&self.service_name
}
}
impl Default for Tracer {
fn default() -> Self {
Self::new()
}
}
fn current_time_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
static GLOBAL_TRACER: std::sync::OnceLock<Tracer> = std::sync::OnceLock::new();
pub fn global_tracer() -> &'static Tracer {
GLOBAL_TRACER.get_or_init(Tracer::new)
}
pub fn init_global_tracer(service_name: impl Into<String>) {
let _ = GLOBAL_TRACER.get_or_init(|| Tracer::with_service_name(service_name));
}
use crate::runtime::logging::LogEntry;
pub trait LogEntryTraceExt {
fn with_trace_context(self, ctx: &TraceContext) -> Self;
}
impl LogEntryTraceExt for LogEntry {
fn with_trace_context(mut self, ctx: &TraceContext) -> Self {
self.fields.insert(
"trace_id".into(),
serde_json::Value::String(ctx.trace_id.to_hex()),
);
self.fields.insert(
"span_id".into(),
serde_json::Value::String(ctx.parent_id.to_hex()),
);
self.fields.insert(
"traceparent".into(),
serde_json::Value::String(ctx.traceparent()),
);
self
}
}
pub trait SpanExporter: std::fmt::Debug {
fn export(&self, span: &Span);
}
#[derive(Debug, Clone, Default)]
pub struct InMemoryExporter {
spans: Arc<std::sync::Mutex<Vec<Span>>>,
}
impl InMemoryExporter {
pub fn new() -> Self {
InMemoryExporter {
spans: Arc::new(std::sync::Mutex::new(Vec::new())),
}
}
pub fn get_spans(&self) -> Vec<Span> {
self.spans.lock().unwrap().clone()
}
pub fn clear(&self) {
self.spans.lock().unwrap().clear();
}
pub fn len(&self) -> usize {
self.spans.lock().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.spans.lock().unwrap().is_empty()
}
}
impl SpanExporter for InMemoryExporter {
fn export(&self, span: &Span) {
self.spans.lock().unwrap().push(span.clone());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trace_id_generation() {
let id1 = TraceId::new();
let id2 = TraceId::new();
assert_ne!(id1, id2);
assert_eq!(id1.to_hex().len(), 32);
}
#[test]
fn test_trace_id_from_hex() {
let hex = "4bf92f3577b34da6a3ce929d0e0e4736";
let id = TraceId::from_hex(hex).unwrap();
assert_eq!(id.to_hex(), hex);
assert!(TraceId::from_hex("00000000000000000000000000000000").is_none());
assert!(TraceId::from_hex("abcd").is_none());
}
#[test]
fn test_span_id_generation() {
let id1 = SpanId::new();
let id2 = SpanId::new();
assert_ne!(id1, id2);
assert_eq!(id1.to_hex().len(), 16);
}
#[test]
fn test_span_id_from_hex() {
let hex = "00f067aa0ba902b7";
let id = SpanId::from_hex(hex).unwrap();
assert_eq!(id.to_hex(), hex);
assert!(SpanId::from_hex("0000000000000000").is_none());
assert!(SpanId::from_hex("abcd").is_none());
}
#[test]
fn test_trace_context_creation() {
let ctx = TraceContext::new();
assert!(ctx.is_sampled());
assert!(ctx.traceparent().starts_with("00-"));
}
#[test]
fn test_trace_context_from_traceparent() {
let traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
let ctx = TraceContext::from_traceparent(traceparent).unwrap();
assert_eq!(ctx.traceparent(), traceparent);
assert!(ctx.is_sampled());
}
#[test]
fn test_trace_context_invalid_traceparent() {
assert!(TraceContext::from_traceparent("01-abcd-efgh-01").is_none());
assert!(TraceContext::from_traceparent("invalid").is_none());
assert!(TraceContext::from_traceparent("00-abcd").is_none());
}
#[test]
fn test_trace_context_child() {
let parent = TraceContext::new();
let child = parent.child();
assert_eq!(parent.trace_id, child.trace_id);
assert_eq!(parent.trace_flags, child.trace_flags);
}
#[test]
fn test_span_creation() {
let tracer = Tracer::new();
let span = tracer.root_span("test-span");
assert_eq!(span.name, "test-span");
assert_eq!(span.kind, SpanKind::Internal);
assert!(span.parent_span_id.is_none());
assert!(!span.is_ended());
}
#[test]
fn test_span_with_parent() {
let tracer = Tracer::new();
let parent_span = tracer.root_span("parent");
let child_span = tracer.child_span("child", &parent_span.context());
assert!(child_span.parent_span_id.is_some());
assert_eq!(child_span.trace_id, parent_span.trace_id);
assert_eq!(child_span.parent_span_id.unwrap(), parent_span.span_id);
}
#[test]
fn test_span_lifecycle() {
let tracer = Tracer::new();
let mut span = tracer.root_span("test");
assert!(!span.is_ended());
span.set_attribute("key", "value");
span.add_event("something happened");
span.end();
assert!(span.is_ended());
assert!(span.duration_ms().is_some());
assert!(span.attributes.contains("key"));
assert_eq!(span.events.len(), 1);
}
#[test]
fn test_span_status() {
let tracer = Tracer::new();
let mut span = tracer.root_span("test");
assert_eq!(span.status, SpanStatus::Ok);
span.end_with_status(SpanStatus::Error);
assert_eq!(span.status, SpanStatus::Error);
assert!(span.is_ended());
}
#[test]
fn test_span_builder() {
let tracer = Tracer::new();
let span = tracer
.span_builder("test")
.with_kind(SpanKind::Server)
.with_attribute("http.method", "GET")
.with_attribute("http.url", "/api/run")
.start(&tracer);
assert_eq!(span.kind, SpanKind::Server);
assert!(span.attributes.contains("http.method"));
assert!(span.attributes.contains("http.url"));
}
#[test]
fn test_span_json() {
let tracer = Tracer::new();
let mut span = tracer.root_span("test");
span.end();
let json = span.to_json();
assert!(json.contains("\"traceId\""));
assert!(json.contains("\"spanId\""));
assert!(json.contains("\"name\":\"test\""));
}
#[test]
fn test_span_attributes() {
let mut attrs = SpanAttributes::new();
attrs.insert("string_key", "value");
attrs.insert("number_key", 42);
attrs.insert("bool_key", true);
assert_eq!(attrs.len(), 3);
assert!(attrs.contains("string_key"));
assert_eq!(attrs.get("number_key").unwrap().as_u64(), Some(42));
}
#[test]
fn test_span_event() {
let event = SpanEvent::new("error")
.with_attribute("error.type", "timeout")
.with_attribute("error.message", "Connection timed out");
assert_eq!(event.name, "error");
assert!(event.attributes.contains_key("error.type"));
}
#[test]
fn test_log_entry_with_trace() {
use crate::runtime::logging::{LogEntry, LogLevel};
let ctx = TraceContext::new();
let entry = LogEntry::new(LogLevel::Info, "test").with_trace_context(&ctx);
assert!(entry.fields.contains_key("trace_id"));
assert!(entry.fields.contains_key("span_id"));
assert!(entry.fields.contains_key("traceparent"));
}
#[test]
fn test_in_memory_exporter() {
let exporter = InMemoryExporter::new();
let tracer = Tracer::new();
let mut span = tracer.root_span("test");
span.end();
exporter.export(&span);
assert_eq!(exporter.len(), 1);
assert_eq!(exporter.get_spans()[0].name, "test");
exporter.clear();
assert!(exporter.is_empty());
}
#[test]
fn test_global_tracer() {
let tracer = global_tracer();
let span = tracer.root_span("test");
assert_eq!(span.name, "test");
}
#[test]
fn test_span_with_run_id() {
let tracer = Tracer::new();
let run_id = RunId::new();
let span = tracer
.span_builder("test")
.with_run_id(run_id.clone())
.start(&tracer);
assert!(span.run_id.is_some());
assert_eq!(span.run_id.unwrap(), run_id.as_str());
}
#[test]
fn test_trace_flags() {
let flags = TraceFlags::SAMPLED;
assert!(flags.is_sampled());
assert_eq!(flags.to_hex(), "01");
let unsampled = TraceFlags::default();
assert!(!unsampled.is_sampled());
assert_eq!(unsampled.to_hex(), "00");
}
#[test]
fn test_tracer_service_name() {
let tracer = Tracer::with_service_name("my-service");
assert_eq!(tracer.service_name(), "my-service");
}
}