1use relay_protocol::{Annotated, Array, Empty, Error, FromValue, IntoValue, Object, Value};
2
3use std::fmt;
4use std::str::FromStr;
5
6use serde::Serialize;
7
8use crate::processor::ProcessValue;
9use crate::protocol::{Attributes, OperationType, SpanId, Timestamp, TraceId};
10
11#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue)]
13pub struct SpanV2 {
14 #[metastructure(required = true, trim = false)]
16 pub trace_id: Annotated<TraceId>,
17
18 pub parent_span_id: Annotated<SpanId>,
20
21 #[metastructure(required = true, trim = false)]
23 pub span_id: Annotated<SpanId>,
24
25 #[metastructure(required = true)]
27 pub name: Annotated<OperationType>,
28
29 #[metastructure(required = true)]
31 pub status: Annotated<SpanV2Status>,
32
33 #[metastructure(required = true)]
42 pub is_remote: Annotated<bool>,
43
44 #[metastructure(skip_serialization = "empty", trim = false)]
49 pub kind: Annotated<SpanV2Kind>,
50
51 #[metastructure(required = true)]
53 pub start_timestamp: Annotated<Timestamp>,
54
55 #[metastructure(required = true)]
57 pub end_timestamp: Annotated<Timestamp>,
58
59 #[metastructure(pii = "maybe")]
61 pub links: Annotated<Array<SpanV2Link>>,
62
63 #[metastructure(pii = "true", trim = false)]
65 pub attributes: Annotated<Attributes>,
66
67 #[metastructure(additional_properties, pii = "maybe")]
69 pub other: Object<Value>,
70}
71
72#[derive(Clone, Debug, PartialEq, Serialize)]
77#[serde(rename_all = "snake_case")]
78pub enum SpanV2Status {
79 Ok,
81 Error,
83 Other(String),
85}
86
87impl SpanV2Status {
88 pub fn as_str(&self) -> &str {
90 match self {
91 Self::Ok => "ok",
92 Self::Error => "error",
93 Self::Other(s) => s,
94 }
95 }
96}
97
98impl Empty for SpanV2Status {
99 #[inline]
100 fn is_empty(&self) -> bool {
101 false
102 }
103}
104
105impl AsRef<str> for SpanV2Status {
106 fn as_ref(&self) -> &str {
107 self.as_str()
108 }
109}
110
111impl fmt::Display for SpanV2Status {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 f.write_str(self.as_str())
114 }
115}
116
117impl From<String> for SpanV2Status {
118 fn from(value: String) -> Self {
119 match value.as_str() {
120 "ok" => Self::Ok,
121 "error" => Self::Error,
122 _ => Self::Other(value),
123 }
124 }
125}
126
127impl FromValue for SpanV2Status {
128 fn from_value(value: Annotated<Value>) -> Annotated<Self>
129 where
130 Self: Sized,
131 {
132 String::from_value(value).map_value(|s| s.into())
133 }
134}
135
136impl IntoValue for SpanV2Status {
137 fn into_value(self) -> Value
138 where
139 Self: Sized,
140 {
141 Value::String(match self {
142 SpanV2Status::Other(s) => s,
143 _ => self.to_string(),
144 })
145 }
146
147 fn serialize_payload<S>(
148 &self,
149 s: S,
150 _behavior: relay_protocol::SkipSerialization,
151 ) -> Result<S::Ok, S::Error>
152 where
153 Self: Sized,
154 S: serde::Serializer,
155 {
156 s.serialize_str(self.as_str())
157 }
158}
159
160#[derive(Clone, Debug, PartialEq, ProcessValue)]
165pub enum SpanV2Kind {
166 Internal,
168 Server,
170 Client,
172 Producer,
174 Consumer,
176}
177
178impl SpanV2Kind {
179 pub fn as_str(&self) -> &'static str {
180 match self {
181 Self::Internal => "internal",
182 Self::Server => "server",
183 Self::Client => "client",
184 Self::Producer => "producer",
185 Self::Consumer => "consumer",
186 }
187 }
188}
189
190impl Empty for SpanV2Kind {
191 fn is_empty(&self) -> bool {
192 false
193 }
194}
195
196impl Default for SpanV2Kind {
197 fn default() -> Self {
198 Self::Internal
199 }
200}
201
202impl fmt::Display for SpanV2Kind {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 write!(f, "{}", self.as_str())
205 }
206}
207
208#[derive(Debug, Clone, Copy)]
209pub struct ParseSpanV2KindError;
210
211impl fmt::Display for ParseSpanV2KindError {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 write!(f, "invalid span kind")
214 }
215}
216
217impl FromStr for SpanV2Kind {
218 type Err = ParseSpanV2KindError;
219
220 fn from_str(s: &str) -> Result<Self, Self::Err> {
221 let kind = match s {
222 "internal" => Self::Internal,
223 "server" => Self::Server,
224 "client" => Self::Client,
225 "producer" => Self::Producer,
226 "consumer" => Self::Consumer,
227 _ => return Err(ParseSpanV2KindError),
228 };
229 Ok(kind)
230 }
231}
232
233impl FromValue for SpanV2Kind {
234 fn from_value(Annotated(value, meta): Annotated<Value>) -> Annotated<Self>
235 where
236 Self: Sized,
237 {
238 match &value {
239 Some(Value::String(s)) => match s.parse() {
240 Ok(kind) => Annotated(Some(kind), meta),
241 Err(_) => Annotated::from_error(Error::expected("a span kind"), value),
242 },
243 Some(_) => Annotated::from_error(Error::expected("a span kind"), value),
244 None => Annotated::empty(),
245 }
246 }
247}
248
249impl IntoValue for SpanV2Kind {
250 fn into_value(self) -> Value
251 where
252 Self: Sized,
253 {
254 Value::String(self.to_string())
255 }
256
257 fn serialize_payload<S>(
258 &self,
259 s: S,
260 _behavior: relay_protocol::SkipSerialization,
261 ) -> Result<S::Ok, S::Error>
262 where
263 Self: Sized,
264 S: serde::Serializer,
265 {
266 s.serialize_str(self.as_str())
267 }
268}
269
270#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
272#[metastructure(trim = false)]
273pub struct SpanV2Link {
274 #[metastructure(required = true, trim = false)]
276 pub trace_id: Annotated<TraceId>,
277
278 #[metastructure(required = true, trim = false)]
280 pub span_id: Annotated<SpanId>,
281
282 #[metastructure(trim = false)]
284 pub sampled: Annotated<bool>,
285
286 #[metastructure(pii = "maybe", trim = false)]
288 pub attributes: Annotated<Attributes>,
289
290 #[metastructure(additional_properties, pii = "maybe", trim = false)]
292 pub other: Object<Value>,
293}
294
295#[cfg(test)]
296mod tests {
297 use chrono::{TimeZone, Utc};
298 use similar_asserts::assert_eq;
299
300 use super::*;
301
302 #[test]
303 fn test_span_serialization() {
304 let json = r#"{
305 "trace_id": "6cf173d587eb48568a9b2e12dcfbea52",
306 "span_id": "438f40bd3b4a41ee",
307 "name": "GET http://app.test/",
308 "status": "ok",
309 "is_remote": true,
310 "kind": "server",
311 "start_timestamp": 1742921669.25,
312 "end_timestamp": 1742921669.75,
313 "links": [
314 {
315 "trace_id": "627a2885119dcc8184fae7eef09438cb",
316 "span_id": "6c71fc6b09b8b716",
317 "sampled": true,
318 "attributes": {
319 "sentry.link.type": {
320 "type": "string",
321 "value": "previous_trace"
322 }
323 }
324 }
325 ],
326 "attributes": {
327 "custom.error_rate": {
328 "type": "double",
329 "value": 0.5
330 },
331 "custom.is_green": {
332 "type": "boolean",
333 "value": true
334 },
335 "http.response.status_code": {
336 "type": "integer",
337 "value": 200
338 },
339 "sentry.environment": {
340 "type": "string",
341 "value": "local"
342 },
343 "sentry.origin": {
344 "type": "string",
345 "value": "manual"
346 },
347 "sentry.platform": {
348 "type": "string",
349 "value": "php"
350 },
351 "sentry.release": {
352 "type": "string",
353 "value": "1.0.0"
354 },
355 "sentry.sdk.name": {
356 "type": "string",
357 "value": "sentry.php"
358 },
359 "sentry.sdk.version": {
360 "type": "string",
361 "value": "4.10.0"
362 },
363 "sentry.transaction_info.source": {
364 "type": "string",
365 "value": "url"
366 },
367 "server.address": {
368 "type": "string",
369 "value": "DHWKN7KX6N.local"
370 }
371 }
372}"#;
373
374 let mut attributes = Attributes::new();
375
376 attributes.insert("custom.error_rate".to_owned(), 0.5);
377 attributes.insert("custom.is_green".to_owned(), true);
378 attributes.insert("sentry.release".to_owned(), "1.0.0".to_owned());
379 attributes.insert("sentry.environment".to_owned(), "local".to_owned());
380 attributes.insert("sentry.platform".to_owned(), "php".to_owned());
381 attributes.insert("sentry.sdk.name".to_owned(), "sentry.php".to_owned());
382 attributes.insert("sentry.sdk.version".to_owned(), "4.10.0".to_owned());
383 attributes.insert(
384 "sentry.transaction_info.source".to_owned(),
385 "url".to_owned(),
386 );
387 attributes.insert("sentry.origin".to_owned(), "manual".to_owned());
388 attributes.insert("server.address".to_owned(), "DHWKN7KX6N.local".to_owned());
389 attributes.insert("http.response.status_code".to_owned(), 200i64);
390
391 let mut link_attributes = Attributes::new();
392 link_attributes.insert("sentry.link.type".to_owned(), "previous_trace".to_owned());
393
394 let links = vec![Annotated::new(SpanV2Link {
395 trace_id: Annotated::new("627a2885119dcc8184fae7eef09438cb".parse().unwrap()),
396 span_id: Annotated::new("6c71fc6b09b8b716".parse().unwrap()),
397 sampled: Annotated::new(true),
398 attributes: Annotated::new(link_attributes),
399 ..Default::default()
400 })];
401 let span = Annotated::new(SpanV2 {
402 start_timestamp: Annotated::new(
403 Utc.timestamp_opt(1742921669, 250000000).unwrap().into(),
404 ),
405 end_timestamp: Annotated::new(Utc.timestamp_opt(1742921669, 750000000).unwrap().into()),
406 name: Annotated::new("GET http://app.test/".to_owned()),
407 trace_id: Annotated::new("6cf173d587eb48568a9b2e12dcfbea52".parse().unwrap()),
408 span_id: Annotated::new("438f40bd3b4a41ee".parse().unwrap()),
409 parent_span_id: Annotated::empty(),
410 status: Annotated::new(SpanV2Status::Ok),
411 kind: Annotated::new(SpanV2Kind::Server),
412 is_remote: Annotated::new(true),
413 links: Annotated::new(links),
414 attributes: Annotated::new(attributes),
415 ..Default::default()
416 });
417 assert_eq!(json, span.to_json_pretty().unwrap());
418
419 let span_from_string = Annotated::from_json(json).unwrap();
420 assert_eq!(span, span_from_string);
421 }
422}