use crate::config::telemetry::hive::TraceBatchProcessorConfig;
use ahash::AHashMap;
use opentelemetry::trace::{SpanId, TraceId};
use opentelemetry::Context;
use opentelemetry_sdk::error::{OTelSdkError, OTelSdkResult};
use opentelemetry_sdk::trace::{Span, SpanData, SpanExporter, SpanProcessor};
use opentelemetry_sdk::Resource;
use tokio::select;
use tokio::task::JoinSet;
use tokio::{
sync::{mpsc, RwLock},
time,
};
use tracing::{debug, error, warn};
use std::fmt;
use std::fmt::Debug;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::telemetry::error::TelemetryError;
use crate::telemetry::logging::targets;
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
enum BatchMessage {
ExportSpan(SpanData),
Flush(Option<std::sync::mpsc::Sender<OTelSdkResult>>),
Shutdown(std::sync::mpsc::Sender<OTelSdkResult>),
SetResource(Arc<Resource>),
}
#[derive(Debug)]
struct ActiveTrace {
spans: Vec<SpanData>,
root_end_time: Option<Instant>,
first_seen: Instant,
}
impl ActiveTrace {
fn new(now: Instant, capacity: usize) -> Self {
Self {
spans: Vec::with_capacity(capacity),
root_end_time: None,
first_seen: now,
}
}
#[inline]
fn is_complete(&self) -> bool {
self.root_end_time.is_some()
}
fn lifetime_exceeded(&self, now: Instant, max_lifetime: Duration) -> bool {
now.duration_since(self.first_seen) > max_lifetime
}
fn is_empty(&self) -> bool {
self.spans.is_empty()
}
fn add_span(
&mut self,
span: SpanData,
now: Instant,
config: &Config,
counters: &DropMetrics,
) -> bool {
let is_root = span.parent_span_id == SpanId::INVALID || span.parent_span_is_remote;
let root_ended_now = is_root && !self.is_complete();
if root_ended_now {
self.root_end_time = Some(now);
}
if self.spans.len() < config.max_spans_per_trace {
self.spans.push(span);
return root_ended_now;
}
let max_spans_per_trace = config.max_spans_per_trace;
warn!(
name: "TraceBatchingProcessor.SpanDiscarded",
target: targets::TELEMETRY,
trace_id = %span.span_context.trace_id(),
%max_spans_per_trace,
"Span discarded due to maximum spans per trace limit"
);
counters
.dropped_spans_per_trace_limit_count
.fetch_add(1, Ordering::Relaxed);
root_ended_now
}
}
#[derive(Debug, Default)]
struct DropMetrics {
dropped_spans_count: AtomicUsize,
dropped_spans_per_trace_limit_count: AtomicUsize,
}
#[derive(Debug, Clone)]
struct Config {
pub max_trace_lifetime: Duration,
pub sweep_interval: Duration,
pub max_traces_in_memory: usize,
pub max_spans_per_trace: usize,
pub max_export_timeout: Duration,
pub max_export_batch_size: usize,
pub scheduled_delay: Duration,
pub max_concurrent_exports: usize,
}
pub struct TraceBatchSpanProcessor {
message_sender: mpsc::Sender<BatchMessage>,
drop_metrics: Arc<DropMetrics>,
config: Config,
}
impl fmt::Debug for TraceBatchSpanProcessor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TraceBatchSpanProcessor")
.field("message_sender", &self.message_sender)
.finish()
}
}
impl SpanProcessor for TraceBatchSpanProcessor {
fn on_start(&self, _span: &mut Span, _cx: &Context) {
}
fn on_end(&self, span: SpanData) {
if !span.span_context.is_sampled() {
return;
}
let result = self.message_sender.try_send(BatchMessage::ExportSpan(span));
if result.is_err() {
let count = self
.drop_metrics
.dropped_spans_count
.fetch_add(1, Ordering::Relaxed);
if count == 0 {
warn!(
name: "TraceBatchSpanProcessor.SpanDroppingStarted",
target: targets::TELEMETRY,
"Beginning to drop span messages due to full/internal errors. No further log will be emitted for next 100 spans. During Shutdown time, a log will be emitted with exact count of total spans dropped."
);
} else if count.is_multiple_of(100) {
warn!(
name: "TraceBatchSpanProcessor.SpanDroppingStarted",
target: targets::TELEMETRY,
message = "Still dropping span messages due to full/internal errors. No further log will be emitted for next 100 spans. During Shutdown time, a log will be emitted with exact count of total spans dropped."
);
}
}
}
fn force_flush(&self) -> OTelSdkResult {
let (res_sender, res_receiver) = std::sync::mpsc::channel();
self.message_sender
.try_send(BatchMessage::Flush(Some(res_sender)))
.map_err(|err| {
OTelSdkError::InternalFailure(format!("Failed to send flush message: {err}"))
})?;
res_receiver.recv().map_err(|err| {
OTelSdkError::InternalFailure(format!("Flush response channel error: {err}"))
})?
}
fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult {
let total_dropped = self
.drop_metrics
.dropped_spans_count
.load(Ordering::Relaxed);
let limit_dropped = self
.drop_metrics
.dropped_spans_per_trace_limit_count
.load(Ordering::Relaxed);
if total_dropped > 0 || limit_dropped > 0 {
let max_traces_in_memory = self.config.max_traces_in_memory;
warn!(
name: "BatchSpanProcessor.Shutdown",
target: targets::TELEMETRY,
total_spans_dropped = total_dropped,
spans_dropped_trace_limit = limit_dropped,
max_traces_in_memory = max_traces_in_memory,
message = "Shutdown complete. Dropped spans statistics."
);
}
let (res_sender, res_receiver) = std::sync::mpsc::channel();
self.message_sender
.try_send(BatchMessage::Shutdown(res_sender))
.map_err(|err| {
OTelSdkError::InternalFailure(format!("Failed to send shutdown message: {err}"))
})?;
res_receiver.recv().map_err(|err| {
OTelSdkError::InternalFailure(format!("Shutdown response channel error: {err}"))
})?
}
fn set_resource(&mut self, resource: &Resource) {
let resource = Arc::new(resource.clone());
let _ = self
.message_sender
.try_send(BatchMessage::SetResource(resource));
}
}
struct TraceAggregator<E> {
exporter: Arc<RwLock<E>>,
export_tasks: JoinSet<OTelSdkResult>,
config: Config,
active_traces: AHashMap<TraceId, ActiveTrace>,
export_queue: Vec<ActiveTrace>,
cached_time: Instant,
drop_metrics: Arc<DropMetrics>,
}
impl<E: SpanExporter + 'static> TraceAggregator<E> {
fn enqueue_for_export(&mut self, trace: ActiveTrace) {
if trace.is_empty() {
return;
}
self.export_queue.push(trace);
}
fn enqueue_completed(&mut self, trace_id: TraceId) {
if let Some(trace) = self.active_traces.remove(&trace_id) {
self.enqueue_for_export(trace);
}
}
async fn process_message(&mut self, message: BatchMessage) -> bool {
match message {
BatchMessage::ExportSpan(span) => {
self.handle_span(span);
if self.export_queue.len() >= self.config.max_export_batch_size {
self.flush_export_queue().await;
}
}
BatchMessage::Flush(channel) => {
self.sweep();
self.flush_export_queue().await;
self.wait_for_exports().await;
let _ = self.exporter.write().await.force_flush();
if let Some(channel) = channel {
let _ = channel.send(Ok(()));
}
}
BatchMessage::Shutdown(channel) => {
self.drain_active_traces();
self.flush_export_queue().await;
tracing::debug!(
target: targets::TELEMETRY,
layer = "trace_batch_processor",
"shutdown scheduled"
);
let result = self.exporter.write().await.shutdown();
tracing::info!(
target: targets::TELEMETRY,
layer = "trace_batch_processor",
"shutdown completed"
);
let _ = channel.send(result);
return false;
}
BatchMessage::SetResource(resource) => {
self.exporter.write().await.set_resource(&resource);
}
}
true
}
async fn export(
batch: Vec<SpanData>,
exporter: Arc<RwLock<E>>,
max_export_timeout: Duration,
) -> OTelSdkResult {
if batch.is_empty() {
return Ok(());
}
let exporter_guard = exporter.read().await;
let export_fut = exporter_guard.export(batch);
select! {
res = export_fut => res,
_ = time::sleep(max_export_timeout) => Err(OTelSdkError::Timeout(max_export_timeout)),
}
}
async fn run(mut self, mut message_receiver: mpsc::Receiver<BatchMessage>) {
let mut sweep_ticker = time::interval(self.config.sweep_interval);
let mut batch_ticker = time::interval(self.config.scheduled_delay);
sweep_ticker.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
batch_ticker.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
loop {
select! {
_ = self.export_tasks.join_next(), if !self.export_tasks.is_empty() => {},
_ = sweep_ticker.tick() => {
self.cached_time = Instant::now();
self.sweep();
if self.export_queue.len() >= self.config.max_export_batch_size {
self.flush_export_queue().await;
}
},
_ = batch_ticker.tick() => {
self.cached_time = Instant::now();
if !self.export_queue.is_empty() {
self.flush_export_queue().await;
}
},
message = message_receiver.recv() => {
self.cached_time = Instant::now();
match message {
Some(message) => {
if !self.process_message(message).await {
break;
}
},
None => break,
}
},
}
}
}
fn handle_span(&mut self, span: SpanData) {
let trace_id = span.span_context.trace_id();
let now = self.cached_time;
if let Some(trace) = self.active_traces.get_mut(&trace_id) {
let root_ended_now = trace.add_span(span, now, &self.config, &self.drop_metrics);
if root_ended_now {
self.enqueue_completed(trace_id);
}
return;
}
if self.active_traces.len() >= self.config.max_traces_in_memory {
self.sweep();
if self.active_traces.len() >= self.config.max_traces_in_memory {
self.drop_metrics
.dropped_spans_count
.fetch_add(1, Ordering::Relaxed);
let max_traces_in_memory = self.config.max_traces_in_memory;
warn!(
name: "TraceBatchingProcessor.SpanDiscarded",
target: targets::TELEMETRY,
trace_id = %span.span_context.trace_id(),
%max_traces_in_memory,
"Memory limit reached, dropping span"
);
return;
}
}
let mut trace = ActiveTrace::new(
now,
std::cmp::min(self.config.max_spans_per_trace, 64),
);
let root_ended_now = trace.add_span(span, now, &self.config, &self.drop_metrics);
if root_ended_now {
self.enqueue_for_export(trace);
return;
}
self.active_traces.insert(trace_id, trace);
}
fn sweep(&mut self) {
let now = self.cached_time;
let mut to_export = Vec::new();
let mut to_drop = Vec::new();
for (trace_id, state) in &self.active_traces {
if state.is_complete() {
to_export.push(*trace_id);
continue;
}
if state.lifetime_exceeded(now, self.config.max_trace_lifetime) {
to_drop.push(*trace_id);
}
}
for trace_id in to_drop {
if let Some(trace) = self.active_traces.remove(&trace_id) {
debug!(name: "TraceBatchingProcessor.TraceDiscarded", target: targets::TELEMETRY, %trace_id, "Trace expired without root end");
self.drop_metrics
.dropped_spans_count
.fetch_add(trace.spans.len(), Ordering::Relaxed);
}
}
for trace_id in to_export {
self.enqueue_completed(trace_id);
}
}
fn drain_active_traces(&mut self) {
let export_queue = &mut self.export_queue;
let drop_metrics = &self.drop_metrics;
for (_, trace) in self.active_traces.drain() {
if trace.is_empty() {
continue;
}
if trace.is_complete() {
export_queue.push(trace);
continue;
}
drop_metrics
.dropped_spans_count
.fetch_add(trace.spans.len(), Ordering::Relaxed);
}
}
async fn flush_export_queue(&mut self) {
while !self.export_queue.is_empty() {
let batch_len =
std::cmp::min(self.export_queue.len(), self.config.max_export_batch_size);
let start_index = self.export_queue.len() - batch_len;
let total_spans: usize = self.export_queue[start_index..]
.iter()
.map(|t| t.spans.len())
.sum();
let mut batch = Vec::with_capacity(total_spans);
for _ in 0..batch_len {
if let Some(mut state) = self.export_queue.pop() {
batch.append(&mut state.spans);
}
}
while !self.export_tasks.is_empty()
&& self.export_tasks.len() >= self.config.max_concurrent_exports
{
self.export_tasks.join_next().await;
}
let exporter = self.exporter.clone();
let max_export_timeout = self.config.max_export_timeout;
let task = async move {
if let Err(err) = Self::export(batch, exporter, max_export_timeout).await {
error!(name: "TraceBatchSpanProcessor.Export.Error", target: targets::TELEMETRY, error = ?err, "failed to export trace batch");
}
Ok(())
};
if self.config.max_concurrent_exports == 1 {
let _ = task.await;
} else {
self.export_tasks.spawn(task);
}
}
}
async fn wait_for_exports(&mut self) {
while self.export_tasks.join_next().await.is_some() {}
}
}
impl TraceBatchSpanProcessor {
pub fn new<E>(exporter: E, config: &TraceBatchProcessorConfig) -> Result<Self, TelemetryError>
where
E: SpanExporter + Send + Sync + 'static,
{
if config.max_traces_in_memory == 0 {
return Err(TelemetryError::TracesExporterSetup(
"'max_traces_in_memory' must be greater than 0".to_string(),
));
}
let (message_sender, message_receiver) = mpsc::channel(config.max_queue_size as usize);
let exporter = Arc::new(RwLock::new(exporter));
let drop_metrics = Arc::new(DropMetrics::default());
let config = Config {
max_trace_lifetime: Duration::from_secs(60),
sweep_interval: Duration::from_millis(200),
max_traces_in_memory: config.max_traces_in_memory as usize,
max_spans_per_trace: config.max_spans_per_trace as usize,
max_export_timeout: config.max_export_timeout,
max_export_batch_size: config.max_export_batch_size as usize,
scheduled_delay: config.scheduled_delay,
max_concurrent_exports: config.max_concurrent_exports as usize,
};
let aggregator = TraceAggregator {
exporter,
export_tasks: JoinSet::new(),
config: config.clone(),
active_traces: AHashMap::new(),
export_queue: Vec::new(),
cached_time: Instant::now(),
drop_metrics: drop_metrics.clone(),
};
std::thread::spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build();
match runtime {
Ok(runtime) => {
runtime.block_on(async move {
aggregator.run(message_receiver).await;
});
}
Err(err) => {
eprintln!(
"TraceBatchSpanProcessor.Runtime.Error: failed to create Tokio runtime for trace batch processor: {}",
err
);
}
}
});
Ok(Self {
message_sender,
drop_metrics,
config,
})
}
}