diff --git a/CHANGELOG.md b/CHANGELOG.md index fa2dead9474..2a256b96d1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - Add metric_bucket data category. ([#2824](https://github.com/getsentry/relay/pull/2824)) - Org rate limit metrics per bucket. ([#2836](https://github.com/getsentry/relay/pull/2836)) - Keep only domain and extension for image resource span grouping. ([#2826](https://github.com/getsentry/relay/pull/2826)) +- Parse timestamps from strings in span OpenTelemetry schema. ([#2857](https://github.com/getsentry/relay/pull/2857)) ## 23.11.2 diff --git a/relay-spans/src/lib.rs b/relay-spans/src/lib.rs index 7e640f434ff..d8c489fe024 100644 --- a/relay-spans/src/lib.rs +++ b/relay-spans/src/lib.rs @@ -13,3 +13,4 @@ mod otel_to_sentry_tags; mod span; mod status_codes; mod trace; +mod utils; diff --git a/relay-spans/src/span.rs b/relay-spans/src/span.rs index c64b2b6a207..c2c4c1f9607 100644 --- a/relay-spans/src/span.rs +++ b/relay-spans/src/span.rs @@ -4,6 +4,7 @@ use chrono::{TimeZone, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; +use crate::utils::deserialize_number_from_string; use relay_event_schema::protocol::{Span as EventSpan, SpanId, SpanStatus, Timestamp, TraceId}; use relay_protocol::{Annotated, Object, Value}; @@ -69,6 +70,7 @@ pub struct OtelSpan { /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. /// /// This field is semantically required and it is expected that end_time >= start_time. + #[serde(deserialize_with = "deserialize_number_from_string")] pub start_time_unix_nano: i64, /// end_time_unix_nano is the end time of the span. On the client side, this is the time /// kept by the local machine where the span execution ends. On the server side, this @@ -76,6 +78,7 @@ pub struct OtelSpan { /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. /// /// This field is semantically required and it is expected that end_time >= start_time. + #[serde(deserialize_with = "deserialize_number_from_string")] pub end_time_unix_nano: i64, /// attributes is a collection of key/value pairs. Note, global attributes /// like server name can be set using the resource API. @@ -250,7 +253,7 @@ pub struct Event { /// This field is semantically required to be set to non-empty string. pub name: String, /// time_unix_nano is the time the event occurred. - #[serde(default)] + #[serde(default, deserialize_with = "deserialize_number_from_string")] pub time_unix_nano: u64, } @@ -409,6 +412,7 @@ pub struct KeyValue { #[cfg(test)] mod tests { use super::*; + use chrono::{DateTime, Utc}; use relay_protocol::{get_path, Annotated}; #[test] @@ -523,4 +527,25 @@ mod tests { let event_span: EventSpan = otel_span.into(); assert_eq!(event_span.exclusive_time, Annotated::new(0.0788)); } + + #[test] + fn parse_span_with_timestamps_as_strings() { + let json = r#"{ + "traceId": "89143b0763095bd9c9955e8175d1fb23", + "spanId": "e342abb1214ca181", + "parentSpanId": "0c7a7dea069bf5a6", + "name": "middleware - fastify -> @fastify/multipart", + "kind": 1, + "startTimeUnixNano": "1697620454980000000", + "endTimeUnixNano": "1697620454980078800" + }"#; + let otel_span: OtelSpan = serde_json::from_str(json).unwrap(); + let event_span: EventSpan = otel_span.into(); + assert_eq!( + event_span.start_timestamp, + Annotated::new(Timestamp( + DateTime::::from_timestamp(1697620454, 980000000).unwrap() + )) + ); + } } diff --git a/relay-spans/src/utils.rs b/relay-spans/src/utils.rs new file mode 100644 index 00000000000..35ccde14066 --- /dev/null +++ b/relay-spans/src/utils.rs @@ -0,0 +1,41 @@ +use std::fmt::Display; +use std::str::FromStr; + +use serde::{de, Deserialize}; +use serde_json::{Map, Value}; + +pub fn deserialize_number_from_string<'de, T, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, + T: FromStr + Deserialize<'de>, + ::Err: Display, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum AnyType { + Array(Vec), + Bool(bool), + Null, + Number(T), + Object(Map), + String(String), + } + + match AnyType::::deserialize(deserializer)? { + AnyType::String(s) => s.parse::().map_err(serde::de::Error::custom), + AnyType::Number(n) => Ok(n), + AnyType::Bool(v) => Err(serde::de::Error::custom(format!( + "unsupported value: {:?}", + v + ))), + AnyType::Array(v) => Err(serde::de::Error::custom(format!( + "unsupported value: {:?}", + v + ))), + AnyType::Object(v) => Err(serde::de::Error::custom(format!( + "unsupported value: {:?}", + v + ))), + AnyType::Null => Err(serde::de::Error::custom("unsupported null value")), + } +}