1use std::collections::VecDeque;
19use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
20use std::time::SystemTime;
21
22use opentelemetry::trace::{Event, SpanId, SpanKind, Status, TraceId};
23use opentelemetry::{Array, KeyValue, Value};
24use parking_lot::Mutex;
25
26use crate::observe::{FilterSpan, TelemetrySink};
27
28pub const DEFAULT_QUEUE_CAPACITY: usize = 2048;
30
31#[derive(Debug, Clone)]
35pub struct SpanRecord {
36 pub trace_id: TraceId,
37 pub span_id: SpanId,
38 pub parent_span_id: Option<SpanId>,
40 pub name: String,
41 pub kind: SpanKind,
42 pub start_time: SystemTime,
43 pub end_time: SystemTime,
44 pub status: Status,
45 pub attributes: Vec<KeyValue>,
46 pub events: Vec<Event>,
47 pub remote_parent: bool,
50 pub sampled: bool,
51}
52
53impl SpanRecord {
54 #[allow(clippy::too_many_arguments)]
58 pub fn request_span(
59 trace: &crate::observe::RequestTrace,
60 method: &str,
61 path: &str,
62 scheme: &str,
63 status_code: u16,
64 start_time: SystemTime,
65 duration: std::time::Duration,
66 ) -> Self {
67 let status = if status_code >= 500 {
68 Status::Error {
69 description: std::borrow::Cow::Borrowed(""),
70 }
71 } else {
72 Status::Unset
73 };
74 Self {
75 trace_id: trace.trace_id(),
76 span_id: trace.request_span_id(),
77 parent_span_id: trace.parent_span_id(),
78 name: method.to_string(),
79 kind: SpanKind::Server,
80 start_time,
81 end_time: start_time + duration,
82 status,
83 attributes: vec![
84 KeyValue::new("http.request.method", method.to_string()),
85 KeyValue::new("url.path", path.to_string()),
86 KeyValue::new("url.scheme", scheme.to_string()),
87 KeyValue::new("http.response.status_code", i64::from(status_code)),
88 ],
89 events: vec![],
90 remote_parent: trace.parent_span_id().is_some(),
91 sampled: trace.is_sampled(),
92 }
93 }
94}
95
96impl From<&FilterSpan> for SpanRecord {
97 fn from(span: &FilterSpan) -> Self {
98 Self {
99 trace_id: span.trace_id,
100 span_id: span.span_id,
101 parent_span_id: Some(span.parent_span_id),
102 name: span.name.clone(),
103 kind: span.kind.clone(),
104 start_time: span.start_time,
105 end_time: span.start_time + span.duration,
106 status: span.status(),
107 attributes: span.attributes.clone(),
108 events: span.events.clone(),
109 remote_parent: false,
110 sampled: span.sampled,
111 }
112 }
113}
114
115pub struct OtlpBuffer {
122 queue: Mutex<VecDeque<SpanRecord>>,
123 capacity: usize,
124 dropped: AtomicU64,
125 drop_logged: AtomicBool,
126}
127
128impl OtlpBuffer {
129 pub fn new(capacity: usize) -> Self {
130 Self {
131 queue: Mutex::new(VecDeque::with_capacity(capacity)),
136 capacity,
137 dropped: AtomicU64::new(0),
138 drop_logged: AtomicBool::new(false),
139 }
140 }
141
142 pub fn push(&self, record: SpanRecord) {
146 {
147 let mut queue = self.queue.lock();
148 if queue.len() < self.capacity {
149 queue.push_back(record);
150 return;
151 }
152 }
153 self.dropped.fetch_add(1, Ordering::Relaxed);
154 if !self.drop_logged.swap(true, Ordering::Relaxed) {
155 tracing::warn!(
156 capacity = self.capacity,
157 "OTLP span queue full; dropping spans (further drops are counted, not logged)"
158 );
159 }
160 }
161
162 pub fn drain(&self, max: usize) -> Vec<SpanRecord> {
165 let mut queue = self.queue.lock();
166 let n = queue.len().min(max);
167 queue.drain(..n).collect()
168 }
169
170 pub fn len(&self) -> usize {
171 self.queue.lock().len()
172 }
173
174 pub fn is_empty(&self) -> bool {
175 self.queue.lock().is_empty()
176 }
177
178 pub fn dropped_spans(&self) -> u64 {
180 self.dropped.load(Ordering::Relaxed)
181 }
182
183 pub fn record_dropped(&self, n: u64) {
186 self.dropped.fetch_add(n, Ordering::Relaxed);
187 }
188}
189
190impl Default for OtlpBuffer {
191 fn default() -> Self {
192 Self::new(DEFAULT_QUEUE_CAPACITY)
193 }
194}
195
196impl TelemetrySink for OtlpBuffer {
197 fn export(&self, span: &FilterSpan) {
198 if !span.sampled {
201 return;
202 }
203 self.push(span.into());
204 }
205}
206
207const WIRE_VARINT: u32 = 0;
216const WIRE_I64: u32 = 1;
217const WIRE_LEN: u32 = 2;
218const WIRE_I32: u32 = 5;
219
220fn put_varint(buf: &mut Vec<u8>, mut v: u64) {
221 loop {
222 let byte = (v & 0x7f) as u8;
223 v >>= 7;
224 if v == 0 {
225 buf.push(byte);
226 return;
227 }
228 buf.push(byte | 0x80);
229 }
230}
231
232fn put_tag(buf: &mut Vec<u8>, field: u32, wire: u32) {
233 put_varint(buf, u64::from((field << 3) | wire));
234}
235
236fn put_len(buf: &mut Vec<u8>, field: u32, bytes: &[u8]) {
239 put_tag(buf, field, WIRE_LEN);
240 put_varint(buf, bytes.len() as u64);
241 buf.extend_from_slice(bytes);
242}
243
244fn put_str(buf: &mut Vec<u8>, field: u32, s: &str) {
245 if !s.is_empty() {
246 put_len(buf, field, s.as_bytes());
247 }
248}
249
250fn put_u64(buf: &mut Vec<u8>, field: u32, v: u64) {
251 if v != 0 {
252 put_tag(buf, field, WIRE_VARINT);
253 put_varint(buf, v);
254 }
255}
256
257fn put_fixed64(buf: &mut Vec<u8>, field: u32, v: u64) {
258 if v != 0 {
259 put_tag(buf, field, WIRE_I64);
260 buf.extend_from_slice(&v.to_le_bytes());
261 }
262}
263
264fn put_fixed32(buf: &mut Vec<u8>, field: u32, v: u32) {
265 if v != 0 {
266 put_tag(buf, field, WIRE_I32);
267 buf.extend_from_slice(&v.to_le_bytes());
268 }
269}
270
271fn unix_nanos(t: SystemTime) -> u64 {
272 t.duration_since(SystemTime::UNIX_EPOCH)
273 .map(|d| d.as_nanos() as u64)
274 .unwrap_or(0)
275}
276
277fn encode_any_value(value: &Value) -> Vec<u8> {
280 let mut buf = Vec::new();
281 match value {
282 Value::String(s) => put_len(&mut buf, 1, s.as_str().as_bytes()),
283 Value::Bool(b) => {
284 put_tag(&mut buf, 2, WIRE_VARINT);
285 put_varint(&mut buf, u64::from(*b));
286 }
287 Value::I64(i) => {
288 put_tag(&mut buf, 3, WIRE_VARINT);
289 put_varint(&mut buf, *i as u64);
290 }
291 Value::F64(f) => {
292 put_tag(&mut buf, 4, WIRE_I64);
293 buf.extend_from_slice(&f.to_bits().to_le_bytes());
294 }
295 Value::Array(array) => {
296 let mut inner = Vec::new();
298 let one = |v: Value| encode_any_value(&v);
299 match array {
300 Array::Bool(items) => {
301 for b in items {
302 put_len(&mut inner, 1, &one(Value::Bool(*b)));
303 }
304 }
305 Array::I64(items) => {
306 for i in items {
307 put_len(&mut inner, 1, &one(Value::I64(*i)));
308 }
309 }
310 Array::F64(items) => {
311 for f in items {
312 put_len(&mut inner, 1, &one(Value::F64(*f)));
313 }
314 }
315 Array::String(items) => {
316 for s in items {
317 put_len(&mut inner, 1, &one(Value::String(s.clone())));
318 }
319 }
320 _ => {}
323 }
324 put_len(&mut buf, 5, &inner);
325 }
326 other => put_len(&mut buf, 1, format!("{other:?}").as_bytes()),
329 }
330 buf
331}
332
333fn encode_key_value(kv: &KeyValue) -> Vec<u8> {
335 let mut buf = Vec::new();
336 put_str(&mut buf, 1, kv.key.as_str());
337 put_len(&mut buf, 2, &encode_any_value(&kv.value));
338 buf
339}
340
341fn encode_event(event: &Event) -> Vec<u8> {
344 let mut buf = Vec::new();
345 put_fixed64(&mut buf, 1, unix_nanos(event.timestamp));
346 put_str(&mut buf, 2, &event.name);
347 for kv in &event.attributes {
348 put_len(&mut buf, 3, &encode_key_value(kv));
349 }
350 buf
351}
352
353fn kind_value(kind: &SpanKind) -> u64 {
356 match kind {
357 SpanKind::Internal => 1,
358 SpanKind::Server => 2,
359 SpanKind::Client => 3,
360 SpanKind::Producer => 4,
361 SpanKind::Consumer => 5,
362 }
363}
364
365fn span_flags(record: &SpanRecord) -> u32 {
369 let sampled = u32::from(record.sampled);
370 let remote = if record.remote_parent { 0x200 } else { 0 };
371 0x100 | remote | sampled
372}
373
374fn encode_status(status: &Status) -> Option<Vec<u8>> {
377 let mut buf = Vec::new();
378 match status {
379 Status::Unset => return None,
380 Status::Ok => put_u64(&mut buf, 3, 1),
381 Status::Error { description } => {
382 put_str(&mut buf, 2, description);
383 put_u64(&mut buf, 3, 2);
384 }
385 }
386 Some(buf)
387}
388
389fn encode_span(record: &SpanRecord) -> Vec<u8> {
393 let mut buf = Vec::new();
394 put_len(&mut buf, 1, &record.trace_id.to_bytes());
395 put_len(&mut buf, 2, &record.span_id.to_bytes());
396 if let Some(parent) = record.parent_span_id {
397 put_len(&mut buf, 4, &parent.to_bytes());
398 }
399 put_str(&mut buf, 5, &record.name);
400 put_u64(&mut buf, 6, kind_value(&record.kind));
401 put_fixed64(&mut buf, 7, unix_nanos(record.start_time));
402 put_fixed64(&mut buf, 8, unix_nanos(record.end_time));
403 for kv in &record.attributes {
404 put_len(&mut buf, 9, &encode_key_value(kv));
405 }
406 for event in &record.events {
407 put_len(&mut buf, 11, &encode_event(event));
408 }
409 if let Some(status) = encode_status(&record.status) {
410 put_len(&mut buf, 15, &status);
411 }
412 put_fixed32(&mut buf, 16, span_flags(record));
413 buf
414}
415
416fn string_attr(key: &str, value: &str) -> Vec<u8> {
417 encode_key_value(&KeyValue::new(key.to_string(), value.to_string()))
418}
419
420pub fn encode_traces_request(service_name: &str, spans: &[SpanRecord]) -> Vec<u8> {
427 let mut resource = Vec::new();
429 put_len(&mut resource, 1, &string_attr("service.name", service_name));
430 put_len(
431 &mut resource,
432 1,
433 &string_attr("telemetry.sdk.name", "plecto"),
434 );
435 put_len(
436 &mut resource,
437 1,
438 &string_attr("telemetry.sdk.language", "rust"),
439 );
440 put_len(
441 &mut resource,
442 1,
443 &string_attr("telemetry.sdk.version", env!("CARGO_PKG_VERSION")),
444 );
445
446 let mut scope = Vec::new();
448 put_str(&mut scope, 1, "plecto");
449 put_str(&mut scope, 2, env!("CARGO_PKG_VERSION"));
450
451 let mut scope_spans = Vec::new();
453 put_len(&mut scope_spans, 1, &scope);
454 for record in spans {
455 put_len(&mut scope_spans, 2, &encode_span(record));
456 }
457
458 let mut resource_spans = Vec::new();
460 put_len(&mut resource_spans, 1, &resource);
461 put_len(&mut resource_spans, 2, &scope_spans);
462
463 let mut request = Vec::new();
464 put_len(&mut request, 1, &resource_spans);
465 request
466}
467
468#[derive(Debug, Clone, PartialEq, Eq)]
473pub struct PartialSuccess {
474 pub rejected_spans: i64,
475 pub error_message: String,
476}
477
478pub fn decode_export_partial_success(body: &[u8]) -> Option<PartialSuccess> {
482 let inner = read_fields(body, |field, payload| match (field, payload) {
483 (1, FieldPayload::Len(bytes)) => Some(bytes.to_vec()),
484 _ => None,
485 })?;
486 let inner = inner?;
487 let mut rejected: i64 = 0;
488 let mut message = String::new();
489 read_fields(&inner, |field, payload| {
490 match (field, payload) {
491 (1, FieldPayload::Varint(v)) => rejected = v as i64,
492 (2, FieldPayload::Len(bytes)) => {
493 message = String::from_utf8_lossy(bytes).into_owned();
494 }
495 _ => {}
496 }
497 None::<()>
498 })?;
499 if rejected == 0 && message.is_empty() {
500 return None;
501 }
502 Some(PartialSuccess {
503 rejected_spans: rejected,
504 error_message: message,
505 })
506}
507
508enum FieldPayload<'a> {
509 Varint(u64),
510 Len(&'a [u8]),
511}
512
513fn read_fields<'a, T>(
516 mut bytes: &'a [u8],
517 mut visit: impl FnMut(u32, FieldPayload<'a>) -> Option<T>,
518) -> Option<Option<T>> {
519 while !bytes.is_empty() {
520 let (tag, rest) = read_varint(bytes)?;
521 let field = (tag >> 3) as u32;
522 let wire = (tag & 0x7) as u32;
523 bytes = rest;
524 let payload = match wire {
525 WIRE_VARINT => {
526 let (v, rest) = read_varint(bytes)?;
527 bytes = rest;
528 FieldPayload::Varint(v)
529 }
530 WIRE_I64 => {
531 let v = u64::from_le_bytes(bytes.get(..8)?.try_into().ok()?);
532 bytes = bytes.get(8..)?;
533 FieldPayload::Varint(v)
534 }
535 WIRE_LEN => {
536 let (len, rest) = read_varint(bytes)?;
537 let len = usize::try_from(len).ok()?;
538 let payload = rest.get(..len)?;
539 bytes = rest.get(len..)?;
540 FieldPayload::Len(payload)
541 }
542 WIRE_I32 => {
543 let v = u32::from_le_bytes(bytes.get(..4)?.try_into().ok()?);
544 bytes = bytes.get(4..)?;
545 FieldPayload::Varint(u64::from(v))
546 }
547 _ => return None,
548 };
549 if let Some(t) = visit(field, payload) {
550 return Some(Some(t));
551 }
552 }
553 Some(None)
554}
555
556fn read_varint(bytes: &[u8]) -> Option<(u64, &[u8])> {
557 let mut value: u64 = 0;
558 for (i, byte) in bytes.iter().enumerate().take(10) {
559 value |= u64::from(byte & 0x7f) << (7 * i);
560 if byte & 0x80 == 0 {
561 return Some((value, bytes.get(i + 1..)?));
562 }
563 }
564 None
565}
566
567#[cfg(test)]
568mod tests {
569 use std::time::Duration;
570
571 use opentelemetry_proto::tonic::collector::trace::v1::{
572 ExportTracePartialSuccess, ExportTraceServiceRequest, ExportTraceServiceResponse,
573 };
574 use opentelemetry_proto::tonic::common::v1::any_value::Value as ProtoValue;
575 use opentelemetry_proto::tonic::trace::v1::status::StatusCode as ProtoStatusCode;
576 use prost::Message;
577
578 use super::*;
579 use crate::observe::{Hook, RequestTrace, SpanOutcome, build_filter_span};
580 use crate::{Isolation, LogLevel, LogLine};
581
582 fn record(name: &str) -> SpanRecord {
583 let start = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
584 SpanRecord {
585 trace_id: TraceId::from_bytes([1; 16]),
586 span_id: SpanId::from_bytes([2; 8]),
587 parent_span_id: None,
588 name: name.to_string(),
589 kind: SpanKind::Server,
590 start_time: start,
591 end_time: start + Duration::from_millis(5),
592 status: Status::Unset,
593 attributes: vec![],
594 events: vec![],
595 remote_parent: false,
596 sampled: true,
597 }
598 }
599
600 fn decode(bytes: &[u8]) -> ExportTraceServiceRequest {
602 ExportTraceServiceRequest::decode(bytes).expect("official decoder accepts our bytes")
603 }
604
605 #[test]
606 fn round_trips_a_full_span_through_the_official_decoder() {
607 let start = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
608 let rec = SpanRecord {
609 trace_id: TraceId::from_bytes([0xAB; 16]),
610 span_id: SpanId::from_bytes([0xCD; 8]),
611 parent_span_id: Some(SpanId::from_bytes([0xEF; 8])),
612 name: "GET".to_string(),
613 kind: SpanKind::Server,
614 start_time: start,
615 end_time: start + Duration::from_millis(12),
616 status: Status::Error {
617 description: "upstream 502".into(),
618 },
619 attributes: vec![
620 KeyValue::new("http.request.method", "GET"),
621 KeyValue::new("http.response.status_code", 502_i64),
622 KeyValue::new("plecto.pi", 3.5_f64),
623 KeyValue::new("plecto.flag", true),
624 KeyValue::new(
625 "plecto.list",
626 Value::Array(Array::String(vec!["a".into(), "b".into()])),
627 ),
628 ],
629 events: vec![Event::new(
630 "auth denied",
631 start,
632 vec![KeyValue::new("log.level", "warn")],
633 0,
634 )],
635 remote_parent: true,
636 sampled: true,
637 };
638
639 let decoded = decode(&encode_traces_request("plecto", &[rec]));
640
641 let resource = decoded.resource_spans[0].resource.as_ref().unwrap();
642 let attr = |key: &str| {
643 resource
644 .attributes
645 .iter()
646 .find(|kv| kv.key == key)
647 .and_then(|kv| kv.value.as_ref())
648 .and_then(|v| v.value.as_ref())
649 .cloned()
650 };
651 assert_eq!(
652 attr("service.name"),
653 Some(ProtoValue::StringValue("plecto".into()))
654 );
655 assert_eq!(
656 attr("telemetry.sdk.name"),
657 Some(ProtoValue::StringValue("plecto".into())),
658 "a hand-written exporter must not claim to be the official SDK"
659 );
660
661 let scope_spans = &decoded.resource_spans[0].scope_spans[0];
662 assert_eq!(scope_spans.scope.as_ref().unwrap().name, "plecto");
663 let span = &scope_spans.spans[0];
664 assert_eq!(span.trace_id, vec![0xAB; 16]);
665 assert_eq!(span.span_id, vec![0xCD; 8]);
666 assert_eq!(span.parent_span_id, vec![0xEF; 8]);
667 assert_eq!(span.name, "GET");
668 assert_eq!(span.kind, 2, "SERVER");
669 assert_eq!(span.start_time_unix_nano, 1_700_000_000_000_000_000);
670 assert_eq!(span.end_time_unix_nano, 1_700_000_000_012_000_000);
671 assert_eq!(
672 span.flags,
673 0x300 | 0x01,
674 "remote parent: has_is_remote + is_remote + sampled"
675 );
676
677 let span_attr = |key: &str| {
678 span.attributes
679 .iter()
680 .find(|kv| kv.key == key)
681 .and_then(|kv| kv.value.as_ref())
682 .and_then(|v| v.value.as_ref())
683 .cloned()
684 };
685 assert_eq!(
686 span_attr("http.request.method"),
687 Some(ProtoValue::StringValue("GET".into()))
688 );
689 assert_eq!(
690 span_attr("http.response.status_code"),
691 Some(ProtoValue::IntValue(502))
692 );
693 assert_eq!(span_attr("plecto.pi"), Some(ProtoValue::DoubleValue(3.5)));
694 assert_eq!(span_attr("plecto.flag"), Some(ProtoValue::BoolValue(true)));
695 match span_attr("plecto.list") {
696 Some(ProtoValue::ArrayValue(arr)) => {
697 let items: Vec<_> = arr
698 .values
699 .iter()
700 .filter_map(|v| v.value.as_ref())
701 .cloned()
702 .collect();
703 assert_eq!(
704 items,
705 vec![
706 ProtoValue::StringValue("a".into()),
707 ProtoValue::StringValue("b".into())
708 ]
709 );
710 }
711 other => panic!("expected an array attribute, got {other:?}"),
712 }
713
714 let status = span.status.as_ref().expect("error status is encoded");
715 assert_eq!(status.code, ProtoStatusCode::Error as i32);
716 assert_eq!(status.message, "upstream 502");
717
718 assert_eq!(span.events.len(), 1);
719 assert_eq!(span.events[0].name, "auth denied");
720 assert_eq!(span.events[0].time_unix_nano, 1_700_000_000_000_000_000);
721 }
722
723 #[test]
724 fn root_span_omits_parent_and_unset_status_and_clears_remote_bit() {
725 let decoded = decode(&encode_traces_request("plecto", &[record("root")]));
726 let span = &decoded.resource_spans[0].scope_spans[0].spans[0];
727 assert!(span.parent_span_id.is_empty(), "no parent field for a root");
728 assert!(span.status.is_none(), "Unset status is omitted entirely");
729 assert_eq!(span.flags, 0x100 | 0x01, "local parent context, sampled");
730 }
731
732 #[test]
733 fn filter_span_converts_with_local_parent_and_derived_end_time() {
734 let trace = RequestTrace::root();
735 let logs = vec![LogLine {
736 level: LogLevel::Info,
737 message: "hi".to_string(),
738 }];
739 let start = SystemTime::now();
740 let span = build_filter_span(
741 &trace,
742 "auth",
743 Isolation::Trusted,
744 Hook::OnRequest,
745 SpanOutcome::Continue,
746 start,
747 Duration::from_micros(250),
748 &logs,
749 );
750 let rec = SpanRecord::from(&span);
751 assert_eq!(rec.parent_span_id, Some(trace.request_span_id()));
752 assert!(!rec.remote_parent, "a filter span's parent is always local");
753 assert_eq!(rec.end_time, start + Duration::from_micros(250));
754 assert_eq!(rec.status, Status::Ok);
755 assert_eq!(rec.events.len(), 1);
756
757 let decoded = decode(&encode_traces_request("plecto", &[rec]));
758 let got = &decoded.resource_spans[0].scope_spans[0].spans[0];
759 assert_eq!(got.kind, 1, "INTERNAL");
760 assert_eq!(got.name, "auth");
761 }
762
763 #[test]
764 fn buffer_is_fifo_bounded_and_counts_drops() {
765 let buffer = OtlpBuffer::new(2);
766 buffer.push(record("a"));
767 buffer.push(record("b"));
768 buffer.push(record("c")); assert_eq!(buffer.len(), 2);
770 assert_eq!(buffer.dropped_spans(), 1);
771
772 let first = buffer.drain(1);
773 assert_eq!(first.len(), 1);
774 assert_eq!(first[0].name, "a", "FIFO");
775 assert_eq!(buffer.drain(10).len(), 1);
776 assert!(buffer.is_empty());
777
778 buffer.record_dropped(3);
779 assert_eq!(buffer.dropped_spans(), 4, "export losses share the counter");
780 }
781
782 #[test]
783 fn sink_export_skips_unsampled_spans() {
784 let buffer = OtlpBuffer::default();
785 let trace = RequestTrace::from_traceparent(
786 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00",
787 )
788 .expect("valid unsampled traceparent");
789 let span = build_filter_span(
790 &trace,
791 "f",
792 Isolation::Untrusted,
793 Hook::OnRequest,
794 SpanOutcome::Continue,
795 SystemTime::now(),
796 Duration::from_nanos(1),
797 &[],
798 );
799 buffer.export(&span);
800 assert!(buffer.is_empty(), "unsampled spans are not exported");
801
802 let sampled = build_filter_span(
803 &RequestTrace::root(),
804 "f",
805 Isolation::Untrusted,
806 Hook::OnRequest,
807 SpanOutcome::Continue,
808 SystemTime::now(),
809 Duration::from_nanos(1),
810 &[],
811 );
812 buffer.export(&sampled);
813 assert_eq!(buffer.len(), 1);
814 }
815
816 #[test]
817 fn decodes_partial_success_and_ignores_clean_responses() {
818 let with_rejection = ExportTraceServiceResponse {
819 partial_success: Some(ExportTracePartialSuccess {
820 rejected_spans: 7,
821 error_message: "bad spans".to_string(),
822 }),
823 }
824 .encode_to_vec();
825 assert_eq!(
826 decode_export_partial_success(&with_rejection),
827 Some(PartialSuccess {
828 rejected_spans: 7,
829 error_message: "bad spans".to_string(),
830 })
831 );
832
833 let clean = ExportTraceServiceResponse {
834 partial_success: None,
835 }
836 .encode_to_vec();
837 assert_eq!(decode_export_partial_success(&clean), None);
838 assert_eq!(decode_export_partial_success(b""), None);
839 assert_eq!(
840 decode_export_partial_success(&[0xff, 0xff, 0xff]),
841 None,
842 "malformed bytes are fail-soft"
843 );
844 }
845}