use crate::error::{ClientError, Result};
use crate::unique::word;
const SAMPLED: u8 = 0x01;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceContext {
trace_id: String,
span_id: String,
flags: u8,
tracestate: Option<String>,
}
impl TraceContext {
#[must_use]
pub fn new() -> Self {
Self {
trace_id: format!("{:016x}{:016x}", word(0), word(1)),
span_id: format!("{:016x}", word(2)),
flags: SAMPLED,
tracestate: None,
}
}
pub fn parse(header: &str) -> Result<Self> {
let header = header.trim();
let parts: Vec<&str> = header.split('-').collect();
let [version, trace_id, span_id, flags] = match parts[..] {
[trace_id, span_id, flags] if is_hex(trace_id, 32) => ["00", trace_id, span_id, flags],
[version, trace_id, _] if is_hex(version, 2) && is_hex(trace_id, 32) => {
return Err(malformed(header, "the flags are missing"));
}
[version, trace_id, span_id, flags] => [version, trace_id, span_id, flags],
[version, trace_id, span_id, flags, ..] if !version.eq_ignore_ascii_case("00") => {
[version, trace_id, span_id, flags]
}
_ => {
return Err(malformed(
header,
"expected version-traceid-spanid-flags, in four hyphenated groups",
));
}
};
if !is_hex(version, 2) {
return Err(malformed(header, "the version is not two hex digits"));
}
if version.eq_ignore_ascii_case("ff") {
return Err(malformed(header, "ff is not a valid version"));
}
if !is_hex(trace_id, 32) {
return Err(malformed(header, "the trace id is not 32 hex digits"));
}
if is_zero(trace_id) {
return Err(malformed(header, "the trace id is all zeros"));
}
if !is_hex(span_id, 16) {
return Err(malformed(header, "the span id is not 16 hex digits"));
}
if is_zero(span_id) {
return Err(malformed(header, "the span id is all zeros"));
}
if !is_hex(flags, 2) {
return Err(malformed(header, "the flags are not two hex digits"));
}
Ok(Self {
trace_id: trace_id.to_ascii_lowercase(),
span_id: span_id.to_ascii_lowercase(),
flags: u8::from_str_radix(flags, 16).unwrap_or_default(),
tracestate: None,
})
}
#[must_use]
pub fn with_tracestate(mut self, state: impl Into<String>) -> Self {
self.tracestate = Some(state.into());
self
}
#[must_use]
pub fn tracestate(&self) -> Option<&str> {
self.tracestate.as_deref()
}
#[must_use]
pub fn trace_id(&self) -> &str {
&self.trace_id
}
#[must_use]
pub fn yt_trace_id(&self) -> String {
let groups: Vec<&str> = (0..4)
.map(|group| {
let group = &self.trace_id[group * 8..group * 8 + 8];
let trimmed = group.trim_start_matches('0');
if trimmed.is_empty() { "0" } else { trimmed }
})
.collect();
groups.join("-")
}
#[must_use]
pub fn span_id(&self) -> &str {
&self.span_id
}
#[must_use]
pub fn is_sampled(&self) -> bool {
self.flags & SAMPLED != 0
}
#[must_use]
pub fn header(&self) -> String {
format!("00-{}-{}-{:02x}", self.trace_id, self.span_id, self.flags)
}
}
impl Default for TraceContext {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for TraceContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.header())
}
}
fn malformed(header: &str, reason: &str) -> ClientError {
ClientError::Config(format!("{header:?} is not a traceparent: {reason}"))
}
fn is_hex(text: &str, digits: usize) -> bool {
text.len() == digits && text.bytes().all(|b| b.is_ascii_hexdigit())
}
fn is_zero(text: &str) -> bool {
text.bytes().all(|b| b == b'0')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_header_is_carried_through_unchanged() {
let header = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
let context = TraceContext::parse(header).expect("parses");
assert_eq!(context.trace_id(), "4bf92f3577b34da6a3ce929d0e0e4736");
assert_eq!(context.span_id(), "00f067aa0ba902b7");
assert!(context.is_sampled());
assert_eq!(context.header(), header);
}
#[test]
fn the_version_less_form_the_go_sdk_sends_is_accepted() {
let context = TraceContext::parse("4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
.expect("parses");
assert_eq!(
context.header(),
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"what is sent on is the four-part form"
);
}
#[test]
fn what_is_sent_is_lowercase_whatever_arrived() {
let context =
TraceContext::parse("00-4BF92F3577B34DA6A3CE929D0E0E4736-00F067AA0BA902B7-01")
.expect("parses");
assert_eq!(
context.header(),
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
);
}
#[test]
fn an_unsampled_trace_stays_unsampled() {
let context =
TraceContext::parse("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00")
.expect("parses");
assert!(!context.is_sampled());
assert!(context.header().ends_with("-00"));
}
#[test]
fn the_debug_flag_survives_the_round_trip() {
let context =
TraceContext::parse("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-03")
.expect("parses");
assert!(context.is_sampled());
assert!(context.header().ends_with("-03"));
}
#[test]
fn a_version_from_the_future_is_read_as_far_as_it_is_understood() {
let context =
TraceContext::parse("01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-af00")
.expect("a later version is read as far as it is understood");
assert_eq!(context.trace_id(), "4bf92f3577b34da6a3ce929d0e0e4736");
assert_eq!(context.span_id(), "00f067aa0ba902b7");
assert!(context.is_sampled());
assert_eq!(
context.header(),
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
);
}
#[test]
fn version_zero_has_exactly_four_fields() {
assert!(
TraceContext::parse("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-af00")
.is_err()
);
}
#[test]
fn a_truncated_header_says_which_field_is_missing() {
let refusal = TraceContext::parse("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7")
.expect_err("the flags are not optional");
let reason = refusal.to_string();
assert!(reason.contains("flags"), "{reason}");
assert!(
!reason.contains("trace id"),
"the trace id in this header is perfectly well formed: {reason}"
);
}
#[test]
fn a_tracestate_is_carried_beside_the_traceparent_untouched() {
let context =
TraceContext::parse("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
.expect("parses")
.with_tracestate("vendora=t61rcWkgMzE,vendorb=x9");
assert_eq!(
context.tracestate(),
Some("vendora=t61rcWkgMzE,vendorb=x9"),
"not rewritten: this client has no vendor entry of its own to add"
);
assert_eq!(
context.header(),
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
);
}
#[test]
fn a_context_without_a_tracestate_has_none() {
assert_eq!(TraceContext::new().tracestate(), None);
assert_eq!(
TraceContext::parse("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
.expect("parses")
.tracestate(),
None
);
}
#[test]
fn a_malformed_header_is_refused_rather_than_sent() {
let refused = [
"",
"nonsense",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7",
"00-4bf92f3577b34da6a3ce929d0e0e473-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b77-01",
"00-4bf92f3577b34da6a3ce929d0e0e473g-00f067aa0ba902b7-01",
"zz-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0",
"00-00000000000000000000000000000000-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01",
"ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
];
for header in refused {
assert!(
TraceContext::parse(header).is_err(),
"{header:?} was accepted"
);
}
}
#[test]
fn the_cluster_spelling_is_the_same_bits_punctuated() {
let context =
TraceContext::parse("00-8e9bcc435c2be9b456f18c4e117ea314-00f067aa0ba902b7-01")
.expect("parses");
assert_eq!(context.yt_trace_id(), "8e9bcc43-5c2be9b4-56f18c4e-117ea314");
}
#[test]
fn a_group_the_cluster_would_shorten_is_shortened_here_too() {
let observed = [
(
"4bf92f3577b34da6a3ce929d0e0e4736",
"4bf92f35-77b34da6-a3ce929d-e0e4736",
),
("00000001000000020000000300000004", "1-2-3-4"),
("00000000000000010000000000000002", "0-1-0-2"),
];
for (sent, echoed) in observed {
let header = format!("00-{sent}-00f067aa0ba902b7-01");
let context = TraceContext::parse(&header).expect("parses");
assert_eq!(context.yt_trace_id(), echoed, "{sent}");
}
}
#[test]
fn a_fresh_context_is_well_formed_and_new_every_time() {
let mine = TraceContext::new();
assert!(mine.is_sampled());
assert_eq!(
TraceContext::parse(&mine.header()).expect("its own header parses"),
mine
);
let ids: std::collections::HashSet<String> =
(0..10_000).map(|_| TraceContext::new().trace_id).collect();
assert_eq!(
ids.len(),
10_000,
"two traces sharing an id would be one trace"
);
}
}