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