#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::time::Instant;
const TRACEPARENT_HEADER: &str = "traceparent";
const TRACESTATE_HEADER: &str = "tracestate";
const B3_HEADER: &str = "b3";
const B3_TRACE_ID_HEADER: &str = "x-b3-traceid";
const B3_SPAN_ID_HEADER: &str = "x-b3-spanid";
const B3_SAMPLED_HEADER: &str = "x-b3-sampled";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceContext {
pub trace_id: String,
pub parent_span_id: String,
pub span_id: String,
pub trace_flags: u8,
pub trace_state: Option<String>,
}
impl TraceContext {
pub fn new_root() -> Self {
Self {
trace_id: generate_trace_id(),
parent_span_id: String::new(),
span_id: generate_span_id(),
trace_flags: 1, trace_state: None,
}
}
pub fn child(&self) -> Self {
Self {
trace_id: self.trace_id.clone(),
parent_span_id: self.span_id.clone(),
span_id: generate_span_id(),
trace_flags: self.trace_flags,
trace_state: self.trace_state.clone(),
}
}
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(value: &str) -> Option<Self> {
let parts: Vec<&str> = value.trim().split('-').collect();
if parts.len() != 4 {
return None;
}
let version = parts[0];
if version != "00" {
return None;
}
let trace_id = parts[1];
let parent_span_id = parts[2];
let flags_str = parts[3];
if trace_id.len() != 32 || parent_span_id.len() != 16 || flags_str.len() != 2 {
return None;
}
if !is_hex(trace_id) || !is_hex(parent_span_id) || !is_hex(flags_str) {
return None;
}
let trace_flags = u8::from_str_radix(flags_str, 16).ok()?;
Some(Self {
trace_id: trace_id.to_string(),
parent_span_id: parent_span_id.to_string(),
span_id: generate_span_id(),
trace_flags,
trace_state: None,
})
}
pub fn to_b3_single(&self) -> String {
let sampled = if self.is_sampled() { "1" } else { "0" };
if self.parent_span_id.is_empty() {
format!("{}-{}-{}", self.trace_id, self.span_id, sampled)
} else {
format!(
"{}-{}-{}-{}",
self.trace_id, self.span_id, sampled, self.parent_span_id
)
}
}
pub fn from_b3_single(value: &str) -> Option<Self> {
let parts: Vec<&str> = value.trim().split('-').collect();
if parts.len() < 3 {
return None;
}
let trace_id = parts[0];
let span_id = parts[1];
let sampled = parts[2];
if !is_hex(trace_id) || !is_hex(span_id) {
return None;
}
let trace_flags = if sampled == "1" || sampled == "true" {
1
} else {
0
};
let parent_span_id = if parts.len() >= 4 {
parts[3].to_string()
} else {
String::new()
};
Some(Self {
trace_id: trace_id.to_string(),
parent_span_id,
span_id: generate_span_id(),
trace_flags,
trace_state: None,
})
}
}
impl fmt::Display for TraceContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"trace_id={} span_id={} sampled={}",
self.trace_id,
self.span_id,
self.is_sampled()
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PropagationFormat {
W3C,
B3,
}
pub fn extract_trace_context(headers: &http::HeaderMap) -> Option<TraceContext> {
let hdr = |name: &str| -> Option<&str> { headers.get(name).and_then(|v| v.to_str().ok()) };
if let Some(traceparent) = hdr(TRACEPARENT_HEADER) {
if let Some(mut ctx) = TraceContext::from_traceparent(traceparent) {
ctx.trace_state = hdr(TRACESTATE_HEADER).map(|s| s.to_string());
return Some(ctx);
}
}
if let Some(b3) = hdr(B3_HEADER) {
return TraceContext::from_b3_single(b3);
}
if let Some(trace_id) = hdr(B3_TRACE_ID_HEADER) {
if let Some(span_id) = hdr(B3_SPAN_ID_HEADER) {
let sampled = hdr(B3_SAMPLED_HEADER)
.map(|s| s == "1" || s == "true")
.unwrap_or(true);
return Some(TraceContext {
trace_id: trace_id.to_string(),
parent_span_id: span_id.to_string(),
span_id: generate_span_id(),
trace_flags: if sampled { 1 } else { 0 },
trace_state: None,
});
}
}
None
}
pub fn inject_trace_context(
ctx: &TraceContext,
headers: &mut HashMap<String, String>,
format: PropagationFormat,
) {
match format {
PropagationFormat::W3C => {
headers.insert(TRACEPARENT_HEADER.to_string(), ctx.to_traceparent());
if let Some(ref state) = ctx.trace_state {
headers.insert(TRACESTATE_HEADER.to_string(), state.clone());
}
}
PropagationFormat::B3 => {
headers.insert(B3_HEADER.to_string(), ctx.to_b3_single());
}
}
}
#[derive(Debug, Clone)]
pub struct GatewaySpan {
pub name: String,
pub trace_context: TraceContext,
pub start: Instant,
pub end: Option<Instant>,
pub attributes: HashMap<String, String>,
pub status: SpanStatus,
}
impl GatewaySpan {
pub fn new(name: impl Into<String>, trace_context: TraceContext) -> Self {
Self {
name: name.into(),
trace_context,
start: Instant::now(),
end: None,
attributes: HashMap::new(),
status: SpanStatus::Unset,
}
}
pub fn set_attribute(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.attributes.insert(key.into(), value.into());
}
pub fn finish(&mut self) {
self.end = Some(Instant::now());
}
pub fn finish_with_error(&mut self, message: impl Into<String>) {
self.status = SpanStatus::Error(message.into());
self.end = Some(Instant::now());
}
pub fn duration(&self) -> Option<std::time::Duration> {
self.end.map(|end| end.duration_since(self.start))
}
pub fn is_finished(&self) -> bool {
self.end.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpanStatus {
Unset,
Ok,
Error(String),
}
impl fmt::Display for SpanStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unset => write!(f, "UNSET"),
Self::Ok => write!(f, "OK"),
Self::Error(msg) => write!(f, "ERROR: {}", msg),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TracingConfig {
#[serde(default)]
pub otlp_endpoint: Option<String>,
#[serde(default = "default_propagation")]
pub propagation: String,
#[serde(default = "default_sample_rate")]
pub sample_rate: f64,
#[serde(default = "default_service_name")]
pub service_name: String,
}
fn default_propagation() -> String {
"w3c".to_string()
}
fn default_sample_rate() -> f64 {
1.0
}
fn default_service_name() -> String {
"a3s-gateway".to_string()
}
impl Default for TracingConfig {
fn default() -> Self {
Self {
otlp_endpoint: None,
propagation: default_propagation(),
sample_rate: default_sample_rate(),
service_name: default_service_name(),
}
}
}
impl TracingConfig {
pub fn propagation_format(&self) -> PropagationFormat {
match self.propagation.to_lowercase().as_str() {
"b3" | "zipkin" => PropagationFormat::B3,
_ => PropagationFormat::W3C,
}
}
pub fn is_export_enabled(&self) -> bool {
self.otlp_endpoint.is_some()
}
}
fn generate_trace_id() -> String {
format!("{:032x}", uuid::Uuid::new_v4().as_u128())
}
fn generate_span_id() -> String {
let bytes: [u8; 8] = rand_bytes();
hex_encode(&bytes)
}
fn rand_bytes() -> [u8; 8] {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let seed = now.as_nanos() as u64;
seed.to_le_bytes()
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
fn is_hex(s: &str) -> bool {
!s.is_empty() && s.chars().all(|c| c.is_ascii_hexdigit())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_root() {
let ctx = TraceContext::new_root();
assert_eq!(ctx.trace_id.len(), 32);
assert_eq!(ctx.span_id.len(), 16);
assert!(ctx.parent_span_id.is_empty());
assert!(ctx.is_sampled());
}
#[test]
fn test_child_span() {
let root = TraceContext::new_root();
let child = root.child();
assert_eq!(child.trace_id, root.trace_id);
assert_eq!(child.parent_span_id, root.span_id);
assert_ne!(child.span_id, root.span_id);
assert_eq!(child.trace_flags, root.trace_flags);
}
#[test]
fn test_is_sampled() {
let mut ctx = TraceContext::new_root();
assert!(ctx.is_sampled());
ctx.trace_flags = 0;
assert!(!ctx.is_sampled());
}
#[test]
fn test_to_traceparent() {
let ctx = TraceContext {
trace_id: "0af7651916cd43dd8448eb211c80319c".to_string(),
parent_span_id: "00f067aa0ba902b7".to_string(),
span_id: "b7ad6b7169203331".to_string(),
trace_flags: 1,
trace_state: None,
};
let tp = ctx.to_traceparent();
assert!(tp.starts_with("00-0af7651916cd43dd8448eb211c80319c-"));
assert!(tp.ends_with("-01"));
}
#[test]
fn test_from_traceparent_valid() {
let tp = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01";
let ctx = TraceContext::from_traceparent(tp).unwrap();
assert_eq!(ctx.trace_id, "0af7651916cd43dd8448eb211c80319c");
assert_eq!(ctx.parent_span_id, "00f067aa0ba902b7");
assert!(ctx.is_sampled());
}
#[test]
fn test_from_traceparent_not_sampled() {
let tp = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-00";
let ctx = TraceContext::from_traceparent(tp).unwrap();
assert!(!ctx.is_sampled());
}
#[test]
fn test_from_traceparent_invalid_version() {
let tp = "01-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01";
assert!(TraceContext::from_traceparent(tp).is_none());
}
#[test]
fn test_from_traceparent_invalid_format() {
assert!(TraceContext::from_traceparent("invalid").is_none());
assert!(TraceContext::from_traceparent("00-short-id-01").is_none());
assert!(TraceContext::from_traceparent("").is_none());
}
#[test]
fn test_from_traceparent_invalid_hex() {
let tp = "00-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz-00f067aa0ba902b7-01";
assert!(TraceContext::from_traceparent(tp).is_none());
}
#[test]
fn test_to_b3_single_with_parent() {
let ctx = TraceContext {
trace_id: "463ac35c9f6413ad48485a3953bb6124".to_string(),
parent_span_id: "0020000000000001".to_string(),
span_id: "b7ad6b7169203331".to_string(),
trace_flags: 1,
trace_state: None,
};
let b3 = ctx.to_b3_single();
assert!(b3.contains("463ac35c9f6413ad48485a3953bb6124"));
assert!(b3.contains("-1-"));
}
#[test]
fn test_to_b3_single_no_parent() {
let ctx = TraceContext {
trace_id: "463ac35c9f6413ad48485a3953bb6124".to_string(),
parent_span_id: String::new(),
span_id: "b7ad6b7169203331".to_string(),
trace_flags: 0,
trace_state: None,
};
let b3 = ctx.to_b3_single();
assert!(b3.contains("-0"));
assert_eq!(b3.split('-').count(), 3);
}
#[test]
fn test_from_b3_single_sampled() {
let b3 = "463ac35c9f6413ad48485a3953bb6124-0020000000000001-1";
let ctx = TraceContext::from_b3_single(b3).unwrap();
assert_eq!(ctx.trace_id, "463ac35c9f6413ad48485a3953bb6124");
assert!(ctx.is_sampled());
}
#[test]
fn test_from_b3_single_with_parent() {
let b3 = "463ac35c9f6413ad48485a3953bb6124-0020000000000001-1-00f067aa0ba902b7";
let ctx = TraceContext::from_b3_single(b3).unwrap();
assert_eq!(ctx.parent_span_id, "00f067aa0ba902b7");
}
#[test]
fn test_from_b3_single_invalid() {
assert!(TraceContext::from_b3_single("invalid").is_none());
assert!(TraceContext::from_b3_single("a-b").is_none());
}
#[test]
fn test_extract_w3c() {
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::HeaderName::from_static("traceparent"),
"00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"
.parse()
.unwrap(),
);
headers.insert(
http::header::HeaderName::from_static("tracestate"),
"vendor=value".parse().unwrap(),
);
let ctx = extract_trace_context(&headers).unwrap();
assert_eq!(ctx.trace_id, "0af7651916cd43dd8448eb211c80319c");
assert_eq!(ctx.trace_state.as_deref(), Some("vendor=value"));
}
#[test]
fn test_extract_b3_single() {
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::HeaderName::from_static("b3"),
"463ac35c9f6413ad48485a3953bb6124-0020000000000001-1"
.parse()
.unwrap(),
);
let ctx = extract_trace_context(&headers).unwrap();
assert_eq!(ctx.trace_id, "463ac35c9f6413ad48485a3953bb6124");
}
#[test]
fn test_extract_b3_multi() {
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::HeaderName::from_static("x-b3-traceid"),
"463ac35c9f6413ad48485a3953bb6124".parse().unwrap(),
);
headers.insert(
http::header::HeaderName::from_static("x-b3-spanid"),
"0020000000000001".parse().unwrap(),
);
headers.insert(
http::header::HeaderName::from_static("x-b3-sampled"),
"1".parse().unwrap(),
);
let ctx = extract_trace_context(&headers).unwrap();
assert_eq!(ctx.trace_id, "463ac35c9f6413ad48485a3953bb6124");
assert!(ctx.is_sampled());
}
#[test]
fn test_extract_no_trace() {
let headers = http::HeaderMap::new();
assert!(extract_trace_context(&headers).is_none());
}
#[test]
fn test_inject_w3c() {
let ctx = TraceContext {
trace_id: "0af7651916cd43dd8448eb211c80319c".to_string(),
parent_span_id: String::new(),
span_id: "00f067aa0ba902b7".to_string(),
trace_flags: 1,
trace_state: Some("vendor=value".to_string()),
};
let mut headers = HashMap::new();
inject_trace_context(&ctx, &mut headers, PropagationFormat::W3C);
assert!(headers.contains_key("traceparent"));
assert_eq!(headers.get("tracestate").unwrap(), "vendor=value");
}
#[test]
fn test_inject_b3() {
let ctx = TraceContext {
trace_id: "463ac35c9f6413ad48485a3953bb6124".to_string(),
parent_span_id: String::new(),
span_id: "0020000000000001".to_string(),
trace_flags: 1,
trace_state: None,
};
let mut headers = HashMap::new();
inject_trace_context(&ctx, &mut headers, PropagationFormat::B3);
assert!(headers.contains_key("b3"));
}
#[test]
fn test_span_new() {
let ctx = TraceContext::new_root();
let span = GatewaySpan::new("gateway.proxy", ctx);
assert_eq!(span.name, "gateway.proxy");
assert!(!span.is_finished());
assert!(span.duration().is_none());
assert_eq!(span.status, SpanStatus::Unset);
}
#[test]
fn test_span_attributes() {
let ctx = TraceContext::new_root();
let mut span = GatewaySpan::new("test", ctx);
span.set_attribute("http.method", "GET");
span.set_attribute("http.url", "/api/data");
assert_eq!(span.attributes.get("http.method").unwrap(), "GET");
assert_eq!(span.attributes.get("http.url").unwrap(), "/api/data");
}
#[test]
fn test_span_finish() {
let ctx = TraceContext::new_root();
let mut span = GatewaySpan::new("test", ctx);
assert!(!span.is_finished());
span.finish();
assert!(span.is_finished());
assert!(span.duration().is_some());
assert_eq!(span.status, SpanStatus::Unset);
}
#[test]
fn test_span_finish_with_error() {
let ctx = TraceContext::new_root();
let mut span = GatewaySpan::new("test", ctx);
span.finish_with_error("connection refused");
assert!(span.is_finished());
assert_eq!(
span.status,
SpanStatus::Error("connection refused".to_string())
);
}
#[test]
fn test_span_status_display() {
assert_eq!(SpanStatus::Unset.to_string(), "UNSET");
assert_eq!(SpanStatus::Ok.to_string(), "OK");
assert_eq!(
SpanStatus::Error("fail".to_string()).to_string(),
"ERROR: fail"
);
}
#[test]
fn test_config_default() {
let config = TracingConfig::default();
assert!(config.otlp_endpoint.is_none());
assert_eq!(config.propagation, "w3c");
assert_eq!(config.sample_rate, 1.0);
assert_eq!(config.service_name, "a3s-gateway");
assert!(!config.is_export_enabled());
}
#[test]
fn test_config_propagation_format() {
let mut config = TracingConfig::default();
assert_eq!(config.propagation_format(), PropagationFormat::W3C);
config.propagation = "b3".to_string();
assert_eq!(config.propagation_format(), PropagationFormat::B3);
config.propagation = "zipkin".to_string();
assert_eq!(config.propagation_format(), PropagationFormat::B3);
}
#[test]
fn test_config_export_enabled() {
let mut config = TracingConfig::default();
assert!(!config.is_export_enabled());
config.otlp_endpoint = Some("http://localhost:4317".to_string());
assert!(config.is_export_enabled());
}
#[test]
fn test_config_serialization() {
let config = TracingConfig {
otlp_endpoint: Some("http://localhost:4317".to_string()),
propagation: "b3".to_string(),
sample_rate: 0.5,
service_name: "my-gateway".to_string(),
};
let json = serde_json::to_string(&config).unwrap();
let parsed: TracingConfig = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed.otlp_endpoint.as_deref(),
Some("http://localhost:4317")
);
assert_eq!(parsed.sample_rate, 0.5);
}
#[test]
fn test_trace_context_display() {
let ctx = TraceContext {
trace_id: "abc123".to_string(),
parent_span_id: String::new(),
span_id: "def456".to_string(),
trace_flags: 1,
trace_state: None,
};
let display = ctx.to_string();
assert!(display.contains("trace_id=abc123"));
assert!(display.contains("span_id=def456"));
assert!(display.contains("sampled=true"));
}
#[test]
fn test_is_hex() {
assert!(is_hex("0af7651916cd43dd"));
assert!(is_hex("ABCDEF0123456789"));
assert!(!is_hex("xyz"));
assert!(!is_hex(""));
assert!(!is_hex("0af765-invalid"));
}
#[test]
fn test_hex_encode() {
assert_eq!(hex_encode(&[0x0a, 0xf7]), "0af7");
assert_eq!(hex_encode(&[0x00, 0xff]), "00ff");
assert_eq!(hex_encode(&[]), "");
}
#[test]
fn test_generate_trace_id_length() {
let id = generate_trace_id();
assert_eq!(id.len(), 32);
assert!(is_hex(&id));
}
#[test]
fn test_generate_span_id_length() {
let id = generate_span_id();
assert_eq!(id.len(), 16);
assert!(is_hex(&id));
}
}