#[cfg(feature = "trace")]
pub use trace::XrayPropagator;
#[cfg(feature = "trace")]
pub mod trace {
use once_cell::sync::Lazy;
use opentelemetry::{
global::{self, Error},
propagation::{text_map_propagator::FieldIter, Extractor, Injector, TextMapPropagator},
trace::{
SpanContext, SpanId, TraceContextExt, TraceError, TraceFlags, TraceId, TraceState,
},
Context,
};
use std::borrow::Cow;
use std::convert::TryFrom;
const AWS_XRAY_TRACE_HEADER: &str = "x-amzn-trace-id";
const AWS_XRAY_VERSION_KEY: &str = "1";
const HEADER_PARENT_KEY: &str = "Parent";
const HEADER_ROOT_KEY: &str = "Root";
const HEADER_SAMPLED_KEY: &str = "Sampled";
const SAMPLED: &str = "1";
const NOT_SAMPLED: &str = "0";
const REQUESTED_SAMPLE_DECISION: &str = "?";
const TRACE_FLAG_DEFERRED: TraceFlags = TraceFlags::new(0x02);
static AWS_XRAY_HEADER_FIELD: Lazy<[String; 1]> =
Lazy::new(|| [AWS_XRAY_TRACE_HEADER.to_owned()]);
#[derive(Clone, Debug, Default)]
pub struct XrayPropagator {
_private: (),
}
pub fn span_context_from_str(value: &str) -> Option<SpanContext> {
let parts: Vec<(&str, &str)> = value
.split_terminator(';')
.filter_map(from_key_value_pair)
.collect();
let mut trace_id = TraceId::INVALID;
let mut parent_segment_id = SpanId::INVALID;
let mut sampling_decision = TRACE_FLAG_DEFERRED;
let mut kv_vec = Vec::with_capacity(parts.len());
for (key, value) in parts {
match key {
HEADER_ROOT_KEY => match TraceId::try_from(XrayTraceId(Cow::from(value))) {
Err(_) => return None,
Ok(parsed) => trace_id = parsed,
},
HEADER_PARENT_KEY => {
parent_segment_id = SpanId::from_hex(value).unwrap_or(SpanId::INVALID)
}
HEADER_SAMPLED_KEY => {
sampling_decision = match value {
NOT_SAMPLED => TraceFlags::default(),
SAMPLED => TraceFlags::SAMPLED,
REQUESTED_SAMPLE_DECISION => TRACE_FLAG_DEFERRED,
_ => TRACE_FLAG_DEFERRED,
}
}
_ => kv_vec.push((key.to_ascii_lowercase(), value.to_string())),
}
}
match TraceState::from_key_value(kv_vec) {
Ok(trace_state) => {
if trace_id == TraceId::INVALID {
return None;
}
Some(SpanContext::new(
trace_id,
parent_segment_id,
sampling_decision,
true,
trace_state,
))
}
Err(trace_state_err) => {
global::handle_error(Error::Trace(TraceError::Other(Box::new(trace_state_err))));
None }
}
}
pub fn span_context_to_string(span_context: &SpanContext) -> Option<String> {
if !span_context.is_valid() {
return None;
}
let xray_trace_id = XrayTraceId::from(span_context.trace_id());
let sampling_decision =
if span_context.trace_flags() & TRACE_FLAG_DEFERRED == TRACE_FLAG_DEFERRED {
REQUESTED_SAMPLE_DECISION
} else if span_context.is_sampled() {
SAMPLED
} else {
NOT_SAMPLED
};
let trace_state_header = span_context
.trace_state()
.header_delimited("=", ";")
.split_terminator(';')
.map(title_case)
.collect::<Vec<_>>()
.join(";");
let trace_state_prefix = if trace_state_header.is_empty() {
""
} else {
";"
};
Some(format!(
"{}={};{}={:016x};{}={}{}{}",
HEADER_ROOT_KEY,
xray_trace_id.0,
HEADER_PARENT_KEY,
span_context.span_id(),
HEADER_SAMPLED_KEY,
sampling_decision,
trace_state_prefix,
trace_state_header
))
}
impl XrayPropagator {
pub fn new() -> Self {
XrayPropagator::default()
}
fn extract_span_context(&self, extractor: &dyn Extractor) -> Option<SpanContext> {
span_context_from_str(extractor.get(AWS_XRAY_TRACE_HEADER)?.trim())
}
}
impl TextMapPropagator for XrayPropagator {
fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) {
let span = cx.span();
let span_context = span.span_context();
if let Some(header_value) = span_context_to_string(span_context) {
injector.set(AWS_XRAY_TRACE_HEADER, header_value);
}
}
fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context {
self.extract_span_context(extractor)
.map(|sc| cx.with_remote_span_context(sc))
.unwrap_or_else(|| cx.clone())
}
fn fields(&self) -> FieldIter<'_> {
FieldIter::new(AWS_XRAY_HEADER_FIELD.as_ref())
}
}
#[derive(Clone, Debug, PartialEq)]
struct XrayTraceId<'a>(Cow<'a, str>);
impl<'a> TryFrom<XrayTraceId<'a>> for TraceId {
type Error = ();
fn try_from(id: XrayTraceId<'a>) -> Result<Self, Self::Error> {
let parts: Vec<&str> = id.0.split_terminator('-').collect();
if parts.len() != 3 {
return Err(());
}
let trace_id: TraceId =
TraceId::from_hex(format!("{}{}", parts[1], parts[2]).as_str()).map_err(|_| ())?;
if trace_id == TraceId::INVALID {
Err(())
} else {
Ok(trace_id)
}
}
}
impl From<TraceId> for XrayTraceId<'static> {
fn from(trace_id: TraceId) -> Self {
let trace_id_as_hex = trace_id.to_string();
let (timestamp, xray_id) = trace_id_as_hex.split_at(8_usize);
XrayTraceId(Cow::from(format!(
"{}-{}-{}",
AWS_XRAY_VERSION_KEY, timestamp, xray_id
)))
}
}
fn from_key_value_pair(pair: &str) -> Option<(&str, &str)> {
let mut key_value_pair: Option<(&str, &str)> = None;
if let Some(index) = pair.find('=') {
let (key, value) = pair.split_at(index);
key_value_pair = Some((key, value.trim_start_matches('=')));
}
key_value_pair
}
fn title_case(s: &str) -> String {
let mut capitalized: String = String::with_capacity(s.len());
if !s.is_empty() {
let mut characters = s.chars();
if let Some(first) = characters.next() {
capitalized.push(first.to_ascii_uppercase())
}
capitalized.extend(characters);
}
capitalized
}
#[cfg(test)]
mod tests {
use super::*;
use opentelemetry::trace::TraceState;
use opentelemetry_sdk::testing::trace::TestSpan;
use std::collections::HashMap;
use std::str::FromStr;
#[rustfmt::skip]
fn extract_test_data() -> Vec<(&'static str, SpanContext)> {
vec![
("", SpanContext::empty_context()),
("Sampled=1;Self=foo", SpanContext::empty_context()),
("Root=1-bogus-bad", SpanContext::empty_context()),
("Root=1-too-many-parts", SpanContext::empty_context()),
("Root=1-58406520-a006649127e371903a2de979;Parent=garbage", SpanContext::new(TraceId::from_hex("58406520a006649127e371903a2de979").unwrap(), SpanId::INVALID, TRACE_FLAG_DEFERRED, true, TraceState::default())),
("Root=1-58406520-a006649127e371903a2de979;Sampled=1", SpanContext::new(TraceId::from_hex("58406520a006649127e371903a2de979").unwrap(), SpanId::INVALID, TraceFlags::SAMPLED, true, TraceState::default())),
("Root=1-58406520-a006649127e371903a2de979;Parent=4c721bf33e3caf8f;Sampled=0", SpanContext::new(TraceId::from_hex("58406520a006649127e371903a2de979").unwrap(), SpanId::from_hex("4c721bf33e3caf8f").unwrap(), TraceFlags::default(), true, TraceState::default())),
("Root=1-58406520-a006649127e371903a2de979;Parent=4c721bf33e3caf8f;Sampled=1", SpanContext::new(TraceId::from_hex("58406520a006649127e371903a2de979").unwrap(), SpanId::from_hex("4c721bf33e3caf8f").unwrap(), TraceFlags::SAMPLED, true, TraceState::default())),
("Root=1-58406520-a006649127e371903a2de979;Parent=4c721bf33e3caf8f", SpanContext::new(TraceId::from_hex("58406520a006649127e371903a2de979").unwrap(), SpanId::from_hex("4c721bf33e3caf8f").unwrap(), TRACE_FLAG_DEFERRED, true, TraceState::default())),
("Root=1-58406520-a006649127e371903a2de979;Parent=4c721bf33e3caf8f;Sampled=?", SpanContext::new(TraceId::from_hex("58406520a006649127e371903a2de979").unwrap(), SpanId::from_hex("4c721bf33e3caf8f").unwrap(), TRACE_FLAG_DEFERRED, true, TraceState::default())),
("Root=1-58406520-a006649127e371903a2de979;Self=1-58406520-bf42676c05e20ba4a90e448e;Parent=4c721bf33e3caf8f;Sampled=1", SpanContext::new(TraceId::from_hex("58406520a006649127e371903a2de979").unwrap(), SpanId::from_hex("4c721bf33e3caf8f").unwrap(), TraceFlags::SAMPLED, true, TraceState::from_str("self=1-58406520-bf42676c05e20ba4a90e448e").unwrap())),
("Root=1-58406520-a006649127e371903a2de979;Self=1-58406520-bf42676c05e20ba4a90e448e;Parent=4c721bf33e3caf8f;Sampled=1;RandomKey=RandomValue", SpanContext::new(TraceId::from_hex("58406520a006649127e371903a2de979").unwrap(), SpanId::from_hex("4c721bf33e3caf8f").unwrap(), TraceFlags::SAMPLED, true, TraceState::from_str("self=1-58406520-bf42676c05e20ba4a90e448e,randomkey=RandomValue").unwrap())),
]
}
#[rustfmt::skip]
fn inject_test_data() -> Vec<(&'static str, SpanContext)> {
vec![
("", SpanContext::empty_context()),
("", SpanContext::new(TraceId::INVALID, SpanId::INVALID, TRACE_FLAG_DEFERRED, true, TraceState::default())),
("", SpanContext::new(TraceId::from_hex("58406520a006649127e371903a2de979").unwrap(), SpanId::INVALID, TRACE_FLAG_DEFERRED, true, TraceState::default())),
("", SpanContext::new(TraceId::from_hex("58406520a006649127e371903a2de979").unwrap(), SpanId::INVALID, TraceFlags::SAMPLED, true, TraceState::default())),
("Root=1-58406520-a006649127e371903a2de979;Parent=4c721bf33e3caf8f;Sampled=0", SpanContext::new(TraceId::from_hex("58406520a006649127e371903a2de979").unwrap(), SpanId::from_hex("4c721bf33e3caf8f").unwrap(), TraceFlags::default(), true, TraceState::default())),
("Root=1-58406520-a006649127e371903a2de979;Parent=4c721bf33e3caf8f;Sampled=1", SpanContext::new(TraceId::from_hex("58406520a006649127e371903a2de979").unwrap(), SpanId::from_hex("4c721bf33e3caf8f").unwrap(), TraceFlags::SAMPLED, true, TraceState::default())),
("Root=1-58406520-a006649127e371903a2de979;Parent=4c721bf33e3caf8f;Sampled=?;Self=1-58406520-bf42676c05e20ba4a90e448e;Randomkey=RandomValue", SpanContext::new(TraceId::from_hex("58406520a006649127e371903a2de979").unwrap(), SpanId::from_hex("4c721bf33e3caf8f").unwrap(), TRACE_FLAG_DEFERRED, true, TraceState::from_str("self=1-58406520-bf42676c05e20ba4a90e448e,randomkey=RandomValue").unwrap())),
]
}
#[test]
fn test_extract() {
for (header, expected) in extract_test_data() {
let map: HashMap<String, String> =
vec![(AWS_XRAY_TRACE_HEADER.to_string(), header.to_string())]
.into_iter()
.collect();
let propagator = XrayPropagator::default();
let context = propagator.extract(&map);
assert_eq!(context.span().span_context(), &expected);
}
}
#[test]
fn test_extract_empty() {
let map: HashMap<String, String> = HashMap::new();
let propagator = XrayPropagator::default();
let context = propagator.extract(&map);
assert_eq!(context.span().span_context(), &SpanContext::empty_context())
}
#[test]
fn test_inject() {
let propagator = XrayPropagator::default();
for (header_value, span_context) in inject_test_data() {
let mut injector: HashMap<String, String> = HashMap::new();
propagator.inject_context(
&Context::current_with_span(TestSpan(span_context)),
&mut injector,
);
let injected_value: Option<&String> = injector.get(AWS_XRAY_TRACE_HEADER);
if header_value.is_empty() {
assert!(injected_value.is_none());
} else {
assert_eq!(injected_value, Some(&header_value.to_string()));
}
}
}
}
}