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