#![cfg(feature = "telemetry")]
use std::{
fmt,
task::{Context, Poll},
};
use tonic::metadata::MetadataMap;
use tower::{Layer, Service};
use tracing::Span;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct W3cTraceContext {
pub trace_id: [u8; 16],
pub span_id: [u8; 8],
pub flags: u8,
}
impl W3cTraceContext {
pub fn is_sampled(&self) -> bool {
self.flags & 0x01 != 0
}
pub fn to_header_value(&self) -> String {
let mut trace_hex = String::with_capacity(32);
for byte in &self.trace_id {
trace_hex.push_str(&format!("{:02x}", byte));
}
let mut span_hex = String::with_capacity(16);
for byte in &self.span_id {
span_hex.push_str(&format!("{:02x}", byte));
}
format!("00-{}-{}-{:02x}", trace_hex, span_hex, self.flags)
}
pub fn trace_id_hex(&self) -> String {
self.trace_id.iter().map(|b| format!("{:02x}", b)).collect()
}
pub fn span_id_hex(&self) -> String {
self.span_id.iter().map(|b| format!("{:02x}", b)).collect()
}
}
impl fmt::Display for W3cTraceContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_header_value())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TraceparentError {
#[error("traceparent header not present")]
Absent,
#[error("traceparent header is not valid ASCII/UTF-8")]
InvalidEncoding,
#[error("traceparent header has wrong number of segments (expected 4, got {0})")]
WrongSegmentCount(usize),
#[error("unsupported traceparent version: '{0}' (only '00' is supported)")]
UnsupportedVersion(String),
#[error("invalid trace-id field '{0}'")]
InvalidTraceId(String),
#[error("invalid parent-id field '{0}'")]
InvalidParentId(String),
#[error("invalid flags field '{0}'")]
InvalidFlags(String),
}
pub struct TraceparentExtractor<'a> {
metadata: &'a MetadataMap,
}
impl<'a> TraceparentExtractor<'a> {
pub fn new(metadata: &'a MetadataMap) -> Self {
Self { metadata }
}
pub fn extract(&self) -> Result<W3cTraceContext, TraceparentError> {
let header_value = self
.metadata
.get("traceparent")
.ok_or(TraceparentError::Absent)?;
let raw = header_value
.to_str()
.map_err(|_| TraceparentError::InvalidEncoding)?;
parse_traceparent(raw)
}
pub fn extract_tracestate(&self) -> Option<String> {
self.metadata
.get("tracestate")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
}
}
pub fn parse_traceparent(raw: &str) -> Result<W3cTraceContext, TraceparentError> {
let parts: Vec<&str> = raw.split('-').collect();
if parts.len() != 4 {
return Err(TraceparentError::WrongSegmentCount(parts.len()));
}
let version = parts[0];
if version != "00" {
return Err(TraceparentError::UnsupportedVersion(version.to_string()));
}
let trace_id_hex = parts[1];
let trace_id = decode_hex16(trace_id_hex)
.ok_or_else(|| TraceparentError::InvalidTraceId(trace_id_hex.to_string()))?;
if trace_id == [0u8; 16] {
return Err(TraceparentError::InvalidTraceId(trace_id_hex.to_string()));
}
let span_id_hex = parts[2];
let span_id = decode_hex8(span_id_hex)
.ok_or_else(|| TraceparentError::InvalidParentId(span_id_hex.to_string()))?;
if span_id == [0u8; 8] {
return Err(TraceparentError::InvalidParentId(span_id_hex.to_string()));
}
let flags_hex = parts[3];
if flags_hex.len() != 2 {
return Err(TraceparentError::InvalidFlags(flags_hex.to_string()));
}
let flags = u8::from_str_radix(flags_hex, 16)
.map_err(|_| TraceparentError::InvalidFlags(flags_hex.to_string()))?;
Ok(W3cTraceContext {
trace_id,
span_id,
flags,
})
}
pub fn inject_trace_context(metadata: &mut MetadataMap) {
use opentelemetry::trace::{SpanContext, TraceContextExt};
let ctx = opentelemetry::Context::current();
let binding = ctx.span();
let span_ctx: &SpanContext = binding.span_context();
if !span_ctx.is_valid() {
return;
}
let trace_id_bytes: [u8; 16] = span_ctx.trace_id().to_bytes();
let span_id_bytes: [u8; 8] = span_ctx.span_id().to_bytes();
let flags: u8 = span_ctx.trace_flags().to_u8();
let w3c = W3cTraceContext {
trace_id: trace_id_bytes,
span_id: span_id_bytes,
flags,
};
let traceparent_value = w3c.to_header_value();
match tonic::metadata::MetadataValue::try_from(traceparent_value.as_str()) {
Ok(val) => {
metadata.insert("traceparent", val);
}
Err(err) => {
tracing::warn!(
%err,
traceparent = %traceparent_value,
"Failed to encode traceparent as gRPC metadata value"
);
}
}
let tracestate = span_ctx.trace_state();
let tracestate_header = tracestate.header();
if !tracestate_header.is_empty() {
match tonic::metadata::MetadataValue::try_from(tracestate_header.as_str()) {
Ok(val) => {
metadata.insert("tracestate", val);
}
Err(err) => {
tracing::warn!(
%err,
"Failed to encode tracestate as gRPC metadata value"
);
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TraceContextPropagatorLayer;
impl TraceContextPropagatorLayer {
pub fn new() -> Self {
Self
}
}
impl<S> Layer<S> for TraceContextPropagatorLayer {
type Service = TraceContextPropagatorService<S>;
fn layer(&self, inner: S) -> Self::Service {
TraceContextPropagatorService { inner }
}
}
#[derive(Debug, Clone)]
pub struct TraceContextPropagatorService<S> {
inner: S,
}
impl<S, Request> Service<Request> for TraceContextPropagatorService<S>
where
S: Service<Request>,
Request: ExtractMetadata,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, request: Request) -> Self::Future {
let span = match request.grpc_metadata() {
Some(metadata) => {
let extractor = TraceparentExtractor::new(metadata);
match extractor.extract() {
Ok(ctx) => {
let trace_hex = ctx.trace_id_hex();
let span_hex = ctx.span_id_hex();
let sampled = ctx.is_sampled();
tracing::info_span!(
"grpc.server",
otel.kind = "SERVER",
trace_id = %trace_hex,
parent_span_id = %span_hex,
sampled = sampled,
)
}
Err(TraceparentError::Absent) => {
tracing::info_span!("grpc.server", otel.kind = "SERVER")
}
Err(err) => {
tracing::warn!(%err, "Received malformed traceparent header; starting fresh span");
tracing::info_span!("grpc.server", otel.kind = "SERVER")
}
}
}
None => tracing::info_span!("grpc.server", otel.kind = "SERVER"),
};
let _guard = span.enter();
self.inner.call(request)
}
}
pub trait ExtractMetadata {
fn grpc_metadata(&self) -> Option<&MetadataMap>;
}
impl<T> ExtractMetadata for tonic::Request<T> {
fn grpc_metadata(&self) -> Option<&MetadataMap> {
Some(self.metadata())
}
}
fn decode_hex16(s: &str) -> Option<[u8; 16]> {
if s.len() != 32 {
return None;
}
let mut out = [0u8; 16];
for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
let hi = hex_nibble(chunk[0])?;
let lo = hex_nibble(chunk[1])?;
out[i] = (hi << 4) | lo;
}
Some(out)
}
fn decode_hex8(s: &str) -> Option<[u8; 8]> {
if s.len() != 16 {
return None;
}
let mut out = [0u8; 8];
for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
let hi = hex_nibble(chunk[0])?;
let lo = hex_nibble(chunk[1])?;
out[i] = (hi << 4) | lo;
}
Some(out)
}
fn hex_nibble(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_traceparent_parse_valid() {
let raw = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
let ctx = parse_traceparent(raw).expect("should parse valid traceparent");
let expected_trace: [u8; 16] = [
0x4b, 0xf9, 0x2f, 0x35, 0x77, 0xb3, 0x4d, 0xa6, 0xa3, 0xce, 0x92, 0x9d, 0x0e, 0x0e,
0x47, 0x36,
];
assert_eq!(ctx.trace_id, expected_trace);
let expected_span: [u8; 8] = [0x00, 0xf0, 0x67, 0xaa, 0x0b, 0xa9, 0x02, 0xb7];
assert_eq!(ctx.span_id, expected_span);
assert_eq!(ctx.flags, 0x01);
assert!(ctx.is_sampled());
assert_eq!(ctx.to_header_value(), raw);
}
#[test]
fn test_traceparent_parse_valid_unsampled() {
let raw = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00";
let ctx = parse_traceparent(raw).expect("should parse unsampled traceparent");
assert_eq!(ctx.flags, 0x00);
assert!(!ctx.is_sampled());
assert_eq!(ctx.to_header_value(), raw);
}
#[test]
fn test_traceparent_parse_invalid() {
assert!(matches!(
parse_traceparent("00-abc"),
Err(TraceparentError::WrongSegmentCount(_))
));
assert!(matches!(
parse_traceparent("ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"),
Err(TraceparentError::UnsupportedVersion(_))
));
assert!(matches!(
parse_traceparent("00-4bf92f35-00f067aa0ba902b7-01"),
Err(TraceparentError::InvalidTraceId(_))
));
assert!(matches!(
parse_traceparent("00-00000000000000000000000000000000-00f067aa0ba902b7-01"),
Err(TraceparentError::InvalidTraceId(_))
));
assert!(matches!(
parse_traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01"),
Err(TraceparentError::InvalidParentId(_))
));
assert!(matches!(
parse_traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-001"),
Err(TraceparentError::InvalidFlags(_))
));
assert!(matches!(
parse_traceparent("00-gggggggggggggggggggggggggggggggg-00f067aa0ba902b7-01"),
Err(TraceparentError::InvalidTraceId(_))
));
}
#[test]
fn test_inject_roundtrip() {
let original = W3cTraceContext {
trace_id: [
0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
0xcd, 0xef,
],
span_id: [0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10],
flags: 0x01,
};
let header = original.to_header_value();
assert_eq!(
header,
"00-deadbeefcafebabe0123456789abcdef-fedcba9876543210-01"
);
let parsed = parse_traceparent(&header).expect("round-trip parse must succeed");
assert_eq!(parsed, original);
}
#[test]
fn test_traceparent_extractor_absent() {
let meta = MetadataMap::new();
let extractor = TraceparentExtractor::new(&meta);
assert!(matches!(extractor.extract(), Err(TraceparentError::Absent)));
}
#[test]
fn test_traceparent_extractor_present() {
let mut meta = MetadataMap::new();
let header = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
meta.insert(
"traceparent",
tonic::metadata::MetadataValue::try_from(header).expect("header value must be valid"),
);
let extractor = TraceparentExtractor::new(&meta);
let ctx = extractor.extract().expect("must parse valid traceparent");
assert!(ctx.is_sampled());
assert_eq!(ctx.to_header_value(), header);
}
#[test]
fn test_tracestate_extraction() {
let mut meta = MetadataMap::new();
meta.insert(
"tracestate",
tonic::metadata::MetadataValue::try_from("vendor1=value1,vendor2=value2")
.expect("tracestate metadata value must be valid"),
);
let extractor = TraceparentExtractor::new(&meta);
let ts = extractor.extract_tracestate();
assert_eq!(ts.as_deref(), Some("vendor1=value1,vendor2=value2"));
}
#[test]
fn test_tracestate_absent() {
let meta = MetadataMap::new();
let extractor = TraceparentExtractor::new(&meta);
assert!(extractor.extract_tracestate().is_none());
}
#[test]
fn test_hex_nibble_valid() {
assert_eq!(hex_nibble(b'0'), Some(0));
assert_eq!(hex_nibble(b'9'), Some(9));
assert_eq!(hex_nibble(b'a'), Some(10));
assert_eq!(hex_nibble(b'f'), Some(15));
assert_eq!(hex_nibble(b'A'), Some(10));
assert_eq!(hex_nibble(b'F'), Some(15));
}
#[test]
fn test_hex_nibble_invalid() {
assert_eq!(hex_nibble(b'g'), None);
assert_eq!(hex_nibble(b'z'), None);
assert_eq!(hex_nibble(b'!'), None);
}
#[test]
fn test_decode_hex16_valid() {
let out = decode_hex16("00112233445566778899aabbccddeeff");
assert_eq!(
out,
Some([
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
0xee, 0xff
])
);
}
#[test]
fn test_decode_hex16_wrong_length() {
assert!(decode_hex16("0011").is_none());
assert!(decode_hex16("00112233445566778899aabbccddeeff00").is_none());
}
#[test]
fn test_decode_hex8_valid() {
let out = decode_hex8("0102030405060708");
assert_eq!(out, Some([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]));
}
#[test]
fn test_decode_hex8_wrong_length() {
assert!(decode_hex8("01020304").is_none());
assert!(decode_hex8("010203040506070809").is_none());
}
#[test]
fn test_w3c_trace_context_display() {
let ctx = W3cTraceContext {
trace_id: [1u8; 16],
span_id: [2u8; 8],
flags: 0x01,
};
let display = format!("{}", ctx);
assert!(display.starts_with("00-"), "must start with version '00-'");
assert!(display.ends_with("-01"), "must end with '-01' (sampled)");
}
#[test]
fn test_w3c_trace_context_hex_helpers() {
let ctx = W3cTraceContext {
trace_id: [0xabu8; 16],
span_id: [0xcdu8; 8],
flags: 0x00,
};
assert_eq!(ctx.trace_id_hex(), "ab".repeat(16));
assert_eq!(ctx.span_id_hex(), "cdcdcdcdcdcdcdcd");
assert!(!ctx.is_sampled());
}
}