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