use std::fmt;
use std::str::FromStr;
use relay_protocol::{Annotated, Empty, Error, FromValue, IntoValue, Object, Value};
use crate::processor::ProcessValue;
use crate::protocol::{OperationType, OriginType, SpanData, SpanStatus};
#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
pub struct TraceId(pub String);
impl FromValue for TraceId {
fn from_value(value: Annotated<Value>) -> Annotated<Self> {
match value {
Annotated(Some(Value::String(mut value)), mut meta) => {
if !is_hex_string(&value, 32) || value.bytes().all(|x| x == b'0') {
meta.add_error(Error::invalid("not a valid trace id"));
meta.set_original_value(Some(value));
Annotated(None, meta)
} else {
value.make_ascii_lowercase();
Annotated(Some(TraceId(value)), meta)
}
}
Annotated(None, meta) => Annotated(None, meta),
Annotated(Some(value), mut meta) => {
meta.add_error(Error::expected("trace id"));
meta.set_original_value(Some(value));
Annotated(None, meta)
}
}
}
}
impl AsRef<str> for TraceId {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Empty, IntoValue, ProcessValue)]
pub struct SpanId(pub String);
relay_common::impl_str_serde!(SpanId, "a span identifier");
impl FromStr for SpanId {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(SpanId(s.to_string()))
}
}
impl fmt::Display for SpanId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromValue for SpanId {
fn from_value(value: Annotated<Value>) -> Annotated<Self> {
match value {
Annotated(Some(Value::String(mut value)), mut meta) => {
if !is_hex_string(&value, 16) || value.bytes().all(|x| x == b'0') {
meta.add_error(Error::invalid("not a valid span id"));
meta.set_original_value(Some(value));
Annotated(None, meta)
} else {
value.make_ascii_lowercase();
Annotated(Some(SpanId(value)), meta)
}
}
Annotated(None, meta) => Annotated(None, meta),
Annotated(Some(value), mut meta) => {
meta.add_error(Error::expected("span id"));
meta.set_original_value(Some(value));
Annotated(None, meta)
}
}
}
}
impl AsRef<str> for SpanId {
fn as_ref(&self) -> &str {
&self.0
}
}
fn is_hex_string(string: &str, len: usize) -> bool {
string.len() == len && string.bytes().all(|b| b.is_ascii_hexdigit())
}
#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
#[metastructure(process_func = "process_trace_context")]
pub struct TraceContext {
#[metastructure(required = "true")]
pub trace_id: Annotated<TraceId>,
#[metastructure(required = "true")]
pub span_id: Annotated<SpanId>,
pub parent_span_id: Annotated<SpanId>,
#[metastructure(max_chars = 128)]
pub op: Annotated<OperationType>,
pub status: Annotated<SpanStatus>,
pub exclusive_time: Annotated<f64>,
pub client_sample_rate: Annotated<f64>,
#[metastructure(max_chars = 128, allow_chars = "a-zA-Z0-9_.")]
pub origin: Annotated<OriginType>,
pub sampled: Annotated<bool>,
#[metastructure(pii = "maybe", skip_serialization = "null")]
pub data: Annotated<SpanData>,
#[metastructure(additional_properties, retain = "true", pii = "maybe")]
pub other: Object<Value>,
}
impl super::DefaultContext for TraceContext {
fn default_key() -> &'static str {
"trace"
}
fn from_context(context: super::Context) -> Option<Self> {
match context {
super::Context::Trace(c) => Some(*c),
_ => None,
}
}
fn cast(context: &super::Context) -> Option<&Self> {
match context {
super::Context::Trace(c) => Some(c),
_ => None,
}
}
fn cast_mut(context: &mut super::Context) -> Option<&mut Self> {
match context {
super::Context::Trace(c) => Some(c),
_ => None,
}
}
fn into_context(self) -> super::Context {
super::Context::Trace(Box::new(self))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::{Context, Route};
#[test]
fn test_trace_context_roundtrip() {
let json = r#"{
"trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
"span_id": "fa90fdead5f74052",
"parent_span_id": "fa90fdead5f74053",
"op": "http",
"status": "ok",
"exclusive_time": 0.0,
"client_sample_rate": 0.5,
"origin": "auto.http",
"data": {
"route": {
"name": "/users",
"params": {
"tok": "test"
},
"custom_field": "something"
},
"custom_field_empty": ""
},
"other": "value",
"type": "trace"
}"#;
let context = Annotated::new(Context::Trace(Box::new(TraceContext {
trace_id: Annotated::new(TraceId("4c79f60c11214eb38604f4ae0781bfb2".into())),
span_id: Annotated::new(SpanId("fa90fdead5f74052".into())),
parent_span_id: Annotated::new(SpanId("fa90fdead5f74053".into())),
op: Annotated::new("http".into()),
status: Annotated::new(SpanStatus::Ok),
exclusive_time: Annotated::new(0.0),
client_sample_rate: Annotated::new(0.5),
origin: Annotated::new("auto.http".to_owned()),
data: Annotated::new(SpanData {
route: Annotated::new(Route {
name: Annotated::new("/users".into()),
params: Annotated::new({
let mut map = Object::new();
map.insert(
"tok".to_string(),
Annotated::new(Value::String("test".into())),
);
map
}),
other: Object::from([(
"custom_field".into(),
Annotated::new(Value::String("something".into())),
)]),
}),
other: Object::from([(
"custom_field_empty".into(),
Annotated::new(Value::String("".into())),
)]),
..Default::default()
}),
other: {
let mut map = Object::new();
map.insert(
"other".to_string(),
Annotated::new(Value::String("value".to_string())),
);
map
},
sampled: Annotated::empty(),
})));
assert_eq!(context, Annotated::from_json(json).unwrap());
assert_eq!(json, context.to_json_pretty().unwrap());
}
#[test]
fn test_trace_context_normalization() {
let json = r#"{
"trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
"span_id": "FA90FDEAD5F74052",
"type": "trace"
}"#;
let context = Annotated::new(Context::Trace(Box::new(TraceContext {
trace_id: Annotated::new(TraceId("4c79f60c11214eb38604f4ae0781bfb2".into())),
span_id: Annotated::new(SpanId("fa90fdead5f74052".into())),
..Default::default()
})));
assert_eq!(context, Annotated::from_json(json).unwrap());
}
#[test]
fn test_trace_context_with_routes() {
let json = r#"{
"trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
"span_id": "FA90FDEAD5F74052",
"type": "trace",
"data": {
"route": "HomeRoute"
}
}"#;
let context = Annotated::new(Context::Trace(Box::new(TraceContext {
trace_id: Annotated::new(TraceId("4c79f60c11214eb38604f4ae0781bfb2".into())),
span_id: Annotated::new(SpanId("fa90fdead5f74052".into())),
data: Annotated::new(SpanData {
route: Annotated::new(Route {
name: Annotated::new("HomeRoute".into()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
})));
assert_eq!(context, Annotated::from_json(json).unwrap());
}
}