1use crate::exp_buckets::ExpBucketsSnapshot;
10use crate::{
11 Counter, Distribution, DynamicCounter, DynamicDistribution, DynamicGauge, DynamicGaugeI64,
12 DynamicHistogram, Gauge, GaugeF64, Histogram, LabelEnum, LabeledCounter, LabeledGauge,
13 LabeledHistogram, LabeledSampledTimer, MaxGauge, MaxGaugeF64, MinGauge, MinGaugeF64,
14 SampledTimer,
15};
16
17pub mod pb {
19 pub use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceRequest;
20 pub use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
21 pub use opentelemetry_proto::tonic::common::v1::{
22 AnyValue, InstrumentationScope, KeyValue, any_value,
23 };
24 pub use opentelemetry_proto::tonic::metrics::v1::{
25 self, AggregationTemporality, ExponentialHistogram as OtlpExpHistogram,
26 ExponentialHistogramDataPoint, Gauge as OtlpGauge, Histogram as OtlpHistogram,
27 HistogramDataPoint, Metric, NumberDataPoint, ResourceMetrics, ScopeMetrics, Sum,
28 exponential_histogram_data_point, metric, number_data_point,
29 };
30 pub use opentelemetry_proto::tonic::resource::v1::Resource;
31 pub use opentelemetry_proto::tonic::trace::v1::{
32 ResourceSpans, ScopeSpans, Span as OtlpSpan, Status as OtlpStatus,
33 span::{Event as OtlpEvent, SpanKind as OtlpSpanKind},
34 status::StatusCode as OtlpStatusCode,
35 };
36}
37
38pub trait OtlpExport {
46 fn export_otlp(
47 &self,
48 metrics: &mut Vec<pb::Metric>,
49 name: &str,
50 description: &str,
51 time_unix_nano: u64,
52 );
53}
54
55fn make_kv(key: &str, value: &str) -> pb::KeyValue {
60 pb::KeyValue {
61 key: key.to_string(),
62 value: Some(pb::AnyValue {
63 value: Some(pb::any_value::Value::StringValue(value.to_string())),
64 }),
65 }
66}
67
68fn pairs_to_attributes(pairs: &[(String, String)]) -> Vec<pb::KeyValue> {
69 pairs.iter().map(|(k, v)| make_kv(k, v)).collect()
70}
71
72fn label_to_attribute<L: LabelEnum>(label: L) -> pb::KeyValue {
73 make_kv(L::LABEL_NAME, label.variant_name())
74}
75
76pub fn now_nanos() -> u64 {
80 std::time::SystemTime::now()
81 .duration_since(std::time::UNIX_EPOCH)
82 .unwrap_or_default()
83 .as_nanos() as u64
84}
85
86fn int_data_point(
87 value: i64,
88 attributes: Vec<pb::KeyValue>,
89 time_unix_nano: u64,
90) -> pb::NumberDataPoint {
91 pb::NumberDataPoint {
92 attributes,
93 time_unix_nano,
94 value: Some(pb::number_data_point::Value::AsInt(value)),
95 ..Default::default()
96 }
97}
98
99fn double_data_point(
100 value: f64,
101 attributes: Vec<pb::KeyValue>,
102 time_unix_nano: u64,
103) -> pb::NumberDataPoint {
104 pb::NumberDataPoint {
105 attributes,
106 time_unix_nano,
107 value: Some(pb::number_data_point::Value::AsDouble(value)),
108 ..Default::default()
109 }
110}
111
112fn cumulative_to_otlp_buckets(cumulative: &[(u64, u64)]) -> (Vec<u64>, Vec<f64>) {
118 cumulative_to_otlp_buckets_iter(cumulative.iter().copied())
119}
120
121fn cumulative_to_otlp_buckets_iter(
122 cumulative: impl IntoIterator<Item = (u64, u64)>,
123) -> (Vec<u64>, Vec<f64>) {
124 let iter = cumulative.into_iter();
125 let (lower, _) = iter.size_hint();
126 let mut bucket_counts = Vec::with_capacity(lower);
127 let mut explicit_bounds = Vec::with_capacity(lower.saturating_sub(1));
128 let mut prev = 0u64;
129
130 for (bound, cum_count) in iter {
131 bucket_counts.push(cum_count.saturating_sub(prev));
132 prev = cum_count;
133 if bound != u64::MAX {
134 explicit_bounds.push(bound as f64);
135 }
136 }
137
138 (bucket_counts, explicit_bounds)
139}
140
141pub fn build_resource(service_name: &str, attrs: &[(&str, &str)]) -> pb::Resource {
143 let mut attributes = vec![make_kv("service.name", service_name)];
144 for (k, v) in attrs {
145 attributes.push(make_kv(k, v));
146 }
147 pb::Resource {
148 attributes,
149 ..Default::default()
150 }
151}
152
153pub fn build_export_request(
157 resource: &pb::Resource,
158 scope_name: &str,
159 metrics: Vec<pb::Metric>,
160) -> pb::ExportMetricsServiceRequest {
161 pb::ExportMetricsServiceRequest {
162 resource_metrics: vec![pb::ResourceMetrics {
163 resource: Some(resource.clone()),
164 scope_metrics: vec![pb::ScopeMetrics {
165 scope: Some(pb::InstrumentationScope {
166 name: scope_name.to_string(),
167 ..Default::default()
168 }),
169 metrics,
170 ..Default::default()
171 }],
172 ..Default::default()
173 }],
174 }
175}
176
177impl OtlpExport for Counter {
182 fn export_otlp(
183 &self,
184 metrics: &mut Vec<pb::Metric>,
185 name: &str,
186 description: &str,
187 time_unix_nano: u64,
188 ) {
189 let value = self.sum() as i64;
190 metrics.push(pb::Metric {
191 name: name.to_string(),
192 description: description.to_string(),
193 data: Some(pb::metric::Data::Sum(pb::Sum {
194 data_points: vec![int_data_point(value, Vec::new(), time_unix_nano)],
197 aggregation_temporality: pb::AggregationTemporality::Cumulative as i32,
198 is_monotonic: false,
199 })),
200 ..Default::default()
201 });
202 }
203}
204
205impl OtlpExport for Gauge {
206 fn export_otlp(
207 &self,
208 metrics: &mut Vec<pb::Metric>,
209 name: &str,
210 description: &str,
211 time_unix_nano: u64,
212 ) {
213 metrics.push(pb::Metric {
214 name: name.to_string(),
215 description: description.to_string(),
216 data: Some(pb::metric::Data::Gauge(pb::OtlpGauge {
217 data_points: vec![int_data_point(self.get(), Vec::new(), time_unix_nano)],
218 })),
219 ..Default::default()
220 });
221 }
222}
223
224impl OtlpExport for GaugeF64 {
225 fn export_otlp(
226 &self,
227 metrics: &mut Vec<pb::Metric>,
228 name: &str,
229 description: &str,
230 time_unix_nano: u64,
231 ) {
232 metrics.push(pb::Metric {
233 name: name.to_string(),
234 description: description.to_string(),
235 data: Some(pb::metric::Data::Gauge(pb::OtlpGauge {
236 data_points: vec![double_data_point(self.get(), Vec::new(), time_unix_nano)],
237 })),
238 ..Default::default()
239 });
240 }
241}
242
243impl OtlpExport for MaxGauge {
244 fn export_otlp(
245 &self,
246 metrics: &mut Vec<pb::Metric>,
247 name: &str,
248 description: &str,
249 time_unix_nano: u64,
250 ) {
251 metrics.push(pb::Metric {
252 name: name.to_string(),
253 description: description.to_string(),
254 data: Some(pb::metric::Data::Gauge(pb::OtlpGauge {
255 data_points: vec![int_data_point(self.get(), Vec::new(), time_unix_nano)],
256 })),
257 ..Default::default()
258 });
259 }
260}
261
262impl OtlpExport for MaxGaugeF64 {
263 fn export_otlp(
264 &self,
265 metrics: &mut Vec<pb::Metric>,
266 name: &str,
267 description: &str,
268 time_unix_nano: u64,
269 ) {
270 metrics.push(pb::Metric {
271 name: name.to_string(),
272 description: description.to_string(),
273 data: Some(pb::metric::Data::Gauge(pb::OtlpGauge {
274 data_points: vec![double_data_point(self.get(), Vec::new(), time_unix_nano)],
275 })),
276 ..Default::default()
277 });
278 }
279}
280
281impl OtlpExport for MinGauge {
282 fn export_otlp(
283 &self,
284 metrics: &mut Vec<pb::Metric>,
285 name: &str,
286 description: &str,
287 time_unix_nano: u64,
288 ) {
289 metrics.push(pb::Metric {
290 name: name.to_string(),
291 description: description.to_string(),
292 data: Some(pb::metric::Data::Gauge(pb::OtlpGauge {
293 data_points: vec![int_data_point(self.get(), Vec::new(), time_unix_nano)],
294 })),
295 ..Default::default()
296 });
297 }
298}
299
300impl OtlpExport for MinGaugeF64 {
301 fn export_otlp(
302 &self,
303 metrics: &mut Vec<pb::Metric>,
304 name: &str,
305 description: &str,
306 time_unix_nano: u64,
307 ) {
308 metrics.push(pb::Metric {
309 name: name.to_string(),
310 description: description.to_string(),
311 data: Some(pb::metric::Data::Gauge(pb::OtlpGauge {
312 data_points: vec![double_data_point(self.get(), Vec::new(), time_unix_nano)],
313 })),
314 ..Default::default()
315 });
316 }
317}
318
319impl OtlpExport for Histogram {
320 fn export_otlp(
321 &self,
322 metrics: &mut Vec<pb::Metric>,
323 name: &str,
324 description: &str,
325 time_unix_nano: u64,
326 ) {
327 let cumulative = self.buckets_cumulative();
328 let count = self.count();
329 let sum = self.sum();
330 let (bucket_counts, explicit_bounds) = cumulative_to_otlp_buckets(&cumulative);
331
332 metrics.push(pb::Metric {
333 name: name.to_string(),
334 description: description.to_string(),
335 data: Some(pb::metric::Data::Histogram(pb::OtlpHistogram {
336 data_points: vec![pb::HistogramDataPoint {
337 time_unix_nano,
338 count,
339 sum: Some(sum as f64),
340 bucket_counts,
341 explicit_bounds,
342 ..Default::default()
343 }],
344 aggregation_temporality: pb::AggregationTemporality::Cumulative as i32,
345 })),
346 ..Default::default()
347 });
348 }
349}
350
351impl OtlpExport for SampledTimer {
352 fn export_otlp(
353 &self,
354 metrics: &mut Vec<pb::Metric>,
355 name: &str,
356 description: &str,
357 time_unix_nano: u64,
358 ) {
359 let calls_name = format!("{name}.calls");
360 let samples_name = format!("{name}.samples");
361 let calls_description = format!("{description} total calls");
362 let samples_description = format!("{description} sampled latency in nanoseconds");
363 self.calls_metric()
364 .export_otlp(metrics, &calls_name, &calls_description, time_unix_nano);
365 self.histogram()
366 .export_otlp(metrics, &samples_name, &samples_description, time_unix_nano);
367 }
368}
369
370fn exp_histogram_data_point(
372 snap: &ExpBucketsSnapshot,
373 attributes: Vec<pb::KeyValue>,
374 time_unix_nano: u64,
375) -> pb::ExponentialHistogramDataPoint {
376 let mut first_nonzero: Option<usize> = None;
378 let mut last_nonzero: Option<usize> = None;
379 for (i, &c) in snap.positive.iter().enumerate() {
380 if c > 0 {
381 if first_nonzero.is_none() {
382 first_nonzero = Some(i);
383 }
384 last_nonzero = Some(i);
385 }
386 }
387
388 let positive = match (first_nonzero, last_nonzero) {
389 (Some(first), Some(last)) => {
390 let bucket_counts: Vec<u64> = snap.positive[first..=last].to_vec();
391 Some(pb::exponential_histogram_data_point::Buckets {
392 offset: first as i32,
393 bucket_counts,
394 })
395 }
396 _ => None,
397 };
398
399 pb::ExponentialHistogramDataPoint {
400 attributes,
401 time_unix_nano,
402 count: snap.count,
403 sum: Some(snap.sum as f64),
404 scale: 0, zero_count: snap.zero_count,
406 positive,
407 negative: None, min: snap.min().map(|v| v as f64),
409 max: snap.max().map(|v| v as f64),
410 ..Default::default()
411 }
412}
413
414impl OtlpExport for Distribution {
415 fn export_otlp(
417 &self,
418 metrics: &mut Vec<pb::Metric>,
419 name: &str,
420 description: &str,
421 time_unix_nano: u64,
422 ) {
423 let snap = self.buckets_snapshot();
424 let dp = exp_histogram_data_point(&snap, Vec::new(), time_unix_nano);
425
426 metrics.push(pb::Metric {
427 name: name.to_string(),
428 description: description.to_string(),
429 data: Some(pb::metric::Data::ExponentialHistogram(
430 pb::OtlpExpHistogram {
431 data_points: vec![dp],
432 aggregation_temporality: pb::AggregationTemporality::Cumulative as i32,
433 },
434 )),
435 ..Default::default()
436 });
437 }
438}
439
440impl<L: LabelEnum> OtlpExport for LabeledCounter<L> {
445 fn export_otlp(
446 &self,
447 metrics: &mut Vec<pb::Metric>,
448 name: &str,
449 description: &str,
450 time_unix_nano: u64,
451 ) {
452 let data_points: Vec<_> = self
453 .iter()
454 .map(|(label, count)| {
455 int_data_point(
456 count as i64,
457 vec![label_to_attribute(label)],
458 time_unix_nano,
459 )
460 })
461 .collect();
462
463 metrics.push(pb::Metric {
464 name: name.to_string(),
465 description: description.to_string(),
466 data: Some(pb::metric::Data::Sum(pb::Sum {
467 data_points,
468 aggregation_temporality: pb::AggregationTemporality::Cumulative as i32,
469 is_monotonic: false,
470 })),
471 ..Default::default()
472 });
473 }
474}
475
476impl<L: LabelEnum> OtlpExport for LabeledGauge<L> {
477 fn export_otlp(
478 &self,
479 metrics: &mut Vec<pb::Metric>,
480 name: &str,
481 description: &str,
482 time_unix_nano: u64,
483 ) {
484 let data_points: Vec<_> = self
485 .iter()
486 .map(|(label, value)| {
487 int_data_point(value, vec![label_to_attribute(label)], time_unix_nano)
488 })
489 .collect();
490
491 metrics.push(pb::Metric {
492 name: name.to_string(),
493 description: description.to_string(),
494 data: Some(pb::metric::Data::Gauge(pb::OtlpGauge { data_points })),
495 ..Default::default()
496 });
497 }
498}
499
500impl<L: LabelEnum> OtlpExport for LabeledHistogram<L> {
501 fn export_otlp(
502 &self,
503 metrics: &mut Vec<pb::Metric>,
504 name: &str,
505 description: &str,
506 time_unix_nano: u64,
507 ) {
508 let mut data_points = Vec::new();
509
510 for (label, buckets, sum, count) in self.iter() {
511 let attrs = vec![label_to_attribute(label)];
512 let (bucket_counts, explicit_bounds) = cumulative_to_otlp_buckets(&buckets);
513
514 data_points.push(pb::HistogramDataPoint {
515 attributes: attrs,
516 time_unix_nano,
517 count,
518 sum: Some(sum as f64),
519 bucket_counts,
520 explicit_bounds,
521 ..Default::default()
522 });
523 }
524
525 metrics.push(pb::Metric {
526 name: name.to_string(),
527 description: description.to_string(),
528 data: Some(pb::metric::Data::Histogram(pb::OtlpHistogram {
529 data_points,
530 aggregation_temporality: pb::AggregationTemporality::Cumulative as i32,
531 })),
532 ..Default::default()
533 });
534 }
535}
536
537impl<L: LabelEnum> OtlpExport for LabeledSampledTimer<L> {
538 fn export_otlp(
539 &self,
540 metrics: &mut Vec<pb::Metric>,
541 name: &str,
542 description: &str,
543 time_unix_nano: u64,
544 ) {
545 let calls_name = format!("{name}.calls");
546 let samples_name = format!("{name}.samples");
547 let calls_description = format!("{description} total calls");
548 let samples_description = format!("{description} sampled latency in nanoseconds");
549
550 let mut call_points = Vec::new();
551 let mut sample_points = Vec::new();
552
553 for (label, calls, histogram) in self.iter() {
554 call_points.push(int_data_point(
555 calls.sum() as i64,
556 vec![label_to_attribute(label)],
557 time_unix_nano,
558 ));
559
560 let (bucket_counts, explicit_bounds) =
561 cumulative_to_otlp_buckets(&histogram.buckets_cumulative());
562 sample_points.push(pb::HistogramDataPoint {
563 attributes: vec![label_to_attribute(label)],
564 time_unix_nano,
565 count: histogram.count(),
566 sum: Some(histogram.sum() as f64),
567 bucket_counts,
568 explicit_bounds,
569 ..Default::default()
570 });
571 }
572
573 metrics.push(pb::Metric {
574 name: calls_name,
575 description: calls_description,
576 data: Some(pb::metric::Data::Sum(pb::Sum {
577 data_points: call_points,
578 aggregation_temporality: pb::AggregationTemporality::Cumulative as i32,
579 is_monotonic: false,
580 })),
581 ..Default::default()
582 });
583
584 metrics.push(pb::Metric {
585 name: samples_name,
586 description: samples_description,
587 data: Some(pb::metric::Data::Histogram(pb::OtlpHistogram {
588 data_points: sample_points,
589 aggregation_temporality: pb::AggregationTemporality::Cumulative as i32,
590 })),
591 ..Default::default()
592 });
593 }
594}
595
596impl OtlpExport for DynamicCounter {
601 fn export_otlp(
602 &self,
603 metrics: &mut Vec<pb::Metric>,
604 name: &str,
605 description: &str,
606 time_unix_nano: u64,
607 ) {
608 let mut data_points = Vec::new();
609 self.visit_series(|pairs, count| {
610 data_points.push(int_data_point(
611 count as i64,
612 pairs_to_attributes(pairs),
613 time_unix_nano,
614 ));
615 });
616
617 if data_points.is_empty() {
618 return;
619 }
620
621 metrics.push(pb::Metric {
622 name: name.to_string(),
623 description: description.to_string(),
624 data: Some(pb::metric::Data::Sum(pb::Sum {
625 data_points,
626 aggregation_temporality: pb::AggregationTemporality::Cumulative as i32,
627 is_monotonic: false,
628 })),
629 ..Default::default()
630 });
631 }
632}
633
634impl OtlpExport for DynamicGauge {
635 fn export_otlp(
636 &self,
637 metrics: &mut Vec<pb::Metric>,
638 name: &str,
639 description: &str,
640 time_unix_nano: u64,
641 ) {
642 let mut data_points = Vec::new();
643 self.visit_series(|pairs, value| {
644 data_points.push(double_data_point(
645 value,
646 pairs_to_attributes(pairs),
647 time_unix_nano,
648 ));
649 });
650
651 if data_points.is_empty() {
652 return;
653 }
654
655 metrics.push(pb::Metric {
656 name: name.to_string(),
657 description: description.to_string(),
658 data: Some(pb::metric::Data::Gauge(pb::OtlpGauge { data_points })),
659 ..Default::default()
660 });
661 }
662}
663
664impl OtlpExport for DynamicGaugeI64 {
665 fn export_otlp(
666 &self,
667 metrics: &mut Vec<pb::Metric>,
668 name: &str,
669 description: &str,
670 time_unix_nano: u64,
671 ) {
672 let mut data_points = Vec::new();
673 self.visit_series(|pairs, value| {
674 data_points.push(int_data_point(
675 value,
676 pairs_to_attributes(pairs),
677 time_unix_nano,
678 ));
679 });
680
681 if data_points.is_empty() {
682 return;
683 }
684
685 metrics.push(pb::Metric {
686 name: name.to_string(),
687 description: description.to_string(),
688 data: Some(pb::metric::Data::Gauge(pb::OtlpGauge { data_points })),
689 ..Default::default()
690 });
691 }
692}
693
694impl OtlpExport for DynamicHistogram {
695 fn export_otlp(
696 &self,
697 metrics: &mut Vec<pb::Metric>,
698 name: &str,
699 description: &str,
700 time_unix_nano: u64,
701 ) {
702 let mut data_points = Vec::new();
703
704 self.visit_series(|pairs, series| {
705 let (bucket_counts, explicit_bounds) =
706 cumulative_to_otlp_buckets_iter(series.buckets_cumulative_iter());
707
708 data_points.push(pb::HistogramDataPoint {
709 attributes: pairs_to_attributes(pairs),
710 time_unix_nano,
711 count: series.count(),
712 sum: Some(series.sum() as f64),
713 bucket_counts,
714 explicit_bounds,
715 ..Default::default()
716 });
717 });
718
719 if data_points.is_empty() {
720 return;
721 }
722
723 metrics.push(pb::Metric {
724 name: name.to_string(),
725 description: description.to_string(),
726 data: Some(pb::metric::Data::Histogram(pb::OtlpHistogram {
727 data_points,
728 aggregation_temporality: pb::AggregationTemporality::Cumulative as i32,
729 })),
730 ..Default::default()
731 });
732 }
733}
734
735impl OtlpExport for DynamicDistribution {
736 fn export_otlp(
738 &self,
739 metrics: &mut Vec<pb::Metric>,
740 name: &str,
741 description: &str,
742 time_unix_nano: u64,
743 ) {
744 let mut data_points = Vec::new();
745
746 self.visit_series(|pairs, _count, _sum, snap| {
747 let attrs = pairs_to_attributes(pairs);
748 data_points.push(exp_histogram_data_point(&snap, attrs, time_unix_nano));
749 });
750
751 if data_points.is_empty() {
752 return;
753 }
754
755 metrics.push(pb::Metric {
756 name: name.to_string(),
757 description: description.to_string(),
758 data: Some(pb::metric::Data::ExponentialHistogram(
759 pb::OtlpExpHistogram {
760 data_points,
761 aggregation_temporality: pb::AggregationTemporality::Cumulative as i32,
762 },
763 )),
764 ..Default::default()
765 });
766 }
767}
768
769use crate::span::{CompletedSpan, SpanKind, SpanStatus, SpanValue};
774
775impl CompletedSpan {
776 pub fn to_otlp(&self) -> pb::OtlpSpan {
778 let kind = match self.kind {
779 SpanKind::Internal => pb::OtlpSpanKind::Internal,
780 SpanKind::Server => pb::OtlpSpanKind::Server,
781 SpanKind::Client => pb::OtlpSpanKind::Client,
782 SpanKind::Producer => pb::OtlpSpanKind::Producer,
783 SpanKind::Consumer => pb::OtlpSpanKind::Consumer,
784 };
785
786 let status = match &self.status {
787 SpanStatus::Unset => None,
788 SpanStatus::Ok => Some(pb::OtlpStatus {
789 code: pb::OtlpStatusCode::Ok as i32,
790 message: String::new(),
791 }),
792 SpanStatus::Error { message } => Some(pb::OtlpStatus {
793 code: pb::OtlpStatusCode::Error as i32,
794 message: message.to_string(),
795 }),
796 };
797
798 let attributes: Vec<pb::KeyValue> = self
799 .attributes
800 .iter()
801 .map(|attr| {
802 let value = match &attr.value {
803 SpanValue::String(s) => pb::any_value::Value::StringValue(s.to_string()),
804 SpanValue::I64(v) => pb::any_value::Value::IntValue(*v),
805 SpanValue::F64(v) => pb::any_value::Value::DoubleValue(*v),
806 SpanValue::Bool(v) => pb::any_value::Value::BoolValue(*v),
807 SpanValue::Uuid(u) => pb::any_value::Value::StringValue(u.to_string()),
808 };
809 pb::KeyValue {
810 key: attr.key.to_string(),
811 value: Some(pb::AnyValue { value: Some(value) }),
812 }
813 })
814 .collect();
815
816 let events: Vec<pb::OtlpEvent> = self
817 .events
818 .iter()
819 .map(|evt| {
820 let attrs: Vec<pb::KeyValue> = evt
821 .attributes
822 .iter()
823 .map(|a| {
824 let v = match &a.value {
825 SpanValue::String(s) => {
826 pb::any_value::Value::StringValue(s.to_string())
827 }
828 SpanValue::I64(v) => pb::any_value::Value::IntValue(*v),
829 SpanValue::F64(v) => pb::any_value::Value::DoubleValue(*v),
830 SpanValue::Bool(v) => pb::any_value::Value::BoolValue(*v),
831 SpanValue::Uuid(u) => pb::any_value::Value::StringValue(u.to_string()),
832 };
833 pb::KeyValue {
834 key: a.key.to_string(),
835 value: Some(pb::AnyValue { value: Some(v) }),
836 }
837 })
838 .collect();
839 pb::OtlpEvent {
840 time_unix_nano: evt.time_ns,
841 name: evt.name.to_string(),
842 attributes: attrs,
843 dropped_attributes_count: 0,
844 }
845 })
846 .collect();
847
848 pb::OtlpSpan {
849 trace_id: self.trace_id.as_bytes().to_vec(),
850 span_id: self.span_id.as_bytes().to_vec(),
851 parent_span_id: if self.parent_span_id.is_invalid() {
852 Vec::new()
853 } else {
854 self.parent_span_id.as_bytes().to_vec()
855 },
856 name: self.name.to_string(),
857 kind: kind as i32,
858 start_time_unix_nano: self.start_time_ns,
859 end_time_unix_nano: self.end_time_ns,
860 attributes,
861 events,
862 status,
863 ..Default::default()
864 }
865 }
866}
867
868pub fn build_trace_export_request(
872 resource: &pb::Resource,
873 scope_name: &str,
874 spans: Vec<pb::OtlpSpan>,
875) -> pb::ExportTraceServiceRequest {
876 pb::ExportTraceServiceRequest {
877 resource_spans: vec![pb::ResourceSpans {
878 resource: Some(resource.clone()),
879 scope_spans: vec![pb::ScopeSpans {
880 scope: Some(pb::InstrumentationScope {
881 name: scope_name.to_string(),
882 ..Default::default()
883 }),
884 spans,
885 ..Default::default()
886 }],
887 ..Default::default()
888 }],
889 }
890}
891
892#[cfg(test)]
893mod tests {
894 use super::*;
895
896 fn test_timestamp() -> u64 {
897 1_000_000_000 }
899
900 #[test]
901 fn test_counter_otlp() {
902 let counter = Counter::new(4);
903 counter.add(42);
904
905 let mut metrics = Vec::new();
906 counter.export_otlp(
907 &mut metrics,
908 "test_counter",
909 "A test counter",
910 test_timestamp(),
911 );
912
913 assert_eq!(metrics.len(), 1);
914 assert_eq!(metrics[0].name, "test_counter");
915 assert_eq!(metrics[0].description, "A test counter");
916
917 let data = metrics[0].data.as_ref().expect("missing data");
918 match data {
919 pb::metric::Data::Sum(sum) => {
920 assert!(!sum.is_monotonic);
922 assert_eq!(
923 sum.aggregation_temporality,
924 pb::AggregationTemporality::Cumulative as i32
925 );
926 assert_eq!(sum.data_points.len(), 1);
927 assert_eq!(
928 sum.data_points[0].value,
929 Some(pb::number_data_point::Value::AsInt(42))
930 );
931 assert_eq!(sum.data_points[0].time_unix_nano, test_timestamp());
932 }
933 _ => panic!("expected Sum, got {:?}", data),
934 }
935 }
936
937 #[test]
938 fn test_gauge_otlp() {
939 let gauge = Gauge::new();
940 gauge.set(-10);
941
942 let mut metrics = Vec::new();
943 gauge.export_otlp(&mut metrics, "test_gauge", "A test gauge", test_timestamp());
944
945 assert_eq!(metrics.len(), 1);
946 match metrics[0].data.as_ref().expect("missing data") {
947 pb::metric::Data::Gauge(g) => {
948 assert_eq!(g.data_points.len(), 1);
949 assert_eq!(
950 g.data_points[0].value,
951 Some(pb::number_data_point::Value::AsInt(-10))
952 );
953 }
954 other => panic!("expected Gauge, got {:?}", other),
955 }
956 }
957
958 #[test]
959 fn test_gauge_f64_otlp() {
960 let gauge = GaugeF64::new();
961 gauge.set(3.125);
962
963 let mut metrics = Vec::new();
964 gauge.export_otlp(&mut metrics, "test_gauge_f64", "", test_timestamp());
965
966 match metrics[0].data.as_ref().expect("missing data") {
967 pb::metric::Data::Gauge(g) => {
968 assert_eq!(g.data_points.len(), 1);
969 match g.data_points[0].value {
970 Some(pb::number_data_point::Value::AsDouble(v)) => {
971 assert!((v - 3.125).abs() < 1e-10);
972 }
973 ref other => panic!("expected AsDouble, got {:?}", other),
974 }
975 }
976 other => panic!("expected Gauge, got {:?}", other),
977 }
978 }
979
980 #[test]
981 fn test_histogram_otlp() {
982 let h = Histogram::new(&[10, 100], 4);
983 h.record(5);
984 h.record(50);
985 h.record(500);
986
987 let mut metrics = Vec::new();
988 h.export_otlp(
989 &mut metrics,
990 "test_hist",
991 "A test histogram",
992 test_timestamp(),
993 );
994
995 assert_eq!(metrics.len(), 1);
996 match metrics[0].data.as_ref().expect("missing data") {
997 pb::metric::Data::Histogram(hist) => {
998 assert_eq!(
999 hist.aggregation_temporality,
1000 pb::AggregationTemporality::Cumulative as i32
1001 );
1002 assert_eq!(hist.data_points.len(), 1);
1003
1004 let dp = &hist.data_points[0];
1005 assert_eq!(dp.count, 3);
1006 assert_eq!(dp.sum, Some(555.0));
1007 assert_eq!(dp.explicit_bounds, vec![10.0, 100.0]);
1008 assert_eq!(dp.bucket_counts, vec![1, 1, 1]);
1009 assert_eq!(dp.time_unix_nano, test_timestamp());
1010 }
1011 other => panic!("expected Histogram, got {:?}", other),
1012 }
1013 }
1014
1015 #[test]
1016 fn test_distribution_otlp() {
1017 let dist = Distribution::new(4);
1018 dist.record(100);
1019 dist.record(200);
1020 dist.record(300);
1021
1022 let mut metrics = Vec::new();
1023 dist.export_otlp(
1024 &mut metrics,
1025 "test_dist",
1026 "A distribution",
1027 test_timestamp(),
1028 );
1029
1030 assert_eq!(metrics.len(), 1);
1031 assert_eq!(metrics[0].name, "test_dist");
1032
1033 match metrics[0].data.as_ref().expect("missing data") {
1034 pb::metric::Data::ExponentialHistogram(hist) => {
1035 assert_eq!(
1036 hist.aggregation_temporality,
1037 pb::AggregationTemporality::Cumulative as i32
1038 );
1039 assert_eq!(hist.data_points.len(), 1);
1040
1041 let dp = &hist.data_points[0];
1042 assert_eq!(dp.count, 3);
1043 assert_eq!(dp.sum, Some(600.0));
1044 assert_eq!(dp.scale, 0);
1045 assert_eq!(dp.zero_count, 0);
1046 assert_eq!(dp.time_unix_nano, test_timestamp());
1047 assert!(dp.positive.is_some());
1049 let positive = dp.positive.as_ref().expect("positive buckets");
1050 assert!(!positive.bucket_counts.is_empty());
1052 }
1053 other => panic!("expected ExponentialHistogram, got {:?}", other),
1054 }
1055 }
1056
1057 #[test]
1058 fn test_dynamic_counter_otlp() {
1059 let counter = DynamicCounter::new(4);
1060 counter.add(&[("env", "prod"), ("region", "us")], 10);
1061 counter.add(&[("env", "staging"), ("region", "eu")], 5);
1062
1063 let mut metrics = Vec::new();
1064 counter.export_otlp(&mut metrics, "requests", "Request count", test_timestamp());
1065
1066 assert_eq!(metrics.len(), 1);
1067 match metrics[0].data.as_ref().expect("missing data") {
1068 pb::metric::Data::Sum(sum) => {
1069 assert!(!sum.is_monotonic);
1070 assert_eq!(sum.data_points.len(), 2);
1071 for dp in &sum.data_points {
1072 assert_eq!(dp.attributes.len(), 2);
1073 }
1074 }
1075 other => panic!("expected Sum, got {:?}", other),
1076 }
1077 }
1078
1079 #[test]
1080 fn test_build_export_request() {
1081 let resource = build_resource("test-service", &[("version", "1.0")]);
1082 let counter = Counter::new(4);
1083 counter.add(1);
1084
1085 let mut metrics = Vec::new();
1086 counter.export_otlp(&mut metrics, "my_counter", "", test_timestamp());
1087
1088 let request = build_export_request(&resource, "fast-telemetry", metrics);
1089
1090 assert_eq!(request.resource_metrics.len(), 1);
1091 let rm = &request.resource_metrics[0];
1092 let res = rm.resource.as_ref().expect("missing resource");
1093 assert_eq!(res.attributes.len(), 2); assert_eq!(res.attributes[0].key, "service.name");
1095
1096 assert_eq!(rm.scope_metrics.len(), 1);
1097 let sm = &rm.scope_metrics[0];
1098 let scope = sm.scope.as_ref().expect("missing scope");
1099 assert_eq!(scope.name, "fast-telemetry");
1100 assert_eq!(sm.metrics.len(), 1);
1101 }
1102
1103 #[test]
1104 fn test_make_kv() {
1105 let kv = make_kv("foo", "bar");
1106 assert_eq!(kv.key, "foo");
1107 match kv
1108 .value
1109 .expect("missing value")
1110 .value
1111 .expect("missing inner")
1112 {
1113 pb::any_value::Value::StringValue(s) => assert_eq!(s, "bar"),
1114 other => panic!("expected StringValue, got {:?}", other),
1115 }
1116 }
1117
1118 #[derive(Copy, Clone, Debug)]
1121 enum TestLabel {
1122 A,
1123 B,
1124 C,
1125 }
1126
1127 impl crate::LabelEnum for TestLabel {
1128 const CARDINALITY: usize = 3;
1129 const LABEL_NAME: &'static str = "test";
1130
1131 fn as_index(self) -> usize {
1132 self as usize
1133 }
1134 fn from_index(index: usize) -> Self {
1135 match index {
1136 0 => Self::A,
1137 1 => Self::B,
1138 _ => Self::C,
1139 }
1140 }
1141 fn variant_name(self) -> &'static str {
1142 match self {
1143 Self::A => "a",
1144 Self::B => "b",
1145 Self::C => "c",
1146 }
1147 }
1148 }
1149
1150 #[test]
1151 fn test_labeled_counter_otlp() {
1152 let counter = LabeledCounter::<TestLabel>::new(4);
1153 counter.add(TestLabel::A, 10);
1154 counter.add(TestLabel::B, 20);
1155
1156 let mut metrics = Vec::new();
1157 counter.export_otlp(
1158 &mut metrics,
1159 "labeled_counter",
1160 "By label",
1161 test_timestamp(),
1162 );
1163
1164 assert_eq!(metrics.len(), 1);
1165 match metrics[0].data.as_ref().expect("missing data") {
1166 pb::metric::Data::Sum(sum) => {
1167 assert!(!sum.is_monotonic);
1168 assert_eq!(sum.data_points.len(), 3); let dp_a = sum.data_points.iter().find(|dp| {
1171 dp.attributes.iter().any(|kv| kv.key == "test" && matches!(&kv.value, Some(v) if matches!(&v.value, Some(pb::any_value::Value::StringValue(s)) if s == "a")))
1172 }).expect("missing data point for label A");
1173 assert_eq!(dp_a.value, Some(pb::number_data_point::Value::AsInt(10)));
1174 }
1175 other => panic!("expected Sum, got {:?}", other),
1176 }
1177 }
1178
1179 #[test]
1180 fn test_labeled_gauge_otlp() {
1181 let gauge = LabeledGauge::<TestLabel>::new();
1182 gauge.set(TestLabel::A, 42);
1183 gauge.set(TestLabel::C, -5);
1184
1185 let mut metrics = Vec::new();
1186 gauge.export_otlp(&mut metrics, "labeled_gauge", "By label", test_timestamp());
1187
1188 assert_eq!(metrics.len(), 1);
1189 match metrics[0].data.as_ref().expect("missing data") {
1190 pb::metric::Data::Gauge(g) => {
1191 assert_eq!(g.data_points.len(), 3);
1192 }
1193 other => panic!("expected Gauge, got {:?}", other),
1194 }
1195 }
1196
1197 #[test]
1198 fn test_labeled_histogram_otlp() {
1199 let h = LabeledHistogram::<TestLabel>::new(&[10, 100], 4);
1200 h.record(TestLabel::A, 5);
1201 h.record(TestLabel::A, 50);
1202 h.record(TestLabel::B, 500);
1203
1204 let mut metrics = Vec::new();
1205 h.export_otlp(&mut metrics, "labeled_hist", "By label", test_timestamp());
1206
1207 assert_eq!(metrics.len(), 1);
1208 match metrics[0].data.as_ref().expect("missing data") {
1209 pb::metric::Data::Histogram(hist) => {
1210 assert_eq!(
1211 hist.aggregation_temporality,
1212 pb::AggregationTemporality::Cumulative as i32
1213 );
1214 assert_eq!(hist.data_points.len(), 3); for dp in &hist.data_points {
1217 assert_eq!(dp.attributes.len(), 1);
1218 assert_eq!(dp.attributes[0].key, "test");
1219 assert_eq!(dp.time_unix_nano, test_timestamp());
1220 }
1221 }
1222 other => panic!("expected Histogram, got {:?}", other),
1223 }
1224 }
1225
1226 #[test]
1229 fn test_dynamic_gauge_otlp() {
1230 let gauge = DynamicGauge::new(4);
1231 gauge.set(&[("host", "node1")], 3.125);
1232 gauge.set(&[("host", "node2")], 2.72);
1233
1234 let mut metrics = Vec::new();
1235 gauge.export_otlp(
1236 &mut metrics,
1237 "cpu_usage",
1238 "CPU percentage",
1239 test_timestamp(),
1240 );
1241
1242 assert_eq!(metrics.len(), 1);
1243 match metrics[0].data.as_ref().expect("missing data") {
1244 pb::metric::Data::Gauge(g) => {
1245 assert_eq!(g.data_points.len(), 2);
1246 for dp in &g.data_points {
1247 assert_eq!(dp.attributes.len(), 1);
1248 assert!(matches!(
1249 dp.value,
1250 Some(pb::number_data_point::Value::AsDouble(_))
1251 ));
1252 }
1253 }
1254 other => panic!("expected Gauge, got {:?}", other),
1255 }
1256 }
1257
1258 #[test]
1259 fn test_dynamic_gauge_i64_otlp() {
1260 let gauge = DynamicGaugeI64::new(4);
1261 gauge.set(&[("region", "us")], 100);
1262 gauge.set(&[("region", "eu")], 200);
1263
1264 let mut metrics = Vec::new();
1265 gauge.export_otlp(
1266 &mut metrics,
1267 "connections",
1268 "Active connections",
1269 test_timestamp(),
1270 );
1271
1272 assert_eq!(metrics.len(), 1);
1273 match metrics[0].data.as_ref().expect("missing data") {
1274 pb::metric::Data::Gauge(g) => {
1275 assert_eq!(g.data_points.len(), 2);
1276 for dp in &g.data_points {
1277 assert_eq!(dp.attributes.len(), 1);
1278 assert!(matches!(
1279 dp.value,
1280 Some(pb::number_data_point::Value::AsInt(_))
1281 ));
1282 }
1283 }
1284 other => panic!("expected Gauge, got {:?}", other),
1285 }
1286 }
1287
1288 #[test]
1289 fn test_dynamic_histogram_otlp() {
1290 let h = DynamicHistogram::new(&[10, 100, 1000], 4);
1291 h.record(&[("endpoint", "/api")], 5);
1292 h.record(&[("endpoint", "/api")], 50);
1293 h.record(&[("endpoint", "/health")], 500);
1294
1295 let mut metrics = Vec::new();
1296 h.export_otlp(&mut metrics, "latency", "Request latency", test_timestamp());
1297
1298 assert_eq!(metrics.len(), 1);
1299 match metrics[0].data.as_ref().expect("missing data") {
1300 pb::metric::Data::Histogram(hist) => {
1301 assert_eq!(
1302 hist.aggregation_temporality,
1303 pb::AggregationTemporality::Cumulative as i32
1304 );
1305 assert_eq!(hist.data_points.len(), 2); for dp in &hist.data_points {
1307 assert_eq!(dp.attributes.len(), 1);
1308 assert_eq!(dp.attributes[0].key, "endpoint");
1309 assert_eq!(dp.time_unix_nano, test_timestamp());
1310 assert_eq!(dp.explicit_bounds, vec![10.0, 100.0, 1000.0]);
1312 }
1313 }
1314 other => panic!("expected Histogram, got {:?}", other),
1315 }
1316 }
1317
1318 #[test]
1319 fn test_dynamic_distribution_otlp() {
1320 let dist = DynamicDistribution::new(4);
1321 dist.record(&[("method", "GET")], 100);
1322 dist.record(&[("method", "GET")], 200);
1323 dist.record(&[("method", "POST")], 300);
1324
1325 let mut metrics = Vec::new();
1326 dist.export_otlp(
1327 &mut metrics,
1328 "response_size",
1329 "Size in bytes",
1330 test_timestamp(),
1331 );
1332
1333 assert_eq!(metrics.len(), 1);
1334 assert_eq!(metrics[0].name, "response_size");
1335
1336 match metrics[0].data.as_ref().expect("missing data") {
1337 pb::metric::Data::ExponentialHistogram(hist) => {
1338 assert_eq!(
1339 hist.aggregation_temporality,
1340 pb::AggregationTemporality::Cumulative as i32
1341 );
1342 assert_eq!(hist.data_points.len(), 2); for dp in &hist.data_points {
1344 assert_eq!(dp.attributes.len(), 1);
1345 assert_eq!(dp.attributes[0].key, "method");
1346 assert_eq!(dp.scale, 0);
1347 assert!(dp.positive.is_some());
1348 }
1349 }
1350 other => panic!("expected ExponentialHistogram, got {:?}", other),
1351 }
1352 }
1353
1354 #[test]
1355 fn test_empty_dynamic_metrics_produce_nothing() {
1356 let counter = DynamicCounter::new(4);
1357 let gauge = DynamicGauge::new(4);
1358 let gauge_i64 = DynamicGaugeI64::new(4);
1359 let hist = DynamicHistogram::new(&[10], 4);
1360 let dist = DynamicDistribution::new(4);
1361
1362 let mut metrics = Vec::new();
1363 let ts = test_timestamp();
1364 counter.export_otlp(&mut metrics, "c", "", ts);
1365 gauge.export_otlp(&mut metrics, "g", "", ts);
1366 gauge_i64.export_otlp(&mut metrics, "gi", "", ts);
1367 hist.export_otlp(&mut metrics, "h", "", ts);
1368 dist.export_otlp(&mut metrics, "d", "", ts);
1369
1370 assert!(
1371 metrics.is_empty(),
1372 "empty dynamic metrics should produce no output"
1373 );
1374 }
1375
1376 #[test]
1377 fn test_cumulative_to_otlp_buckets_helper() {
1378 let cumulative = vec![(10, 1), (100, 3), (u64::MAX, 5)];
1381 let (counts, bounds) = cumulative_to_otlp_buckets(&cumulative);
1382 assert_eq!(counts, vec![1, 2, 2]);
1383 assert_eq!(bounds, vec![10.0, 100.0]);
1384 }
1385
1386 #[test]
1389 fn test_completed_span_to_otlp() {
1390 use crate::span::{SpanAttribute, SpanEvent, SpanKind, SpanStatus};
1391 use crate::span::{SpanId, TraceId};
1392
1393 let completed = CompletedSpan {
1394 trace_id: TraceId::from_hex("4bf92f3577b34da6a3ce929d0e0e4736").unwrap(),
1395 span_id: SpanId::from_hex("00f067aa0ba902b7").unwrap(),
1396 parent_span_id: SpanId::from_hex("1234567890abcdef").unwrap(),
1397 name: "test_operation".into(),
1398 kind: SpanKind::Server,
1399 start_time_ns: 1_000_000_000,
1400 end_time_ns: 2_000_000_000,
1401 status: SpanStatus::Ok,
1402 attributes: vec![
1403 SpanAttribute::new("http.method", "GET"),
1404 SpanAttribute::new("http.status_code", 200i64),
1405 ],
1406 events: vec![SpanEvent {
1407 name: "processing".into(),
1408 time_ns: 1_500_000_000,
1409 attributes: vec![SpanAttribute::new("step", "validate")],
1410 }],
1411 };
1412
1413 let otlp = completed.to_otlp();
1414
1415 assert_eq!(
1416 otlp.trace_id,
1417 &[
1418 0x4b, 0xf9, 0x2f, 0x35, 0x77, 0xb3, 0x4d, 0xa6, 0xa3, 0xce, 0x92, 0x9d, 0x0e, 0x0e,
1419 0x47, 0x36
1420 ]
1421 );
1422 assert_eq!(
1423 otlp.span_id,
1424 &[0x00, 0xf0, 0x67, 0xaa, 0x0b, 0xa9, 0x02, 0xb7]
1425 );
1426 assert_eq!(
1427 otlp.parent_span_id,
1428 &[0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef]
1429 );
1430 assert_eq!(otlp.name, "test_operation");
1431 assert_eq!(otlp.kind, pb::OtlpSpanKind::Server as i32);
1432 assert_eq!(otlp.start_time_unix_nano, 1_000_000_000);
1433 assert_eq!(otlp.end_time_unix_nano, 2_000_000_000);
1434
1435 let status = otlp.status.unwrap();
1437 assert_eq!(status.code, pb::OtlpStatusCode::Ok as i32);
1438
1439 assert_eq!(otlp.attributes.len(), 2);
1441 assert_eq!(otlp.attributes[0].key, "http.method");
1442 assert_eq!(otlp.attributes[1].key, "http.status_code");
1443
1444 assert_eq!(otlp.events.len(), 1);
1446 assert_eq!(otlp.events[0].name, "processing");
1447 assert_eq!(otlp.events[0].time_unix_nano, 1_500_000_000);
1448 assert_eq!(otlp.events[0].attributes.len(), 1);
1449 }
1450
1451 #[test]
1452 fn test_completed_span_root_has_empty_parent() {
1453 use crate::span::{SpanId, TraceId};
1454
1455 let completed = CompletedSpan {
1456 trace_id: TraceId::random(),
1457 span_id: SpanId::random(),
1458 parent_span_id: SpanId::INVALID,
1459 name: "root".into(),
1460 kind: SpanKind::Server,
1461 start_time_ns: 1_000_000_000,
1462 end_time_ns: 2_000_000_000,
1463 status: SpanStatus::Unset,
1464 attributes: Vec::new(),
1465 events: Vec::new(),
1466 };
1467
1468 let otlp = completed.to_otlp();
1469 assert!(
1470 otlp.parent_span_id.is_empty(),
1471 "root span should have empty parent_span_id"
1472 );
1473 assert!(otlp.status.is_none(), "Unset status should map to None");
1474 }
1475
1476 #[test]
1477 fn test_completed_span_error_status() {
1478 use crate::span::{SpanId, TraceId};
1479
1480 let completed = CompletedSpan {
1481 trace_id: TraceId::random(),
1482 span_id: SpanId::random(),
1483 parent_span_id: SpanId::INVALID,
1484 name: "failing_op".into(),
1485 kind: SpanKind::Internal,
1486 start_time_ns: 1_000_000_000,
1487 end_time_ns: 2_000_000_000,
1488 status: SpanStatus::Error {
1489 message: "connection refused".into(),
1490 },
1491 attributes: Vec::new(),
1492 events: Vec::new(),
1493 };
1494
1495 let otlp = completed.to_otlp();
1496 let status = otlp.status.unwrap();
1497 assert_eq!(status.code, pb::OtlpStatusCode::Error as i32);
1498 assert_eq!(status.message, "connection refused");
1499 }
1500
1501 #[test]
1502 fn test_build_trace_export_request() {
1503 use crate::span::{SpanId, TraceId};
1504
1505 let resource = build_resource("test-service", &[("version", "1.0")]);
1506 let completed = CompletedSpan {
1507 trace_id: TraceId::random(),
1508 span_id: SpanId::random(),
1509 parent_span_id: SpanId::INVALID,
1510 name: "test".into(),
1511 kind: SpanKind::Server,
1512 start_time_ns: 1_000_000_000,
1513 end_time_ns: 2_000_000_000,
1514 status: SpanStatus::Ok,
1515 attributes: Vec::new(),
1516 events: Vec::new(),
1517 };
1518
1519 let otlp_span = completed.to_otlp();
1520 let request = build_trace_export_request(&resource, "fast-telemetry", vec![otlp_span]);
1521
1522 assert_eq!(request.resource_spans.len(), 1);
1523 let rs = &request.resource_spans[0];
1524 let res = rs.resource.as_ref().unwrap();
1525 assert_eq!(res.attributes.len(), 2); assert_eq!(rs.scope_spans.len(), 1);
1528 let ss = &rs.scope_spans[0];
1529 let scope = ss.scope.as_ref().unwrap();
1530 assert_eq!(scope.name, "fast-telemetry");
1531 assert_eq!(ss.spans.len(), 1);
1532 }
1533}