Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions relay-spans/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ mod otel_to_sentry_tags;
mod span;
mod status_codes;
mod trace;
mod utils;
27 changes: 26 additions & 1 deletion relay-spans/src/span.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -69,13 +70,15 @@ 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
/// is the time when the server application handler stops running.
/// 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.
Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -409,6 +412,7 @@ pub struct KeyValue {
#[cfg(test)]
mod tests {
use super::*;
use chrono::{DateTime, Utc};
use relay_protocol::{get_path, Annotated};

#[test]
Expand Down Expand Up @@ -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::<Utc>::from_timestamp(1697620454, 980000000).unwrap()
))
);
}
}
41 changes: 41 additions & 0 deletions relay-spans/src/utils.rs
Original file line number Diff line number Diff line change
@@ -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<T, D::Error>
where
D: de::Deserializer<'de>,
T: FromStr + Deserialize<'de>,
<T as FromStr>::Err: Display,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum AnyType<T> {
Array(Vec<Value>),
Bool(bool),
Null,
Number(T),
Object(Map<String, Value>),
String(String),
}

match AnyType::<T>::deserialize(deserializer)? {
AnyType::String(s) => s.parse::<T>().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")),
}
}