1use std::fmt;
23#[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
24use std::sync::atomic::AtomicBool;
25use std::sync::atomic::AtomicU64;
26#[cfg(feature = "otlp-logs")]
27use std::sync::atomic::AtomicUsize;
28use std::sync::atomic::Ordering;
29#[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
30use std::sync::Arc;
31use std::sync::Mutex;
32use std::sync::TryLockError;
33use std::time::Instant;
34
35use rocketmq_runtime::BudgetDimension;
36use rocketmq_runtime::BudgetLimit;
37use rocketmq_runtime::BudgetedItem;
38use rocketmq_runtime::BudgetedQueue;
39use rocketmq_runtime::FullPolicy;
40use rocketmq_runtime::QueuePushOutcome;
41use rocketmq_runtime::QueuePushRejection;
42use rocketmq_runtime::ResourceBudgetTree;
43
44pub const DEFAULT_MAX_QUEUE_ITEMS: usize = 2_048;
46
47pub const DEFAULT_MAX_QUEUE_BYTES: usize = 8 * 1024 * 1024;
49
50pub const DEFAULT_MAX_RECORD_BYTES: usize = 64 * 1024;
52
53pub const DEFAULT_MAX_EXPORT_BATCH_ITEMS: usize = 512;
55
56pub const DEFAULT_SCHEDULED_DELAY_MILLIS: u64 = 5_000;
58
59pub const DEFAULT_EXPORT_TIMEOUT_MILLIS: u64 = 3_000;
61
62pub const DEFAULT_SHUTDOWN_TIMEOUT_MILLIS: u64 = 5_000;
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct TelemetryQueueLimits {
68 max_items: usize,
70 max_bytes: usize,
72 max_record_bytes: usize,
74}
75
76impl TelemetryQueueLimits {
77 pub fn new(max_items: usize, max_bytes: usize, max_record_bytes: usize) -> Result<Self, TelemetryQueueConfigError> {
84 if max_items == 0 {
85 return Err(TelemetryQueueConfigError::ZeroLimit("max_items"));
86 }
87 if max_bytes == 0 {
88 return Err(TelemetryQueueConfigError::ZeroLimit("max_bytes"));
89 }
90 if max_record_bytes == 0 {
91 return Err(TelemetryQueueConfigError::ZeroLimit("max_record_bytes"));
92 }
93 if max_record_bytes > max_bytes {
94 return Err(TelemetryQueueConfigError::RecordExceedsQueue {
95 max_record_bytes,
96 max_queue_bytes: max_bytes,
97 });
98 }
99 Ok(Self {
100 max_items,
101 max_bytes,
102 max_record_bytes,
103 })
104 }
105
106 pub const fn max_items(self) -> usize {
108 self.max_items
109 }
110
111 pub const fn max_bytes(self) -> usize {
113 self.max_bytes
114 }
115
116 pub const fn max_record_bytes(self) -> usize {
118 self.max_record_bytes
119 }
120}
121
122impl Default for TelemetryQueueLimits {
123 fn default() -> Self {
124 Self {
125 max_items: DEFAULT_MAX_QUEUE_ITEMS,
126 max_bytes: DEFAULT_MAX_QUEUE_BYTES,
127 max_record_bytes: DEFAULT_MAX_RECORD_BYTES,
128 }
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
134pub enum TelemetryQueueConfigError {
135 #[error("telemetry queue limit {0} must be greater than zero")]
137 ZeroLimit(&'static str),
138 #[error("telemetry max_record_bytes {max_record_bytes} exceeds max_queue_bytes {max_queue_bytes}")]
140 RecordExceedsQueue {
141 max_record_bytes: usize,
143 max_queue_bytes: usize,
145 },
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum TelemetryDropReason {
151 Closed,
153 ItemLimit,
155 ByteLimit,
157 RecordTooLarge,
159 LockUnavailable,
164}
165
166impl TelemetryDropReason {
167 #[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
168 const fn as_str(self) -> &'static str {
169 match self {
170 Self::Closed => "closed",
171 Self::ItemLimit => "item_limit",
172 Self::ByteLimit => "byte_limit",
173 Self::RecordTooLarge => "record_too_large",
174 Self::LockUnavailable => "lock_unavailable",
175 }
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub enum TelemetryEnqueueOutcome {
182 Accepted,
184 Dropped(TelemetryDropReason),
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub struct TelemetryQueueSnapshot {
191 pub queued_items: usize,
193 pub queued_bytes: usize,
195 pub accepted_items: u64,
197 pub accepted_bytes: u64,
199 pub drained_items: u64,
201 pub drained_bytes: u64,
203 pub dropped_items: u64,
205 pub dropped_bytes: u64,
207 pub closed: bool,
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub struct TelemetryOutageShutdownReport {
214 pub timed_out: bool,
216 pub deadline_dropped_items: u64,
218 pub deadline_dropped_bytes: u64,
220 pub snapshot: TelemetryQueueSnapshot,
222}
223
224struct Queued<T> {
225 item: T,
226 estimated_bytes: usize,
227}
228
229pub struct TelemetryOutageQueue<T> {
234 limits: TelemetryQueueLimits,
235 admission_gate: Mutex<()>,
236 queue: BudgetedQueue<Queued<T>>,
237 deferred: Mutex<Option<BudgetedItem<Queued<T>>>>,
238 shutdown_deadline: Mutex<Option<Instant>>,
239 accepted_items: AtomicU64,
240 accepted_bytes: AtomicU64,
241 drained_items: AtomicU64,
242 drained_bytes: AtomicU64,
243 dropped_items: AtomicU64,
244 dropped_bytes: AtomicU64,
245}
246
247impl<T> fmt::Debug for TelemetryOutageQueue<T> {
248 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
249 formatter
250 .debug_struct("TelemetryOutageQueue")
251 .field("limits", &self.limits)
252 .field("queue", &self.queue)
253 .field("snapshot", &self.snapshot())
254 .finish_non_exhaustive()
255 }
256}
257
258impl<T> TelemetryOutageQueue<T> {
259 pub fn new(limits: TelemetryQueueLimits) -> Self {
261 let budget = match ResourceBudgetTree::new(
264 "telemetry-export",
265 BudgetLimit::new(limits.max_items, limits.max_bytes, FullPolicy::Reject),
266 ) {
267 Ok(tree) => tree.root(),
268 Err(error) => unreachable!("validated telemetry queue limits must form a resource budget: {error}"),
269 };
270 Self {
271 limits,
272 admission_gate: Mutex::new(()),
273 queue: BudgetedQueue::new(budget),
274 deferred: Mutex::new(None),
275 shutdown_deadline: Mutex::new(None),
276 accepted_items: AtomicU64::new(0),
277 accepted_bytes: AtomicU64::new(0),
278 drained_items: AtomicU64::new(0),
279 drained_bytes: AtomicU64::new(0),
280 dropped_items: AtomicU64::new(0),
281 dropped_bytes: AtomicU64::new(0),
282 }
283 }
284
285 pub fn try_enqueue(&self, item: T, estimated_bytes: usize) -> TelemetryEnqueueOutcome {
290 self.try_enqueue_with(item, estimated_bytes, || {})
291 }
292
293 fn try_enqueue_with<F>(&self, item: T, estimated_bytes: usize, forward: F) -> TelemetryEnqueueOutcome
301 where
302 F: FnOnce(),
303 {
304 let estimated_bytes = estimated_bytes.max(1);
305 if estimated_bytes > self.limits.max_record_bytes {
306 self.record_drop(estimated_bytes);
307 return TelemetryEnqueueOutcome::Dropped(TelemetryDropReason::RecordTooLarge);
308 }
309
310 let _admission = match self.admission_gate.try_lock() {
311 Ok(guard) => guard,
312 Err(TryLockError::WouldBlock | TryLockError::Poisoned(_)) => {
313 self.record_drop(estimated_bytes);
314 return TelemetryEnqueueOutcome::Dropped(TelemetryDropReason::LockUnavailable);
315 }
316 };
317 if self.queue.is_closed() {
320 self.record_drop(estimated_bytes);
321 return TelemetryEnqueueOutcome::Dropped(TelemetryDropReason::Closed);
322 }
323 let record = Queued { item, estimated_bytes };
324 if let QueuePushOutcome::Rejected {
325 item: _record,
326 rejection,
327 } = self.queue.try_push_data(record, estimated_bytes)
328 {
329 let reason = match rejection {
330 QueuePushRejection::Closed | QueuePushRejection::SlowConsumerClosed => TelemetryDropReason::Closed,
331 QueuePushRejection::DeadlineExceeded => TelemetryDropReason::ItemLimit,
332 QueuePushRejection::BudgetExhausted(error) => match error.dimension() {
333 Some(BudgetDimension::Bytes) => TelemetryDropReason::ByteLimit,
334 Some(BudgetDimension::Count | BudgetDimension::Rate) => TelemetryDropReason::ItemLimit,
337 None => TelemetryDropReason::Closed,
338 },
339 };
340 self.record_drop(estimated_bytes);
341 return TelemetryEnqueueOutcome::Dropped(reason);
342 }
343
344 self.accepted_items.fetch_add(1, Ordering::Relaxed);
345 self.accepted_bytes.fetch_add(estimated_bytes as u64, Ordering::Relaxed);
346 forward();
347 TelemetryEnqueueOutcome::Accepted
348 }
349
350 pub fn drain_batch(&self, max_items: usize, max_bytes: usize) -> Vec<T> {
355 let _admission = self.lock_admission();
356 let mut deferred = self.lock_deferred();
357 let mut items = Vec::new();
358 let mut drained_bytes = 0usize;
359 let max_items = max_items.max(1);
360 let max_bytes = max_bytes.max(1);
361 while items.len() < max_items {
362 let Some(record) = deferred.take().or_else(|| self.queue.try_pop_budgeted()) else {
363 break;
364 };
365 let record_bytes = record.retained_bytes();
366 if !items.is_empty() && drained_bytes.saturating_add(record_bytes) > max_bytes {
367 *deferred = Some(record);
368 break;
369 }
370 let (record, permit, _) = record.into_parts();
371 debug_assert_eq!(record.estimated_bytes, record_bytes);
372 drop(permit);
373 drained_bytes += record_bytes;
374 items.push(record.item);
375 }
376 self.drained_items.fetch_add(items.len() as u64, Ordering::Relaxed);
377 self.drained_bytes.fetch_add(drained_bytes as u64, Ordering::Relaxed);
378 items
379 }
380
381 pub fn begin_shutdown(&self, deadline: Instant) {
383 let _admission = self.lock_admission();
384 *self.lock_shutdown_deadline() = Some(deadline);
385 self.queue.close();
386 }
387
388 pub fn poll_shutdown(&self, now: Instant) -> Option<TelemetryOutageShutdownReport> {
393 let _admission = self.lock_admission();
394 let snapshot = self.snapshot_locked();
395 if !snapshot.closed {
396 return None;
397 }
398 if snapshot.queued_items == 0 {
399 return Some(TelemetryOutageShutdownReport {
400 timed_out: false,
401 deadline_dropped_items: 0,
402 deadline_dropped_bytes: 0,
403 snapshot,
404 });
405 }
406 let deadline = (*self.lock_shutdown_deadline())?;
407 if now < deadline {
408 return None;
409 }
410
411 let mut deadline_dropped_items = 0u64;
412 let mut deadline_dropped_bytes = 0u64;
413 if let Some(record) = self.lock_deferred().take() {
414 deadline_dropped_items += 1;
415 deadline_dropped_bytes += record.retained_bytes() as u64;
416 drop(record);
417 }
418 while let Some(record) = self.queue.try_pop_budgeted() {
419 deadline_dropped_items += 1;
420 deadline_dropped_bytes += record.retained_bytes() as u64;
421 drop(record);
422 }
423 self.dropped_items.fetch_add(deadline_dropped_items, Ordering::Relaxed);
424 self.dropped_bytes.fetch_add(deadline_dropped_bytes, Ordering::Relaxed);
425 Some(TelemetryOutageShutdownReport {
426 timed_out: deadline_dropped_items > 0,
427 deadline_dropped_items,
428 deadline_dropped_bytes,
429 snapshot: self.snapshot_locked(),
430 })
431 }
432
433 pub fn snapshot(&self) -> TelemetryQueueSnapshot {
435 let _admission = self.lock_admission();
436 self.snapshot_locked()
437 }
438
439 fn snapshot_locked(&self) -> TelemetryQueueSnapshot {
440 let queue = self.queue.snapshot();
441 TelemetryQueueSnapshot {
442 queued_items: queue.reserved_count,
445 queued_bytes: queue.retained_bytes,
446 accepted_items: self.accepted_items.load(Ordering::Relaxed),
447 accepted_bytes: self.accepted_bytes.load(Ordering::Relaxed),
448 drained_items: self.drained_items.load(Ordering::Relaxed),
449 drained_bytes: self.drained_bytes.load(Ordering::Relaxed),
450 dropped_items: self.dropped_items.load(Ordering::Relaxed),
451 dropped_bytes: self.dropped_bytes.load(Ordering::Relaxed),
452 closed: queue.closed,
453 }
454 }
455
456 fn record_drop(&self, estimated_bytes: usize) {
457 self.dropped_items.fetch_add(1, Ordering::Relaxed);
458 self.dropped_bytes.fetch_add(estimated_bytes as u64, Ordering::Relaxed);
459 }
460
461 #[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
462 fn drop_remaining(&self) {
463 let _admission = self.lock_admission();
464 let mut dropped_items = 0u64;
465 let mut dropped_bytes = 0u64;
466 if let Some(record) = self.lock_deferred().take() {
467 dropped_items += 1;
468 dropped_bytes += record.retained_bytes() as u64;
469 drop(record);
470 }
471 while let Some(record) = self.queue.try_pop_budgeted() {
472 dropped_items += 1;
473 dropped_bytes += record.retained_bytes() as u64;
474 drop(record);
475 }
476 self.dropped_items.fetch_add(dropped_items, Ordering::Relaxed);
477 self.dropped_bytes.fetch_add(dropped_bytes, Ordering::Relaxed);
478 }
479
480 fn lock_deferred(&self) -> std::sync::MutexGuard<'_, Option<BudgetedItem<Queued<T>>>> {
481 self.deferred.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
482 }
483
484 fn lock_admission(&self) -> std::sync::MutexGuard<'_, ()> {
485 self.admission_gate
486 .lock()
487 .unwrap_or_else(std::sync::PoisonError::into_inner)
488 }
489
490 fn lock_shutdown_deadline(&self) -> std::sync::MutexGuard<'_, Option<Instant>> {
491 self.shutdown_deadline
492 .lock()
493 .unwrap_or_else(std::sync::PoisonError::into_inner)
494 }
495}
496
497impl<T> Default for TelemetryOutageQueue<T> {
498 fn default() -> Self {
499 Self::new(TelemetryQueueLimits::default())
500 }
501}
502
503#[cfg(feature = "otlp-traces")]
504fn trace_batch_config(limits: TelemetryQueueLimits) -> opentelemetry_sdk::trace::BatchConfig {
505 opentelemetry_sdk::trace::BatchConfigBuilder::default()
506 .with_max_queue_size(limits.max_items())
510 .with_max_export_batch_size(DEFAULT_MAX_EXPORT_BATCH_ITEMS.min(limits.max_items()))
511 .with_scheduled_delay(std::time::Duration::from_millis(DEFAULT_SCHEDULED_DELAY_MILLIS))
512 .build()
513}
514
515#[cfg(feature = "otlp-logs")]
516fn log_batch_config(limits: TelemetryQueueLimits) -> opentelemetry_sdk::logs::BatchConfig {
517 let max_export_batch_size = log_export_batch_size(limits);
518 opentelemetry_sdk::logs::BatchConfigBuilder::default()
519 .with_max_queue_size(limits.max_items())
520 .with_max_export_batch_size(max_export_batch_size)
524 .with_scheduled_delay(std::time::Duration::from_millis(DEFAULT_SCHEDULED_DELAY_MILLIS))
525 .build()
526}
527
528#[cfg(feature = "otlp-logs")]
529fn log_export_batch_size(limits: TelemetryQueueLimits) -> usize {
530 assert!(
531 limits.max_items() >= 2 && limits.max_bytes() >= limits.max_record_bytes().saturating_mul(2),
532 "log outage admission requires count and byte headroom for one maximum-sized record"
533 );
534 let count_bound = limits.max_items() - 1;
535 let byte_bound = (limits.max_bytes() - limits.max_record_bytes()) / limits.max_record_bytes();
536 DEFAULT_MAX_EXPORT_BATCH_ITEMS.min(count_bound).min(byte_bound)
537}
538
539#[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
540#[derive(Debug)]
541struct AdmissionDropReporter {
542 signal: &'static str,
543 warning_emitted: AtomicBool,
544}
545
546#[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
547impl AdmissionDropReporter {
548 const fn new(signal: &'static str) -> Self {
549 Self {
550 signal,
551 warning_emitted: AtomicBool::new(false),
552 }
553 }
554
555 fn record(&self, reason: TelemetryDropReason, limits: TelemetryQueueLimits) {
556 if !self.warning_emitted.swap(true, Ordering::Relaxed) {
557 let _suppressed = opentelemetry::Context::enter_telemetry_suppressed_scope();
558 tracing::warn!(
559 target: "rocketmq_observability",
560 signal = self.signal,
561 reason = reason.as_str(),
562 max_queue_items = limits.max_items(),
563 max_queue_bytes = limits.max_bytes(),
564 max_record_bytes = limits.max_record_bytes(),
565 "telemetry was rejected by the non-blocking count-and-byte admission boundary; further drops are summarized during shutdown"
566 );
567 }
568 }
569
570 fn report_shutdown(&self, snapshot: TelemetryQueueSnapshot) {
571 if snapshot.dropped_items == 0 {
572 return;
573 }
574 let _suppressed = opentelemetry::Context::enter_telemetry_suppressed_scope();
575 tracing::warn!(
576 target: "rocketmq_observability",
577 signal = self.signal,
578 accepted_items = snapshot.accepted_items,
579 accepted_bytes = snapshot.accepted_bytes,
580 drained_items = snapshot.drained_items,
581 drained_bytes = snapshot.drained_bytes,
582 dropped_items = snapshot.dropped_items,
583 dropped_bytes = snapshot.dropped_bytes,
584 "telemetry outage admission and shutdown drop totals"
585 );
586 }
587}
588
589#[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
590fn shutdown_deadline(timeout: std::time::Duration) -> Instant {
591 let now = Instant::now();
592 now.checked_add(timeout).unwrap_or(now)
593}
594
595#[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
596fn release_exported(queue: &TelemetryOutageQueue<()>, count: usize) {
597 if count > 0 {
598 let _ = queue.drain_batch(count, usize::MAX);
599 }
600}
601
602#[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
603struct ByteEstimator {
604 total: usize,
605 limit: usize,
606}
607
608#[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
609impl ByteEstimator {
610 fn new(limit: usize) -> Self {
611 Self { total: 0, limit }
612 }
613
614 fn add(&mut self, bytes: usize) {
615 if self.exceeded() {
616 return;
617 }
618 self.total = self.total.saturating_add(bytes);
619 if self.total > self.limit {
620 self.total = self.limit.saturating_add(1);
621 }
622 }
623
624 fn add_allocation_str(&mut self, value: &str) {
625 self.add(value.len().saturating_add(2 * std::mem::size_of::<usize>()));
628 }
629
630 fn exceeded(&self) -> bool {
631 self.total > self.limit
632 }
633
634 fn finish(self) -> usize {
635 self.total.max(1)
636 }
637}
638
639#[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
640fn add_attribute(estimator: &mut ByteEstimator, attribute: &opentelemetry::KeyValue) {
641 estimator.add_allocation_str(attribute.key.as_str());
642 add_attribute_value(estimator, &attribute.value);
643}
644
645#[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
646fn add_attribute_value(estimator: &mut ByteEstimator, value: &opentelemetry::Value) {
647 match value {
648 opentelemetry::Value::Bool(_) | opentelemetry::Value::I64(_) | opentelemetry::Value::F64(_) => {}
649 opentelemetry::Value::String(value) => estimator.add_allocation_str(value.as_str()),
650 opentelemetry::Value::Array(values) => match values {
651 opentelemetry::Array::Bool(values) => {
652 estimator.add(values.capacity());
655 }
656 opentelemetry::Array::I64(values) => {
657 estimator.add(values.capacity().saturating_mul(std::mem::size_of::<i64>()));
658 }
659 opentelemetry::Array::F64(values) => {
660 estimator.add(values.capacity().saturating_mul(std::mem::size_of::<f64>()));
661 }
662 opentelemetry::Array::String(values) => {
663 estimator.add(
664 values
665 .capacity()
666 .saturating_mul(std::mem::size_of::<opentelemetry::StringValue>()),
667 );
668 for value in values {
669 estimator.add_allocation_str(value.as_str());
670 if estimator.exceeded() {
671 break;
672 }
673 }
674 }
675 _ => estimator.total = estimator.limit.saturating_add(1),
676 },
677 _ => estimator.total = estimator.limit.saturating_add(1),
678 }
679}
680
681#[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
682fn add_instrumentation_scope(estimator: &mut ByteEstimator, scope: &opentelemetry::InstrumentationScope) {
683 estimator.add_allocation_str(scope.name());
687 if let Some(version) = scope.version() {
688 estimator.add_allocation_str(version);
689 }
690 if let Some(schema_url) = scope.schema_url() {
691 estimator.add_allocation_str(schema_url);
692 }
693 let attribute_count = scope.attributes().count();
694 estimator.add(
695 attribute_count
696 .saturating_mul(2)
697 .saturating_mul(std::mem::size_of::<opentelemetry::KeyValue>()),
698 );
699 for attribute in scope.attributes() {
700 add_attribute(estimator, attribute);
701 if estimator.exceeded() {
702 break;
703 }
704 }
705}
706
707#[cfg(feature = "otlp-traces")]
708fn add_trace_state(estimator: &mut ByteEstimator, context: &opentelemetry::trace::SpanContext) {
709 let entries = context.trace_state().into_iter();
710 let entry_count = entries.len();
711 let rounded_slots = if entry_count == 0 {
712 0
713 } else {
714 entry_count
715 .checked_next_power_of_two()
716 .unwrap_or(usize::MAX)
717 .saturating_mul(2)
718 };
719 estimator.add(rounded_slots.saturating_mul(std::mem::size_of::<(String, String)>()));
720 for (key, value) in entries {
721 estimator.add_allocation_str(key);
722 estimator.add_allocation_str(value);
723 if estimator.exceeded() {
724 break;
725 }
726 }
727}
728
729#[cfg(feature = "otlp-traces")]
730fn estimate_span_bytes(span: &opentelemetry_sdk::trace::SpanData, limit: usize) -> usize {
731 let mut estimator = ByteEstimator::new(limit);
732 estimator.add(std::mem::size_of::<opentelemetry_sdk::trace::SpanData>());
733 if let std::borrow::Cow::Owned(name) = &span.name {
734 estimator.add(name.capacity());
735 }
736 add_trace_state(&mut estimator, &span.span_context);
737
738 estimator.add(
739 span.attributes
740 .capacity()
741 .saturating_mul(std::mem::size_of::<opentelemetry::KeyValue>()),
742 );
743 for attribute in &span.attributes {
744 add_attribute(&mut estimator, attribute);
745 if estimator.exceeded() {
746 return estimator.finish();
747 }
748 }
749
750 estimator.add(
751 span.events
752 .events
753 .capacity()
754 .saturating_mul(std::mem::size_of::<opentelemetry::trace::Event>()),
755 );
756 for event in &span.events.events {
757 if let std::borrow::Cow::Owned(name) = &event.name {
758 estimator.add(name.capacity());
759 }
760 estimator.add(
761 event
762 .attributes
763 .capacity()
764 .saturating_mul(std::mem::size_of::<opentelemetry::KeyValue>()),
765 );
766 for attribute in &event.attributes {
767 add_attribute(&mut estimator, attribute);
768 if estimator.exceeded() {
769 return estimator.finish();
770 }
771 }
772 }
773
774 estimator.add(
775 span.links
776 .links
777 .capacity()
778 .saturating_mul(std::mem::size_of::<opentelemetry::trace::Link>()),
779 );
780 for link in &span.links.links {
781 add_trace_state(&mut estimator, &link.span_context);
782 estimator.add(
783 link.attributes
784 .capacity()
785 .saturating_mul(std::mem::size_of::<opentelemetry::KeyValue>()),
786 );
787 for attribute in &link.attributes {
788 add_attribute(&mut estimator, attribute);
789 if estimator.exceeded() {
790 return estimator.finish();
791 }
792 }
793 }
794
795 if let opentelemetry::trace::Status::Error {
796 description: std::borrow::Cow::Owned(description),
797 } = &span.status
798 {
799 estimator.add(description.capacity());
800 }
801 add_instrumentation_scope(&mut estimator, &span.instrumentation_scope);
802 estimator.finish()
803}
804
805#[cfg(feature = "otlp-logs")]
806const LOG_INLINE_ATTRIBUTE_CAPACITY: usize = 5;
807
808#[cfg(feature = "otlp-logs")]
809const MAX_LOG_VALUE_DEPTH: usize = 64;
810
811#[cfg(feature = "otlp-logs")]
812fn estimate_log_bytes(
813 record: &opentelemetry_sdk::logs::SdkLogRecord,
814 instrumentation: &opentelemetry::InstrumentationScope,
815 limit: usize,
816) -> usize {
817 let mut estimator = ByteEstimator::new(limit);
818 estimator.add(std::mem::size_of::<(
819 opentelemetry_sdk::logs::SdkLogRecord,
820 opentelemetry::InstrumentationScope,
821 )>());
822 estimator.add(std::mem::size_of::<
823 Box<(
824 opentelemetry_sdk::logs::SdkLogRecord,
825 opentelemetry::InstrumentationScope,
826 )>,
827 >());
828 if let Some(target) = record.target() {
829 estimator.add_allocation_str(target.as_ref());
832 }
833 if let Some(body) = record.body() {
834 add_log_value(&mut estimator, body, 0);
835 }
836
837 let attribute_count = record.attributes_iter().count();
838 let overflow_items = attribute_count.saturating_sub(LOG_INLINE_ATTRIBUTE_CAPACITY);
839 if overflow_items > 0 {
840 let overflow_capacity = overflow_items
841 .checked_next_power_of_two()
842 .unwrap_or(usize::MAX)
843 .max(LOG_INLINE_ATTRIBUTE_CAPACITY);
844 estimator.add(overflow_capacity.saturating_mul(std::mem::size_of::<
845 Option<(opentelemetry::Key, opentelemetry::logs::AnyValue)>,
846 >()));
847 }
848 for (key, value) in record.attributes_iter() {
849 estimator.add_allocation_str(key.as_str());
850 add_log_value(&mut estimator, value, 0);
851 if estimator.exceeded() {
852 return estimator.finish();
853 }
854 }
855 add_instrumentation_scope(&mut estimator, instrumentation);
856 estimator.finish()
857}
858
859#[cfg(feature = "otlp-logs")]
860fn add_log_value(estimator: &mut ByteEstimator, value: &opentelemetry::logs::AnyValue, depth: usize) {
861 if depth >= MAX_LOG_VALUE_DEPTH {
862 estimator.total = estimator.limit.saturating_add(1);
863 return;
864 }
865 match value {
866 opentelemetry::logs::AnyValue::Int(_)
867 | opentelemetry::logs::AnyValue::Double(_)
868 | opentelemetry::logs::AnyValue::Boolean(_) => {}
869 opentelemetry::logs::AnyValue::String(value) => {
870 estimator.add_allocation_str(value.as_str());
871 }
872 opentelemetry::logs::AnyValue::Bytes(values) => estimator.add(values.capacity()),
873 opentelemetry::logs::AnyValue::ListAny(values) => {
874 estimator.add(
875 values
876 .capacity()
877 .saturating_mul(std::mem::size_of::<opentelemetry::logs::AnyValue>()),
878 );
879 for value in values.iter() {
880 add_log_value(estimator, value, depth + 1);
881 if estimator.exceeded() {
882 break;
883 }
884 }
885 }
886 opentelemetry::logs::AnyValue::Map(values) => {
887 estimator.add(values.capacity().saturating_mul(2).saturating_mul(
890 std::mem::size_of::<(opentelemetry::Key, opentelemetry::logs::AnyValue)>().saturating_add(1),
891 ));
892 for (key, value) in values.iter() {
893 estimator.add_allocation_str(key.as_str());
894 add_log_value(estimator, value, depth + 1);
895 if estimator.exceeded() {
896 break;
897 }
898 }
899 }
900 _ => estimator.total = estimator.limit.saturating_add(1),
901 }
902}
903
904#[cfg(feature = "otlp-traces")]
905#[derive(Debug)]
906struct QueueReleasingSpanExporter<E> {
907 exporter: E,
908 queue: Arc<TelemetryOutageQueue<()>>,
909}
910
911#[cfg(feature = "otlp-traces")]
912impl<E> opentelemetry_sdk::trace::SpanExporter for QueueReleasingSpanExporter<E>
913where
914 E: opentelemetry_sdk::trace::SpanExporter,
915{
916 async fn export(&self, batch: Vec<opentelemetry_sdk::trace::SpanData>) -> opentelemetry_sdk::error::OTelSdkResult {
917 let batch_items = batch.len();
918 let result = self.exporter.export(batch).await;
919 release_exported(&self.queue, batch_items);
922 result
923 }
924
925 fn shutdown_with_timeout(&self, timeout: std::time::Duration) -> opentelemetry_sdk::error::OTelSdkResult {
926 self.exporter.shutdown_with_timeout(timeout)
927 }
928
929 fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
930 self.exporter.force_flush()
931 }
932
933 fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) {
934 self.exporter.set_resource(resource);
935 }
936}
937
938#[cfg(feature = "otlp-traces")]
944#[derive(Debug)]
945pub(crate) struct OutageBoundedBatchSpanProcessor {
946 inner: opentelemetry_sdk::trace::BatchSpanProcessor,
947 queue: Arc<TelemetryOutageQueue<()>>,
948 limits: TelemetryQueueLimits,
949 drop_reporter: AdmissionDropReporter,
950}
951
952#[cfg(feature = "otlp-traces")]
953impl OutageBoundedBatchSpanProcessor {
954 pub(crate) fn new<E>(exporter: E) -> Self
955 where
956 E: opentelemetry_sdk::trace::SpanExporter + Send + 'static,
957 {
958 Self::new_with_limits(exporter, TelemetryQueueLimits::default())
959 }
960
961 fn new_with_limits<E>(exporter: E, limits: TelemetryQueueLimits) -> Self
962 where
963 E: opentelemetry_sdk::trace::SpanExporter + Send + 'static,
964 {
965 let queue = Arc::new(TelemetryOutageQueue::new(limits));
966 let exporter = QueueReleasingSpanExporter {
967 exporter,
968 queue: Arc::clone(&queue),
969 };
970 let inner = opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
971 .with_batch_config(trace_batch_config(limits))
972 .build();
973 Self {
974 inner,
975 queue,
976 limits,
977 drop_reporter: AdmissionDropReporter::new("traces"),
978 }
979 }
980
981 #[cfg(test)]
982 fn admission_queue(&self) -> Arc<TelemetryOutageQueue<()>> {
983 Arc::clone(&self.queue)
984 }
985}
986
987#[cfg(feature = "otlp-traces")]
988impl opentelemetry_sdk::trace::SpanProcessor for OutageBoundedBatchSpanProcessor {
989 fn on_start(&self, span: &mut opentelemetry_sdk::trace::Span, context: &opentelemetry::Context) {
990 self.inner.on_start(span, context);
991 }
992
993 fn on_end(&self, span: opentelemetry_sdk::trace::SpanData) {
994 if !span.span_context.is_sampled() {
995 return;
996 }
997 let estimated_bytes = estimate_span_bytes(&span, self.limits.max_record_bytes());
998 match self
999 .queue
1000 .try_enqueue_with((), estimated_bytes, || self.inner.on_end(span))
1001 {
1002 TelemetryEnqueueOutcome::Accepted => {}
1003 TelemetryEnqueueOutcome::Dropped(reason) => self.drop_reporter.record(reason, self.limits),
1004 }
1005 }
1006
1007 fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
1008 self.inner.force_flush()
1009 }
1010
1011 fn shutdown_with_timeout(&self, timeout: std::time::Duration) -> opentelemetry_sdk::error::OTelSdkResult {
1012 let deadline = shutdown_deadline(timeout);
1013 self.queue.begin_shutdown(deadline);
1014 let result = self
1015 .inner
1016 .shutdown_with_timeout(deadline.saturating_duration_since(Instant::now()));
1017 self.queue.drop_remaining();
1021 self.drop_reporter.report_shutdown(self.queue.snapshot());
1022 result
1023 }
1024
1025 fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) {
1026 self.inner.set_resource(resource);
1027 }
1028}
1029
1030#[cfg(feature = "otlp-logs")]
1031#[derive(Debug)]
1032struct QueueReleasingLogExporter<E> {
1033 exporter: E,
1034 queue: Arc<TelemetryOutageQueue<()>>,
1035 completed_batch_items: Arc<AtomicUsize>,
1036}
1037
1038#[cfg(feature = "otlp-logs")]
1039impl<E> QueueReleasingLogExporter<E> {
1040 fn release_completed(&self) {
1041 release_exported(&self.queue, self.completed_batch_items.swap(0, Ordering::AcqRel));
1042 }
1043}
1044
1045#[cfg(feature = "otlp-logs")]
1046impl<E> opentelemetry_sdk::logs::LogExporter for QueueReleasingLogExporter<E>
1047where
1048 E: opentelemetry_sdk::logs::LogExporter,
1049{
1050 async fn export(&self, batch: opentelemetry_sdk::logs::LogBatch<'_>) -> opentelemetry_sdk::error::OTelSdkResult {
1051 self.release_completed();
1055 let batch_items = batch.iter().count();
1056 let result = self.exporter.export(batch).await;
1057 self.completed_batch_items.fetch_add(batch_items, Ordering::Release);
1058 result
1059 }
1060
1061 fn shutdown_with_timeout(&self, timeout: std::time::Duration) -> opentelemetry_sdk::error::OTelSdkResult {
1062 self.release_completed();
1063 self.exporter.shutdown_with_timeout(timeout)
1064 }
1065
1066 fn event_enabled(&self, level: opentelemetry::logs::Severity, target: &str, name: Option<&str>) -> bool {
1067 self.exporter.event_enabled(level, target, name)
1068 }
1069
1070 fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) {
1071 self.exporter.set_resource(resource);
1072 }
1073}
1074
1075#[cfg(feature = "otlp-logs")]
1077#[derive(Debug)]
1078pub(crate) struct OutageBoundedBatchLogProcessor {
1079 inner: opentelemetry_sdk::logs::BatchLogProcessor,
1080 queue: Arc<TelemetryOutageQueue<()>>,
1081 completed_batch_items: Arc<AtomicUsize>,
1082 limits: TelemetryQueueLimits,
1083 drop_reporter: AdmissionDropReporter,
1084}
1085
1086#[cfg(feature = "otlp-logs")]
1087impl OutageBoundedBatchLogProcessor {
1088 pub(crate) fn new<E>(exporter: E) -> Self
1089 where
1090 E: opentelemetry_sdk::logs::LogExporter + Send + Sync + 'static,
1091 {
1092 Self::new_with_limits(exporter, TelemetryQueueLimits::default())
1093 }
1094
1095 fn new_with_limits<E>(exporter: E, limits: TelemetryQueueLimits) -> Self
1096 where
1097 E: opentelemetry_sdk::logs::LogExporter + Send + Sync + 'static,
1098 {
1099 let queue = Arc::new(TelemetryOutageQueue::new(limits));
1100 let completed_batch_items = Arc::new(AtomicUsize::new(0));
1101 let exporter = QueueReleasingLogExporter {
1102 exporter,
1103 queue: Arc::clone(&queue),
1104 completed_batch_items: Arc::clone(&completed_batch_items),
1105 };
1106 let inner = opentelemetry_sdk::logs::BatchLogProcessor::builder(exporter)
1107 .with_batch_config(log_batch_config(limits))
1108 .build();
1109 Self {
1110 inner,
1111 queue,
1112 completed_batch_items,
1113 limits,
1114 drop_reporter: AdmissionDropReporter::new("logs"),
1115 }
1116 }
1117
1118 fn release_completed(&self) {
1119 release_exported(&self.queue, self.completed_batch_items.swap(0, Ordering::AcqRel));
1120 }
1121
1122 #[cfg(test)]
1123 fn admission_queue(&self) -> Arc<TelemetryOutageQueue<()>> {
1124 Arc::clone(&self.queue)
1125 }
1126}
1127
1128#[cfg(feature = "otlp-logs")]
1129impl opentelemetry_sdk::logs::LogProcessor for OutageBoundedBatchLogProcessor {
1130 fn emit(
1131 &self,
1132 record: &mut opentelemetry_sdk::logs::SdkLogRecord,
1133 instrumentation: &opentelemetry::InstrumentationScope,
1134 ) {
1135 let estimated_bytes = estimate_log_bytes(record, instrumentation, self.limits.max_record_bytes());
1136 match self
1137 .queue
1138 .try_enqueue_with((), estimated_bytes, || self.inner.emit(record, instrumentation))
1139 {
1140 TelemetryEnqueueOutcome::Accepted => {}
1141 TelemetryEnqueueOutcome::Dropped(reason) => self.drop_reporter.record(reason, self.limits),
1142 }
1143 }
1144
1145 fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
1146 let result = self.inner.force_flush();
1147 self.release_completed();
1149 result
1150 }
1151
1152 fn shutdown_with_timeout(&self, timeout: std::time::Duration) -> opentelemetry_sdk::error::OTelSdkResult {
1153 let deadline = shutdown_deadline(timeout);
1154 self.queue.begin_shutdown(deadline);
1155 let result = self
1156 .inner
1157 .shutdown_with_timeout(deadline.saturating_duration_since(Instant::now()));
1158 self.release_completed();
1159 self.queue.drop_remaining();
1160 self.drop_reporter.report_shutdown(self.queue.snapshot());
1161 result
1162 }
1163
1164 fn event_enabled(&self, level: opentelemetry::logs::Severity, target: &str, name: Option<&str>) -> bool {
1165 self.inner.event_enabled(level, target, name)
1166 }
1167
1168 fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) {
1169 self.inner.set_resource(resource);
1170 }
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175 #[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
1176 use std::sync::Arc;
1177 #[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
1178 use std::sync::Condvar;
1179 #[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
1180 use std::sync::Mutex;
1181 use std::time::Duration;
1182
1183 use super::*;
1184
1185 #[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
1186 #[derive(Debug, Default)]
1187 struct ExportGate {
1188 state: Mutex<ExportGateState>,
1189 changed: Condvar,
1190 }
1191
1192 #[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
1193 #[derive(Debug, Default)]
1194 struct ExportGateState {
1195 started_count: usize,
1196 released: bool,
1197 }
1198
1199 #[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
1200 impl ExportGate {
1201 fn block_export(&self) {
1202 let mut state = self.state.lock().expect("export gate should remain available");
1203 state.started_count += 1;
1204 self.changed.notify_all();
1205 while !state.released {
1206 state = self.changed.wait(state).expect("export gate should remain available");
1207 }
1208 }
1209
1210 fn wait_until_started(&self, timeout: Duration) -> bool {
1211 self.wait_until_started_count(1, timeout)
1212 }
1213
1214 fn wait_until_started_count(&self, expected: usize, timeout: Duration) -> bool {
1215 let state = self.state.lock().expect("export gate should remain available");
1216 if state.started_count >= expected {
1217 return true;
1218 }
1219 self.changed
1220 .wait_timeout_while(state, timeout, |state| state.started_count < expected)
1221 .expect("export gate should remain available")
1222 .0
1223 .started_count
1224 >= expected
1225 }
1226
1227 fn release(&self) {
1228 let mut state = self.state.lock().expect("export gate should remain available");
1229 state.released = true;
1230 self.changed.notify_all();
1231 }
1232 }
1233
1234 #[cfg(feature = "otlp-traces")]
1235 #[derive(Debug, Clone)]
1236 struct BlockingSpanExporter {
1237 gate: Arc<ExportGate>,
1238 }
1239
1240 #[cfg(feature = "otlp-traces")]
1241 impl opentelemetry_sdk::trace::SpanExporter for BlockingSpanExporter {
1242 async fn export(
1243 &self,
1244 _batch: Vec<opentelemetry_sdk::trace::SpanData>,
1245 ) -> opentelemetry_sdk::error::OTelSdkResult {
1246 self.gate.block_export();
1247 Ok(())
1248 }
1249 }
1250
1251 #[cfg(feature = "otlp-logs")]
1252 #[derive(Debug, Clone)]
1253 struct BlockingLogExporter {
1254 gate: Arc<ExportGate>,
1255 }
1256
1257 #[cfg(feature = "otlp-logs")]
1258 impl opentelemetry_sdk::logs::LogExporter for BlockingLogExporter {
1259 async fn export(
1260 &self,
1261 _batch: opentelemetry_sdk::logs::LogBatch<'_>,
1262 ) -> opentelemetry_sdk::error::OTelSdkResult {
1263 self.gate.block_export();
1264 Ok(())
1265 }
1266 }
1267
1268 fn queue() -> TelemetryOutageQueue<&'static str> {
1269 TelemetryOutageQueue::new(TelemetryQueueLimits::new(2, 8, 6).expect("test limits should be valid"))
1270 }
1271
1272 #[test]
1273 fn queue_is_count_and_byte_bounded_and_drops_are_measurable() {
1274 let queue = queue();
1275
1276 assert_eq!(queue.try_enqueue("one", 3), TelemetryEnqueueOutcome::Accepted);
1277 assert_eq!(queue.try_enqueue("two", 5), TelemetryEnqueueOutcome::Accepted);
1278 assert_eq!(
1279 queue.try_enqueue("count-full", 1),
1280 TelemetryEnqueueOutcome::Dropped(TelemetryDropReason::ItemLimit)
1281 );
1282
1283 let snapshot = queue.snapshot();
1284 assert_eq!(snapshot.queued_items, 2);
1285 assert_eq!(snapshot.queued_bytes, 8);
1286 assert_eq!(snapshot.accepted_items, 2);
1287 assert_eq!(snapshot.dropped_items, 1);
1288 assert_eq!(snapshot.dropped_bytes, 1);
1289 }
1290
1291 #[test]
1292 fn record_and_byte_limits_fail_closed() {
1293 let queue = queue();
1294
1295 assert_eq!(
1296 queue.try_enqueue("oversized", 7),
1297 TelemetryEnqueueOutcome::Dropped(TelemetryDropReason::RecordTooLarge)
1298 );
1299 assert_eq!(queue.try_enqueue("five", 5), TelemetryEnqueueOutcome::Accepted);
1300 assert_eq!(
1301 queue.try_enqueue("byte-full", 4),
1302 TelemetryEnqueueOutcome::Dropped(TelemetryDropReason::ByteLimit)
1303 );
1304 assert_eq!(queue.snapshot().queued_bytes, 5);
1305 }
1306
1307 #[test]
1308 fn data_plane_drops_instead_of_waiting_for_queue_state() {
1309 let queue = queue();
1310 let _state_operation = queue
1311 .admission_gate
1312 .lock()
1313 .expect("test should hold the queue admission gate");
1314
1315 assert_eq!(
1316 queue.try_enqueue("contended", 2),
1317 TelemetryEnqueueOutcome::Dropped(TelemetryDropReason::LockUnavailable)
1318 );
1319 assert_eq!(queue.dropped_items.load(Ordering::Relaxed), 1);
1320 assert_eq!(queue.dropped_bytes.load(Ordering::Relaxed), 2);
1321 }
1322
1323 #[cfg(any(feature = "otlp-traces", feature = "otlp-logs"))]
1324 #[test]
1325 fn sdk_forwarding_cannot_reorder_resource_permits() {
1326 let queue = Arc::new(queue());
1327 let (forward_entered_tx, forward_entered_rx) = std::sync::mpsc::channel();
1328 let (release_forward_tx, release_forward_rx) = std::sync::mpsc::channel();
1329 let first_queue = Arc::clone(&queue);
1330 let first = std::thread::spawn(move || {
1331 first_queue.try_enqueue_with("first", 6, || {
1332 forward_entered_tx
1333 .send(())
1334 .expect("test should observe the first SDK forwarding boundary");
1335 release_forward_rx
1336 .recv()
1337 .expect("test should release the first SDK forwarding boundary");
1338 })
1339 });
1340 forward_entered_rx
1341 .recv()
1342 .expect("first admission should reach its SDK forwarding boundary");
1343
1344 let mut second_forwarded = false;
1345 assert_eq!(
1346 queue.try_enqueue_with("second", 1, || second_forwarded = true),
1347 TelemetryEnqueueOutcome::Dropped(TelemetryDropReason::LockUnavailable)
1348 );
1349 assert!(!second_forwarded);
1350
1351 release_forward_tx
1352 .send(())
1353 .expect("test should release the first forwarding boundary");
1354 assert_eq!(
1355 first.join().expect("first admission thread should finish"),
1356 TelemetryEnqueueOutcome::Accepted
1357 );
1358 assert_eq!(queue.drain_batch(2, usize::MAX), vec!["first"]);
1359 assert_eq!(queue.snapshot().queued_bytes, 0);
1360 }
1361
1362 #[test]
1363 fn deferred_lookahead_retains_its_shared_resource_budget() {
1364 let queue = queue();
1365 assert_eq!(queue.try_enqueue("one", 3), TelemetryEnqueueOutcome::Accepted);
1366 assert_eq!(queue.try_enqueue("two", 5), TelemetryEnqueueOutcome::Accepted);
1367
1368 assert_eq!(queue.drain_batch(2, 4), vec!["one"]);
1369 let snapshot = queue.snapshot();
1370 assert_eq!(snapshot.queued_items, 1);
1371 assert_eq!(snapshot.queued_bytes, 5);
1372 assert_eq!(
1373 queue.try_enqueue("over-budget", 4),
1374 TelemetryEnqueueOutcome::Dropped(TelemetryDropReason::ByteLimit)
1375 );
1376 }
1377
1378 #[test]
1379 fn exporter_drains_fifo_batches_with_bounded_accounting() {
1380 let queue = queue();
1381 assert_eq!(queue.try_enqueue("one", 3), TelemetryEnqueueOutcome::Accepted);
1382 assert_eq!(queue.try_enqueue("two", 5), TelemetryEnqueueOutcome::Accepted);
1383
1384 assert_eq!(queue.drain_batch(2, 4), vec!["one"]);
1385 assert_eq!(queue.drain_batch(2, 8), vec!["two"]);
1386 let snapshot = queue.snapshot();
1387 assert_eq!(snapshot.queued_items, 0);
1388 assert_eq!(snapshot.queued_bytes, 0);
1389 assert_eq!(snapshot.drained_items, 2);
1390 assert_eq!(snapshot.drained_bytes, 8);
1391 }
1392
1393 #[test]
1394 fn absolute_shutdown_deadline_reports_collector_outage() {
1395 let queue = queue();
1396 assert_eq!(queue.try_enqueue("one", 3), TelemetryEnqueueOutcome::Accepted);
1397 assert_eq!(queue.try_enqueue("two", 5), TelemetryEnqueueOutcome::Accepted);
1398 let deadline = Instant::now() + Duration::from_millis(10);
1399 queue.begin_shutdown(deadline);
1400
1401 assert_eq!(
1402 queue.try_enqueue("closed", 1),
1403 TelemetryEnqueueOutcome::Dropped(TelemetryDropReason::Closed)
1404 );
1405 assert!(queue.poll_shutdown(deadline - Duration::from_millis(1)).is_none());
1406 let report = queue
1407 .poll_shutdown(deadline)
1408 .expect("absolute deadline should finish shutdown");
1409
1410 assert!(report.timed_out);
1411 assert_eq!(report.deadline_dropped_items, 2);
1412 assert_eq!(report.deadline_dropped_bytes, 8);
1413 assert_eq!(report.snapshot.queued_items, 0);
1414 assert_eq!(report.snapshot.dropped_items, 3);
1415 assert_eq!(report.snapshot.dropped_bytes, 9);
1416 }
1417
1418 #[test]
1419 fn drained_queue_finishes_before_deadline_without_timeout() {
1420 let queue = queue();
1421 assert_eq!(queue.try_enqueue("one", 3), TelemetryEnqueueOutcome::Accepted);
1422 let deadline = Instant::now() + Duration::from_secs(1);
1423 queue.begin_shutdown(deadline);
1424 assert_eq!(queue.drain_batch(1, 8), vec!["one"]);
1425
1426 let report = queue.poll_shutdown(Instant::now()).expect("empty queue should finish");
1427 assert!(!report.timed_out);
1428 assert_eq!(report.deadline_dropped_items, 0);
1429 assert_eq!(report.snapshot.drained_items, 1);
1430 }
1431
1432 #[test]
1433 fn invalid_limits_are_rejected() {
1434 assert_eq!(
1435 TelemetryQueueLimits::new(0, 8, 4),
1436 Err(TelemetryQueueConfigError::ZeroLimit("max_items"))
1437 );
1438 assert_eq!(
1439 TelemetryQueueLimits::new(1, 4, 5),
1440 Err(TelemetryQueueConfigError::RecordExceedsQueue {
1441 max_record_bytes: 5,
1442 max_queue_bytes: 4,
1443 })
1444 );
1445 }
1446
1447 #[cfg(feature = "otlp-traces")]
1448 #[test]
1449 fn sdk_trace_batch_config_matches_resource_budget_count_capacity() {
1450 let limits = TelemetryQueueLimits::new(7, 8_192, 4_096).expect("test limits should be valid");
1451 let rendered = format!("{:?}", trace_batch_config(limits));
1452
1453 assert!(rendered.contains("max_queue_size: 7"));
1454 assert!(rendered.contains("max_export_batch_size: 7"));
1455 }
1456
1457 #[cfg(feature = "otlp-logs")]
1458 #[test]
1459 fn sdk_log_batch_config_matches_resource_budget_count_capacity() {
1460 let limits = TelemetryQueueLimits::new(7, 8_192, 1_024).expect("test limits should be valid");
1461 let rendered = format!("{:?}", log_batch_config(limits));
1462
1463 assert!(rendered.contains("max_queue_size: 7"));
1464 assert!(rendered.contains("max_export_batch_size: 6"));
1465 }
1466
1467 #[cfg(feature = "otlp-traces")]
1468 #[test]
1469 fn trace_budget_covers_sdk_queue_and_in_flight_export_batch() {
1470 use opentelemetry_sdk::trace::SpanProcessor as _;
1471
1472 let gate = Arc::new(ExportGate::default());
1473 let limits = TelemetryQueueLimits::new(2, 8_192, 4_096).expect("test limits should be valid");
1474 let processor = OutageBoundedBatchSpanProcessor::new_with_limits(
1475 BlockingSpanExporter {
1476 gate: Arc::clone(&gate),
1477 },
1478 limits,
1479 );
1480 let queue = processor.admission_queue();
1481
1482 processor.on_end(sampled_span("one"));
1483 processor.on_end(sampled_span("two"));
1484 let export_started = gate.wait_until_started(Duration::from_secs(2));
1485 let in_flight = queue.snapshot();
1486 processor.on_end(sampled_span("rejected"));
1487 let after_rejection = queue.snapshot();
1488
1489 gate.release();
1490 let shutdown_result = processor.shutdown_with_timeout(Duration::from_secs(2));
1491 let final_snapshot = queue.snapshot();
1492
1493 assert!(export_started, "the SDK should begin exporting the full batch");
1494 assert_eq!(in_flight.queued_items, 2);
1495 assert!(in_flight.queued_bytes > 0);
1496 assert!(in_flight.queued_bytes <= limits.max_bytes());
1497 assert_eq!(after_rejection.queued_items, 2);
1498 assert_eq!(after_rejection.dropped_items, 1);
1499 assert!(shutdown_result.is_ok());
1500 assert_eq!(final_snapshot.queued_items, 0);
1501 assert_eq!(final_snapshot.drained_items, 2);
1502 }
1503
1504 #[cfg(feature = "otlp-traces")]
1505 #[test]
1506 fn oversized_span_is_rejected_before_the_sdk_queue() {
1507 use opentelemetry_sdk::trace::SpanProcessor as _;
1508
1509 let gate = Arc::new(ExportGate::default());
1510 let limits = TelemetryQueueLimits::new(2, 1_024, 512).expect("test limits should be valid");
1511 let processor = OutageBoundedBatchSpanProcessor::new_with_limits(
1512 BlockingSpanExporter {
1513 gate: Arc::clone(&gate),
1514 },
1515 limits,
1516 );
1517 let queue = processor.admission_queue();
1518
1519 processor.on_end(sampled_span(&"x".repeat(2_048)));
1520 let rejected = queue.snapshot();
1521 gate.release();
1522 let shutdown_result = processor.shutdown_with_timeout(Duration::from_secs(2));
1523
1524 assert_eq!(rejected.accepted_items, 0);
1525 assert_eq!(rejected.dropped_items, 1);
1526 assert_eq!(rejected.queued_items, 0);
1527 assert!(shutdown_result.is_ok());
1528 }
1529
1530 #[cfg(feature = "otlp-logs")]
1531 #[test]
1532 fn log_budget_retains_borrowed_batches_without_starving_follow_up_exports() {
1533 use opentelemetry::logs::LoggerProvider as _;
1534
1535 let gate = Arc::new(ExportGate::default());
1536 let limits = TelemetryQueueLimits::new(2, 8_192, 4_096).expect("test limits should be valid");
1537 let processor = OutageBoundedBatchLogProcessor::new_with_limits(
1538 BlockingLogExporter {
1539 gate: Arc::clone(&gate),
1540 },
1541 limits,
1542 );
1543 let queue = processor.admission_queue();
1544 let provider = opentelemetry_sdk::logs::SdkLoggerProvider::builder()
1545 .with_log_processor(processor)
1546 .build();
1547 let logger = provider.logger("outage-test");
1548
1549 emit_log(&logger, "one");
1550 emit_log(&logger, "two");
1551 let export_started = gate.wait_until_started(Duration::from_secs(2));
1552 let in_flight = queue.snapshot();
1553 emit_log(&logger, "rejected");
1554 let after_rejection = queue.snapshot();
1555
1556 gate.release();
1557 let follow_up_export_started = gate.wait_until_started_count(2, Duration::from_secs(2));
1558 emit_log(&logger, "continued");
1559 let after_continuation = queue.snapshot();
1560 let shutdown_result = provider.shutdown_with_timeout(Duration::from_secs(2));
1561 let final_snapshot = queue.snapshot();
1562
1563 assert!(export_started, "the SDK should begin exporting the full batch");
1564 assert_eq!(in_flight.queued_items, 2);
1565 assert!(in_flight.queued_bytes > 0);
1566 assert!(in_flight.queued_bytes <= limits.max_bytes());
1567 assert_eq!(after_rejection.queued_items, 2);
1568 assert_eq!(after_rejection.dropped_items, 1);
1569 assert!(
1570 follow_up_export_started,
1571 "reserved headroom should let a second batch start without an explicit flush"
1572 );
1573 assert_eq!(after_continuation.accepted_items, 3);
1574 assert_eq!(after_continuation.dropped_items, 1);
1575 assert!(after_continuation.queued_items <= limits.max_items());
1576 assert!(shutdown_result.is_ok());
1577 assert_eq!(final_snapshot.queued_items, 0);
1578 assert_eq!(final_snapshot.drained_items, 3);
1579 }
1580
1581 #[cfg(feature = "otlp-traces")]
1582 fn sampled_span(name: &str) -> opentelemetry_sdk::trace::SpanData {
1583 opentelemetry_sdk::trace::SpanData {
1584 span_context: opentelemetry::trace::SpanContext::new(
1585 opentelemetry::TraceId::from(1),
1586 opentelemetry::SpanId::from(1),
1587 opentelemetry::TraceFlags::SAMPLED,
1588 false,
1589 opentelemetry::trace::TraceState::default(),
1590 ),
1591 parent_span_id: opentelemetry::SpanId::INVALID,
1592 parent_span_is_remote: false,
1593 span_kind: opentelemetry::trace::SpanKind::Internal,
1594 name: name.to_owned().into(),
1595 start_time: std::time::SystemTime::now(),
1596 end_time: std::time::SystemTime::now(),
1597 attributes: Vec::new(),
1598 dropped_attributes_count: 0,
1599 events: opentelemetry_sdk::trace::SpanEvents::default(),
1600 links: opentelemetry_sdk::trace::SpanLinks::default(),
1601 status: opentelemetry::trace::Status::Unset,
1602 instrumentation_scope: opentelemetry::InstrumentationScope::builder("outage-test").build(),
1603 }
1604 }
1605
1606 #[cfg(feature = "otlp-logs")]
1607 fn emit_log<L>(logger: &L, body: &str)
1608 where
1609 L: opentelemetry::logs::Logger,
1610 {
1611 use opentelemetry::logs::LogRecord as _;
1612
1613 let mut record = logger.create_log_record();
1614 record.set_body(body.to_owned().into());
1615 logger.emit(record);
1616 }
1617}