relay_event_schema/protocol/
span_v2.rs1use relay_protocol::{Annotated, Array, Empty, FromValue, Getter, IntoValue, Object, Value};
2
3use std::fmt;
4
5use serde::Serialize;
6
7use crate::processor::ProcessValue;
8use crate::protocol::{Attributes, OperationType, SpanId, Timestamp, TraceId};
9
10#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
12pub struct SpanV2 {
13 #[metastructure(required = true, nonempty = true, trim = false)]
15 pub trace_id: Annotated<TraceId>,
16
17 #[metastructure(trim = false)]
19 pub parent_span_id: Annotated<SpanId>,
20
21 #[metastructure(required = true, nonempty = true, trim = false)]
23 pub span_id: Annotated<SpanId>,
24
25 #[metastructure(required = true, trim = false)]
27 pub name: Annotated<OperationType>,
28
29 #[metastructure(required = true, trim = false)]
31 pub status: Annotated<SpanV2Status>,
32
33 #[metastructure(trim = false)]
35 pub is_segment: Annotated<bool>,
36
37 #[metastructure(required = true, trim = false)]
39 pub start_timestamp: Annotated<Timestamp>,
40
41 #[metastructure(required = true, trim = false)]
43 pub end_timestamp: Annotated<Timestamp>,
44
45 #[metastructure(pii = "maybe", trim = true)]
47 pub links: Annotated<Array<SpanV2Link>>,
48
49 #[metastructure(pii = "true", trim = true)]
51 pub attributes: Annotated<Attributes>,
52
53 #[metastructure(additional_properties, pii = "maybe")]
55 pub other: Object<Value>,
56}
57
58impl Getter for SpanV2 {
59 fn get_value(&self, path: &str) -> Option<relay_protocol::Val<'_>> {
60 Some(match path.strip_prefix("span.")? {
61 "name" => self.name.value()?.as_str().into(),
62 "status" => self.status.value()?.as_str().into(),
63 path => {
64 if let Some(key) = path.strip_prefix("attributes.") {
65 let key = key.strip_suffix(".value")?;
66 self.attributes.value()?.get_value(key)?.into()
67 } else {
68 return None;
69 }
70 }
71 })
72 }
73}
74
75#[derive(Clone, Debug, PartialEq, Serialize)]
80#[serde(rename_all = "snake_case")]
81pub enum SpanV2Status {
82 Ok,
84 Error,
86 Other(String),
88}
89
90impl SpanV2Status {
91 pub fn as_str(&self) -> &str {
93 match self {
94 Self::Ok => "ok",
95 Self::Error => "error",
96 Self::Other(s) => s,
97 }
98 }
99}
100
101impl Empty for SpanV2Status {
102 #[inline]
103 fn is_empty(&self) -> bool {
104 false
105 }
106}
107
108impl ProcessValue for SpanV2Status {}
109
110impl AsRef<str> for SpanV2Status {
111 fn as_ref(&self) -> &str {
112 self.as_str()
113 }
114}
115
116impl fmt::Display for SpanV2Status {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 f.write_str(self.as_str())
119 }
120}
121
122impl From<String> for SpanV2Status {
123 fn from(value: String) -> Self {
124 match value.as_str() {
125 "ok" => Self::Ok,
126 "error" => Self::Error,
127 _ => Self::Other(value),
128 }
129 }
130}
131
132impl FromValue for SpanV2Status {
133 fn from_value(value: Annotated<Value>) -> Annotated<Self>
134 where
135 Self: Sized,
136 {
137 String::from_value(value).map_value(|s| s.into())
138 }
139}
140
141impl IntoValue for SpanV2Status {
142 fn into_value(self) -> Value
143 where
144 Self: Sized,
145 {
146 Value::String(match self {
147 SpanV2Status::Other(s) => s,
148 _ => self.to_string(),
149 })
150 }
151
152 fn serialize_payload<S>(
153 &self,
154 s: S,
155 _behavior: relay_protocol::SkipSerialization,
156 ) -> Result<S::Ok, S::Error>
157 where
158 Self: Sized,
159 S: serde::Serializer,
160 {
161 s.serialize_str(self.as_str())
162 }
163}
164
165#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
167#[metastructure(trim = false)]
168pub struct SpanV2Link {
169 #[metastructure(required = true, trim = false)]
171 pub trace_id: Annotated<TraceId>,
172
173 #[metastructure(required = true, trim = false)]
175 pub span_id: Annotated<SpanId>,
176
177 #[metastructure(trim = false)]
179 pub sampled: Annotated<bool>,
180
181 #[metastructure(pii = "maybe", trim = true)]
183 pub attributes: Annotated<Attributes>,
184
185 #[metastructure(additional_properties, pii = "maybe")]
187 pub other: Object<Value>,
188}
189
190#[cfg(test)]
191mod tests {
192 use chrono::{TimeZone, Utc};
193 use similar_asserts::assert_eq;
194
195 use super::*;
196
197 #[test]
198 fn test_span_serialization() {
199 let json = r#"{
200 "trace_id": "6cf173d587eb48568a9b2e12dcfbea52",
201 "span_id": "438f40bd3b4a41ee",
202 "name": "GET http://app.test/",
203 "status": "ok",
204 "is_segment": true,
205 "start_timestamp": 1742921669.25,
206 "end_timestamp": 1742921669.75,
207 "links": [
208 {
209 "trace_id": "627a2885119dcc8184fae7eef09438cb",
210 "span_id": "6c71fc6b09b8b716",
211 "sampled": true,
212 "attributes": {
213 "sentry.link.type": {
214 "type": "string",
215 "value": "previous_trace"
216 }
217 }
218 }
219 ],
220 "attributes": {
221 "custom.error_rate": {
222 "type": "double",
223 "value": 0.5
224 },
225 "custom.is_green": {
226 "type": "boolean",
227 "value": true
228 },
229 "http.response.status_code": {
230 "type": "integer",
231 "value": 200
232 },
233 "sentry.environment": {
234 "type": "string",
235 "value": "local"
236 },
237 "sentry.origin": {
238 "type": "string",
239 "value": "manual"
240 },
241 "sentry.platform": {
242 "type": "string",
243 "value": "php"
244 },
245 "sentry.release": {
246 "type": "string",
247 "value": "1.0.0"
248 },
249 "sentry.sdk.name": {
250 "type": "string",
251 "value": "sentry.php"
252 },
253 "sentry.sdk.version": {
254 "type": "string",
255 "value": "4.10.0"
256 },
257 "sentry.transaction_info.source": {
258 "type": "string",
259 "value": "url"
260 },
261 "server.address": {
262 "type": "string",
263 "value": "DHWKN7KX6N.local"
264 }
265 }
266}"#;
267
268 let mut attributes = Attributes::new();
269
270 attributes.insert("custom.error_rate".to_owned(), 0.5);
271 attributes.insert("custom.is_green".to_owned(), true);
272 attributes.insert("sentry.release".to_owned(), "1.0.0".to_owned());
273 attributes.insert("sentry.environment".to_owned(), "local".to_owned());
274 attributes.insert("sentry.platform".to_owned(), "php".to_owned());
275 attributes.insert("sentry.sdk.name".to_owned(), "sentry.php".to_owned());
276 attributes.insert("sentry.sdk.version".to_owned(), "4.10.0".to_owned());
277 attributes.insert(
278 "sentry.transaction_info.source".to_owned(),
279 "url".to_owned(),
280 );
281 attributes.insert("sentry.origin".to_owned(), "manual".to_owned());
282 attributes.insert("server.address".to_owned(), "DHWKN7KX6N.local".to_owned());
283 attributes.insert("http.response.status_code".to_owned(), 200i64);
284
285 let mut link_attributes = Attributes::new();
286 link_attributes.insert("sentry.link.type".to_owned(), "previous_trace".to_owned());
287
288 let links = vec![Annotated::new(SpanV2Link {
289 trace_id: Annotated::new("627a2885119dcc8184fae7eef09438cb".parse().unwrap()),
290 span_id: Annotated::new("6c71fc6b09b8b716".parse().unwrap()),
291 sampled: Annotated::new(true),
292 attributes: Annotated::new(link_attributes),
293 ..Default::default()
294 })];
295 let span = Annotated::new(SpanV2 {
296 start_timestamp: Annotated::new(
297 Utc.timestamp_opt(1742921669, 250000000).unwrap().into(),
298 ),
299 end_timestamp: Annotated::new(Utc.timestamp_opt(1742921669, 750000000).unwrap().into()),
300 name: Annotated::new("GET http://app.test/".to_owned()),
301 trace_id: Annotated::new("6cf173d587eb48568a9b2e12dcfbea52".parse().unwrap()),
302 span_id: Annotated::new("438f40bd3b4a41ee".parse().unwrap()),
303 parent_span_id: Annotated::empty(),
304 status: Annotated::new(SpanV2Status::Ok),
305 is_segment: Annotated::new(true),
306 links: Annotated::new(links),
307 attributes: Annotated::new(attributes),
308 ..Default::default()
309 });
310 assert_eq!(json, span.to_json_pretty().unwrap());
311
312 let span_from_string = Annotated::from_json(json).unwrap();
313 assert_eq!(span, span_from_string);
314 }
315}