Skip to content

Commit 77676d5

Browse files
authored
feat: Implement conversion from OTEL to span V2 (#4752)
This implements the conversion from OTEL spans to spans V2 and replaces the implementation of `otel_to_sentry::otel_to_sentry_span` by the composition of `OTEL -> v2` and `v2 -> v1`. In the course of this, it also moves some conversion logic from `v2 -> v1` to `OTEL -> v2`. In particular, the `v2 -> v1` conversion now assumes that the `name` and `description` of the V2 span are already normalized and correct. I've documented which fields are set based on which attributes on each of the conversion functions.
1 parent 431f58b commit 77676d5

File tree

4 files changed

+930
-442
lines changed

4 files changed

+930
-442
lines changed

relay-spans/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ pub use crate::otel_to_sentry::otel_to_sentry_span;
1111
pub use opentelemetry_proto::tonic::trace::v1 as otel_trace;
1212

1313
mod otel_to_sentry;
14+
mod otel_to_sentry_v2;
1415
mod status_codes;
1516
mod v2_to_v1;

relay-spans/src/otel_to_sentry.rs

Lines changed: 25 additions & 267 deletions
Original file line numberDiff line numberDiff line change
@@ -1,267 +1,34 @@
1-
use std::collections::BTreeMap;
2-
use std::str::FromStr;
3-
4-
use chrono::{TimeZone, Utc};
5-
use opentelemetry_proto::tonic::common::v1::any_value::Value as OtelValue;
6-
use opentelemetry_proto::tonic::trace::v1::span::Link as OtelLink;
7-
use opentelemetry_proto::tonic::trace::v1::span::SpanKind as OtelSpanKind;
8-
use relay_event_schema::protocol::SpanKind;
9-
10-
use crate::otel_trace::{
11-
Span as OtelSpan, SpanFlags as OtelSpanFlags, status::StatusCode as OtelStatusCode,
12-
};
13-
use crate::status_codes;
14-
use relay_event_schema::protocol::{
15-
EventId, Span as EventSpan, SpanData, SpanId, SpanLink, SpanStatus, Timestamp, TraceId,
16-
};
17-
use relay_protocol::{Annotated, Error, FromValue, Object, Value};
18-
19-
/// convert_from_otel_to_sentry_status returns a status as defined by Sentry based on the OTel status.
20-
fn convert_from_otel_to_sentry_status(
21-
status_code: Option<i32>,
22-
http_status_code: Option<i64>,
23-
grpc_status_code: Option<i64>,
24-
) -> SpanStatus {
25-
if let Some(status_code) = status_code {
26-
if status_code == OtelStatusCode::Unset as i32 || status_code == OtelStatusCode::Ok as i32 {
27-
return SpanStatus::Ok;
28-
}
29-
}
30-
31-
if let Some(code) = http_status_code {
32-
if let Some(sentry_status) = status_codes::HTTP.get(&code) {
33-
if let Ok(span_status) = SpanStatus::from_str(sentry_status) {
34-
return span_status;
35-
}
36-
}
37-
}
38-
39-
if let Some(code) = grpc_status_code {
40-
if let Some(sentry_status) = status_codes::GRPC.get(&code) {
41-
if let Ok(span_status) = SpanStatus::from_str(sentry_status) {
42-
return span_status;
43-
}
44-
}
45-
}
46-
47-
SpanStatus::Unknown
48-
}
49-
50-
fn otel_value_to_i64(value: OtelValue) -> Option<i64> {
51-
match value {
52-
OtelValue::IntValue(v) => Some(v),
53-
_ => None,
54-
}
55-
}
56-
57-
fn otel_value_to_string(value: OtelValue) -> Option<String> {
58-
match value {
59-
OtelValue::StringValue(v) => Some(v),
60-
OtelValue::BoolValue(v) => Some(v.to_string()),
61-
OtelValue::IntValue(v) => Some(v.to_string()),
62-
OtelValue::DoubleValue(v) => Some(v.to_string()),
63-
OtelValue::BytesValue(v) => String::from_utf8(v).ok(),
64-
_ => None,
65-
}
66-
}
67-
68-
fn otel_value_to_span_id(value: OtelValue) -> Option<String> {
69-
let decoded = match value {
70-
OtelValue::StringValue(s) => hex::decode(s).ok()?,
71-
OtelValue::BytesValue(b) => b,
72-
_ => None?,
73-
};
74-
Some(hex::encode(decoded))
75-
}
76-
77-
fn otel_flags_is_remote(value: u32) -> Option<bool> {
78-
if value & OtelSpanFlags::ContextHasIsRemoteMask as u32 == 0 {
79-
None
80-
} else {
81-
Some(value & OtelSpanFlags::ContextIsRemoteMask as u32 != 0)
82-
}
83-
}
84-
85-
/// Transform an OtelSpan to a Sentry span.
1+
use crate::otel_to_sentry_v2;
2+
use crate::otel_trace::Span as OtelSpan;
3+
use crate::v2_to_v1;
4+
use relay_event_schema::protocol::Span as EventSpan;
5+
use relay_protocol::Error;
6+
7+
/// Transforms an OTEL span to a Sentry span.
8+
///
9+
/// This uses attributes in the OTEL span to populate various fields in the Sentry span.
10+
/// * The Sentry span's `name` field may be set based on `db` or `http` attributes
11+
/// if the OTEL span's `name` is empty.
12+
/// * The Sentry span's `description` field may be set based on `db` or `http` attributes
13+
/// if the OTEL span's `sentry.description` attribute is empty.
14+
/// * The Sentry span's `status` field is set based on the OTEL span's `status` field and
15+
/// `http.status_code` and `rpc.grpc.status_code` attributes.
16+
/// * The Sentry span's `exclusive_time` field is set based on the OTEL span's `exclusive_time_nano`
17+
/// attribute, or the difference between the start and end timestamp if that attribute is not set.
18+
/// * The Sentry span's `platform` field is set based on the OTEL span's `sentry.platform` attribute.
19+
/// * The Sentry span's `profile_id` field is set based on the OTEL span's `sentry.profile.id` attribute.
20+
/// * The Sentry span's `segment_id` field is set based on the OTEL span's `sentry.segment.id` attribute.
21+
///
22+
/// All other attributes are carried over from the OTEL span to the Sentry span's `data`.
8623
pub fn otel_to_sentry_span(otel_span: OtelSpan) -> Result<EventSpan, Error> {
87-
let mut exclusive_time_ms = 0f64;
88-
let mut data = Object::new();
89-
let start_timestamp = Utc.timestamp_nanos(otel_span.start_time_unix_nano as i64);
90-
let end_timestamp = Utc.timestamp_nanos(otel_span.end_time_unix_nano as i64);
91-
let OtelSpan {
92-
trace_id,
93-
span_id,
94-
parent_span_id,
95-
flags,
96-
name,
97-
kind,
98-
attributes,
99-
status,
100-
links,
101-
..
102-
} = otel_span;
103-
104-
let span_id = hex::encode(span_id);
105-
let trace_id: TraceId = hex::encode(trace_id).parse()?;
106-
let parent_span_id = match parent_span_id.as_slice() {
107-
&[] => None,
108-
_ => Some(hex::encode(parent_span_id)),
109-
};
110-
111-
let mut op = if name.is_empty() { None } else { Some(name) };
112-
let mut description = None;
113-
let mut http_method = None;
114-
let mut http_route = None;
115-
let mut http_status_code = None;
116-
let mut grpc_status_code = None;
117-
let mut platform = None;
118-
let mut segment_id = None;
119-
let mut profile_id = None;
120-
for attribute in attributes.into_iter() {
121-
if let Some(value) = attribute.value.and_then(|v| v.value) {
122-
match attribute.key.as_str() {
123-
"sentry.description" => {
124-
description = otel_value_to_string(value);
125-
}
126-
key if key.starts_with("db") => {
127-
op = op.or(Some("db".to_string()));
128-
if key == "db.statement" {
129-
description = description.or_else(|| otel_value_to_string(value));
130-
}
131-
}
132-
"http.method" | "http.request.method" => {
133-
let http_op = match kind {
134-
2 => "http.server",
135-
3 => "http.client",
136-
_ => "http",
137-
};
138-
op = op.or(Some(http_op.to_string()));
139-
http_method = otel_value_to_string(value);
140-
}
141-
"http.route" | "url.path" => {
142-
http_route = otel_value_to_string(value);
143-
}
144-
key if key.contains("exclusive_time_nano") => {
145-
let value = match value {
146-
OtelValue::IntValue(v) => v as f64,
147-
OtelValue::DoubleValue(v) => v,
148-
OtelValue::StringValue(v) => v.parse::<f64>().unwrap_or_default(),
149-
_ => 0f64,
150-
};
151-
exclusive_time_ms = value / 1e6f64;
152-
}
153-
"http.status_code" => {
154-
http_status_code = otel_value_to_i64(value);
155-
}
156-
"rpc.grpc.status_code" => {
157-
grpc_status_code = otel_value_to_i64(value);
158-
}
159-
"sentry.platform" => {
160-
platform = otel_value_to_string(value);
161-
}
162-
"sentry.segment.id" => {
163-
segment_id = otel_value_to_span_id(value);
164-
}
165-
"sentry.profile.id" => {
166-
profile_id = otel_value_to_string(value);
167-
}
168-
_ => {
169-
let key = attribute.key;
170-
if let Some(v) = otel_to_sentry_value(value) {
171-
data.insert(key, Annotated::new(v));
172-
}
173-
}
174-
}
175-
}
176-
}
177-
if exclusive_time_ms == 0f64 {
178-
exclusive_time_ms =
179-
(otel_span.end_time_unix_nano - otel_span.start_time_unix_nano) as f64 / 1e6f64;
180-
}
181-
182-
if let (Some(http_method), Some(http_route)) = (http_method, http_route) {
183-
description = description.or(Some(format!("{http_method} {http_route}")));
184-
}
185-
186-
let sentry_links: Vec<Annotated<SpanLink>> = links
187-
.into_iter()
188-
.map(|link| otel_to_sentry_link(link).map(Into::into))
189-
.collect::<Result<_, _>>()?;
190-
191-
let event_span = EventSpan {
192-
op: op.into(),
193-
description: description.into(),
194-
data: SpanData::from_value(Annotated::new(data.into())),
195-
exclusive_time: exclusive_time_ms.into(),
196-
parent_span_id: parent_span_id.map(SpanId).into(),
197-
segment_id: segment_id.map(SpanId).into(),
198-
span_id: Annotated::new(SpanId(span_id)),
199-
is_remote: Annotated::from(otel_flags_is_remote(flags)),
200-
profile_id: profile_id
201-
.as_deref()
202-
.and_then(|s| EventId::from_str(s).ok())
203-
.into(),
204-
start_timestamp: Timestamp(start_timestamp).into(),
205-
status: Annotated::new(convert_from_otel_to_sentry_status(
206-
status.map(|s| s.code),
207-
http_status_code,
208-
grpc_status_code,
209-
)),
210-
timestamp: Timestamp(end_timestamp).into(),
211-
trace_id: Annotated::new(trace_id),
212-
platform: platform.into(),
213-
kind: OtelSpanKind::try_from(kind)
214-
.map_or(SpanKind::Unspecified, SpanKind::from)
215-
.into(),
216-
links: sentry_links.into(),
217-
..Default::default()
218-
};
219-
220-
Ok(event_span)
221-
}
222-
223-
fn otel_to_sentry_link(otel_link: OtelLink) -> Result<SpanLink, Error> {
224-
// See the W3C trace context specification:
225-
// <https://www.w3.org/TR/trace-context-2/#sampled-flag>
226-
const W3C_TRACE_CONTEXT_SAMPLED: u32 = 1 << 0;
227-
228-
let attributes = BTreeMap::from_iter(otel_link.attributes.into_iter().flat_map(|kv| {
229-
kv.value
230-
.and_then(|v| v.value)
231-
.and_then(otel_to_sentry_value)
232-
.map(|v| (kv.key, v.into()))
233-
}))
234-
.into();
235-
236-
let span_link = SpanLink {
237-
trace_id: Annotated::new(hex::encode(otel_link.trace_id).parse()?),
238-
span_id: SpanId(hex::encode(otel_link.span_id)).into(),
239-
sampled: (otel_link.flags & W3C_TRACE_CONTEXT_SAMPLED != 0).into(),
240-
attributes,
241-
other: Default::default(),
242-
};
243-
244-
Ok(span_link)
245-
}
246-
247-
fn otel_to_sentry_value(value: OtelValue) -> Option<Value> {
248-
match value {
249-
OtelValue::BoolValue(v) => Some(Value::Bool(v)),
250-
OtelValue::DoubleValue(v) => Some(Value::F64(v)),
251-
OtelValue::IntValue(v) => Some(Value::I64(v)),
252-
OtelValue::StringValue(v) => Some(Value::String(v)),
253-
OtelValue::BytesValue(v) => {
254-
String::from_utf8(v).map_or(None, |str| Some(Value::String(str)))
255-
}
256-
OtelValue::ArrayValue(_) => None,
257-
OtelValue::KvlistValue(_) => None,
258-
}
24+
let span_v2 = otel_to_sentry_v2::otel_to_sentry_span(otel_span)?;
25+
Ok(v2_to_v1::span_v2_to_span_v1(span_v2))
25926
}
26027

26128
#[cfg(test)]
26229
mod tests {
26330
use super::*;
264-
use relay_protocol::SerializableAnnotated;
31+
use relay_protocol::{Annotated, SerializableAnnotated};
26532

26633
#[test]
26734
fn parse_span() {
@@ -830,15 +597,6 @@ mod tests {
830597
"###);
831598
}
832599

833-
#[test]
834-
fn uppercase_span_id() {
835-
let input = OtelValue::StringValue("FA90FDEAD5F74052".to_owned());
836-
assert_eq!(
837-
otel_value_to_span_id(input).as_deref(),
838-
Some("fa90fdead5f74052")
839-
);
840-
}
841-
842600
#[test]
843601
fn parse_link() {
844602
let json = r#"{

0 commit comments

Comments
 (0)