Skip to content

Commit 7b05a89

Browse files
authored
feat(otlp): process incoming span links (#4590)
Take incoming span links and convert them into Sentry's span links (introduced in #4486), attaching them to the Sentry span. Note that these links are not currently being written to the `SpanKafkaMessage` struct, so they don't actually affect anything downstream yet.
1 parent ad4faca commit 7b05a89

File tree

1 file changed

+124
-23
lines changed

1 file changed

+124
-23
lines changed

relay-spans/src/span.rs

Lines changed: 124 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
use std::collections::BTreeMap;
12
use std::str::FromStr;
23

34
use chrono::{TimeZone, Utc};
45
use opentelemetry_proto::tonic::common::v1::any_value::Value as OtelValue;
6+
use opentelemetry_proto::tonic::trace::v1::span::Link as OtelLink;
57

68
use crate::otel_trace::{
79
status::StatusCode as OtelStatusCode, Span as OtelSpan, SpanFlags as OtelSpanFlags,
810
};
911
use crate::status_codes;
1012
use relay_event_schema::protocol::{
11-
EventId, Span as EventSpan, SpanData, SpanId, SpanStatus, Timestamp, TraceId,
13+
EventId, Span as EventSpan, SpanData, SpanId, SpanLink, SpanStatus, Timestamp, TraceId,
1214
};
13-
use relay_protocol::{Annotated, FromValue, Object};
15+
use relay_protocol::{Annotated, FromValue, Object, Value};
1416

1517
/// convert_from_otel_to_sentry_status returns a status as defined by Sentry based on the OTel status.
1618
fn convert_from_otel_to_sentry_status(
@@ -96,6 +98,7 @@ pub fn otel_to_sentry_span(otel_span: OtelSpan) -> EventSpan {
9698
kind,
9799
attributes,
98100
status,
101+
links,
99102
..
100103
} = otel_span;
101104

@@ -165,26 +168,8 @@ pub fn otel_to_sentry_span(otel_span: OtelSpan) -> EventSpan {
165168
}
166169
_ => {
167170
let key = attribute.key;
168-
match value {
169-
OtelValue::ArrayValue(_) => {}
170-
OtelValue::BoolValue(v) => {
171-
data.insert(key, Annotated::new(v.into()));
172-
}
173-
OtelValue::BytesValue(v) => {
174-
if let Ok(v) = String::from_utf8(v) {
175-
data.insert(key, Annotated::new(v.into()));
176-
}
177-
}
178-
OtelValue::DoubleValue(v) => {
179-
data.insert(key, Annotated::new(v.into()));
180-
}
181-
OtelValue::IntValue(v) => {
182-
data.insert(key, Annotated::new(v.into()));
183-
}
184-
OtelValue::KvlistValue(_) => {}
185-
OtelValue::StringValue(v) => {
186-
data.insert(key, Annotated::new(v.into()));
187-
}
171+
if let Some(v) = otel_to_sentry_value(value) {
172+
data.insert(key, Annotated::new(v));
188173
}
189174
}
190175
}
@@ -199,6 +184,11 @@ pub fn otel_to_sentry_span(otel_span: OtelSpan) -> EventSpan {
199184
description = description.or(Some(format!("{http_method} {http_route}")));
200185
}
201186

187+
let sentry_links: Vec<Annotated<SpanLink>> = links
188+
.into_iter()
189+
.map(|link| otel_to_sentry_link(link).into())
190+
.collect();
191+
202192
EventSpan {
203193
op: op.into(),
204194
description: description.into(),
@@ -221,10 +211,49 @@ pub fn otel_to_sentry_span(otel_span: OtelSpan) -> EventSpan {
221211
timestamp: Timestamp(end_timestamp).into(),
222212
trace_id: TraceId(trace_id).into(),
223213
platform: platform.into(),
214+
links: sentry_links.into(),
224215
..Default::default()
225216
}
226217
}
227218

219+
fn otel_to_sentry_link(otel_link: OtelLink) -> SpanLink {
220+
// See the W3C trace context specification:
221+
// <https://www.w3.org/TR/trace-context-2/#sampled-flag>
222+
const W3C_TRACE_CONTEXT_SAMPLED: u32 = 1 << 0;
223+
224+
let attributes = BTreeMap::from_iter(otel_link.attributes.into_iter().flat_map(|kv| {
225+
kv.value
226+
.and_then(|v| v.value)
227+
.and_then(otel_to_sentry_value)
228+
.map(|v| (kv.key, v.into()))
229+
}))
230+
.into();
231+
232+
SpanLink {
233+
trace_id: TraceId(hex::encode(otel_link.trace_id)).into(),
234+
span_id: SpanId(hex::encode(otel_link.span_id)).into(),
235+
sampled: (otel_link.flags & W3C_TRACE_CONTEXT_SAMPLED != 0).into(),
236+
attributes,
237+
// The parent span ID is not available over OTLP.
238+
parent_span_id: Annotated::empty(),
239+
other: Default::default(),
240+
}
241+
}
242+
243+
fn otel_to_sentry_value(value: OtelValue) -> Option<Value> {
244+
match value {
245+
OtelValue::BoolValue(v) => Some(Value::Bool(v)),
246+
OtelValue::DoubleValue(v) => Some(Value::F64(v)),
247+
OtelValue::IntValue(v) => Some(Value::I64(v)),
248+
OtelValue::StringValue(v) => Some(Value::String(v)),
249+
OtelValue::BytesValue(v) => {
250+
String::from_utf8(v).map_or(None, |str| Some(Value::String(str)))
251+
}
252+
OtelValue::ArrayValue(_) => None,
253+
OtelValue::KvlistValue(_) => None,
254+
}
255+
}
256+
228257
#[cfg(test)]
229258
mod tests {
230259
use super::*;
@@ -707,7 +736,7 @@ mod tests {
707736
),
708737
},
709738
},
710-
links: ~,
739+
links: [],
711740
sentry_tags: ~,
712741
received: ~,
713742
measurements: ~,
@@ -756,4 +785,76 @@ mod tests {
756785
Some("fa90fdead5f74052")
757786
);
758787
}
788+
789+
#[test]
790+
fn parse_link() {
791+
let json = r#"{
792+
"links": [
793+
{
794+
"traceId": "4c79f60c11214eb38604f4ae0781bfb2",
795+
"spanId": "fa90fdead5f74052",
796+
"attributes": [
797+
{
798+
"key": "str_key",
799+
"value": {
800+
"stringValue": "str_value"
801+
}
802+
},
803+
{
804+
"key": "bool_key",
805+
"value": {
806+
"boolValue": true
807+
}
808+
},
809+
{
810+
"key": "int_key",
811+
"value": {
812+
"intValue": "123"
813+
}
814+
},
815+
{
816+
"key": "double_key",
817+
"value": {
818+
"doubleValue": 1.23
819+
}
820+
}
821+
],
822+
"flags": 1
823+
}
824+
]
825+
}"#;
826+
let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
827+
let event_span: EventSpan = otel_to_sentry_span(otel_span);
828+
let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
829+
assert_eq!(
830+
get_path!(annotated_span.links[0].trace_id),
831+
Some(&Annotated::new(TraceId(
832+
"4c79f60c11214eb38604f4ae0781bfb2".into()
833+
)))
834+
);
835+
assert_eq!(
836+
get_path!(annotated_span.links[0].span_id),
837+
Some(&Annotated::new(SpanId("fa90fdead5f74052".into())))
838+
);
839+
assert_eq!(
840+
get_path!(annotated_span.links[0].attributes["str_key"]),
841+
Some(&Annotated::new(Value::String("str_value".into())))
842+
);
843+
assert_eq!(
844+
get_path!(annotated_span.links[0].attributes["bool_key"]),
845+
Some(&Annotated::new(Value::Bool(true)))
846+
);
847+
assert_eq!(
848+
get_path!(annotated_span.links[0].attributes["int_key"]),
849+
Some(&Annotated::new(Value::I64(123)))
850+
);
851+
assert_eq!(
852+
get_path!(annotated_span.links[0].attributes["double_key"]),
853+
Some(&Annotated::new(Value::F64(1.23)))
854+
);
855+
assert_eq!(
856+
get_path!(annotated_span.links[0].sampled),
857+
Some(&Annotated::new(true))
858+
);
859+
}
759860
}

0 commit comments

Comments
 (0)