relay_event_schema/protocol/span_v2/
mod.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
10pub mod container;
11
12#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
14pub struct SpanV2 {
15 #[metastructure(required = true, nonempty = true, trim = false)]
17 pub trace_id: Annotated<TraceId>,
18
19 #[metastructure(trim = false)]
21 pub parent_span_id: Annotated<SpanId>,
22
23 #[metastructure(required = true, nonempty = true, trim = false)]
25 pub span_id: Annotated<SpanId>,
26
27 #[metastructure(required = true, trim = false)]
29 pub name: Annotated<OperationType>,
30
31 #[metastructure(required = true, trim = false)]
33 pub status: Annotated<SpanV2Status>,
34
35 #[metastructure(trim = false)]
37 pub is_segment: Annotated<bool>,
38
39 #[metastructure(required = true, trim = false)]
41 pub start_timestamp: Annotated<Timestamp>,
42
43 #[metastructure(required = true, trim = false)]
45 pub end_timestamp: Annotated<Timestamp>,
46
47 #[metastructure(pii = "maybe", trim = true)]
49 pub links: Annotated<Array<SpanV2Link>>,
50
51 #[metastructure(pii = "true", trim = true)]
53 pub attributes: Annotated<Attributes>,
54
55 #[metastructure(additional_properties, pii = "maybe")]
57 pub other: Object<Value>,
58}
59
60impl Getter for SpanV2 {
61 fn get_value(&self, path: &str) -> Option<relay_protocol::Val<'_>> {
62 Some(match path.strip_prefix("span.")? {
63 "name" => self.name.value()?.as_str().into(),
64 "status" => self.status.value()?.as_str().into(),
65 path => {
66 let key = path.strip_prefix("attributes.")?;
67 let key = key.strip_suffix(".value")?;
68 self.attributes.value()?.get_value(key)?.into()
69 }
70 })
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Serialize)]
79#[serde(rename_all = "snake_case")]
80pub enum SpanV2Status {
81 Ok,
83 Error,
85 Other(String),
87}
88
89impl SpanV2Status {
90 pub fn as_str(&self) -> &str {
92 match self {
93 Self::Ok => "ok",
94 Self::Error => "error",
95 Self::Other(s) => s,
96 }
97 }
98}
99
100impl Empty for SpanV2Status {
101 #[inline]
102 fn is_empty(&self) -> bool {
103 false
104 }
105}
106
107impl ProcessValue for SpanV2Status {}
108
109impl AsRef<str> for SpanV2Status {
110 fn as_ref(&self) -> &str {
111 self.as_str()
112 }
113}
114
115impl fmt::Display for SpanV2Status {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 f.write_str(self.as_str())
118 }
119}
120
121impl 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
131impl 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
140impl 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#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
166#[metastructure(trim = false)]
167pub struct SpanV2Link {
168 #[metastructure(required = true, trim = false)]
170 pub trace_id: Annotated<TraceId>,
171
172 #[metastructure(required = true, trim = false)]
174 pub span_id: Annotated<SpanId>,
175
176 #[metastructure(trim = false)]
178 pub sampled: Annotated<bool>,
179
180 #[metastructure(pii = "maybe", trim = true)]
182 pub attributes: Annotated<Attributes>,
183
184 #[metastructure(additional_properties, pii = "maybe")]
186 pub other: Object<Value>,
187}
188
189#[cfg(test)]
190mod tests {
191 use chrono::{TimeZone, Utc};
192 use similar_asserts::assert_eq;
193
194 use super::*;
195
196 #[test]
197 fn test_span_serialization() {
198 let json = r#"{
199 "trace_id": "6cf173d587eb48568a9b2e12dcfbea52",
200 "span_id": "438f40bd3b4a41ee",
201 "name": "GET http://app.test/",
202 "status": "ok",
203 "is_segment": true,
204 "start_timestamp": 1742921669.25,
205 "end_timestamp": 1742921669.75,
206 "links": [
207 {
208 "trace_id": "627a2885119dcc8184fae7eef09438cb",
209 "span_id": "6c71fc6b09b8b716",
210 "sampled": true,
211 "attributes": {
212 "sentry.link.type": {
213 "type": "string",
214 "value": "previous_trace"
215 }
216 }
217 }
218 ],
219 "attributes": {
220 "custom.error_rate": {
221 "type": "double",
222 "value": 0.5
223 },
224 "custom.is_green": {
225 "type": "boolean",
226 "value": true
227 },
228 "http.response.status_code": {
229 "type": "integer",
230 "value": 200
231 },
232 "sentry.environment": {
233 "type": "string",
234 "value": "local"
235 },
236 "sentry.origin": {
237 "type": "string",
238 "value": "manual"
239 },
240 "sentry.platform": {
241 "type": "string",
242 "value": "php"
243 },
244 "sentry.release": {
245 "type": "string",
246 "value": "1.0.0"
247 },
248 "sentry.sdk.name": {
249 "type": "string",
250 "value": "sentry.php"
251 },
252 "sentry.sdk.version": {
253 "type": "string",
254 "value": "4.10.0"
255 },
256 "sentry.transaction_info.source": {
257 "type": "string",
258 "value": "url"
259 },
260 "server.address": {
261 "type": "string",
262 "value": "DHWKN7KX6N.local"
263 }
264 }
265}"#;
266
267 let mut attributes = Attributes::new();
268
269 attributes.insert("custom.error_rate".to_owned(), 0.5);
270 attributes.insert("custom.is_green".to_owned(), true);
271 attributes.insert("sentry.release".to_owned(), "1.0.0".to_owned());
272 attributes.insert("sentry.environment".to_owned(), "local".to_owned());
273 attributes.insert("sentry.platform".to_owned(), "php".to_owned());
274 attributes.insert("sentry.sdk.name".to_owned(), "sentry.php".to_owned());
275 attributes.insert("sentry.sdk.version".to_owned(), "4.10.0".to_owned());
276 attributes.insert(
277 "sentry.transaction_info.source".to_owned(),
278 "url".to_owned(),
279 );
280 attributes.insert("sentry.origin".to_owned(), "manual".to_owned());
281 attributes.insert("server.address".to_owned(), "DHWKN7KX6N.local".to_owned());
282 attributes.insert("http.response.status_code".to_owned(), 200i64);
283
284 let mut link_attributes = Attributes::new();
285 link_attributes.insert("sentry.link.type".to_owned(), "previous_trace".to_owned());
286
287 let links = vec![Annotated::new(SpanV2Link {
288 trace_id: Annotated::new("627a2885119dcc8184fae7eef09438cb".parse().unwrap()),
289 span_id: Annotated::new("6c71fc6b09b8b716".parse().unwrap()),
290 sampled: Annotated::new(true),
291 attributes: Annotated::new(link_attributes),
292 ..Default::default()
293 })];
294 let span = Annotated::new(SpanV2 {
295 start_timestamp: Annotated::new(
296 Utc.timestamp_opt(1742921669, 250000000).unwrap().into(),
297 ),
298 end_timestamp: Annotated::new(Utc.timestamp_opt(1742921669, 750000000).unwrap().into()),
299 name: Annotated::new("GET http://app.test/".to_owned()),
300 trace_id: Annotated::new("6cf173d587eb48568a9b2e12dcfbea52".parse().unwrap()),
301 span_id: Annotated::new("438f40bd3b4a41ee".parse().unwrap()),
302 parent_span_id: Annotated::empty(),
303 status: Annotated::new(SpanV2Status::Ok),
304 is_segment: Annotated::new(true),
305 links: Annotated::new(links),
306 attributes: Annotated::new(attributes),
307 ..Default::default()
308 });
309 assert_eq!(json, span.to_json_pretty().unwrap());
310
311 let span_from_string = Annotated::from_json(json).unwrap();
312 assert_eq!(span, span_from_string);
313 }
314}