Skip to content

Commit 42df33e

Browse files
fix(internal_logs source): report broadcast lag drops in component_discarded_events_total (#25218)
1 parent 8686642 commit 42df33e

3 files changed

Lines changed: 134 additions & 7 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixed the `internal_logs` source silently dropping events under high load. Events dropped when the underlying broadcast receiver lags now increment the standard `component_discarded_events_total{intentional="false"}` metric so the loss is observable.
2+
3+
authors: thomasqueirozb

‎src/sources/internal_logs.rs‎

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ async fn run(
158158

159159
// Note: This loop, or anything called within it, MUST NOT generate
160160
// any logs that don't break the loop, as that could cause an
161-
// infinite loop since it receives all such logs.
161+
// infinite loop since it receives all such logs. Broadcast lag is
162+
// reported by `TraceSubscription::into_stream` itself, after an event
163+
// has been received, so its log cannot lag the receiver again.
162164
while let Some(mut log) = rx.next().await {
163165
// TODO: Should this actually be in memory size?
164166
let byte_size = log.estimated_json_encoded_size_of().get();
@@ -502,4 +504,99 @@ mod tests {
502504

503505
assert_eq!(definitions, Some(expected_definition))
504506
}
507+
508+
// Verify that broadcast lag is surfaced via `component_discarded_events_total` rather
509+
// than being silently swallowed.
510+
//
511+
// Strategy: run inside a single-threaded tokio runtime (the default for `#[tokio::test]`).
512+
// While the current task holds the CPU without yielding, no other tokio tasks are scheduled.
513+
// We flood the broadcast channel (capacity 99) with more events than it can hold. The source
514+
// task cannot poll between emits, so the broadcast overflows and records a lag count. After
515+
// we yield, the source observes the lag and emits `ComponentEventsDropped`.
516+
#[tokio::test]
517+
#[serial]
518+
async fn broadcast_lag_increments_discarded_metric() {
519+
trace::init(false, false, "error", 10, None);
520+
vector_lib::metrics::init_test();
521+
trace::reset_early_buffer();
522+
523+
// The downstream receiver is kept alive but never polled, so the source does not stop
524+
// on a closed output and no events are consumed downstream before we check the metric.
525+
let (tx, _rx) = SourceSender::new_test();
526+
let source = InternalLogsConfig::default()
527+
.build(SourceContext::new_test(tx, None))
528+
.await
529+
.unwrap();
530+
tokio::spawn(source);
531+
532+
// Yield so the source task subscribes and starts polling the broadcast, then stop early
533+
// buffering so new events go to the live broadcast.
534+
tokio::task::yield_now().await;
535+
trace::stop_early_buffering();
536+
tokio::task::yield_now().await;
537+
538+
let controller =
539+
vector_lib::metrics::Controller::get().expect("metrics controller must be initialized");
540+
controller.reset();
541+
542+
// Emit more events than the broadcast capacity (99) without yielding. In a
543+
// single-threaded runtime this guarantees the source task cannot poll between emits,
544+
// so the broadcast overflows and accumulates a lag count.
545+
for i in 0usize..200 {
546+
error!(message = "Broadcast lag test.", i);
547+
}
548+
549+
// Yield enough times for the source task to observe the lag and emit
550+
// `ComponentEventsDropped`.
551+
for _ in 0..20 {
552+
tokio::task::yield_now().await;
553+
}
554+
555+
let discarded_any = controller
556+
.capture_metrics()
557+
.into_iter()
558+
.any(|m| m.name() == "component_discarded_events_total");
559+
560+
assert!(
561+
discarded_any,
562+
"expected component_discarded_events_total to be emitted when broadcast lags"
563+
);
564+
}
565+
566+
// After a lag, the broadcast receiver points at the oldest slot of a full buffer. Any log
567+
// emitted before the next receive, such as the `ComponentEventsDropped` error, overwrites
568+
// that slot and lags the receiver again. Verify the source still delivers the newest event of
569+
// the burst instead of repeating that cycle.
570+
#[tokio::test]
571+
#[serial]
572+
async fn recovers_after_broadcast_lag() {
573+
trace::init(false, false, "error", 10, None);
574+
trace::reset_early_buffer();
575+
576+
let mut rx = start_source().await;
577+
578+
// Overflow the broadcast (capacity 99) without yielding, so the source lags on its next
579+
// receive. The last event is the newest one retained by the broadcast.
580+
for i in 0usize..200 {
581+
error!(message = "Broadcast lag test.", i);
582+
}
583+
error!(message = "Last event after lag.");
584+
585+
let last = Value::from("Last event after lag.");
586+
let delivered = tokio::time::timeout(Duration::from_secs(5), async {
587+
while let Some(event) = rx.next().await {
588+
if event.as_log().get(event_path!("message")) == Some(&last) {
589+
return true;
590+
}
591+
}
592+
false
593+
})
594+
.await
595+
.unwrap_or(false);
596+
597+
assert!(
598+
delivered,
599+
"source did not deliver events after broadcast lag"
600+
);
601+
}
505602
}

‎src/trace.rs‎

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use tokio::sync::{
1616
broadcast::{self, Receiver, Sender},
1717
oneshot,
1818
};
19-
use tokio_stream::wrappers::BroadcastStream;
19+
use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError};
2020
use tracing::{Event, Subscriber};
2121
use tracing_limit::RateLimitedLayer;
2222
use tracing_subscriber::{
@@ -27,8 +27,11 @@ use tracing_subscriber::{
2727
util::SubscriberInitExt,
2828
};
2929
pub use tracing_tower::{InstrumentableService, InstrumentedService};
30-
use vector_lib::SpanField;
31-
use vector_lib::lookup::event_path;
30+
use vector_lib::{
31+
SpanField,
32+
internal_event::{ComponentEventsDropped, UNINTENTIONAL},
33+
lookup::event_path,
34+
};
3235
use vrl::value::Value;
3336

3437
use crate::event::LogEvent;
@@ -308,10 +311,34 @@ impl TraceSubscription {
308311
}
309312

310313
/// Converts this subscription into a raw stream of log events.
314+
///
315+
/// Events dropped because the underlying broadcast receiver lagged are reported via
316+
/// `ComponentEventsDropped` when the next event is received, not when the lag is detected.
317+
/// After a lag, the receiver points at the oldest slot of a full buffer, so any log emitted
318+
/// before the next receive (including the `ComponentEventsDropped` error itself) overwrites
319+
/// that slot and lags the receiver again, indefinitely. Once an event has been received, that
320+
/// slot is consumed and the drop can be logged safely.
311321
pub fn into_stream(self) -> impl Stream<Item = LogEvent> + Unpin {
312-
// We ignore errors because the only error we get is when the broadcast receiver lags, and there's nothing we
313-
// can actually do about that so there's no reason to force callers to even deal with it.
314-
BroadcastStream::new(self.trace_rx).filter_map(|event| ready(event.ok()))
322+
BroadcastStream::new(self.trace_rx)
323+
.scan(0u64, |dropped, event| {
324+
ready(Some(match event {
325+
Ok(log) => {
326+
let count = std::mem::take(dropped);
327+
if count > 0 {
328+
emit!(ComponentEventsDropped::<UNINTENTIONAL> {
329+
count: usize::try_from(count).unwrap_or(usize::MAX),
330+
reason: "Internal logs broadcast receiver lagged.",
331+
});
332+
}
333+
Some(log)
334+
}
335+
Err(BroadcastStreamRecvError::Lagged(n)) => {
336+
*dropped += n;
337+
None
338+
}
339+
}))
340+
})
341+
.filter_map(ready)
315342
}
316343
}
317344

0 commit comments

Comments
 (0)