use crate::error::{Mecha10Error, Result};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{debug, info, span, Level, Span};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceContext {
pub trace_id: String,
pub span_id: String,
pub parent_span_id: Option<String>,
pub sampled: bool,
#[serde(default)]
pub baggage: HashMap<String, String>,
}
impl TraceContext {
pub fn new() -> Self {
Self {
trace_id: generate_trace_id(),
span_id: generate_span_id(),
parent_span_id: None,
sampled: true,
baggage: HashMap::new(),
}
}
pub fn child(&self) -> Self {
Self {
trace_id: self.trace_id.clone(),
span_id: generate_span_id(),
parent_span_id: Some(self.span_id.clone()),
sampled: self.sampled,
baggage: self.baggage.clone(),
}
}
crate::builder_setter!(
insert, with_baggage, baggage, impl Into<String>, impl Into<String>
);
pub fn to_w3c_traceparent(&self) -> String {
let flags = if self.sampled { "01" } else { "00" };
format!("00-{}-{}-{}", self.trace_id, self.span_id, flags)
}
pub fn from_w3c_traceparent(traceparent: &str) -> Option<Self> {
let parts: Vec<&str> = traceparent.split('-').collect();
if parts.len() != 4 || parts[0] != "00" {
return None;
}
Some(Self {
trace_id: parts[1].to_string(),
span_id: parts[2].to_string(),
parent_span_id: None,
sampled: parts[3] == "01",
baggage: HashMap::new(),
})
}
}
impl Default for TraceContext {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TracedMessage<T> {
pub payload: T,
pub trace_context: TraceContext,
pub timestamp_us: u64,
pub source_node: String,
}
impl<T> TracedMessage<T> {
pub fn new(payload: T, source_node: impl Into<String>) -> Self {
Self {
payload,
trace_context: TraceContext::new(),
timestamp_us: crate::prelude::now_micros(),
source_node: source_node.into(),
}
}
pub fn with_context(payload: T, trace_context: TraceContext, source_node: impl Into<String>) -> Self {
Self {
payload,
trace_context,
timestamp_us: crate::prelude::now_micros(),
source_node: source_node.into(),
}
}
pub fn create_span(&self, operation: &str) -> Span {
span!(
Level::INFO,
"message",
operation = operation,
trace_id = %self.trace_context.trace_id,
span_id = %self.trace_context.span_id,
source = %self.source_node,
)
}
}
#[derive(Debug, Clone)]
pub struct TracingConfig {
pub service_name: String,
pub jaeger_endpoint: Option<String>,
pub zipkin_endpoint: Option<String>,
pub sampling_rate: f64,
pub console_output: bool,
}
impl Default for TracingConfig {
fn default() -> Self {
Self {
service_name: "mecha10".to_string(),
jaeger_endpoint: None,
zipkin_endpoint: None,
sampling_rate: 1.0,
console_output: true,
}
}
}
impl TracingConfig {
crate::builder_setter!(
into, with_service_name, service_name, impl Into<String>
);
crate::builder_setter!(
into_some, with_jaeger, jaeger_endpoint, impl Into<String>
);
crate::builder_setter!(
into_some, with_zipkin, zipkin_endpoint, impl Into<String>
);
pub fn with_sampling_rate(mut self, rate: f64) -> Self {
self.sampling_rate = rate.clamp(0.0, 1.0);
self
}
pub fn without_console(mut self) -> Self {
self.console_output = false;
self
}
pub fn init(self) -> Result<TracingHandle> {
info!("Initializing distributed tracing: {}", self.service_name);
let fmt_layer = if self.console_output {
Some(tracing_subscriber::fmt::layer())
} else {
None
};
tracing_subscriber::registry()
.with(fmt_layer)
.try_init()
.map_err(|e| Mecha10Error::Runtime(format!("Failed to initialize tracing: {}", e)))?;
info!(
"Tracing initialized - Service: {}, Jaeger: {:?}, Sampling: {}",
self.service_name, self.jaeger_endpoint, self.sampling_rate
);
let sampling_rate = self.sampling_rate;
Ok(TracingHandle {
config: self,
sampler: Arc::new(ProbabilitySampler::new(sampling_rate)),
})
}
}
pub struct TracingHandle {
config: TracingConfig,
sampler: Arc<ProbabilitySampler>,
}
impl TracingHandle {
pub fn should_sample(&self) -> bool {
self.sampler.should_sample()
}
pub fn start_span(&self, name: &str) -> Span {
let trace_ctx = TraceContext::new();
span!(
Level::INFO,
"operation",
name = name,
trace_id = %trace_ctx.trace_id,
span_id = %trace_ctx.span_id,
)
}
pub fn service_name(&self) -> &str {
&self.config.service_name
}
}
struct ProbabilitySampler {
rate: f64,
}
impl ProbabilitySampler {
fn new(rate: f64) -> Self {
Self {
rate: rate.clamp(0.0, 1.0),
}
}
fn should_sample(&self) -> bool {
if self.rate >= 1.0 {
true
} else if self.rate <= 0.0 {
false
} else {
use std::collections::hash_map::RandomState;
use std::hash::BuildHasher;
let hash = RandomState::new().hash_one(std::time::SystemTime::now());
(hash as f64 / u64::MAX as f64) < self.rate
}
}
}
fn generate_trace_id() -> String {
use std::time::SystemTime;
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
format!("{:016x}{:016x}", now.as_nanos(), rand_u64())
}
fn generate_span_id() -> String {
format!("{:016x}", rand_u64())
}
fn rand_u64() -> u64 {
use std::collections::hash_map::RandomState;
use std::hash::BuildHasher;
RandomState::new().hash_one(std::time::SystemTime::now())
}
pub trait TracingExt {
fn publish_traced<T: crate::messages::Message + Serialize + Clone + 'static>(
&self,
topic: &str,
message: &T,
) -> impl std::future::Future<Output = Result<()>> + Send;
fn publish_with_context<T: crate::messages::Message + Serialize + Clone + 'static>(
&self,
topic: &str,
message: &T,
trace_context: TraceContext,
) -> impl std::future::Future<Output = Result<()>> + Send;
fn subscribe_traced<T: crate::messages::Message + DeserializeOwned + Send + 'static>(
&self,
topic: &str,
) -> impl std::future::Future<Output = Result<crate::context::Receiver<TracedMessage<T>>>> + Send;
fn current_trace_context(&self) -> Option<TraceContext>;
fn extract_trace_context(&self, metadata: &HashMap<String, String>) -> Option<TraceContext>;
fn inject_trace_context(&self, metadata: &mut HashMap<String, String>, ctx: &TraceContext);
}
impl TracingExt for crate::context::Context {
async fn publish_traced<T: crate::messages::Message + Serialize + Clone + 'static>(
&self,
topic: &str,
message: &T,
) -> Result<()> {
let trace_context = self.current_trace_context().unwrap_or_default();
self.publish_with_context(topic, message, trace_context).await
}
async fn publish_with_context<T: crate::messages::Message + Serialize + Clone + 'static>(
&self,
topic: &str,
message: &T,
trace_context: TraceContext,
) -> Result<()> {
let traced_msg = TracedMessage::with_context(message.clone(), trace_context.child(), self.node_id());
self.publish_raw(topic, &traced_msg).await
}
async fn subscribe_traced<T: crate::messages::Message + DeserializeOwned + Send + 'static>(
&self,
topic: &str,
) -> Result<crate::context::Receiver<TracedMessage<T>>> {
self.subscribe_raw::<TracedMessage<T>>(topic).await
}
fn current_trace_context(&self) -> Option<TraceContext> {
use tracing::field::Visit;
struct TraceIdVisitor {
trace_id: Option<String>,
span_id: Option<String>,
}
impl Visit for TraceIdVisitor {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
match field.name() {
"trace_id" => self.trace_id = Some(value.to_string()),
"span_id" => self.span_id = Some(value.to_string()),
_ => {}
}
}
fn record_debug(&mut self, _field: &tracing::field::Field, _value: &dyn std::fmt::Debug) {}
}
let _visitor = TraceIdVisitor {
trace_id: None,
span_id: None,
};
let span = Span::current();
span.record("trace_id", &tracing::field::Empty);
None
}
fn extract_trace_context(&self, metadata: &HashMap<String, String>) -> Option<TraceContext> {
if let Some(traceparent) = metadata.get("traceparent") {
return TraceContext::from_w3c_traceparent(traceparent);
}
if let Some(trace_ctx_json) = metadata.get("trace_context") {
return serde_json::from_str(trace_ctx_json).ok();
}
None
}
fn inject_trace_context(&self, metadata: &mut HashMap<String, String>, ctx: &TraceContext) {
metadata.insert("traceparent".to_string(), ctx.to_w3c_traceparent());
if let Ok(json) = serde_json::to_string(ctx) {
metadata.insert("trace_context".to_string(), json);
}
for (key, value) in &ctx.baggage {
metadata.insert(format!("baggage_{}", key), value.clone());
}
}
}
pub struct SpanBuilder {
name: String,
trace_context: Option<TraceContext>,
attributes: HashMap<String, String>,
}
impl SpanBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
trace_context: None,
attributes: HashMap::new(),
}
}
pub fn with_context(mut self, ctx: TraceContext) -> Self {
self.trace_context = Some(ctx);
self
}
crate::builder_setter!(
insert, with_attribute, attributes, impl Into<String>, impl Into<String>
);
pub fn start(self) -> (Span, TraceContext) {
let trace_ctx = self.trace_context.unwrap_or_default();
let span = span!(
Level::INFO,
"span",
name = %self.name,
trace_id = %trace_ctx.trace_id,
span_id = %trace_ctx.span_id,
);
(span, trace_ctx)
}
}
pub trait TraceExporter: Send + Sync {
fn export_span(&self, span_data: &SpanData);
fn flush(&self);
}
#[derive(Debug, Clone)]
pub struct SpanData {
pub trace_id: String,
pub span_id: String,
pub parent_span_id: Option<String>,
pub name: String,
pub start_time: u64,
pub end_time: u64,
pub attributes: HashMap<String, String>,
}
pub struct JaegerExporter {
#[allow(dead_code)]
endpoint: String,
#[allow(dead_code)]
service_name: String,
}
impl JaegerExporter {
pub fn new(endpoint: impl Into<String>, service_name: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
service_name: service_name.into(),
}
}
}
fn log_export_span(exporter_name: &str, endpoint: &str, span_data: &SpanData) {
debug!(
"Exporting span to {} {}: {} (trace: {})",
exporter_name, endpoint, span_data.name, span_data.trace_id
);
}
impl TraceExporter for JaegerExporter {
fn export_span(&self, span_data: &SpanData) {
log_export_span("Jaeger", &self.endpoint, span_data);
}
fn flush(&self) {
debug!("Flushing Jaeger exporter");
}
}
pub struct ZipkinExporter {
#[allow(dead_code)]
endpoint: String,
#[allow(dead_code)]
service_name: String,
}
impl ZipkinExporter {
pub fn new(endpoint: impl Into<String>, service_name: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
service_name: service_name.into(),
}
}
}
impl TraceExporter for ZipkinExporter {
fn export_span(&self, span_data: &SpanData) {
log_export_span("Zipkin", &self.endpoint, span_data);
}
fn flush(&self) {
debug!("Flushing Zipkin exporter");
}
}