use std::{
collections::HashMap,
sync::{
atomic::{AtomicU64, Ordering},
Arc, RwLock,
},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceContext {
pub trace_id: String,
pub parent_span_id: Option<String>,
pub trace_flags: u8,
pub trace_state: Option<String>,
}
impl TraceContext {
#[must_use]
pub fn new() -> Self {
Self {
trace_id: generate_trace_id(),
parent_span_id: None,
trace_flags: 0x01, trace_state: None,
}
}
#[must_use]
pub fn child(&self, parent_span_id: &str) -> Self {
Self {
trace_id: self.trace_id.clone(),
parent_span_id: Some(parent_span_id.to_string()),
trace_flags: self.trace_flags,
trace_state: self.trace_state.clone(),
}
}
#[must_use]
pub fn from_traceparent(header: &str) -> Option<Self> {
let parts: Vec<&str> = header.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 = u8::from_str_radix(parts[3], 16).ok()?;
if trace_id.len() != 32 || parent_span_id.len() != 16 {
return None;
}
Some(Self {
trace_id: trace_id.to_string(),
parent_span_id: Some(parent_span_id.to_string()),
trace_flags: flags,
trace_state: None,
})
}
#[must_use]
pub fn to_traceparent(&self, span_id: &str) -> String {
format!("00-{}-{}-{:02x}", self.trace_id, span_id, self.trace_flags)
}
#[must_use]
pub fn with_tracestate(mut self, state: impl Into<String>) -> Self {
self.trace_state = Some(state.into());
self
}
#[must_use]
pub fn is_sampled(&self) -> bool {
self.trace_flags & 0x01 != 0
}
pub fn set_sampled(&mut self, sampled: bool) {
if sampled {
self.trace_flags |= 0x01;
} else {
self.trace_flags &= !0x01;
}
}
}
impl Default for TraceContext {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LatencyHistogram {
buckets: Vec<u64>,
counts: Vec<u64>,
total: u64,
sum: u64,
min: Option<u64>,
max: Option<u64>,
}
impl LatencyHistogram {
#[must_use]
pub fn new() -> Self {
let buckets = vec![
1_000, 2_000, 5_000, 10_000, 25_000, 50_000, 100_000, 250_000, 500_000, 1_000_000, 2_500_000, 5_000_000, 10_000_000, 30_000_000, 60_000_000, ];
let counts = vec![0; buckets.len() + 1]; Self {
buckets,
counts,
total: 0,
sum: 0,
min: None,
max: None,
}
}
#[must_use]
pub fn with_buckets(mut buckets: Vec<u64>) -> Self {
buckets.sort_unstable();
let counts = vec![0; buckets.len() + 1];
Self {
buckets,
counts,
total: 0,
sum: 0,
min: None,
max: None,
}
}
pub fn observe(&mut self, value_us: u64) {
self.total += 1;
self.sum += value_us;
self.min = Some(self.min.map_or(value_us, |m| m.min(value_us)));
self.max = Some(self.max.map_or(value_us, |m| m.max(value_us)));
let bucket_idx = self.buckets.iter().position(|&b| value_us <= b);
match bucket_idx {
Some(idx) => self.counts[idx] += 1,
None => *self.counts.last_mut().unwrap_or(&mut 0) += 1,
}
}
pub fn observe_duration(&mut self, duration: Duration) {
self.observe(duration.as_micros() as u64);
}
#[must_use]
pub fn percentile(&self, p: f64) -> Option<u64> {
if self.total == 0 || !(0.0..=100.0).contains(&p) {
return None;
}
let target = ((p / 100.0) * self.total as f64).ceil() as u64;
let mut cumulative = 0u64;
for (i, &count) in self.counts.iter().enumerate() {
cumulative += count;
if cumulative >= target {
return if i < self.buckets.len() {
Some(self.buckets[i])
} else {
self.max };
}
}
self.max
}
#[must_use]
pub fn p50(&self) -> Option<u64> {
self.percentile(50.0)
}
#[must_use]
pub fn p95(&self) -> Option<u64> {
self.percentile(95.0)
}
#[must_use]
pub fn p99(&self) -> Option<u64> {
self.percentile(99.0)
}
#[must_use]
pub fn mean(&self) -> Option<f64> {
if self.total == 0 {
None
} else {
Some(self.sum as f64 / self.total as f64)
}
}
#[must_use]
pub fn count(&self) -> u64 {
self.total
}
#[must_use]
pub fn min(&self) -> Option<u64> {
self.min
}
#[must_use]
pub fn max_val(&self) -> Option<u64> {
self.max
}
#[must_use]
pub fn to_prometheus(&self, name: &str, labels: &str) -> String {
use std::fmt::Write;
let mut output = String::new();
let mut cumulative = 0u64;
for (i, &boundary) in self.buckets.iter().enumerate() {
cumulative += self.counts[i];
let le = boundary as f64 / 1_000_000.0; writeln!(
output,
"{name}_bucket{{le=\"{le:.6}\",{labels}}} {cumulative}"
)
.expect("fmt::Write for String is infallible");
}
cumulative += self.counts.last().copied().unwrap_or(0);
writeln!(output, "{name}_bucket{{le=\"+Inf\",{labels}}} {cumulative}")
.expect("fmt::Write for String is infallible");
let sum_secs = self.sum as f64 / 1_000_000.0;
writeln!(output, "{name}_sum{{{labels}}} {sum_secs:.6}")
.expect("fmt::Write for String is infallible");
writeln!(output, "{name}_count{{{labels}}} {}", self.total)
.expect("fmt::Write for String is infallible");
output
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OtelSpan {
#[serde(rename = "traceId")]
pub trace_id: String,
#[serde(rename = "spanId")]
pub span_id: String,
#[serde(rename = "parentSpanId", skip_serializing_if = "Option::is_none")]
pub parent_span_id: Option<String>,
#[serde(rename = "operationName")]
pub operation_name: String,
#[serde(rename = "serviceName")]
pub service_name: String,
#[serde(rename = "startTimeUnixNano")]
pub start_time: u64,
#[serde(rename = "endTimeUnixNano")]
pub end_time: u64,
#[serde(rename = "kind")]
pub kind: SpanKind,
pub status: OtelStatus,
pub attributes: Vec<OtelAttribute>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SpanKind {
#[default]
Internal,
Server,
Client,
Producer,
Consumer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OtelStatus {
pub code: OtelStatusCode,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OtelStatusCode {
#[default]
Unset,
Ok,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OtelAttribute {
pub key: String,
pub value: OtelValue,
}
include!("mod_otel_value.rs");
include!("mod_variant.rs");