opentelemetry_proto/transform/
trace.rs

1#[cfg(feature = "gen-tonic-messages")]
2/// Builds span flags based on the parent span's remote property.
3/// This follows the OTLP specification for span flags.
4pub(crate) fn build_span_flags(parent_span_is_remote: bool, base_flags: u32) -> u32 {
5    use crate::proto::tonic::trace::v1::SpanFlags;
6    let mut flags = base_flags;
7    flags |= SpanFlags::ContextHasIsRemoteMask as u32;
8    if parent_span_is_remote {
9        flags |= SpanFlags::ContextIsRemoteMask as u32;
10    }
11    flags
12}
13
14#[cfg(feature = "gen-tonic-messages")]
15pub mod tonic {
16    use crate::proto::tonic::resource::v1::Resource;
17    use crate::proto::tonic::trace::v1::{span, status, ResourceSpans, ScopeSpans, Span, Status};
18    use crate::transform::common::{
19        to_nanos,
20        tonic::{Attributes, ResourceAttributesWithSchema},
21    };
22    use opentelemetry::trace;
23    use opentelemetry::trace::{Link, SpanId, SpanKind};
24    use opentelemetry_sdk::trace::SpanData;
25    use std::collections::HashMap;
26
27    impl From<SpanKind> for span::SpanKind {
28        fn from(span_kind: SpanKind) -> Self {
29            match span_kind {
30                SpanKind::Client => span::SpanKind::Client,
31                SpanKind::Consumer => span::SpanKind::Consumer,
32                SpanKind::Internal => span::SpanKind::Internal,
33                SpanKind::Producer => span::SpanKind::Producer,
34                SpanKind::Server => span::SpanKind::Server,
35            }
36        }
37    }
38
39    impl From<&trace::Status> for status::StatusCode {
40        fn from(status: &trace::Status) -> Self {
41            match status {
42                trace::Status::Ok => status::StatusCode::Ok,
43                trace::Status::Unset => status::StatusCode::Unset,
44                trace::Status::Error { .. } => status::StatusCode::Error,
45            }
46        }
47    }
48
49    impl From<Link> for span::Link {
50        fn from(link: Link) -> Self {
51            span::Link {
52                trace_id: link.span_context.trace_id().to_bytes().to_vec(),
53                span_id: link.span_context.span_id().to_bytes().to_vec(),
54                trace_state: link.span_context.trace_state().header(),
55                attributes: Attributes::from(link.attributes).0,
56                dropped_attributes_count: link.dropped_attributes_count,
57                flags: super::build_span_flags(
58                    link.span_context.is_remote(),
59                    link.span_context.trace_flags().to_u8() as u32,
60                ),
61            }
62        }
63    }
64    impl From<opentelemetry_sdk::trace::SpanData> for Span {
65        fn from(source_span: opentelemetry_sdk::trace::SpanData) -> Self {
66            let span_kind: span::SpanKind = source_span.span_kind.into();
67            Span {
68                trace_id: source_span.span_context.trace_id().to_bytes().to_vec(),
69                span_id: source_span.span_context.span_id().to_bytes().to_vec(),
70                trace_state: source_span.span_context.trace_state().header(),
71                parent_span_id: {
72                    if source_span.parent_span_id != SpanId::INVALID {
73                        source_span.parent_span_id.to_bytes().to_vec()
74                    } else {
75                        vec![]
76                    }
77                },
78                flags: super::build_span_flags(
79                    source_span.parent_span_is_remote,
80                    source_span.span_context.trace_flags().to_u8() as u32,
81                ),
82                name: source_span.name.into_owned(),
83                kind: span_kind as i32,
84                start_time_unix_nano: to_nanos(source_span.start_time),
85                end_time_unix_nano: to_nanos(source_span.end_time),
86                dropped_attributes_count: source_span.dropped_attributes_count,
87                attributes: Attributes::from(source_span.attributes).0,
88                dropped_events_count: source_span.events.dropped_count,
89                events: source_span
90                    .events
91                    .into_iter()
92                    .map(|event| span::Event {
93                        time_unix_nano: to_nanos(event.timestamp),
94                        name: event.name.into(),
95                        attributes: Attributes::from(event.attributes).0,
96                        dropped_attributes_count: event.dropped_attributes_count,
97                    })
98                    .collect(),
99                dropped_links_count: source_span.links.dropped_count,
100                links: source_span.links.into_iter().map(Into::into).collect(),
101                status: Some(Status {
102                    code: status::StatusCode::from(&source_span.status).into(),
103                    message: match source_span.status {
104                        trace::Status::Error { description } => description.to_string(),
105                        _ => Default::default(),
106                    },
107                }),
108            }
109        }
110    }
111
112    impl ResourceSpans {
113        pub fn new(source_span: SpanData, resource: &ResourceAttributesWithSchema) -> Self {
114            let span_kind: span::SpanKind = source_span.span_kind.into();
115            ResourceSpans {
116                resource: Some(Resource {
117                    attributes: resource.attributes.0.clone(),
118                    dropped_attributes_count: 0,
119                    entity_refs: vec![],
120                }),
121                schema_url: resource.schema_url.clone().unwrap_or_default(),
122                scope_spans: vec![ScopeSpans {
123                    schema_url: source_span
124                        .instrumentation_scope
125                        .schema_url()
126                        .map(ToOwned::to_owned)
127                        .unwrap_or_default(),
128                    scope: Some((source_span.instrumentation_scope, None).into()),
129                    spans: vec![Span {
130                        trace_id: source_span.span_context.trace_id().to_bytes().to_vec(),
131                        span_id: source_span.span_context.span_id().to_bytes().to_vec(),
132                        trace_state: source_span.span_context.trace_state().header(),
133                        parent_span_id: {
134                            if source_span.parent_span_id != SpanId::INVALID {
135                                source_span.parent_span_id.to_bytes().to_vec()
136                            } else {
137                                vec![]
138                            }
139                        },
140                        flags: super::build_span_flags(
141                            source_span.parent_span_is_remote,
142                            source_span.span_context.trace_flags().to_u8() as u32,
143                        ),
144                        name: source_span.name.into_owned(),
145                        kind: span_kind as i32,
146                        start_time_unix_nano: to_nanos(source_span.start_time),
147                        end_time_unix_nano: to_nanos(source_span.end_time),
148                        dropped_attributes_count: source_span.dropped_attributes_count,
149                        attributes: Attributes::from(source_span.attributes).0,
150                        dropped_events_count: source_span.events.dropped_count,
151                        events: source_span
152                            .events
153                            .into_iter()
154                            .map(|event| span::Event {
155                                time_unix_nano: to_nanos(event.timestamp),
156                                name: event.name.into(),
157                                attributes: Attributes::from(event.attributes).0,
158                                dropped_attributes_count: event.dropped_attributes_count,
159                            })
160                            .collect(),
161                        dropped_links_count: source_span.links.dropped_count,
162                        links: source_span.links.into_iter().map(Into::into).collect(),
163                        status: Some(Status {
164                            code: status::StatusCode::from(&source_span.status).into(),
165                            message: match source_span.status {
166                                trace::Status::Error { description } => description.to_string(),
167                                _ => Default::default(),
168                            },
169                        }),
170                    }],
171                }],
172            }
173        }
174    }
175
176    pub fn group_spans_by_resource_and_scope(
177        spans: Vec<SpanData>,
178        resource: &ResourceAttributesWithSchema,
179    ) -> Vec<ResourceSpans> {
180        // Group spans by their instrumentation scope
181        let scope_map = spans.iter().fold(
182            HashMap::new(),
183            |mut scope_map: HashMap<&opentelemetry::InstrumentationScope, Vec<&SpanData>>, span| {
184                let instrumentation = &span.instrumentation_scope;
185                scope_map.entry(instrumentation).or_default().push(span);
186                scope_map
187            },
188        );
189
190        // Convert the grouped spans into ScopeSpans
191        let scope_spans = scope_map
192            .into_iter()
193            .map(|(instrumentation, span_records)| ScopeSpans {
194                scope: Some((instrumentation, None).into()),
195                schema_url: instrumentation
196                    .schema_url()
197                    .map(ToOwned::to_owned)
198                    .unwrap_or_default(),
199                spans: span_records
200                    .into_iter()
201                    .map(|span_data| span_data.clone().into())
202                    .collect(),
203            })
204            .collect();
205
206        // Wrap ScopeSpans into a single ResourceSpans
207        vec![ResourceSpans {
208            resource: Some(Resource {
209                attributes: resource.attributes.0.clone(),
210                dropped_attributes_count: 0,
211                entity_refs: vec![],
212            }),
213            scope_spans,
214            schema_url: resource.schema_url.clone().unwrap_or_default(),
215        }]
216    }
217}
218
219#[cfg(all(test, feature = "gen-tonic-messages"))]
220mod span_flags_tests {
221    use crate::proto::tonic::trace::v1::{Span, SpanFlags};
222    use opentelemetry::trace::{SpanContext, SpanId, TraceFlags, TraceId, TraceState};
223    use opentelemetry::InstrumentationScope;
224    use opentelemetry_sdk::trace::SpanData;
225    use std::borrow::Cow;
226
227    #[test]
228    fn test_build_span_flags_local_parent() {
229        let flags = super::build_span_flags(false, 0); // is_remote = false
230        assert_eq!(flags, SpanFlags::ContextHasIsRemoteMask as u32); // 0x100
231    }
232
233    #[test]
234    fn test_build_span_flags_remote_parent() {
235        let flags = super::build_span_flags(true, 0); // is_remote = true
236        assert_eq!(
237            flags,
238            (SpanFlags::ContextHasIsRemoteMask as u32) | (SpanFlags::ContextIsRemoteMask as u32)
239        ); // 0x300
240    }
241
242    #[test]
243    fn test_build_span_flags_no_parent() {
244        let flags = super::build_span_flags(false, 0); // no parent = false
245        assert_eq!(flags, SpanFlags::ContextHasIsRemoteMask as u32); // 0x100
246    }
247
248    #[test]
249    fn test_build_span_flags_preserves_base_flags() {
250        let flags = super::build_span_flags(false, 0x01); // SAMPLED flag, local parent
251        assert_eq!(flags, 0x01 | (SpanFlags::ContextHasIsRemoteMask as u32)); // 0x101
252    }
253
254    #[test]
255    fn test_span_transformation_with_flags() {
256        let span_data = SpanData {
257            span_context: SpanContext::new(
258                TraceId::from(789),
259                SpanId::from(101112),
260                TraceFlags::default(),
261                false,
262                TraceState::default(),
263            ),
264            parent_span_id: SpanId::from(456),
265            parent_span_is_remote: false,
266            span_kind: opentelemetry::trace::SpanKind::Internal,
267            name: Cow::Borrowed("test_span"),
268            start_time: std::time::SystemTime::now(),
269            end_time: std::time::SystemTime::now(),
270            attributes: vec![],
271            dropped_attributes_count: 0,
272            events: opentelemetry_sdk::trace::SpanEvents::default(),
273            links: opentelemetry_sdk::trace::SpanLinks::default(),
274            status: opentelemetry::trace::Status::Unset,
275            instrumentation_scope: InstrumentationScope::builder("test").build(),
276        };
277
278        let otlp_span: Span = span_data.into();
279        assert_eq!(otlp_span.flags, SpanFlags::ContextHasIsRemoteMask as u32); // 0x100
280    }
281
282    #[test]
283    fn test_span_transformation_with_remote_parent() {
284        let span_data = SpanData {
285            span_context: SpanContext::new(
286                TraceId::from(789),
287                SpanId::from(101112),
288                TraceFlags::default(),
289                false,
290                TraceState::default(),
291            ),
292            parent_span_id: SpanId::from(456),
293            parent_span_is_remote: true,
294            span_kind: opentelemetry::trace::SpanKind::Internal,
295            name: Cow::Borrowed("test_span"),
296            start_time: std::time::SystemTime::now(),
297            end_time: std::time::SystemTime::now(),
298            attributes: vec![],
299            dropped_attributes_count: 0,
300            events: opentelemetry_sdk::trace::SpanEvents::default(),
301            links: opentelemetry_sdk::trace::SpanLinks::default(),
302            status: opentelemetry::trace::Status::Unset,
303            instrumentation_scope: InstrumentationScope::builder("test").build(),
304        };
305
306        let otlp_span: Span = span_data.into();
307        assert_eq!(
308            otlp_span.flags,
309            (SpanFlags::ContextHasIsRemoteMask as u32) | (SpanFlags::ContextIsRemoteMask as u32)
310        ); // 0x300
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use crate::tonic::common::v1::any_value::Value;
317    use crate::transform::common::tonic::ResourceAttributesWithSchema;
318    use opentelemetry::time::now;
319    use opentelemetry::trace::{
320        SpanContext, SpanId, SpanKind, Status, TraceFlags, TraceId, TraceState,
321    };
322    use opentelemetry::InstrumentationScope;
323    use opentelemetry::KeyValue;
324    use opentelemetry_sdk::resource::Resource;
325    use opentelemetry_sdk::trace::SpanData;
326    use opentelemetry_sdk::trace::{SpanEvents, SpanLinks};
327    use std::borrow::Cow;
328    use std::time::Duration;
329
330    fn create_test_span_data(instrumentation_name: &'static str) -> SpanData {
331        let span_context = SpanContext::new(
332            TraceId::from(123),
333            SpanId::from(456),
334            TraceFlags::default(),
335            false,
336            TraceState::default(),
337        );
338
339        SpanData {
340            span_context,
341            parent_span_id: SpanId::from(0),
342            parent_span_is_remote: false,
343            span_kind: SpanKind::Internal,
344            name: Cow::Borrowed("test_span"),
345            start_time: now(),
346            end_time: now() + Duration::from_secs(1),
347            attributes: vec![KeyValue::new("key", "value")],
348            dropped_attributes_count: 0,
349            events: SpanEvents::default(),
350            links: SpanLinks::default(),
351            status: Status::Unset,
352            instrumentation_scope: InstrumentationScope::builder(instrumentation_name).build(),
353        }
354    }
355
356    #[test]
357    fn test_group_spans_by_resource_and_scope_single_scope() {
358        let resource = Resource::builder_empty()
359            .with_attribute(KeyValue::new("resource_key", "resource_value"))
360            .build();
361        let span_data = create_test_span_data("lib1");
362
363        let spans = vec![span_data.clone()];
364        let resource: ResourceAttributesWithSchema = (&resource).into(); // Convert Resource to ResourceAttributesWithSchema
365
366        let grouped_spans =
367            crate::transform::trace::tonic::group_spans_by_resource_and_scope(spans, &resource);
368
369        assert_eq!(grouped_spans.len(), 1);
370
371        let resource_spans = &grouped_spans[0];
372        assert_eq!(
373            resource_spans.resource.as_ref().unwrap().attributes.len(),
374            1
375        );
376        assert_eq!(
377            resource_spans.resource.as_ref().unwrap().attributes[0].key,
378            "resource_key"
379        );
380        assert_eq!(
381            resource_spans.resource.as_ref().unwrap().attributes[0]
382                .value
383                .clone()
384                .unwrap()
385                .value
386                .unwrap(),
387            Value::StringValue("resource_value".to_string())
388        );
389
390        let scope_spans = &resource_spans.scope_spans;
391        assert_eq!(scope_spans.len(), 1);
392
393        let scope_span = &scope_spans[0];
394        assert_eq!(scope_span.scope.as_ref().unwrap().name, "lib1");
395        assert_eq!(scope_span.spans.len(), 1);
396
397        assert_eq!(
398            scope_span.spans[0].trace_id,
399            span_data.span_context.trace_id().to_bytes().to_vec()
400        );
401    }
402
403    #[test]
404    fn test_group_spans_by_resource_and_scope_multiple_scopes() {
405        let resource = Resource::builder_empty()
406            .with_attribute(KeyValue::new("resource_key", "resource_value"))
407            .build();
408        let span_data1 = create_test_span_data("lib1");
409        let span_data2 = create_test_span_data("lib1");
410        let span_data3 = create_test_span_data("lib2");
411
412        let spans = vec![span_data1.clone(), span_data2.clone(), span_data3.clone()];
413        let resource: ResourceAttributesWithSchema = (&resource).into(); // Convert Resource to ResourceAttributesWithSchema
414
415        let grouped_spans =
416            crate::transform::trace::tonic::group_spans_by_resource_and_scope(spans, &resource);
417
418        assert_eq!(grouped_spans.len(), 1);
419
420        let resource_spans = &grouped_spans[0];
421        assert_eq!(
422            resource_spans.resource.as_ref().unwrap().attributes.len(),
423            1
424        );
425        assert_eq!(
426            resource_spans.resource.as_ref().unwrap().attributes[0].key,
427            "resource_key"
428        );
429        assert_eq!(
430            resource_spans.resource.as_ref().unwrap().attributes[0]
431                .value
432                .clone()
433                .unwrap()
434                .value
435                .unwrap(),
436            Value::StringValue("resource_value".to_string())
437        );
438
439        let scope_spans = &resource_spans.scope_spans;
440        assert_eq!(scope_spans.len(), 2);
441
442        // Check the scope spans for both lib1 and lib2
443        let mut lib1_scope_span = None;
444        let mut lib2_scope_span = None;
445
446        for scope_span in scope_spans {
447            match scope_span.scope.as_ref().unwrap().name.as_str() {
448                "lib1" => lib1_scope_span = Some(scope_span),
449                "lib2" => lib2_scope_span = Some(scope_span),
450                _ => {}
451            }
452        }
453
454        let lib1_scope_span = lib1_scope_span.expect("lib1 scope span not found");
455        let lib2_scope_span = lib2_scope_span.expect("lib2 scope span not found");
456
457        assert_eq!(lib1_scope_span.scope.as_ref().unwrap().name, "lib1");
458        assert_eq!(lib2_scope_span.scope.as_ref().unwrap().name, "lib2");
459
460        assert_eq!(lib1_scope_span.spans.len(), 2);
461        assert_eq!(lib2_scope_span.spans.len(), 1);
462
463        assert_eq!(
464            lib1_scope_span.spans[0].trace_id,
465            span_data1.span_context.trace_id().to_bytes().to_vec()
466        );
467        assert_eq!(
468            lib1_scope_span.spans[1].trace_id,
469            span_data2.span_context.trace_id().to_bytes().to_vec()
470        );
471        assert_eq!(
472            lib2_scope_span.spans[0].trace_id,
473            span_data3.span_context.trace_id().to_bytes().to_vec()
474        );
475    }
476
477    #[test]
478    fn test_scope_spans_uses_instrumentation_schema_url_not_resource() {
479        let resource = Resource::builder_empty()
480            .with_schema_url(vec![], "http://resource-schema")
481            .build();
482
483        let instrumentation_scope = InstrumentationScope::builder("test-lib")
484            .with_schema_url("http://instrumentation-schema")
485            .build();
486
487        let span_data = SpanData {
488            span_context: SpanContext::new(
489                TraceId::from(123),
490                SpanId::from(456),
491                TraceFlags::default(),
492                false,
493                TraceState::default(),
494            ),
495            parent_span_id: SpanId::from(0),
496            parent_span_is_remote: false,
497            span_kind: SpanKind::Internal,
498            name: Cow::Borrowed("test_span"),
499            start_time: now(),
500            end_time: now() + Duration::from_secs(1),
501            attributes: vec![],
502            dropped_attributes_count: 0,
503            events: SpanEvents::default(),
504            links: SpanLinks::default(),
505            status: Status::Unset,
506            instrumentation_scope,
507        };
508
509        let resource: ResourceAttributesWithSchema = (&resource).into();
510        let grouped_spans = crate::transform::trace::tonic::group_spans_by_resource_and_scope(
511            vec![span_data],
512            &resource,
513        );
514
515        assert_eq!(grouped_spans.len(), 1);
516        let resource_spans = &grouped_spans[0];
517        assert_eq!(resource_spans.schema_url, "http://resource-schema");
518
519        let scope_spans = &resource_spans.scope_spans;
520        assert_eq!(scope_spans.len(), 1);
521        assert_eq!(scope_spans[0].schema_url, "http://instrumentation-schema");
522    }
523}