#![allow(dead_code)]
#![cfg(test)]
use super::*;
use crate::resilience::{_reset_resilience_for_tests, set_exporter_policy};
use crate::testing::acquire_test_state_lock;
use opentelemetry::InstrumentationScope;
use opentelemetry_sdk::logs::SdkLogRecord;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
fn rt() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime")
}
#[derive(Debug, Clone)]
struct StubSpanExporter {
calls: Arc<AtomicU32>,
fail_first_n: u32,
fail_as_timeout: bool,
}
impl StubSpanExporter {
fn new_always_ok() -> Self {
Self {
calls: Arc::new(AtomicU32::new(0)),
fail_first_n: 0,
fail_as_timeout: false,
}
}
fn new_fail_first(n: u32) -> Self {
Self {
calls: Arc::new(AtomicU32::new(0)),
fail_first_n: n,
fail_as_timeout: false,
}
}
fn new_always_fail() -> Self {
Self {
calls: Arc::new(AtomicU32::new(0)),
fail_first_n: u32::MAX,
fail_as_timeout: false,
}
}
fn new_always_timeout() -> Self {
Self {
calls: Arc::new(AtomicU32::new(0)),
fail_first_n: u32::MAX,
fail_as_timeout: true,
}
}
}
impl SpanExporter for StubSpanExporter {
async fn export(&self, _batch: Vec<SpanData>) -> OTelSdkResult {
let n = self.calls.fetch_add(1, Ordering::SeqCst);
if n < self.fail_first_n {
if self.fail_as_timeout {
Err(OTelSdkError::Timeout(Duration::from_secs(1)))
} else {
Err(OTelSdkError::InternalFailure("stub failure".into()))
}
} else {
Ok(())
}
}
}
#[derive(Debug, Clone)]
struct StubLogExporter {
calls: Arc<AtomicU32>,
fail_first_n: u32,
fail_as_timeout: bool,
}
impl StubLogExporter {
fn new_always_ok() -> Self {
Self {
calls: Arc::new(AtomicU32::new(0)),
fail_first_n: 0,
fail_as_timeout: false,
}
}
fn new_fail_first(n: u32) -> Self {
Self {
calls: Arc::new(AtomicU32::new(0)),
fail_first_n: n,
fail_as_timeout: false,
}
}
fn new_always_fail() -> Self {
Self {
calls: Arc::new(AtomicU32::new(0)),
fail_first_n: u32::MAX,
fail_as_timeout: false,
}
}
fn new_always_timeout() -> Self {
Self {
calls: Arc::new(AtomicU32::new(0)),
fail_first_n: u32::MAX,
fail_as_timeout: true,
}
}
}
impl LogExporter for StubLogExporter {
async fn export(&self, _batch: LogBatch<'_>) -> OTelSdkResult {
let n = self.calls.fetch_add(1, Ordering::SeqCst);
if n < self.fail_first_n {
if self.fail_as_timeout {
Err(OTelSdkError::Timeout(Duration::from_secs(1)))
} else {
Err(OTelSdkError::InternalFailure("stub failure".into()))
}
} else {
Ok(())
}
}
}
#[derive(Debug)]
struct StubMetricExporter {
calls: Arc<AtomicU32>,
fail_first_n: u32,
fail_as_timeout: bool,
}
impl StubMetricExporter {
fn new_always_ok() -> Self {
Self {
calls: Arc::new(AtomicU32::new(0)),
fail_first_n: 0,
fail_as_timeout: false,
}
}
fn new_fail_first(n: u32) -> Self {
Self {
calls: Arc::new(AtomicU32::new(0)),
fail_first_n: n,
fail_as_timeout: false,
}
}
fn new_always_fail() -> Self {
Self {
calls: Arc::new(AtomicU32::new(0)),
fail_first_n: u32::MAX,
fail_as_timeout: false,
}
}
fn new_always_timeout() -> Self {
Self {
calls: Arc::new(AtomicU32::new(0)),
fail_first_n: u32::MAX,
fail_as_timeout: true,
}
}
}
impl PushMetricExporter for StubMetricExporter {
async fn export(&self, _metrics: &ResourceMetrics) -> OTelSdkResult {
let n = self.calls.fetch_add(1, Ordering::SeqCst);
if n < self.fail_first_n {
if self.fail_as_timeout {
Err(OTelSdkError::Timeout(Duration::from_secs(1)))
} else {
Err(OTelSdkError::InternalFailure("stub failure".into()))
}
} else {
Ok(())
}
}
fn force_flush(&self) -> OTelSdkResult {
Ok(())
}
fn shutdown_with_timeout(&self, _t: Duration) -> OTelSdkResult {
Ok(())
}
fn temporality(&self) -> Temporality {
Temporality::Cumulative
}
}
fn empty_log_batch_data() -> Vec<(SdkLogRecord, InstrumentationScope)> {
vec![]
}
fn empty_resource_metrics() -> ResourceMetrics {
ResourceMetrics::default()
}
#[test]
fn span_exporter_success_passthrough() {
let _g = acquire_test_state_lock();
_reset_resilience_for_tests();
let stub = StubSpanExporter::new_always_ok();
let calls = stub.calls.clone();
let w = ResilientSpanExporter::new(stub);
rt().block_on(async move {
w.export(vec![]).await.expect("must succeed");
});
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn span_exporter_fail_open_drop_returns_ok() {
let _g = acquire_test_state_lock();
_reset_resilience_for_tests();
set_exporter_policy(
Signal::Traces,
ExporterPolicy {
retries: 0,
fail_open: true,
..ExporterPolicy::default()
},
)
.unwrap();
let stub = StubSpanExporter::new_fail_first(1);
let calls = stub.calls.clone();
let w = ResilientSpanExporter::new(stub);
rt().block_on(async move {
w.export(vec![]).await.expect("fail-open must return Ok");
});
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn span_exporter_fail_closed_surfaces_error() {
let _g = acquire_test_state_lock();
_reset_resilience_for_tests();
set_exporter_policy(
Signal::Traces,
ExporterPolicy {
retries: 0,
fail_open: false,
..ExporterPolicy::default()
},
)
.unwrap();
let stub = StubSpanExporter::new_fail_first(1);
let w = ResilientSpanExporter::new(stub);
rt().block_on(async move {
w.export(vec![])
.await
.expect_err("fail-closed must return Err");
});
}
#[test]
fn span_exporter_retries_invoke_inner_n_times() {
let _g = acquire_test_state_lock();
_reset_resilience_for_tests();
set_exporter_policy(
Signal::Traces,
ExporterPolicy {
retries: 2,
backoff_seconds: 0.0,
timeout_seconds: 0.0,
fail_open: true,
..ExporterPolicy::default()
},
)
.unwrap();
let stub = StubSpanExporter::new_fail_first(2);
let calls = stub.calls.clone();
let w = ResilientSpanExporter::new(stub);
rt().block_on(async move {
w.export(vec![]).await.expect("must succeed after retry");
});
assert_eq!(calls.load(Ordering::SeqCst), 3);
}
#[test]
fn span_exporter_circuit_breaker_trips_after_threshold_failures() {
let _g = acquire_test_state_lock();
_reset_resilience_for_tests();
set_exporter_policy(
Signal::Traces,
ExporterPolicy {
retries: 0,
backoff_seconds: 0.0,
timeout_seconds: 1.0,
fail_open: true,
..ExporterPolicy::default()
},
)
.unwrap();
let stub = StubSpanExporter::new_always_timeout();
let calls = stub.calls.clone();
let w = ResilientSpanExporter::new(stub);
rt().block_on(async {
for _ in 0..3 {
w.export(vec![]).await.ok();
}
assert_eq!(calls.load(Ordering::SeqCst), 3);
w.export(vec![])
.await
.expect("fail-open circuit must return Ok");
assert_eq!(
calls.load(Ordering::SeqCst),
3,
"circuit open: inner must not be called"
);
});
}
#[test]
fn span_exporter_circuit_breaker_fail_closed_returns_error() {
let _g = acquire_test_state_lock();
_reset_resilience_for_tests();
set_exporter_policy(
Signal::Traces,
ExporterPolicy {
retries: 0,
backoff_seconds: 0.0,
timeout_seconds: 1.0,
fail_open: false,
..ExporterPolicy::default()
},
)
.unwrap();
let stub = StubSpanExporter::new_always_timeout();
let calls = stub.calls.clone();
let w = ResilientSpanExporter::new(stub);
rt().block_on(async {
for _ in 0..3 {
w.export(vec![]).await.expect_err("fail-closed must error");
}
assert_eq!(calls.load(Ordering::SeqCst), 3);
w.export(vec![])
.await
.expect_err("fail-closed circuit must return Err");
assert_eq!(
calls.load(Ordering::SeqCst),
3,
"circuit open: inner must not be called"
);
});
}
#[test]
fn span_exporter_shutdown_and_force_flush_forwarded() {
let _g = acquire_test_state_lock();
_reset_resilience_for_tests();
let stub = StubSpanExporter::new_always_ok();
let mut w = ResilientSpanExporter::new(stub);
w.force_flush().expect("force_flush must succeed");
w.shutdown().expect("shutdown must succeed");
}