Skip to content

Commit 95f8912

Browse files
authored
feat: Add SpanV2 type (#4717)
This adds a new protocol type for "version 2" (transactionless) spans, though it's not yet used anywhere. Depends on #4715. Closes RELAY-62. #skip-changelog
1 parent cf008e0 commit 95f8912

File tree

2 files changed

+373
-0
lines changed

2 files changed

+373
-0
lines changed

relay-event-schema/src/protocol/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ mod request;
2626
mod security_report;
2727
mod session;
2828
mod span;
29+
mod span_v2;
2930
mod stacktrace;
3031
mod tags;
3132
mod templateinfo;
@@ -64,6 +65,7 @@ pub use self::request::*;
6465
pub use self::security_report::*;
6566
pub use self::session::*;
6667
pub use self::span::*;
68+
pub use self::span_v2::*;
6769
pub use self::stacktrace::*;
6870
pub use self::tags::*;
6971
pub use self::templateinfo::*;
Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value};
2+
3+
use std::fmt;
4+
5+
use serde::Serialize;
6+
7+
use crate::processor::ProcessValue;
8+
use crate::protocol::{Attribute, SpanId, Timestamp, TraceId};
9+
10+
use super::OperationType;
11+
12+
/// A version 2 (transactionless) span.
13+
#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue)]
14+
pub struct SpanV2 {
15+
/// The ID of the trace to which this span belongs.
16+
#[metastructure(required = true, trim = false)]
17+
pub trace_id: Annotated<TraceId>,
18+
19+
/// The ID of the span enclosing this span.
20+
pub parent_span_id: Annotated<SpanId>,
21+
22+
/// The Span ID.
23+
#[metastructure(required = true, trim = false)]
24+
pub span_id: Annotated<SpanId>,
25+
26+
/// Span type (see `OperationType` docs).
27+
#[metastructure(required = true)]
28+
pub name: Annotated<OperationType>,
29+
30+
/// The span's status.
31+
#[metastructure(required = true)]
32+
pub status: Annotated<SpanV2Status>,
33+
34+
/// Indicates whether a span's parent is remote.
35+
///
36+
/// For OpenTelemetry spans, this is derived from span flags bits 8 and 9. See
37+
/// `SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK` and `SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK`.
38+
///
39+
/// The states are:
40+
/// - `false`: is not remote
41+
/// - `true`: is remote
42+
#[metastructure(required = true)]
43+
pub is_remote: Annotated<bool>,
44+
45+
/// Used to clarify the relationship between parents and children, or to distinguish between
46+
/// spans, e.g. a `server` and `client` span with the same name.
47+
///
48+
/// See <https://opentelemetry.io/docs/specs/otel/trace/api/#spankind>
49+
#[metastructure(required = true, skip_serialization = "empty", trim = false)]
50+
pub kind: Annotated<SpanV2Kind>,
51+
52+
/// Timestamp when the span started.
53+
#[metastructure(required = true)]
54+
pub start_timestamp: Annotated<Timestamp>,
55+
56+
/// Timestamp when the span was ended.
57+
#[metastructure(required = true)]
58+
pub end_timestamp: Annotated<Timestamp>,
59+
60+
/// Arbitrary attributes on a span.
61+
#[metastructure(pii = "true", trim = false)]
62+
pub attributes: Annotated<Object<Attribute>>,
63+
64+
/// Additional arbitrary fields for forwards compatibility.
65+
#[metastructure(additional_properties, pii = "maybe")]
66+
pub other: Object<Value>,
67+
}
68+
69+
impl SpanV2 {
70+
/// Returns the value of the attribute with the given name.
71+
pub fn attribute(&self, key: &str) -> Option<&Annotated<Value>> {
72+
Some(&self.attributes.value()?.get(key)?.value()?.value.value)
73+
}
74+
}
75+
76+
/// Status of a V2 span.
77+
///
78+
/// This is a subset of OTEL's statuses (unset, ok, error), plus
79+
/// a catchall variant for forward compatibility.
80+
#[derive(Clone, Debug, PartialEq, Serialize)]
81+
#[serde(rename_all = "snake_case")]
82+
pub enum SpanV2Status {
83+
/// The span completed successfully.
84+
Ok,
85+
/// The span contains an error.
86+
Error,
87+
/// Catchall variant for forward compatibility.
88+
Other(String),
89+
}
90+
91+
impl SpanV2Status {
92+
/// Returns the string representation of the status.
93+
pub fn as_str(&self) -> &str {
94+
match self {
95+
Self::Ok => "ok",
96+
Self::Error => "error",
97+
Self::Other(s) => s,
98+
}
99+
}
100+
}
101+
102+
impl Empty for SpanV2Status {
103+
#[inline]
104+
fn is_empty(&self) -> bool {
105+
false
106+
}
107+
}
108+
109+
impl AsRef<str> for SpanV2Status {
110+
fn as_ref(&self) -> &str {
111+
self.as_str()
112+
}
113+
}
114+
115+
impl fmt::Display for SpanV2Status {
116+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117+
f.write_str(self.as_str())
118+
}
119+
}
120+
121+
impl From<String> for SpanV2Status {
122+
fn from(value: String) -> Self {
123+
match value.as_str() {
124+
"ok" => Self::Ok,
125+
"error" => Self::Error,
126+
_ => Self::Other(value),
127+
}
128+
}
129+
}
130+
131+
impl FromValue for SpanV2Status {
132+
fn from_value(value: Annotated<Value>) -> Annotated<Self>
133+
where
134+
Self: Sized,
135+
{
136+
String::from_value(value).map_value(|s| s.into())
137+
}
138+
}
139+
140+
impl IntoValue for SpanV2Status {
141+
fn into_value(self) -> Value
142+
where
143+
Self: Sized,
144+
{
145+
Value::String(match self {
146+
SpanV2Status::Other(s) => s,
147+
_ => self.to_string(),
148+
})
149+
}
150+
151+
fn serialize_payload<S>(
152+
&self,
153+
s: S,
154+
_behavior: relay_protocol::SkipSerialization,
155+
) -> Result<S::Ok, S::Error>
156+
where
157+
Self: Sized,
158+
S: serde::Serializer,
159+
{
160+
s.serialize_str(self.as_str())
161+
}
162+
}
163+
164+
/// The kind of a V2 span.
165+
///
166+
/// This corresponds to OTEL's kind enum, plus a
167+
/// catchall variant for forward compatibility.
168+
#[derive(Clone, Debug, PartialEq, ProcessValue)]
169+
pub enum SpanV2Kind {
170+
/// An operation internal to an application.
171+
Internal,
172+
/// Server-side processing requested by a client.
173+
Server,
174+
/// A request from a client to a server.
175+
Client,
176+
/// Scheduling of an operation.
177+
Producer,
178+
/// Processing of a scheduled operation.
179+
Consumer,
180+
/// Catchall variant for forward compatibility.
181+
Other(String),
182+
}
183+
184+
impl SpanV2Kind {
185+
pub fn as_str(&self) -> &str {
186+
match self {
187+
Self::Internal => "internal",
188+
Self::Server => "server",
189+
Self::Client => "client",
190+
Self::Producer => "producer",
191+
Self::Consumer => "consumer",
192+
Self::Other(s) => s,
193+
}
194+
}
195+
}
196+
197+
impl Empty for SpanV2Kind {
198+
fn is_empty(&self) -> bool {
199+
false
200+
}
201+
}
202+
203+
impl Default for SpanV2Kind {
204+
fn default() -> Self {
205+
Self::Internal
206+
}
207+
}
208+
209+
impl fmt::Display for SpanV2Kind {
210+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211+
write!(f, "{}", self.as_str())
212+
}
213+
}
214+
215+
impl From<String> for SpanV2Kind {
216+
fn from(value: String) -> Self {
217+
match value.as_str() {
218+
"internal" => Self::Internal,
219+
"server" => Self::Server,
220+
"client" => Self::Client,
221+
"producer" => Self::Producer,
222+
"consumer" => Self::Consumer,
223+
_ => Self::Other(value),
224+
}
225+
}
226+
}
227+
228+
impl FromValue for SpanV2Kind {
229+
fn from_value(value: Annotated<Value>) -> Annotated<Self>
230+
where
231+
Self: Sized,
232+
{
233+
String::from_value(value).map_value(|s| s.into())
234+
}
235+
}
236+
237+
impl IntoValue for SpanV2Kind {
238+
fn into_value(self) -> Value
239+
where
240+
Self: Sized,
241+
{
242+
Value::String(self.to_string())
243+
}
244+
245+
fn serialize_payload<S>(
246+
&self,
247+
s: S,
248+
_behavior: relay_protocol::SkipSerialization,
249+
) -> Result<S::Ok, S::Error>
250+
where
251+
Self: Sized,
252+
S: serde::Serializer,
253+
{
254+
s.serialize_str(self.as_str())
255+
}
256+
}
257+
258+
#[cfg(test)]
259+
mod tests {
260+
use chrono::{TimeZone, Utc};
261+
use similar_asserts::assert_eq;
262+
263+
use super::*;
264+
265+
macro_rules! attrs {
266+
($($name:expr => $val:expr , $ty:ident),* $(,)?) => {
267+
std::collections::BTreeMap::from([$((
268+
$name.to_owned(),
269+
relay_protocol::Annotated::new(
270+
$crate::protocol::Attribute::new(
271+
$crate::protocol::AttributeType::$ty,
272+
$val.into()
273+
)
274+
)
275+
),)*])
276+
};
277+
}
278+
279+
#[test]
280+
fn test_span_serialization() {
281+
let json = r#"{
282+
"trace_id": "6cf173d587eb48568a9b2e12dcfbea52",
283+
"span_id": "438f40bd3b4a41ee",
284+
"name": "GET http://app.test/",
285+
"status": "ok",
286+
"is_remote": true,
287+
"kind": "server",
288+
"start_timestamp": 1742921669.25,
289+
"end_timestamp": 1742921669.75,
290+
"attributes": {
291+
"custom.error_rate": {
292+
"type": "double",
293+
"value": 0.5
294+
},
295+
"custom.is_green": {
296+
"type": "boolean",
297+
"value": true
298+
},
299+
"http.response.status_code": {
300+
"type": "integer",
301+
"value": 200
302+
},
303+
"sentry.environment": {
304+
"type": "string",
305+
"value": "local"
306+
},
307+
"sentry.origin": {
308+
"type": "string",
309+
"value": "manual"
310+
},
311+
"sentry.platform": {
312+
"type": "string",
313+
"value": "php"
314+
},
315+
"sentry.release": {
316+
"type": "string",
317+
"value": "1.0.0"
318+
},
319+
"sentry.sdk.name": {
320+
"type": "string",
321+
"value": "sentry.php"
322+
},
323+
"sentry.sdk.version": {
324+
"type": "string",
325+
"value": "4.10.0"
326+
},
327+
"sentry.transaction_info.source": {
328+
"type": "string",
329+
"value": "url"
330+
},
331+
"server.address": {
332+
"type": "string",
333+
"value": "DHWKN7KX6N.local"
334+
}
335+
}
336+
}"#;
337+
338+
let attributes = attrs!(
339+
"custom.error_rate" => 0.5, Double,
340+
"custom.is_green" => true, Boolean,
341+
"sentry.release" => "1.0.0" , String,
342+
"sentry.environment" => "local", String,
343+
"sentry.platform" => "php", String,
344+
"sentry.sdk.name" => "sentry.php", String,
345+
"sentry.sdk.version" => "4.10.0", String,
346+
"sentry.transaction_info.source" => "url", String,
347+
"sentry.origin" => "manual", String,
348+
"server.address" => "DHWKN7KX6N.local", String,
349+
"http.response.status_code" => 200i64, Integer,
350+
);
351+
let span = Annotated::new(SpanV2 {
352+
start_timestamp: Annotated::new(
353+
Utc.timestamp_opt(1742921669, 250000000).unwrap().into(),
354+
),
355+
end_timestamp: Annotated::new(Utc.timestamp_opt(1742921669, 750000000).unwrap().into()),
356+
name: Annotated::new("GET http://app.test/".to_owned()),
357+
trace_id: Annotated::new("6cf173d587eb48568a9b2e12dcfbea52".parse().unwrap()),
358+
span_id: Annotated::new(SpanId("438f40bd3b4a41ee".into())),
359+
parent_span_id: Annotated::empty(),
360+
status: Annotated::new(SpanV2Status::Ok),
361+
kind: Annotated::new(SpanV2Kind::Server),
362+
is_remote: Annotated::new(true),
363+
attributes: Annotated::new(attributes),
364+
..Default::default()
365+
});
366+
assert_eq!(json, span.to_json_pretty().unwrap());
367+
368+
let span_from_string = Annotated::from_json(json).unwrap();
369+
assert_eq!(span, span_from_string);
370+
}
371+
}

0 commit comments

Comments
 (0)