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