|
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`. |
86 | 23 | 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)) |
259 | 26 | } |
260 | 27 |
|
261 | 28 | #[cfg(test)] |
262 | 29 | mod tests { |
263 | 30 | use super::*; |
264 | | - use relay_protocol::SerializableAnnotated; |
| 31 | + use relay_protocol::{Annotated, SerializableAnnotated}; |
265 | 32 |
|
266 | 33 | #[test] |
267 | 34 | fn parse_span() { |
@@ -830,15 +597,6 @@ mod tests { |
830 | 597 | "###); |
831 | 598 | } |
832 | 599 |
|
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 | | - |
842 | 600 | #[test] |
843 | 601 | fn parse_link() { |
844 | 602 | let json = r#"{ |
|
0 commit comments