From d79fc2fbb626e5d323a402665f5569f04dc9d7e8 Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Mon, 5 Oct 2026 13:59:53 +0200 Subject: [PATCH 01/18] fix(socket source): report bind errors in task failures --- changelog.d/socket_bind_error_context.fix.md | 3 + src/config/mod.rs | 2 +- src/config/source.rs | 27 ++++++++- src/sources/socket/mod.rs | 2 + src/sources/socket/udp.rs | 7 ++- src/sources/util/net/tcp/mod.rs | 3 +- src/topology/builder.rs | 11 +++- src/topology/test/crash.rs | 61 +++++++++++++++++++- 8 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 changelog.d/socket_bind_error_context.fix.md diff --git a/changelog.d/socket_bind_error_context.fix.md b/changelog.d/socket_bind_error_context.fix.md new file mode 100644 index 0000000000000..349acb9c94e1d --- /dev/null +++ b/changelog.d/socket_bind_error_context.fix.md @@ -0,0 +1,3 @@ +Report the underlying socket bind error when a TCP or UDP source fails to start, instead of a generic task error. + +authors: gwenaskell diff --git a/src/config/mod.rs b/src/config/mod.rs index ee897487bb0b0..a93d365ba91f9 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -66,7 +66,7 @@ pub use provider::ProviderConfig; pub use secret::SecretBackend; pub use sink::{BoxedSink, SinkConfig, SinkContext, SinkHealthcheckOptions, SinkOuter}; pub use sink_validated::{DynValidatedSink, ValidatedSink}; -pub use source::{BoxedSource, SourceConfig, SourceContext, SourceOuter}; +pub use source::{BoxedSource, SourceConfig, SourceContext, SourceErrorReporter, SourceOuter}; pub use transform::{ BoxedTransform, TransformConfig, TransformContext, TransformOuter, get_transform_output_ids, }; diff --git a/src/config/source.rs b/src/config/source.rs index 7f339e8058e50..a458da6d788b6 100644 --- a/src/config/source.rs +++ b/src/config/source.rs @@ -1,6 +1,11 @@ #![warn(clippy::pedantic)] -use std::{cell::RefCell, collections::HashMap, time::Duration}; +use std::{ + cell::RefCell, + collections::HashMap, + sync::{Arc, Mutex}, + time::Duration, +}; use async_trait::async_trait; use dyn_clone::DynClone; @@ -130,6 +135,21 @@ pub trait SourceConfig: DynClone + NamedComponent + core::fmt::Debug + Send + Sy dyn_clone::clone_trait_object!(SourceConfig); +/// Carries a source's runtime error to the topology task that reports its failure. +#[derive(Clone, Default)] +pub struct SourceErrorReporter(Arc>>); + +impl SourceErrorReporter { + /// Records an error before the source returns an opaque failure. + pub fn report(&self, error: impl Into) { + *self.0.lock().expect("source error lock poisoned") = Some(error.into()); + } + + pub(crate) fn take(&self) -> Option { + self.0.lock().expect("source error lock poisoned").take() + } +} + pub struct SourceContext { pub key: ComponentKey, pub globals: GlobalOptions, @@ -150,6 +170,9 @@ pub struct SourceContext { /// Extra context data provided by the running app and shared across all components. This can be /// used to pass shared settings or other data from outside the components. pub extra_context: ExtraContext, + + /// Reports source errors that would otherwise be lost when the source task exits. + pub error_reporter: SourceErrorReporter, } impl SourceContext { @@ -179,6 +202,7 @@ impl SourceContext { schema_definitions: HashMap::default(), schema: Default::default(), extra_context: ExtraContext::default(), + error_reporter: Default::default(), }, shutdown, ) @@ -207,6 +231,7 @@ impl SourceContext { schema_definitions: schema_definitions.unwrap_or_default(), schema: Default::default(), extra_context: ExtraContext::default(), + error_reporter: Default::default(), } } diff --git a/src/sources/socket/mod.rs b/src/sources/socket/mod.rs index 946fad688ba88..ab450a61e7581 100644 --- a/src/sources/socket/mod.rs +++ b/src/sources/socket/mod.rs @@ -167,6 +167,7 @@ impl SourceConfig for SocketConfig { cx.shutdown, cx.out, log_namespace, + cx.error_reporter, )) } Mode::UnixDatagram(config) => config.on_unix( @@ -1119,6 +1120,7 @@ mod test { schema_definitions: HashMap::default(), extra_context: Default::default(), metrics_storage: Default::default(), + error_reporter: Default::default(), }) .await .unwrap(); diff --git a/src/sources/socket/udp.rs b/src/sources/socket/udp.rs index be6e9264e6064..ccdb16a97bb22 100644 --- a/src/sources/socket/udp.rs +++ b/src/sources/socket/udp.rs @@ -20,6 +20,7 @@ use super::default_host_key; use crate::{ SourceSender, codecs::Decoder, + config::SourceErrorReporter, event::Event, internal_events::{ SocketBindError, SocketEventsReceived, SocketMode, SocketMulticastGroupJoinError, @@ -172,6 +173,7 @@ pub(super) fn udp( mut shutdown: ShutdownSignal, mut out: SourceSender, log_namespace: LogNamespace, + error_reporter: SourceErrorReporter, ) -> Source { Box::pin(async move { let listenfd = ListenFd::from_env(); @@ -180,8 +182,9 @@ pub(super) fn udp( .map_err(|error| { emit!(SocketBindError { mode: SocketMode::Udp, - error, - }) + error: &error, + }); + error_reporter.report(error); })?; if !config.multicast_groups.is_empty() { diff --git a/src/sources/util/net/tcp/mod.rs b/src/sources/util/net/tcp/mod.rs index 89edd0675c316..5a896417b0500 100644 --- a/src/sources/util/net/tcp/mod.rs +++ b/src/sources/util/net/tcp/mod.rs @@ -147,7 +147,8 @@ where emit!(SocketBindError { mode: SocketMode::Tcp, error: &error, - }) + }); + cx.error_reporter.report(error); })?; info!( diff --git a/src/topology/builder.rs b/src/topology/builder.rs index b5979fc96f181..57536c79ebbe8 100644 --- a/src/topology/builder.rs +++ b/src/topology/builder.rs @@ -51,8 +51,9 @@ use crate::{ SourceSender, config::{ ComponentKey, Config, DataType, EnrichmentTableConfig, Input, Inputs, OutputId, - ProxyConfig, SinkContext, SinkOuter, SourceContext, SourceOuter, TransformContext, - TransformOuter, TransformOutput, enrichment_table_sinks, enrichment_table_sources, + ProxyConfig, SinkContext, SinkOuter, SourceContext, SourceErrorReporter, SourceOuter, + TransformContext, TransformOuter, TransformOutput, enrichment_table_sinks, + enrichment_table_sources, }, cpu_time::{CpuTimedExt, spawn_timed}, event::{EventArray, EventContainer}, @@ -412,6 +413,7 @@ impl<'a> Builder<'a> { .shutdown_coordinator .register_source(key, INTERNAL_SOURCES.contains(&typetag)); + let error_reporter = SourceErrorReporter::default(); let context = SourceContext { key: key.clone(), globals: self.config.global.clone(), @@ -424,6 +426,7 @@ impl<'a> Builder<'a> { schema_definitions, schema: self.config.schema, extra_context: self.extra_context.clone(), + error_reporter: error_reporter.clone(), }; let server = match source.inner.build(context).await { Err(error) => { @@ -457,7 +460,9 @@ impl<'a> Builder<'a> { Ok(e) = &mut pump_error_rx => Err(e), // The source finished normally. - result = server => result.map_err(|_| TaskError::Opaque), + result = server => result.map_err(|_| { + error_reporter.take().map_or(TaskError::Opaque, TaskError::wrapped) + }), }; // Even though we already tried to receive any pump task error above, we may have exited diff --git a/src/topology/test/crash.rs b/src/topology/test/crash.rs index 499ad256f10f8..ec78130bf5b74 100644 --- a/src/topology/test/crash.rs +++ b/src/topology/test/crash.rs @@ -1,19 +1,74 @@ +use std::net::{TcpListener, UdpSocket}; + use futures_util::StreamExt; -use tokio::time::{Duration, sleep}; +use tokio::time::{Duration, sleep, timeout}; use tokio_stream::wrappers::UnboundedReceiverStream; use crate::{ config::Config, + signal::ShutdownError, sinks::socket::SocketSinkConfig, - sources::socket::SocketConfig, + sources::socket::{SocketConfig, udp::UdpConfig}, test_util::{ CountReceiver, addr::next_addr, - mock::{error_sink, error_source, panic_sink, panic_source}, + mock::{basic_sink, error_sink, error_source, panic_sink, panic_source}, random_lines, send_lines, start_topology, trace_init, wait_for_tcp, }, }; +#[tokio::test] +async fn test_tcp_socket_bind_error_is_reported() { + trace_init(); + + let (_guard, address) = next_addr(); + let _listener = TcpListener::bind(address).unwrap(); + let expected_error = TcpListener::bind(address).unwrap_err().to_string(); + + let mut config = Config::builder(); + config.add_source("in", SocketConfig::make_basic_tcp_config(address)); + config.add_sink("out", &["in"], basic_sink(1).1); + + let (topology, mut errors) = start_topology(config.build().unwrap(), false).await; + let error = timeout(Duration::from_secs(5), errors.recv()) + .await + .expect("source error timed out") + .expect("source error missing"); + assert!(matches!( + error, + ShutdownError::SourceAborted { error, .. } + if error.contains("TCP bind failed") && error.contains(&expected_error) + )); + topology.stop().await; +} + +#[tokio::test] +async fn test_udp_socket_bind_error_is_reported() { + trace_init(); + + let (_guard, address) = next_addr(); + let _socket = UdpSocket::bind(address).unwrap(); + let expected_error = UdpSocket::bind(address).unwrap_err().to_string(); + + let mut config = Config::builder(); + config.add_source( + "in", + SocketConfig::from(UdpConfig::from_address(address.into())), + ); + config.add_sink("out", &["in"], basic_sink(1).1); + + let (topology, mut errors) = start_topology(config.build().unwrap(), false).await; + let error = timeout(Duration::from_secs(5), errors.recv()) + .await + .expect("source error timed out") + .expect("source error missing"); + assert!(matches!( + error, + ShutdownError::SourceAborted { error, .. } if error == expected_error + )); + topology.stop().await; +} + /// Ensures that an unrelated source completing immediately with an error does not prematurely terminate the topology. #[tokio::test] async fn test_source_error() { From b59daf4201a9a2938f3370c172b7c1b10c7bd771 Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Mon, 5 Oct 2026 16:55:11 +0200 Subject: [PATCH 02/18] fix(source): preserve failure details in task errors --- changelog.d/socket_bind_error_context.fix.md | 2 +- lib/vector-core/src/source.rs | 31 +++++++++++++++++- src/config/mod.rs | 2 +- src/config/source.rs | 27 +--------------- src/config/unit_test/unit_test_components.rs | 4 +-- src/enrichment_tables/memory/config.rs | 2 +- src/sources/amqp.rs | 2 +- src/sources/apache_metrics/mod.rs | 2 +- src/sources/aws_ecs_metrics/mod.rs | 2 +- src/sources/aws_kinesis_firehose/mod.rs | 1 + src/sources/aws_s3/mod.rs | 2 +- src/sources/aws_sqs/config.rs | 2 +- src/sources/datadog_agent/mod.rs | 1 + src/sources/demo_logs.rs | 2 +- src/sources/exec/mod.rs | 4 +-- src/sources/file.rs | 4 +-- src/sources/file_descriptors/mod.rs | 2 +- src/sources/gcp_pubsub.rs | 5 ++- src/sources/host_metrics/mod.rs | 4 ++- src/sources/http_client/client.rs | 8 +++-- src/sources/internal_logs.rs | 2 +- src/sources/internal_metrics.rs | 2 +- src/sources/journald.rs | 2 +- src/sources/kafka.rs | 2 +- src/sources/kubernetes_logs/mod.rs | 1 + src/sources/mod.rs | 2 +- src/sources/mongodb_metrics/mod.rs | 2 +- src/sources/mqtt/config.rs | 2 +- src/sources/nats/config.rs | 4 +-- src/sources/nats/integration_tests.rs | 2 +- src/sources/nginx_metrics/mod.rs | 2 +- src/sources/okta/client.rs | 5 ++- src/sources/postgresql_metrics.rs | 2 +- src/sources/prometheus/scrape.rs | 8 +++-- src/sources/pulsar.rs | 2 +- src/sources/socket/mod.rs | 14 +++++--- src/sources/socket/udp.rs | 17 +++++----- src/sources/splunk_hec/mod.rs | 1 + src/sources/static_metrics.rs | 2 +- src/sources/statsd/mod.rs | 8 +++-- src/sources/syslog.rs | 5 +-- src/sources/util/framestream.rs | 14 +++++--- src/sources/util/http/prelude.rs | 2 ++ src/sources/util/net/tcp/mod.rs | 3 +- src/sources/util/unix_datagram.rs | 2 +- src/sources/vector/mod.rs | 1 + src/sources/websocket/config.rs | 2 +- src/test_util/mock/sources/basic.rs | 2 +- src/test_util/mock/sources/error.rs | 2 +- src/test_util/mock/sources/tripwire.rs | 3 +- src/topology/builder.rs | 13 ++++---- src/topology/test/crash.rs | 34 ++++++++++++++++++++ 52 files changed, 172 insertions(+), 102 deletions(-) diff --git a/changelog.d/socket_bind_error_context.fix.md b/changelog.d/socket_bind_error_context.fix.md index 349acb9c94e1d..740316f09ee07 100644 --- a/changelog.d/socket_bind_error_context.fix.md +++ b/changelog.d/socket_bind_error_context.fix.md @@ -1,3 +1,3 @@ -Report the underlying socket bind error when a TCP or UDP source fails to start, instead of a generic task error. +Preserve source errors in topology task failures, including socket bind errors that previously appeared as generic task failures. authors: gwenaskell diff --git a/lib/vector-core/src/source.rs b/lib/vector-core/src/source.rs index fb042bc7b3123..5fb1b394b82ab 100644 --- a/lib/vector-core/src/source.rs +++ b/lib/vector-core/src/source.rs @@ -1,3 +1,32 @@ +use std::future::Future; + use futures::future::BoxFuture; -pub type Source = BoxFuture<'static, Result<(), ()>>; +/// Error returned by a running source. +#[derive(Debug)] +pub enum SourceError { + /// The source did not retain its failure reason. + Opaque, + /// The error that stopped the source. + Detailed(vector_common::Error), +} + +impl From for SourceError +where + E: std::error::Error + Send + Sync + 'static, +{ + fn from(error: E) -> Self { + Self::Detailed(Box::new(error)) + } +} + +/// Future that runs a source until shutdown or failure. +pub type Source = BoxFuture<'static, Result<(), SourceError>>; + +/// Adapt a source that only reports whether it failed. +pub fn opaque_source(future: F) -> Source +where + F: Future> + Send + 'static, +{ + Box::pin(async move { future.await.map_err(|()| SourceError::Opaque) }) +} diff --git a/src/config/mod.rs b/src/config/mod.rs index a93d365ba91f9..ee897487bb0b0 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -66,7 +66,7 @@ pub use provider::ProviderConfig; pub use secret::SecretBackend; pub use sink::{BoxedSink, SinkConfig, SinkContext, SinkHealthcheckOptions, SinkOuter}; pub use sink_validated::{DynValidatedSink, ValidatedSink}; -pub use source::{BoxedSource, SourceConfig, SourceContext, SourceErrorReporter, SourceOuter}; +pub use source::{BoxedSource, SourceConfig, SourceContext, SourceOuter}; pub use transform::{ BoxedTransform, TransformConfig, TransformContext, TransformOuter, get_transform_output_ids, }; diff --git a/src/config/source.rs b/src/config/source.rs index a458da6d788b6..7f339e8058e50 100644 --- a/src/config/source.rs +++ b/src/config/source.rs @@ -1,11 +1,6 @@ #![warn(clippy::pedantic)] -use std::{ - cell::RefCell, - collections::HashMap, - sync::{Arc, Mutex}, - time::Duration, -}; +use std::{cell::RefCell, collections::HashMap, time::Duration}; use async_trait::async_trait; use dyn_clone::DynClone; @@ -135,21 +130,6 @@ pub trait SourceConfig: DynClone + NamedComponent + core::fmt::Debug + Send + Sy dyn_clone::clone_trait_object!(SourceConfig); -/// Carries a source's runtime error to the topology task that reports its failure. -#[derive(Clone, Default)] -pub struct SourceErrorReporter(Arc>>); - -impl SourceErrorReporter { - /// Records an error before the source returns an opaque failure. - pub fn report(&self, error: impl Into) { - *self.0.lock().expect("source error lock poisoned") = Some(error.into()); - } - - pub(crate) fn take(&self) -> Option { - self.0.lock().expect("source error lock poisoned").take() - } -} - pub struct SourceContext { pub key: ComponentKey, pub globals: GlobalOptions, @@ -170,9 +150,6 @@ pub struct SourceContext { /// Extra context data provided by the running app and shared across all components. This can be /// used to pass shared settings or other data from outside the components. pub extra_context: ExtraContext, - - /// Reports source errors that would otherwise be lost when the source task exits. - pub error_reporter: SourceErrorReporter, } impl SourceContext { @@ -202,7 +179,6 @@ impl SourceContext { schema_definitions: HashMap::default(), schema: Default::default(), extra_context: ExtraContext::default(), - error_reporter: Default::default(), }, shutdown, ) @@ -231,7 +207,6 @@ impl SourceContext { schema_definitions: schema_definitions.unwrap_or_default(), schema: Default::default(), extra_context: ExtraContext::default(), - error_reporter: Default::default(), } } diff --git a/src/config/unit_test/unit_test_components.rs b/src/config/unit_test/unit_test_components.rs index 047d874938654..6690cfbfa9e06 100644 --- a/src/config/unit_test/unit_test_components.rs +++ b/src/config/unit_test/unit_test_components.rs @@ -41,7 +41,7 @@ impl SourceConfig for UnitTestSourceConfig { Ok(Box::pin(async move { let mut out = cx.out; let _shutdown = cx.shutdown; - out.send_batch(events).await.map_err(|_| ())?; + out.send_batch(events).await?; Ok(()) })) } @@ -98,7 +98,7 @@ impl SourceConfig for UnitTestStreamSourceConfig { Ok(Box::pin(async move { let mut out = cx.out; let _shutdown = cx.shutdown; - out.send_event_stream(stream).await.map_err(|_| ())?; + out.send_event_stream(stream).await?; Ok(()) })) } diff --git a/src/enrichment_tables/memory/config.rs b/src/enrichment_tables/memory/config.rs index 488edb6d092d2..31e001d5632e7 100644 --- a/src/enrichment_tables/memory/config.rs +++ b/src/enrichment_tables/memory/config.rs @@ -375,7 +375,7 @@ impl SourceConfig for MemoryConfig { let log_namespace = cx.log_namespace(self.log_namespace); - Ok(Box::pin( + Ok(crate::sources::opaque_source( memory.as_source(cx.shutdown, cx.out, log_namespace).run(), )) } diff --git a/src/sources/amqp.rs b/src/sources/amqp.rs index 179f2dbc5f5c2..95d39a3c304b1 100644 --- a/src/sources/amqp.rs +++ b/src/sources/amqp.rs @@ -226,7 +226,7 @@ pub(crate) async fn amqp_source( .await .map_err(|source| BuildError::AmqpCreateError { source })?; - Ok(Box::pin(run_amqp_source( + Ok(crate::sources::opaque_source(run_amqp_source( config, shutdown, out, diff --git a/src/sources/apache_metrics/mod.rs b/src/sources/apache_metrics/mod.rs index c6a5d37f8495a..f396641bb12bd 100644 --- a/src/sources/apache_metrics/mod.rs +++ b/src/sources/apache_metrics/mod.rs @@ -149,7 +149,7 @@ fn apache_metrics( mut out: SourceSender, proxy: ProxyConfig, ) -> super::Source { - Box::pin(async move { + crate::sources::opaque_source(async move { let mut stream = IntervalStream::new(tokio::time::interval(interval)) .take_until(shutdown) .map(move |_| stream::iter(urls.clone())) diff --git a/src/sources/aws_ecs_metrics/mod.rs b/src/sources/aws_ecs_metrics/mod.rs index 79cba4b6f2386..05de242d84fab 100644 --- a/src/sources/aws_ecs_metrics/mod.rs +++ b/src/sources/aws_ecs_metrics/mod.rs @@ -157,7 +157,7 @@ impl SourceConfig for AwsEcsMetricsSourceConfig { let namespace = Some(self.namespace.clone()).filter(|namespace| !namespace.is_empty()); let http_client = HttpClient::new(None, &cx.proxy)?; - Ok(Box::pin(aws_ecs_metrics( + Ok(crate::sources::opaque_source(aws_ecs_metrics( http_client, self.stats_endpoint(), self.scrape_interval_secs, diff --git a/src/sources/aws_kinesis_firehose/mod.rs b/src/sources/aws_kinesis_firehose/mod.rs index 61e876743730f..c04d8cdc024d2 100644 --- a/src/sources/aws_kinesis_firehose/mod.rs +++ b/src/sources/aws_kinesis_firehose/mod.rs @@ -221,6 +221,7 @@ impl SourceConfig for AwsKinesisFirehoseConfig { .await .map_err(|err| { error!("An error occurred: {err:?}."); + err })?; Ok(()) diff --git a/src/sources/aws_s3/mod.rs b/src/sources/aws_s3/mod.rs index cd013f566fadc..f02f9835b91c6 100644 --- a/src/sources/aws_s3/mod.rs +++ b/src/sources/aws_s3/mod.rs @@ -193,7 +193,7 @@ impl SourceConfig for AwsS3Config { .transpose()?; match self.strategy { - Strategy::Sqs => Ok(Box::pin( + Strategy::Sqs => Ok(crate::sources::opaque_source( self.create_sqs_ingestor(multiline_config, &cx.proxy, log_namespace) .await? .run(cx, self.acknowledgements, log_namespace), diff --git a/src/sources/aws_sqs/config.rs b/src/sources/aws_sqs/config.rs index cdde70ee58e3e..33382bcd17e6e 100644 --- a/src/sources/aws_sqs/config.rs +++ b/src/sources/aws_sqs/config.rs @@ -111,7 +111,7 @@ impl SourceConfig for AwsSqsConfig { .build()?; let acknowledgements = cx.do_acknowledgements(self.acknowledgements); - Ok(Box::pin( + Ok(crate::sources::opaque_source( SqsSource { client, queue_url: self.queue_url.clone(), diff --git a/src/sources/datadog_agent/mod.rs b/src/sources/datadog_agent/mod.rs index c025c4c1e71ed..f53bae9a5ab8e 100644 --- a/src/sources/datadog_agent/mod.rs +++ b/src/sources/datadog_agent/mod.rs @@ -256,6 +256,7 @@ impl SourceConfig for DatadogAgentConfig { .await .map_err(|err| { error!("An error occurred: {err:?}."); + err })?; Ok(()) diff --git a/src/sources/demo_logs.rs b/src/sources/demo_logs.rs index a83dae13bf364..394c2d4b891a2 100644 --- a/src/sources/demo_logs.rs +++ b/src/sources/demo_logs.rs @@ -293,7 +293,7 @@ impl SourceConfig for DemoLogsConfig { let decoder = DecodingConfig::new(self.framing.clone(), self.decoding.clone(), log_namespace) .build()?; - Ok(Box::pin(demo_logs_source( + Ok(crate::sources::opaque_source(demo_logs_source( self.interval, self.count, self.format.clone(), diff --git a/src/sources/exec/mod.rs b/src/sources/exec/mod.rs index 07f7dddcd2780..c44c64a45fd7f 100644 --- a/src/sources/exec/mod.rs +++ b/src/sources/exec/mod.rs @@ -255,7 +255,7 @@ impl SourceConfig for ExecConfig { Mode::Scheduled => { let exec_interval_secs = self.exec_interval_secs_or_default(); - Ok(Box::pin(run_scheduled( + Ok(crate::sources::opaque_source(run_scheduled( self.clone(), hostname, exec_interval_secs, @@ -269,7 +269,7 @@ impl SourceConfig for ExecConfig { let respawn_on_exit = self.respawn_on_exit_or_default(); let respawn_interval_secs = self.respawn_interval_secs_or_default(); - Ok(Box::pin(run_streaming( + Ok(crate::sources::opaque_source(run_streaming( self.clone(), hostname, respawn_on_exit, diff --git a/src/sources/file.rs b/src/sources/file.rs index a375dcf17c926..3c7b336ba8dec 100644 --- a/src/sources/file.rs +++ b/src/sources/file.rs @@ -484,7 +484,7 @@ pub fn file_source( message = "`include` configuration option must contain at least one file pattern.", internal_log_rate_limit = false ); - return Box::pin(future::ready(Err(()))); + return Box::pin(future::ready(Err(super::SourceError::Opaque))); } let exclude_patterns = config @@ -587,7 +587,7 @@ pub fn file_source( let checkpoints = checkpointer.view(); let include_file_metric_tag = config.internal_metrics.include_file_tag; - Box::pin(async move { + crate::sources::opaque_source(async move { info!(message = "Starting file server.", include = ?include, exclude = ?exclude); let mut encoding_decoder = encoding_charset.map(Decoder::new); diff --git a/src/sources/file_descriptors/mod.rs b/src/sources/file_descriptors/mod.rs index b3c60d50a54c0..cf4f6799f6983 100644 --- a/src/sources/file_descriptors/mod.rs +++ b/src/sources/file_descriptors/mod.rs @@ -74,7 +74,7 @@ pub trait FileDescriptorConfig: NamedComponent { read_from_fd(reader, sender); }); - Ok(Box::pin(process_stream( + Ok(crate::sources::opaque_source(process_stream( receiver, decoder, out, diff --git a/src/sources/gcp_pubsub.rs b/src/sources/gcp_pubsub.rs index 2d0a27ae03fe6..1d8021faf77ac 100644 --- a/src/sources/gcp_pubsub.rs +++ b/src/sources/gcp_pubsub.rs @@ -327,7 +327,10 @@ impl SourceConfig for PubsubConfig { events_received: register!(EventsReceived), } .run_all(self.max_concurrency, self.poll_time_seconds) - .map_err(|error| error!(message = "Source failed.", %error)); + .map_err(|error| { + error!(message = "Source failed.", %error); + crate::sources::SourceError::Detailed(error) + }); Ok(Box::pin(source)) } diff --git a/src/sources/host_metrics/mod.rs b/src/sources/host_metrics/mod.rs index 191acb3f4e846..7fa7c959e4603 100644 --- a/src/sources/host_metrics/mod.rs +++ b/src/sources/host_metrics/mod.rs @@ -304,7 +304,9 @@ impl SourceConfig for HostMetricsConfig { let mut config = self.clone(); config.namespace = config.namespace.filter(|namespace| !namespace.is_empty()); - Ok(Box::pin(config.run(cx.out, cx.shutdown))) + Ok(crate::sources::opaque_source( + config.run(cx.out, cx.shutdown), + )) } fn outputs(&self, _global_log_namespace: LogNamespace) -> Vec { diff --git a/src/sources/http_client/client.rs b/src/sources/http_client/client.rs index 9363a58f54c0f..ec82406885dbd 100644 --- a/src/sources/http_client/client.rs +++ b/src/sources/http_client/client.rs @@ -5,7 +5,6 @@ use std::{collections::HashMap, time::Duration}; use bytes::{Bytes, BytesMut}; use chrono::Utc; -use futures_util::FutureExt; use http::{Uri, response::Parts}; use serde_with::serde_as; use snafu::ResultExt; @@ -378,7 +377,12 @@ impl SourceConfig for HttpClientConfig { shutdown: cx.shutdown, }; - Ok(call(inputs, context, cx.out, self.method).boxed()) + Ok(crate::sources::opaque_source(call( + inputs, + context, + cx.out, + self.method, + ))) } fn outputs(&self, global_log_namespace: LogNamespace) -> Vec { diff --git a/src/sources/internal_logs.rs b/src/sources/internal_logs.rs index 7f22b9dc232d5..77756544a85a4 100644 --- a/src/sources/internal_logs.rs +++ b/src/sources/internal_logs.rs @@ -113,7 +113,7 @@ impl SourceConfig for InternalLogsConfig { let log_namespace = cx.log_namespace(self.log_namespace); - Ok(Box::pin(run( + Ok(crate::sources::opaque_source(run( host_key, pid_key, subscription, diff --git a/src/sources/internal_metrics.rs b/src/sources/internal_metrics.rs index 60846eda4d8af..c8d0e8f030f0a 100644 --- a/src/sources/internal_metrics.rs +++ b/src/sources/internal_metrics.rs @@ -112,7 +112,7 @@ impl SourceConfig for InternalMetricsConfig { .as_deref() .and_then(|tag| (!tag.is_empty()).then(|| tag.to_owned())); - Ok(Box::pin( + Ok(crate::sources::opaque_source( InternalMetrics { namespace, host_key, diff --git a/src/sources/journald.rs b/src/sources/journald.rs index 2d5a20e684060..e4b5e834f2155 100644 --- a/src/sources/journald.rs +++ b/src/sources/journald.rs @@ -395,7 +395,7 @@ impl SourceConfig for JournaldConfig { let acknowledgements = cx.do_acknowledgements(self.acknowledgements); let log_namespace = cx.log_namespace(self.log_namespace); - Ok(Box::pin( + Ok(crate::sources::opaque_source( JournaldSource { include_matches, exclude_matches, diff --git a/src/sources/kafka.rs b/src/sources/kafka.rs index 8183be2559bf2..31cd443651dba 100644 --- a/src/sources/kafka.rs +++ b/src/sources/kafka.rs @@ -350,7 +350,7 @@ impl SourceConfig for KafkaSourceConfig { let (consumer, callback_rx) = create_consumer(self, acknowledgements)?; - Ok(Box::pin(kafka_source( + Ok(crate::sources::opaque_source(kafka_source( self.clone(), consumer, callback_rx, diff --git a/src/sources/kubernetes_logs/mod.rs b/src/sources/kubernetes_logs/mod.rs index 4b1c2432f6afe..b372535e50d7c 100644 --- a/src/sources/kubernetes_logs/mod.rs +++ b/src/sources/kubernetes_logs/mod.rs @@ -364,6 +364,7 @@ impl SourceConfig for Config { .map(|result| { result.map_err(|error| { error!(message = "Source future failed.", %error); + crate::sources::SourceError::Detailed(error) }) }), )) diff --git a/src/sources/mod.rs b/src/sources/mod.rs index bac6ad727e76f..accafb2aa747c 100644 --- a/src/sources/mod.rs +++ b/src/sources/mod.rs @@ -99,7 +99,7 @@ pub mod windows_event_log; pub mod util; -pub use vector_lib::source::Source; +pub use vector_lib::source::{Source, SourceError, opaque_source}; #[allow(dead_code)] // Easier than listing out all the features that use this /// Common build errors diff --git a/src/sources/mongodb_metrics/mod.rs b/src/sources/mongodb_metrics/mod.rs index 2043d642adf60..f3e6e48bd7d77 100644 --- a/src/sources/mongodb_metrics/mod.rs +++ b/src/sources/mongodb_metrics/mod.rs @@ -135,7 +135,7 @@ impl SourceConfig for MongoDbMetricsConfig { let duration = self.scrape_interval_secs; let shutdown = cx.shutdown; - Ok(Box::pin(async move { + Ok(crate::sources::opaque_source(async move { let mut interval = IntervalStream::new(time::interval(duration)).take_until(shutdown); while interval.next().await.is_some() { let start = Instant::now(); diff --git a/src/sources/mqtt/config.rs b/src/sources/mqtt/config.rs index 6af3861a35f7a..8fe7c94978a27 100644 --- a/src/sources/mqtt/config.rs +++ b/src/sources/mqtt/config.rs @@ -81,7 +81,7 @@ impl SourceConfig for MqttSourceConfig { .build()?; let sink = MqttSource::new(connector, decoder, log_namespace, self.clone())?; - Ok(Box::pin(sink.run(cx.out, cx.shutdown))) + Ok(crate::sources::opaque_source(sink.run(cx.out, cx.shutdown))) } fn outputs(&self, global_log_namespace: LogNamespace) -> Vec { diff --git a/src/sources/nats/config.rs b/src/sources/nats/config.rs index ed01df00ff9b8..33cb5d7be69ea 100644 --- a/src/sources/nats/config.rs +++ b/src/sources/nats/config.rs @@ -202,7 +202,7 @@ impl SourceConfig for NatsSourceConfig { let connection = self.connect().await?; let messages = create_consumer_stream(&connection, js_config).await?; - Ok(Box::pin(run_nats_jetstream( + Ok(crate::sources::opaque_source(run_nats_jetstream( self.clone(), connection, messages, @@ -215,7 +215,7 @@ impl SourceConfig for NatsSourceConfig { NatsMode::Core => { let (connection, subscription) = create_subscription(self).await?; - Ok(Box::pin(run_nats_core( + Ok(crate::sources::opaque_source(run_nats_core( self.clone(), connection, subscription, diff --git a/src/sources/nats/integration_tests.rs b/src/sources/nats/integration_tests.rs index ffab3402cbf1a..23898e394c129 100644 --- a/src/sources/nats/integration_tests.rs +++ b/src/sources/nats/integration_tests.rs @@ -612,7 +612,7 @@ struct JetStreamTestHarness { subject: String, stream_name: String, consumer_name: String, - _source_handle: tokio::task::JoinHandle>, + _source_handle: tokio::task::JoinHandle>, } impl JetStreamTestHarness { diff --git a/src/sources/nginx_metrics/mod.rs b/src/sources/nginx_metrics/mod.rs index cdde937dd05ef..142186558d34c 100644 --- a/src/sources/nginx_metrics/mod.rs +++ b/src/sources/nginx_metrics/mod.rs @@ -120,7 +120,7 @@ impl SourceConfig for NginxMetricsConfig { let duration = self.scrape_interval_secs; let shutdown = cx.shutdown; - Ok(Box::pin(async move { + Ok(crate::sources::opaque_source(async move { let mut interval = IntervalStream::new(time::interval(duration)).take_until(shutdown); while interval.next().await.is_some() { let start = Instant::now(); diff --git a/src/sources/okta/client.rs b/src/sources/okta/client.rs index 0caf7a8109b12..3ccfa8af0d9f0 100644 --- a/src/sources/okta/client.rs +++ b/src/sources/okta/client.rs @@ -153,7 +153,7 @@ impl SourceConfig for OktaConfig { warn_if_interval_too_low(self.timeout, self.interval); - Ok(run( + Ok(crate::sources::opaque_source(run( url, tls, cx.proxy, @@ -163,8 +163,7 @@ impl SourceConfig for OktaConfig { log_namespace, cx.shutdown, cx.out, - ) - .boxed()) + ))) } fn outputs(&self, global_log_namespace: LogNamespace) -> Vec { diff --git a/src/sources/postgresql_metrics.rs b/src/sources/postgresql_metrics.rs index 19dbcc59adbab..4d18ec8ae8a08 100644 --- a/src/sources/postgresql_metrics.rs +++ b/src/sources/postgresql_metrics.rs @@ -218,7 +218,7 @@ impl SourceConfig for PostgresqlMetricsConfig { let duration = self.scrape_interval_secs; let shutdown = cx.shutdown; - Ok(Box::pin(async move { + Ok(crate::sources::opaque_source(async move { let mut interval = IntervalStream::new(time::interval(duration)).take_until(shutdown); while interval.next().await.is_some() { let start = Instant::now(); diff --git a/src/sources/prometheus/scrape.rs b/src/sources/prometheus/scrape.rs index b142cd9b816eb..4e7a02484f72f 100644 --- a/src/sources/prometheus/scrape.rs +++ b/src/sources/prometheus/scrape.rs @@ -1,7 +1,6 @@ use std::{collections::HashMap, time::Duration}; use bytes::Bytes; -use futures_util::FutureExt; use http::{Uri, response::Parts}; use serde_with::serde_as; use snafu::ResultExt; @@ -172,7 +171,12 @@ impl SourceConfig for PrometheusScrapeConfig { shutdown: cx.shutdown, }; - Ok(call(inputs, builder, cx.out, HttpMethod::Get).boxed()) + Ok(crate::sources::opaque_source(call( + inputs, + builder, + cx.out, + HttpMethod::Get, + ))) } fn outputs(&self, _global_log_namespace: LogNamespace) -> Vec { diff --git a/src/sources/pulsar.rs b/src/sources/pulsar.rs index 78da7ff8eedc6..fcba3f5457a8a 100644 --- a/src/sources/pulsar.rs +++ b/src/sources/pulsar.rs @@ -205,7 +205,7 @@ impl SourceConfig for PulsarSourceConfig { .build()?; let acknowledgements = cx.do_acknowledgements(self.acknowledgements); - Ok(Box::pin(pulsar_source( + Ok(crate::sources::opaque_source(pulsar_source( consumer, decoder, cx.shutdown, diff --git a/src/sources/socket/mod.rs b/src/sources/socket/mod.rs index ab450a61e7581..2e864cc646ec7 100644 --- a/src/sources/socket/mod.rs +++ b/src/sources/socket/mod.rs @@ -167,7 +167,6 @@ impl SourceConfig for SocketConfig { cx.shutdown, cx.out, log_namespace, - cx.error_reporter, )) } Mode::UnixDatagram(config) => config.on_unix( @@ -925,7 +924,7 @@ mod test { assert_eq!(shutdown_result, Ok(true)); let source_result = source_handle.await.expect("source task should not panic"); - assert_eq!(source_result, Ok(())); + assert!(source_result.is_ok()); } #[tokio::test] @@ -1049,7 +1048,10 @@ mod test { sender: SourceSender, source_id: &ComponentKey, shutdown: &mut SourceShutdownCoordinator, - ) -> (SocketAddr, JoinHandle>) { + ) -> ( + SocketAddr, + JoinHandle>, + ) { let (shutdown_signal, _) = shutdown.register_source(source_id, false); init_udp_inner(sender, source_id, shutdown_signal, None, false).await } @@ -1084,7 +1086,10 @@ mod test { shutdown_signal: ShutdownSignal, config: Option, use_vector_namespace: bool, - ) -> (SocketAddr, JoinHandle>) { + ) -> ( + SocketAddr, + JoinHandle>, + ) { let (guard, address, mut config) = match config { Some(config) => match config.address() { SocketListenAddr::SocketAddr(addr) => (None, addr, config), @@ -1120,7 +1125,6 @@ mod test { schema_definitions: HashMap::default(), extra_context: Default::default(), metrics_storage: Default::default(), - error_reporter: Default::default(), }) .await .unwrap(); diff --git a/src/sources/socket/udp.rs b/src/sources/socket/udp.rs index ccdb16a97bb22..122ddd52a8e6d 100644 --- a/src/sources/socket/udp.rs +++ b/src/sources/socket/udp.rs @@ -20,7 +20,6 @@ use super::default_host_key; use crate::{ SourceSender, codecs::Decoder, - config::SourceErrorReporter, event::Event, internal_events::{ SocketBindError, SocketEventsReceived, SocketMode, SocketMulticastGroupJoinError, @@ -29,6 +28,7 @@ use crate::{ net, serde::default_decoding, shutdown::ShutdownSignal, + sources::SourceError, sources::{ Source, socket::SocketConfig, @@ -173,7 +173,6 @@ pub(super) fn udp( mut shutdown: ShutdownSignal, mut out: SourceSender, log_namespace: LogNamespace, - error_reporter: SourceErrorReporter, ) -> Source { Box::pin(async move { let listenfd = ListenFd::from_env(); @@ -184,7 +183,7 @@ pub(super) fn udp( mode: SocketMode::Udp, error: &error, }); - error_reporter.report(error); + SourceError::from(error) })?; if !config.multicast_groups.is_empty() { @@ -207,10 +206,11 @@ pub(super) fn udp( .join_multicast_v4(group_addr, interface) .map_err(|error| { emit!(SocketMulticastGroupJoinError { - error, + error: &error, group_addr, interface, - }) + }); + SourceError::from(error) })?; info!(message = "Joined multicast group.", group = %group_addr); } @@ -252,10 +252,11 @@ pub(super) fn udp( } } - return Err(emit!(SocketReceiveError { + emit!(SocketReceiveError { mode: SocketMode::Udp, - error - })); + error: &error + }); + return Err(SourceError::from(error)); } }; diff --git a/src/sources/splunk_hec/mod.rs b/src/sources/splunk_hec/mod.rs index 1ab2a2973ea1a..68101787f1800 100644 --- a/src/sources/splunk_hec/mod.rs +++ b/src/sources/splunk_hec/mod.rs @@ -295,6 +295,7 @@ impl SplunkConfig { .await .map_err(|err| { error!("An error occurred: {err:?}."); + err })?; Ok(()) diff --git a/src/sources/static_metrics.rs b/src/sources/static_metrics.rs index 1398562ec82d6..d7fe5a934eada 100644 --- a/src/sources/static_metrics.rs +++ b/src/sources/static_metrics.rs @@ -103,7 +103,7 @@ impl SourceConfig for StaticMetricsConfig { let metrics = self.metrics.clone(); - Ok(Box::pin( + Ok(crate::sources::opaque_source( StaticMetrics { namespace, metrics, diff --git a/src/sources/statsd/mod.rs b/src/sources/statsd/mod.rs index b4ee1f577ceff..573e2135361f5 100644 --- a/src/sources/statsd/mod.rs +++ b/src/sources/statsd/mod.rs @@ -216,9 +216,11 @@ impl GenerateConfig for StatsdConfig { impl SourceConfig for StatsdConfig { async fn build(&self, cx: SourceContext) -> crate::Result { match self { - StatsdConfig::Udp(config) => { - Ok(Box::pin(statsd_udp(config.clone(), cx.shutdown, cx.out))) - } + StatsdConfig::Udp(config) => Ok(crate::sources::opaque_source(statsd_udp( + config.clone(), + cx.shutdown, + cx.out, + ))), StatsdConfig::Tcp(config) => { let tls_config = config.tls.as_ref().map(|tls| tls.tls_config.clone()); let tls_client_metadata_key = config diff --git a/src/sources/syslog.rs b/src/sources/syslog.rs index 8202b73050e28..ea4cb65b515de 100644 --- a/src/sources/syslog.rs +++ b/src/sources/syslog.rs @@ -354,7 +354,8 @@ pub fn udp( emit!(SocketBindError { mode: SocketMode::Udp, error: &error, - }) + }); + super::SourceError::from(error) })?; if let Some(receive_buffer_bytes) = receive_buffer_bytes @@ -418,7 +419,7 @@ pub fn udp( Err(_) => { let (count, _) = stream.size_hint(); emit!(StreamClosedError { count }); - Err(()) + Err(super::SourceError::Opaque) } } }) diff --git a/src/sources/util/framestream.rs b/src/sources/util/framestream.rs index 743f6408df400..6c872de365617 100644 --- a/src/sources/util/framestream.rs +++ b/src/sources/util/framestream.rs @@ -427,7 +427,8 @@ pub fn build_framestream_tcp_source( emit!(SocketBindError { mode: SocketMode::Tcp, error: &error, - }) + }); + crate::sources::SourceError::Detailed(error) })?; info!( @@ -1239,7 +1240,10 @@ mod test { addr: &SocketAddr, frame_handler: impl TcpFrameHandler + Send + Sync + Clone + 'static, pipeline: SourceSender, - ) -> (JoinHandle>, SourceShutdownCoordinator) { + ) -> ( + JoinHandle>, + SourceShutdownCoordinator, + ) { let source_id = ComponentKey::from(source_id); let mut shutdown = SourceShutdownCoordinator::default(); let (shutdown_signal, _) = shutdown.register_source(&source_id, false); @@ -1261,7 +1265,7 @@ mod test { pipeline: SourceSender, ) -> ( PathBuf, - JoinHandle>, + JoinHandle>, SourceShutdownCoordinator, ) { let source_id = ComponentKey::from(source_id); @@ -1386,7 +1390,7 @@ mod test { mut sock_stream: U, rx: V, mut shutdown: SourceShutdownCoordinator, - source_handle: JoinHandle>, + source_handle: JoinHandle>, ) { //1 - send READY frame (with content_type) let content_type = Bytes::from(&b"test_content"[..]); @@ -1441,7 +1445,7 @@ mod test { mut sock_sink: T, mut sock_stream: U, mut shutdown: SourceShutdownCoordinator, - source_handle: JoinHandle>, + source_handle: JoinHandle>, ) { //1 - send READY frame (with content_type) let content_type = Bytes::from(&b"test_content"[..]); diff --git a/src/sources/util/http/prelude.rs b/src/sources/util/http/prelude.rs index 7afddbd9463d7..5c2cc3a901c82 100644 --- a/src/sources/util/http/prelude.rs +++ b/src/sources/util/http/prelude.rs @@ -274,6 +274,7 @@ pub trait HttpSource: Clone + Send + Sync + 'static { .await .map_err(|err| { error!("An error occurred: {err:?}."); + err })? .with_keepalive(keepalive_settings.tcp_keepalive); @@ -283,6 +284,7 @@ pub trait HttpSource: Clone + Send + Sync + 'static { .await .map_err(|err| { error!("An error occurred: {err:?}."); + err })?; Ok(()) diff --git a/src/sources/util/net/tcp/mod.rs b/src/sources/util/net/tcp/mod.rs index 5a896417b0500..b10c3f2eef539 100644 --- a/src/sources/util/net/tcp/mod.rs +++ b/src/sources/util/net/tcp/mod.rs @@ -43,6 +43,7 @@ use crate::{ TcpSocketTlsConnectionError, TcpSocketTlsHandshakeTimeout, TcpSourceConnectionClosed, }, net::is_graceful_tls_shutdown, + sources::SourceError, sources::util::{AfterReadExt, LenientFramedRead}, }; @@ -148,7 +149,7 @@ where mode: SocketMode::Tcp, error: &error, }); - cx.error_reporter.report(error); + SourceError::Detailed(error) })?; info!( diff --git a/src/sources/util/unix_datagram.rs b/src/sources/util/unix_datagram.rs index 49c9970524665..d6390d24884fe 100644 --- a/src/sources/util/unix_datagram.rs +++ b/src/sources/util/unix_datagram.rs @@ -55,7 +55,7 @@ pub fn build_unix_datagram_source( }); } - result + result.map_err(|_| crate::sources::SourceError::Opaque) })) } diff --git a/src/sources/vector/mod.rs b/src/sources/vector/mod.rs index 9c82d62e3401a..9c25953b323a6 100644 --- a/src/sources/vector/mod.rs +++ b/src/sources/vector/mod.rs @@ -234,6 +234,7 @@ impl SourceConfig for VectorConfig { ) .map_err(|error| { error!(message = "Source future failed.", %error); + crate::sources::SourceError::Detailed(error) }); Ok(Box::pin(source)) diff --git a/src/sources/websocket/config.rs b/src/sources/websocket/config.rs index 8214f302eb4ca..a4dd68f7c3b70 100644 --- a/src/sources/websocket/config.rs +++ b/src/sources/websocket/config.rs @@ -172,7 +172,7 @@ impl SourceConfig for WebSocketConfig { let source = WebSocketSource::new(self.clone(), params); - Ok(Box::pin(source.run(cx).map_err(|_err| ()))) + Ok(Box::pin(source.run(cx).map_err(Into::into))) } fn outputs(&self, global_log_namespace: LogNamespace) -> Vec { diff --git a/src/test_util/mock/sources/basic.rs b/src/test_util/mock/sources/basic.rs index c1c09477fcc38..88a4dd3fd86f8 100644 --- a/src/test_util/mock/sources/basic.rs +++ b/src/test_util/mock/sources/basic.rs @@ -127,7 +127,7 @@ impl SourceConfig for BasicSourceConfig { if let Err(e) = out.send_event(array).await { error!(message = "Error sending in sink..", %e); - return Err(()) + return Err(e.into()) } }, diff --git a/src/test_util/mock/sources/error.rs b/src/test_util/mock/sources/error.rs index e6fef2845e641..90eefc60a0bdc 100644 --- a/src/test_util/mock/sources/error.rs +++ b/src/test_util/mock/sources/error.rs @@ -23,7 +23,7 @@ impl_generate_config_from_default!(ErrorSourceConfig); #[typetag::serde(name = "test_error")] impl SourceConfig for ErrorSourceConfig { async fn build(&self, _cx: SourceContext) -> crate::Result { - Ok(err(()).boxed()) + Ok(err(crate::sources::SourceError::Opaque).boxed()) } fn outputs(&self, _global_log_namespace: LogNamespace) -> Vec { diff --git a/src/test_util/mock/sources/tripwire.rs b/src/test_util/mock/sources/tripwire.rs index dfacfbe7e3455..b2842a30f97f7 100644 --- a/src/test_util/mock/sources/tripwire.rs +++ b/src/test_util/mock/sources/tripwire.rs @@ -61,7 +61,8 @@ impl SourceConfig for TripwireSourceConfig { .boxed(), ) .map(|_| drop(out)) - .unit_error(), + .unit_error() + .map(|result| result.map_err(|_| crate::sources::SourceError::Opaque)), )) } diff --git a/src/topology/builder.rs b/src/topology/builder.rs index 57536c79ebbe8..5b176ee829c20 100644 --- a/src/topology/builder.rs +++ b/src/topology/builder.rs @@ -35,6 +35,7 @@ use vector_lib::{ internal_event::{self, CountByteSize, EventsSent, InternalEventHandle as _, Registered}, latency::LatencyRecorder, schema::Definition, + source::SourceError, source_sender::{DEFAULT_CHUNK_SIZE_EVENTS, SourceSenderItem}, transform::update_runtime_schema_definition, }; @@ -51,9 +52,8 @@ use crate::{ SourceSender, config::{ ComponentKey, Config, DataType, EnrichmentTableConfig, Input, Inputs, OutputId, - ProxyConfig, SinkContext, SinkOuter, SourceContext, SourceErrorReporter, SourceOuter, - TransformContext, TransformOuter, TransformOutput, enrichment_table_sinks, - enrichment_table_sources, + ProxyConfig, SinkContext, SinkOuter, SourceContext, SourceOuter, TransformContext, + TransformOuter, TransformOutput, enrichment_table_sinks, enrichment_table_sources, }, cpu_time::{CpuTimedExt, spawn_timed}, event::{EventArray, EventContainer}, @@ -413,7 +413,6 @@ impl<'a> Builder<'a> { .shutdown_coordinator .register_source(key, INTERNAL_SOURCES.contains(&typetag)); - let error_reporter = SourceErrorReporter::default(); let context = SourceContext { key: key.clone(), globals: self.config.global.clone(), @@ -426,7 +425,6 @@ impl<'a> Builder<'a> { schema_definitions, schema: self.config.schema, extra_context: self.extra_context.clone(), - error_reporter: error_reporter.clone(), }; let server = match source.inner.build(context).await { Err(error) => { @@ -460,8 +458,9 @@ impl<'a> Builder<'a> { Ok(e) = &mut pump_error_rx => Err(e), // The source finished normally. - result = server => result.map_err(|_| { - error_reporter.take().map_or(TaskError::Opaque, TaskError::wrapped) + result = server => result.map_err(|error| match error { + SourceError::Opaque => TaskError::Opaque, + SourceError::Detailed(error) => TaskError::wrapped(error), }), }; diff --git a/src/topology/test/crash.rs b/src/topology/test/crash.rs index ec78130bf5b74..71b374581d8a8 100644 --- a/src/topology/test/crash.rs +++ b/src/topology/test/crash.rs @@ -17,6 +17,9 @@ use crate::{ }, }; +#[cfg(feature = "sources-syslog")] +use crate::sources::syslog::{Mode as SyslogMode, SyslogConfig}; + #[tokio::test] async fn test_tcp_socket_bind_error_is_reported() { trace_init(); @@ -69,6 +72,37 @@ async fn test_udp_socket_bind_error_is_reported() { topology.stop().await; } +#[cfg(feature = "sources-syslog")] +#[tokio::test] +async fn test_udp_syslog_bind_error_is_reported() { + trace_init(); + + let (_guard, address) = next_addr(); + let _socket = UdpSocket::bind(address).unwrap(); + let expected_error = UdpSocket::bind(address).unwrap_err().to_string(); + + let mut config = Config::builder(); + config.add_source( + "in", + SyslogConfig::from_mode(SyslogMode::Udp { + address: address.into(), + receive_buffer_bytes: None, + }), + ); + config.add_sink("out", &["in"], basic_sink(1).1); + + let (topology, mut errors) = start_topology(config.build().unwrap(), false).await; + let error = timeout(Duration::from_secs(5), errors.recv()) + .await + .expect("source error timed out") + .expect("source error missing"); + assert!(matches!( + error, + ShutdownError::SourceAborted { error, .. } if error == expected_error + )); + topology.stop().await; +} + /// Ensures that an unrelated source completing immediately with an error does not prematurely terminate the topology. #[tokio::test] async fn test_source_error() { From 97a039c66f2f0dbc206c254a6825e07e59fabdcc Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Mon, 12 Oct 2026 14:14:56 +0200 Subject: [PATCH 03/18] fix(topology): log panic payload as a field --- .../component_panic_details.enhancement.md | 3 + src/topology/builder.rs | 3 +- src/topology/mod.rs | 11 ++- src/topology/task.rs | 15 ++- src/topology/test/mod.rs | 1 + src/topology/test/panic.rs | 93 +++++++++++++++++++ 6 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 changelog.d/component_panic_details.enhancement.md create mode 100644 src/topology/test/panic.rs diff --git a/changelog.d/component_panic_details.enhancement.md b/changelog.d/component_panic_details.enhancement.md new file mode 100644 index 0000000000000..92595e8b623de --- /dev/null +++ b/changelog.d/component_panic_details.enhancement.md @@ -0,0 +1,3 @@ +Include the panic payload as a structured `panic` field when a component task panics. + +authors: gwenaskell diff --git a/src/topology/builder.rs b/src/topology/builder.rs index 5b176ee829c20..f75980450c703 100644 --- a/src/topology/builder.rs +++ b/src/topology/builder.rs @@ -1368,7 +1368,8 @@ impl Runner { self.send_outputs(&mut outputs_buf).await .map_err(TaskError::wrapped)?; } - _ => unreachable!("join error or bad poll"), + Some(Err(error)) => return Err(TaskError::from(error)), + None => unreachable!("bad poll"), } } diff --git a/src/topology/mod.rs b/src/topology/mod.rs index 24e3ff4953de8..a6a560c47bd89 100644 --- a/src/topology/mod.rs +++ b/src/topology/mod.rs @@ -65,10 +65,17 @@ async fn handle_errors( AssertUnwindSafe(task) .catch_unwind() .await - .map_err(|_| TaskError::Panicked) + .map_err(TaskError::panicked) .and_then(|res| res) .map_err(|e| { - error!("An error occurred that Vector couldn't handle: {e}."); + if let TaskError::Panicked { panic } = &e { + error!( + panic = panic.as_str(), + "An error occurred that Vector couldn't handle: {e}." + ); + } else { + error!("An error occurred that Vector couldn't handle: {e}."); + } _ = abort_tx.send(error(e.to_string())); e }) diff --git a/src/topology/task.rs b/src/topology/task.rs index 9364b13e7f42f..6e118ff9e7ccc 100644 --- a/src/topology/task.rs +++ b/src/topology/task.rs @@ -1,4 +1,5 @@ use std::{ + any::Any, fmt, future::Future, pin::Pin, @@ -27,7 +28,7 @@ pub(crate) enum TaskError { #[snafu(display("the task was cancelled before it completed"))] Cancelled, #[snafu(display("the task panicked and was aborted"))] - Panicked, + Panicked { panic: String }, #[snafu(display("the task completed with an error"))] Opaque, #[snafu(display("{}", source))] @@ -38,6 +39,16 @@ impl TaskError { pub fn wrapped(e: crate::Error) -> Self { Self::Wrapped { source: e } } + + pub fn panicked(payload: Box) -> Self { + let panic = payload + .downcast_ref::() + .map(String::as_str) + .or_else(|| payload.downcast_ref::<&str>().copied()) + .unwrap_or("non-string panic payload") + .to_owned(); + Self::Panicked { panic } + } } impl From for TaskError { @@ -45,7 +56,7 @@ impl From for TaskError { if e.is_cancelled() { Self::Cancelled } else { - Self::Panicked + Self::panicked(e.into_panic()) } } } diff --git a/src/topology/test/mod.rs b/src/topology/test/mod.rs index 00807cc5f03b8..dd5e5762f57aa 100644 --- a/src/topology/test/mod.rs +++ b/src/topology/test/mod.rs @@ -43,6 +43,7 @@ mod doesnt_reload; #[cfg(all(feature = "sources-http_server", feature = "sinks-http"))] mod end_to_end; mod latency_metrics; +mod panic; #[cfg(all( feature = "sources-prometheus", feature = "sinks-prometheus", diff --git a/src/topology/test/panic.rs b/src/topology/test/panic.rs new file mode 100644 index 0000000000000..b70d942fa00a1 --- /dev/null +++ b/src/topology/test/panic.rs @@ -0,0 +1,93 @@ +use std::{ + fmt, + sync::{Arc, Mutex}, +}; + +use tokio::sync::mpsc; +use tracing::{ + Event, Subscriber, + field::{Field, Visit}, +}; +use tracing_subscriber::{ + Layer, + layer::{Context, SubscriberExt}, +}; + +use crate::{ + config::ComponentKey, + signal::ShutdownError, + topology::{ + handle_errors, + task::{TaskError, TaskOutput}, + }, +}; + +#[derive(Default)] +struct LogFields { + message: Option, + panic: Option, +} + +impl Visit for LogFields { + fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { + if field.name() == "message" { + self.message = Some(format!("{value:?}")); + } + } + + fn record_str(&mut self, field: &Field, value: &str) { + if field.name() == "panic" { + self.panic = Some(value.to_owned()); + } + } +} + +struct Capture(Arc>>); + +impl Layer for Capture { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let mut fields = LogFields::default(); + event.record(&mut fields); + self.0.lock().unwrap().push(fields); + } +} + +#[tokio::test(flavor = "current_thread")] +async fn panic_payload_is_logged_as_a_field() { + let logs = Arc::new(Mutex::new(Vec::new())); + let subscriber = tracing_subscriber::registry().with(Capture(Arc::clone(&logs))); + let _guard = tracing::subscriber::set_default(subscriber); + let (abort_tx, mut abort_rx) = mpsc::unbounded_channel(); + + let task = async { + std::panic::panic_any("source panic value"); + #[allow(unreachable_code)] + Ok(TaskOutput::Source) + }; + let result = handle_errors(task, abort_tx, |error| ShutdownError::SourceAborted { + key: ComponentKey::from("source"), + error, + }) + .await; + + assert!(matches!(result, Err(TaskError::Panicked { .. }))); + let shutdown_error = abort_rx.recv().await.unwrap(); + assert!(!shutdown_error.to_string().contains("source panic value")); + + let logs = logs.lock().unwrap(); + let log = logs.iter().find(|log| log.panic.is_some()).unwrap(); + assert_eq!(log.panic.as_deref(), Some("source panic value")); + assert!(!log.message.as_ref().unwrap().contains("source panic value")); +} + +#[tokio::test(flavor = "current_thread")] +async fn joined_task_panic_retains_payload() { + let error = tokio::spawn(async { std::panic::panic_any(String::from("child panic value")) }) + .await + .unwrap_err(); + let error = TaskError::from(error); + assert!(matches!( + error, + TaskError::Panicked { panic } if panic == "child panic value" + )); +} From c7982e21668701a966e71a9dd230411e7fcff8a1 Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Mon, 12 Oct 2026 14:57:31 +0200 Subject: [PATCH 04/18] fix(sink): preserve runtime error details --- changelog.d/sink_runtime_error_context.fix.md | 3 ++ lib/vector-core/src/sink.rs | 54 +++++++++++++++---- src/sinks/websocket_server/config.rs | 2 +- src/sinks/websocket_server/sink.rs | 16 +++--- src/test_util/components.rs | 7 +++ src/topology/builder.rs | 7 ++- src/topology/test/crash.rs | 32 +++++++++++ 7 files changed, 103 insertions(+), 18 deletions(-) create mode 100644 changelog.d/sink_runtime_error_context.fix.md diff --git a/changelog.d/sink_runtime_error_context.fix.md b/changelog.d/sink_runtime_error_context.fix.md new file mode 100644 index 0000000000000..6a0abf69fd678 --- /dev/null +++ b/changelog.d/sink_runtime_error_context.fix.md @@ -0,0 +1,3 @@ +Preserve sink runtime errors in topology task failures. WebSocket server bind errors now include their causes. + +authors: gwenaskell diff --git a/lib/vector-core/src/sink.rs b/lib/vector-core/src/sink.rs index ab866c1ce5bd9..844692b1921b9 100644 --- a/lib/vector-core/src/sink.rs +++ b/lib/vector-core/src/sink.rs @@ -7,9 +7,28 @@ use futures::{ use crate::event::{Event, EventArray, EventContainer, into_event_stream}; +/// Error returned by a running sink. +#[derive(Debug)] +pub enum SinkError { + /// The sink did not retain its failure reason. + Opaque, + /// The error that stopped the sink. + Detailed(vector_common::Error), +} + +impl From for SinkError +where + E: std::error::Error + Send + Sync + 'static, +{ + fn from(error: E) -> Self { + Self::Detailed(Box::new(error)) + } +} + pub enum VectorSink { Sink(Box + Send + Unpin>), Stream(Box + Send>), + DetailedStream(Box + Send>), } impl VectorSink { @@ -17,11 +36,16 @@ impl VectorSink { /// /// # Errors /// - /// It is unclear under what conditions this function will error. - pub async fn run(self, input: impl Stream + Send) -> Result<(), ()> { + /// Returns an error if the sink reports a failure. + pub async fn run(self, input: impl Stream + Send) -> Result<(), SinkError> { match self { - Self::Sink(sink) => input.map(Ok).forward(sink).await, - Self::Stream(s) => s.run(Box::pin(input)).await, + Self::Sink(sink) => input + .map(Ok) + .forward(sink) + .await + .map_err(|()| SinkError::Opaque), + Self::Stream(s) => s.run(Box::pin(input)).await.map_err(|()| SinkError::Opaque), + Self::DetailedStream(s) => s.run(Box::pin(input)).await, } } @@ -30,7 +54,7 @@ impl VectorSink { /// # Errors /// /// See `VectorSink::run` for errors. - pub async fn run_events(self, input: I) -> Result<(), ()> + pub async fn run_events(self, input: I) -> Result<(), SinkError> where I: IntoIterator + Send, I::IntoIter: Send, @@ -78,6 +102,14 @@ impl VectorSink { let sink = Box::new(sink); VectorSink::Stream(Box::new(EventStream { sink })) } + + /// Converts a sink that retains its runtime errors into a `VectorSink`. + pub fn from_detailed_event_streamsink( + sink: impl StreamSink + Send + 'static, + ) -> Self { + let sink = Box::new(sink); + VectorSink::DetailedStream(Box::new(EventStream { sink })) + } } impl fmt::Debug for VectorSink { @@ -89,8 +121,8 @@ impl fmt::Debug for VectorSink { // === StreamSink === #[async_trait::async_trait] -pub trait StreamSink { - async fn run(self: Box, input: stream::BoxStream<'_, T>) -> Result<(), ()>; +pub trait StreamSink { + async fn run(self: Box, input: stream::BoxStream<'_, T>) -> Result<(), E>; } /// Wrapper for sinks implementing `Sink` to implement @@ -175,8 +207,12 @@ struct EventStream { } #[async_trait::async_trait] -impl + Send> StreamSink for EventStream { - async fn run(self: Box, input: stream::BoxStream<'_, EventArray>) -> Result<(), ()> { +impl StreamSink for EventStream +where + T: StreamSink + Send, + E: Send, +{ + async fn run(self: Box, input: stream::BoxStream<'_, EventArray>) -> Result<(), E> { let input = Box::pin(input.flat_map(into_event_stream)); self.sink.run(input).await } diff --git a/src/sinks/websocket_server/config.rs b/src/sinks/websocket_server/config.rs index 310b7d8b5fa1a..a8a7ecf057360 100644 --- a/src/sinks/websocket_server/config.rs +++ b/src/sinks/websocket_server/config.rs @@ -178,7 +178,7 @@ impl ValidatedSink for WebSocketListenerSinkConfig { let ws_sink = WebSocketListenerSink::from_validated(self.clone(), validated, tls, cx)?; Ok(( - VectorSink::from_event_streamsink(ws_sink), + VectorSink::from_detailed_event_streamsink(ws_sink), Box::pin(async move { Ok(()) }), )) } diff --git a/src/sinks/websocket_server/sink.rs b/src/sinks/websocket_server/sink.rs index 6da5350d09756..43d2ad88ad6dc 100644 --- a/src/sinks/websocket_server/sink.rs +++ b/src/sinks/websocket_server/sink.rs @@ -49,7 +49,7 @@ use vector_lib::{ internal_event::{ ByteSize, BytesSent, CountByteSize, EventsSent, InternalEventHandle, Output, Protocol, }, - sink::StreamSink, + sink::{SinkError, StreamSink}, tls::{MaybeTlsIncomingStream, MaybeTlsListener, MaybeTlsSettings}, }; @@ -355,8 +355,8 @@ impl WebSocketListenerSink { } #[async_trait] -impl StreamSink for WebSocketListenerSink { - async fn run(mut self: Box, input: BoxStream<'_, Event>) -> Result<(), ()> { +impl StreamSink for WebSocketListenerSink { + async fn run(mut self: Box, input: BoxStream<'_, Event>) -> Result<(), SinkError> { let input = input.fuse().peekable(); pin_mut!(input); @@ -364,7 +364,11 @@ impl StreamSink for WebSocketListenerSink { let events_sent = register!(EventsSent::from(Output(None))); let encode_as_binary = self.encoder.serializer().is_binary(); - let listener = self.tls.bind(&self.address).await.map_err(|_| ())?; + let listener = self + .tls + .bind(&self.address) + .await + .map_err(SinkError::from)?; let peers = Arc::new(Mutex::new(HashMap::default())); let message_buffer = Arc::new(Mutex::new(VecDeque::with_capacity( @@ -680,7 +684,7 @@ mod tests { .unwrap(); run_and_assert_sink_compliance( - VectorSink::from_event_streamsink(sink), + VectorSink::from_detailed_event_streamsink(sink), stream::once(ready(event)), &SINK_TAGS, ) @@ -834,7 +838,7 @@ mod tests { let sink = WebSocketListenerSink::new(config, SinkContext::default()).unwrap(); let compliance_assertion = tokio::spawn(run_and_assert_sink_compliance( - VectorSink::from_event_streamsink(sink), + VectorSink::from_detailed_event_streamsink(sink), events, &SINK_TAGS, )); diff --git a/src/test_util/components.rs b/src/test_util/components.rs index a5f339c0fc70d..3db1ebc11e4b0 100644 --- a/src/test_util/components.rs +++ b/src/test_util/components.rs @@ -711,6 +711,13 @@ where .await .expect("Sending event stream to sink failed"); } + VectorSink::DetailedStream(stream) => { + let events = events.filter_map(|x| async move { x.ok() }).boxed(); + stream + .run(events) + .await + .expect("Sending event stream to sink failed"); + } } SINK_TESTS.assert(tags); } diff --git a/src/topology/builder.rs b/src/topology/builder.rs index f75980450c703..798e56978a45c 100644 --- a/src/topology/builder.rs +++ b/src/topology/builder.rs @@ -765,9 +765,12 @@ impl<'a> Builder<'a> { debug!("Sink finished normally."); TaskOutput::Sink(rx) }) - .map_err(|_| { + .map_err(|error| { debug!("Sink finished with an error."); - TaskError::Opaque + match error { + vector_lib::sink::SinkError::Opaque => TaskError::Opaque, + vector_lib::sink::SinkError::Detailed(error) => TaskError::wrapped(error), + } }) }; diff --git a/src/topology/test/crash.rs b/src/topology/test/crash.rs index 71b374581d8a8..55b1f76d5c73d 100644 --- a/src/topology/test/crash.rs +++ b/src/topology/test/crash.rs @@ -17,9 +17,41 @@ use crate::{ }, }; +#[cfg(feature = "sinks-websocket_server")] +use crate::sinks::websocket_server::WebSocketListenerSinkConfig; #[cfg(feature = "sources-syslog")] use crate::sources::syslog::{Mode as SyslogMode, SyslogConfig}; +#[cfg(feature = "sinks-websocket_server")] +#[tokio::test] +async fn test_websocket_sink_bind_error_is_reported() { + trace_init(); + + let (_guard, address) = next_addr(); + let _listener = TcpListener::bind(address).unwrap(); + let expected_error = TcpListener::bind(address).unwrap_err().to_string(); + let (_source_guard, source_address) = next_addr(); + + let sink = WebSocketListenerSinkConfig { + address, + ..Default::default() + }; + let mut config = Config::builder(); + config.add_source("in", SocketConfig::make_basic_tcp_config(source_address)); + config.add_sink("out", &["in"], sink); + + let (topology, mut errors) = start_topology(config.build().unwrap(), false).await; + let error = timeout(Duration::from_secs(5), errors.recv()) + .await + .expect("sink error timed out") + .expect("sink error missing"); + assert!(matches!( + error, + ShutdownError::SinkAborted { error, .. } if error.contains(&expected_error) + )); + topology.stop().await; +} + #[tokio::test] async fn test_tcp_socket_bind_error_is_reported() { trace_init(); From 8d052cbd0998e50ccab64a2400aa5e89aa7d6501 Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Mon, 12 Oct 2026 15:56:06 +0200 Subject: [PATCH 05/18] fix(sink): retain console runtime error causes --- changelog.d/sink_runtime_error_context.fix.md | 2 +- src/sinks/console/config.rs | 4 +- src/sinks/console/sink.rs | 45 ++++++++++++++++--- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/changelog.d/sink_runtime_error_context.fix.md b/changelog.d/sink_runtime_error_context.fix.md index 6a0abf69fd678..a33cd7c21be7c 100644 --- a/changelog.d/sink_runtime_error_context.fix.md +++ b/changelog.d/sink_runtime_error_context.fix.md @@ -1,3 +1,3 @@ -Preserve sink runtime errors in topology task failures. WebSocket server bind errors now include their causes. +Preserve sink runtime errors in topology task failures. WebSocket server bind errors and console encoding or write errors now include their causes. authors: gwenaskell diff --git a/src/sinks/console/config.rs b/src/sinks/console/config.rs index 7d939b0390ec9..d442f1ee2589b 100644 --- a/src/sinks/console/config.rs +++ b/src/sinks/console/config.rs @@ -102,12 +102,12 @@ impl ValidatedSink for ConsoleSinkConfig { let encoder = Encoder::::new(framer, serializer); let sink: VectorSink = match self.target { - Target::Stdout => VectorSink::from_event_streamsink(WriterSink { + Target::Stdout => VectorSink::from_detailed_event_streamsink(WriterSink { output: io::stdout(), transformer, encoder, }), - Target::Stderr => VectorSink::from_event_streamsink(WriterSink { + Target::Stderr => VectorSink::from_detailed_event_streamsink(WriterSink { output: io::stderr(), transformer, encoder, diff --git a/src/sinks/console/sink.rs b/src/sinks/console/sink.rs index b6c5fc39ee95c..66e1d56dec0ab 100644 --- a/src/sinks/console/sink.rs +++ b/src/sinks/console/sink.rs @@ -9,6 +9,7 @@ use vector_lib::{ internal_event::{ ByteSize, BytesSent, CountByteSize, EventsSent, InternalEventHandle as _, Output, Protocol, }, + sink::SinkError, }; use crate::{ @@ -24,11 +25,11 @@ pub struct WriterSink { } #[async_trait] -impl StreamSink for WriterSink +impl StreamSink for WriterSink where T: io::AsyncWrite + Send + Sync + Unpin, { - async fn run(mut self: Box, mut input: BoxStream<'_, Event>) -> Result<(), ()> { + async fn run(mut self: Box, mut input: BoxStream<'_, Event>) -> Result<(), SinkError> { let bytes_sent = register!(BytesSent::from(Protocol("console".into(),))); let events_sent = register!(EventsSent::from(Output(None))); while let Some(mut event) = input.next().await { @@ -37,9 +38,9 @@ where let finalizers = event.take_finalizers(); let mut bytes = BytesMut::new(); - self.encoder.encode(event, &mut bytes).map_err(|_| { - // Error is handled by `Encoder`. + self.encoder.encode(event, &mut bytes).map_err(|error| { finalizers.update_status(EventStatus::Errored); + SinkError::from(error) })?; if let Err(error) = self.output.write_all(&bytes).await { @@ -47,7 +48,7 @@ where // so stop the sink. error!(message = "Error writing to output. Stopping sink.", %error, internal_log_rate_limit = false); finalizers.update_status(EventStatus::Errored); - return Err(()); + return Err(SinkError::from(error)); } finalizers.update_status(EventStatus::Delivered); @@ -90,10 +91,42 @@ mod test { }; run_and_assert_sink_compliance( - VectorSink::from_event_streamsink(sink), + VectorSink::from_detailed_event_streamsink(sink), stream::once(ready(event)), &SINK_TAGS, ) .await; } + + #[tokio::test] + async fn write_error_retains_cause() { + let (output, input) = io::duplex(64); + drop(input); + + let encoder = Encoder::::new( + NewlineDelimitedEncoder::default().into(), + JsonSerializerConfig::default().build().into(), + ); + let sink = WriterSink { + output, + transformer: Transformer::default(), + encoder, + }; + + let error = VectorSink::from_detailed_event_streamsink(sink) + .run_events([Event::Log(LogEvent::from("foo"))]) + .await + .expect_err("writing to a closed pipe should fail"); + + match error { + SinkError::Detailed(error) => assert_eq!( + error + .downcast_ref::() + .expect("the sink should retain the I/O error") + .kind(), + io::ErrorKind::BrokenPipe + ), + SinkError::Opaque => panic!("the sink should retain the I/O error"), + } + } } From c1afff821aaacdcf207ee6c929a8266ed243f460 Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Mon, 12 Oct 2026 16:26:15 +0200 Subject: [PATCH 06/18] fix(topology): retain more component runtime errors --- .../component_panic_details.enhancement.md | 3 - changelog.d/sink_runtime_error_context.fix.md | 3 - changelog.d/socket_bind_error_context.fix.md | 2 +- src/internal_events/amqp.rs | 6 +- src/internal_events/journald.rs | 6 +- src/sinks/prometheus/exporter.rs | 27 ++++---- src/sources/amqp.rs | 16 +++-- src/sources/journald.rs | 10 ++- src/sources/kafka.rs | 6 +- src/sources/mqtt/config.rs | 2 +- src/sources/mqtt/source.rs | 12 ++-- src/sources/statsd/mod.rs | 21 +++--- src/topology/test/crash.rs | 64 +++++++++++++++++++ 13 files changed, 126 insertions(+), 52 deletions(-) delete mode 100644 changelog.d/component_panic_details.enhancement.md delete mode 100644 changelog.d/sink_runtime_error_context.fix.md diff --git a/changelog.d/component_panic_details.enhancement.md b/changelog.d/component_panic_details.enhancement.md deleted file mode 100644 index 92595e8b623de..0000000000000 --- a/changelog.d/component_panic_details.enhancement.md +++ /dev/null @@ -1,3 +0,0 @@ -Include the panic payload as a structured `panic` field when a component task panics. - -authors: gwenaskell diff --git a/changelog.d/sink_runtime_error_context.fix.md b/changelog.d/sink_runtime_error_context.fix.md deleted file mode 100644 index a33cd7c21be7c..0000000000000 --- a/changelog.d/sink_runtime_error_context.fix.md +++ /dev/null @@ -1,3 +0,0 @@ -Preserve sink runtime errors in topology task failures. WebSocket server bind errors and console encoding or write errors now include their causes. - -authors: gwenaskell diff --git a/changelog.d/socket_bind_error_context.fix.md b/changelog.d/socket_bind_error_context.fix.md index 740316f09ee07..ce6229e43036b 100644 --- a/changelog.d/socket_bind_error_context.fix.md +++ b/changelog.d/socket_bind_error_context.fix.md @@ -1,3 +1,3 @@ -Preserve source errors in topology task failures, including socket bind errors that previously appeared as generic task failures. +Retain underlying errors from several sources and sinks in topology task failures, including bind, subscription, checkpoint, AMQP consumer, and console errors. Include component panic payloads in a structured `panic` field. authors: gwenaskell diff --git a/src/internal_events/amqp.rs b/src/internal_events/amqp.rs index e7864acc44e98..18b770ad72851 100644 --- a/src/internal_events/amqp.rs +++ b/src/internal_events/amqp.rs @@ -27,11 +27,11 @@ pub mod source { } #[derive(Debug, NamedInternalEvent)] - pub struct AmqpEventError { - pub error: lapin::Error, + pub struct AmqpEventError<'a> { + pub error: &'a lapin::Error, } - impl InternalEvent for AmqpEventError { + impl InternalEvent for AmqpEventError<'_> { fn emit(self) { error!(message = "Failed to read message.", error = ?self.error, diff --git a/src/internal_events/journald.rs b/src/internal_events/journald.rs index 02f6300cb6905..af7b34569806f 100644 --- a/src/internal_events/journald.rs +++ b/src/internal_events/journald.rs @@ -98,12 +98,12 @@ impl InternalEvent for JournaldCheckpointSetError { } #[derive(Debug, NamedInternalEvent)] -pub struct JournaldCheckpointFileOpenError { - pub error: std::io::Error, +pub struct JournaldCheckpointFileOpenError<'a> { + pub error: &'a std::io::Error, pub path: String, } -impl InternalEvent for JournaldCheckpointFileOpenError { +impl InternalEvent for JournaldCheckpointFileOpenError<'_> { fn emit(self) { error!( message = "Unable to open checkpoint file.", diff --git a/src/sinks/prometheus/exporter.rs b/src/sinks/prometheus/exporter.rs index 6ecb8e2fffd16..ac347e18bcd8f 100644 --- a/src/sinks/prometheus/exporter.rs +++ b/src/sinks/prometheus/exporter.rs @@ -29,6 +29,7 @@ use vector_lib::{ ByteSize, BytesSent, CountByteSize, EventsSent, InternalEventHandle as _, Output, Protocol, Registered, }, + sink::SinkError, }; use super::collector::{MetricCollector, StringCollector, metric_identifiers_have_no_line_breaks}; @@ -219,7 +220,10 @@ impl ValidatedSink for PrometheusExporterConfig { let sink = PrometheusExporter::new(self.clone()); let healthcheck = future::ok(()).boxed(); - Ok((VectorSink::from_event_streamsink(sink), healthcheck)) + Ok(( + VectorSink::from_detailed_event_streamsink(sink), + healthcheck, + )) } } @@ -540,11 +544,12 @@ impl PrometheusExporter { } #[async_trait] -impl StreamSink for PrometheusExporter { - async fn run(mut self: Box, mut input: BoxStream<'_, Event>) -> Result<(), ()> { - self.start_server_if_needed() - .await - .map_err(|error| error!("Failed to start Prometheus exporter: {error}."))?; +impl StreamSink for PrometheusExporter { + async fn run(mut self: Box, mut input: BoxStream<'_, Event>) -> Result<(), SinkError> { + self.start_server_if_needed().await.map_err(|error| { + error!("Failed to start Prometheus exporter: {error}."); + SinkError::Detailed(error) + })?; let mut last_flush = Instant::now(); let flush_period = self.config.flush_period_secs; @@ -1187,7 +1192,7 @@ mod tests { let sink = PrometheusExporter::new(config); let metrics_handle = Arc::clone(&sink.metrics); - let sink = VectorSink::from_event_streamsink(sink); + let sink = VectorSink::from_detailed_event_streamsink(sink); sink.run(stream::iter(events).map(Into::into)) .await .unwrap(); @@ -1227,7 +1232,7 @@ mod tests { let metrics_handle = Arc::clone(&sink.metrics); - let sink = VectorSink::from_event_streamsink(sink); + let sink = VectorSink::from_detailed_event_streamsink(sink); let input_events = stream::iter(events).map(Into::into); sink.run(input_events).await.unwrap(); @@ -1340,7 +1345,7 @@ mod tests { .map(Event::Metric) .collect::>(); - let sink = VectorSink::from_event_streamsink(sink); + let sink = VectorSink::from_detailed_event_streamsink(sink); let input_events = stream::iter(events).map(Into::into); sink.run(input_events).await.unwrap(); @@ -1457,7 +1462,7 @@ mod tests { .map(Event::Metric) .collect::>(); - let sink = VectorSink::from_event_streamsink(sink); + let sink = VectorSink::from_detailed_event_streamsink(sink); let input_events = stream::iter(events).map(Into::into); sink.run(input_events).await.unwrap(); @@ -1526,7 +1531,7 @@ mod tests { .map(Event::Metric) .collect::>(); - let sink = VectorSink::from_event_streamsink(sink); + let sink = VectorSink::from_detailed_event_streamsink(sink); let input_events = stream::iter(events).map(Into::into); sink.run(input_events).await.unwrap(); diff --git a/src/sources/amqp.rs b/src/sources/amqp.rs index 95d39a3c304b1..165e99887c53b 100644 --- a/src/sources/amqp.rs +++ b/src/sources/amqp.rs @@ -226,7 +226,7 @@ pub(crate) async fn amqp_source( .await .map_err(|source| BuildError::AmqpCreateError { source })?; - Ok(crate::sources::opaque_source(run_amqp_source( + Ok(Box::pin(run_amqp_source( config, shutdown, out, @@ -318,9 +318,11 @@ async fn receive_event( log_namespace: LogNamespace, finalizer: Option<&UnorderedFinalizer>, msg: Delivery, -) -> Result<(), ()> { +) -> Result<(), crate::sources::SourceError> { let payload = Cursor::new(Bytes::copy_from_slice(&msg.data)); - let decoder = config.decoder(log_namespace).map_err(|_e| ())?; + let decoder = config + .decoder(log_namespace) + .map_err(crate::sources::SourceError::Detailed)?; let mut stream = DecoderFramedRead::new(payload, decoder); // Extract timestamp from AMQP message @@ -428,7 +430,7 @@ async fn run_amqp_source( channel: Channel, log_namespace: LogNamespace, acknowledgements: bool, -) -> Result<(), ()> { +) -> Result<(), crate::sources::SourceError> { let (finalizer, mut ack_stream) = UnorderedFinalizer::::maybe_new(acknowledgements, Some(shutdown.clone())); @@ -440,6 +442,7 @@ async fn run_amqp_source( .await .map_err(|error| { error!(message = "Failed to apply basic_qos.", ?error); + crate::sources::SourceError::from(error) })?; } @@ -454,6 +457,7 @@ async fn run_amqp_source( .await .map_err(|error| { error!(message = "Failed to consume.", ?error); + crate::sources::SourceError::from(error) })? .fuse(); let mut shutdown = shutdown.fuse(); @@ -469,8 +473,8 @@ async fn run_amqp_source( if let Some(try_m) = opt_m { match try_m { Err(error) => { - emit!(AmqpEventError { error }); - return Err(()); + emit!(AmqpEventError { error: &error }); + return Err(crate::sources::SourceError::from(error)); } Ok(msg) => { receive_event(&config, &mut out, log_namespace, finalizer.as_ref(), msg).await? diff --git a/src/sources/journald.rs b/src/sources/journald.rs index e4b5e834f2155..07b5b3b622a81 100644 --- a/src/sources/journald.rs +++ b/src/sources/journald.rs @@ -395,7 +395,7 @@ impl SourceConfig for JournaldConfig { let acknowledgements = cx.do_acknowledgements(self.acknowledgements); let log_namespace = cx.log_namespace(self.log_namespace); - Ok(crate::sources::opaque_source( + Ok(Box::pin( JournaldSource { include_matches, exclude_matches, @@ -441,18 +441,22 @@ struct JournaldSource { } impl JournaldSource { - async fn run_shutdown(self, shutdown: ShutdownSignal) -> Result<(), ()> { + async fn run_shutdown( + self, + shutdown: ShutdownSignal, + ) -> Result<(), crate::sources::SourceError> { let checkpointer = StatefulCheckpointer::new(self.checkpoint_path.clone()) .await .map_err(|error| { emit!(JournaldCheckpointFileOpenError { - error, + error: &error, path: self .checkpoint_path .to_str() .unwrap_or("unknown") .to_string(), }); + crate::sources::SourceError::from(error) })?; let checkpointer = SharedCheckpointer::new(checkpointer); diff --git a/src/sources/kafka.rs b/src/sources/kafka.rs index 31cd443651dba..4bbd07ffa3bf5 100644 --- a/src/sources/kafka.rs +++ b/src/sources/kafka.rs @@ -350,7 +350,7 @@ impl SourceConfig for KafkaSourceConfig { let (consumer, callback_rx) = create_consumer(self, acknowledgements)?; - Ok(crate::sources::opaque_source(kafka_source( + Ok(Box::pin(kafka_source( self.clone(), consumer, callback_rx, @@ -436,7 +436,7 @@ async fn kafka_source( shutdown: ShutdownSignal, eof: bool, log_namespace: LogNamespace, -) -> Result<(), ()> { +) -> Result<(), crate::sources::SourceError> { let span = info_span!("kafka_source"); let consumer = Arc::new(consumer); @@ -452,7 +452,7 @@ async fn kafka_source( let topics: Vec<&str> = config.topics.iter().map(|s| s.as_str()).collect(); if let Err(e) = consumer.subscribe(&topics).context(SubscribeSnafu) { error!("{e}"); - return Err(()); + return Err(crate::sources::SourceError::from(e)); } let coordination_task = { diff --git a/src/sources/mqtt/config.rs b/src/sources/mqtt/config.rs index 8fe7c94978a27..6af3861a35f7a 100644 --- a/src/sources/mqtt/config.rs +++ b/src/sources/mqtt/config.rs @@ -81,7 +81,7 @@ impl SourceConfig for MqttSourceConfig { .build()?; let sink = MqttSource::new(connector, decoder, log_namespace, self.clone())?; - Ok(crate::sources::opaque_source(sink.run(cx.out, cx.shutdown))) + Ok(Box::pin(sink.run(cx.out, cx.shutdown))) } fn outputs(&self, global_log_namespace: LogNamespace) -> Vec { diff --git a/src/sources/mqtt/source.rs b/src/sources/mqtt/source.rs index d0f3efea5b0f7..088c4c69c9b2d 100644 --- a/src/sources/mqtt/source.rs +++ b/src/sources/mqtt/source.rs @@ -14,7 +14,7 @@ use crate::{ internal_events::{EndpointBytesReceived, StreamClosedError}, serde::OneOrMany, shutdown::ShutdownSignal, - sources::{mqtt::MqttSourceConfig, util}, + sources::{SourceError, mqtt::MqttSourceConfig, util}, }; pub struct MqttSource { @@ -39,7 +39,11 @@ impl MqttSource { }) } - pub async fn run(self, mut out: SourceSender, shutdown: ShutdownSignal) -> Result<(), ()> { + pub async fn run( + self, + mut out: SourceSender, + shutdown: ShutdownSignal, + ) -> Result<(), SourceError> { let (client, mut connection) = self.connector.connect(); match &self.config.topic { @@ -47,7 +51,7 @@ impl MqttSource { client .subscribe(topic, QoS::AtLeastOnce) .await - .map_err(|_| ())?; + .map_err(SourceError::from)?; } OneOrMany::Many(topics) => { client @@ -58,7 +62,7 @@ impl MqttSource { .map(|topic| SubscribeFilter::new(topic, QoS::AtLeastOnce)), ) .await - .map_err(|_| ())?; + .map_err(SourceError::from)?; } } diff --git a/src/sources/statsd/mod.rs b/src/sources/statsd/mod.rs index 573e2135361f5..67258f883b651 100644 --- a/src/sources/statsd/mod.rs +++ b/src/sources/statsd/mod.rs @@ -6,7 +6,7 @@ use std::{ }; use bytes::Bytes; -use futures::{StreamExt, TryFutureExt}; +use futures::StreamExt; use listenfd::ListenFd; use serde_with::serde_as; use smallvec::{SmallVec, smallvec}; @@ -216,11 +216,9 @@ impl GenerateConfig for StatsdConfig { impl SourceConfig for StatsdConfig { async fn build(&self, cx: SourceContext) -> crate::Result { match self { - StatsdConfig::Udp(config) => Ok(crate::sources::opaque_source(statsd_udp( - config.clone(), - cx.shutdown, - cx.out, - ))), + StatsdConfig::Udp(config) => { + Ok(Box::pin(statsd_udp(config.clone(), cx.shutdown, cx.out))) + } StatsdConfig::Tcp(config) => { let tls_config = config.tls.as_ref().map(|tls| tls.tls_config.clone()); let tls_client_metadata_key = config @@ -349,16 +347,17 @@ async fn statsd_udp( config: UdpConfig, shutdown: ShutdownSignal, mut out: SourceSender, -) -> Result<(), ()> { +) -> Result<(), super::SourceError> { let listenfd = ListenFd::from_env(); let socket = try_bind_udp_socket(config.address, listenfd) + .await .map_err(|error| { emit!(SocketBindError { mode: SocketMode::Udp, - error - }) - }) - .await?; + error: &error, + }); + super::SourceError::from(error) + })?; if let Some(receive_buffer_bytes) = config.receive_buffer_bytes && let Err(error) = net::set_receive_buffer_size(&socket, receive_buffer_bytes) diff --git a/src/topology/test/crash.rs b/src/topology/test/crash.rs index 55b1f76d5c73d..7c9665344b20f 100644 --- a/src/topology/test/crash.rs +++ b/src/topology/test/crash.rs @@ -17,10 +17,16 @@ use crate::{ }, }; +#[cfg(feature = "sinks-prometheus")] +use crate::sinks::prometheus::exporter::PrometheusExporterConfig; #[cfg(feature = "sinks-websocket_server")] use crate::sinks::websocket_server::WebSocketListenerSinkConfig; +#[cfg(feature = "sources-statsd")] +use crate::sources::statsd::{StatsdConfig, UdpConfig as StatsdUdpConfig}; #[cfg(feature = "sources-syslog")] use crate::sources::syslog::{Mode as SyslogMode, SyslogConfig}; +#[cfg(feature = "sinks-prometheus")] +use crate::test_util::mock::basic_source; #[cfg(feature = "sinks-websocket_server")] #[tokio::test] @@ -52,6 +58,36 @@ async fn test_websocket_sink_bind_error_is_reported() { topology.stop().await; } +#[cfg(feature = "sinks-prometheus")] +#[tokio::test] +async fn test_prometheus_exporter_bind_error_is_reported() { + trace_init(); + + let (_guard, address) = next_addr(); + let _listener = TcpListener::bind(address).unwrap(); + let expected_error = TcpListener::bind(address).unwrap_err().to_string(); + let (_input, source) = basic_source(); + + let sink = PrometheusExporterConfig { + address, + ..Default::default() + }; + let mut config = Config::builder(); + config.add_source("in", source); + config.add_sink("out", &["in"], sink); + + let (topology, mut errors) = start_topology(config.build().unwrap(), false).await; + let error = timeout(Duration::from_secs(5), errors.recv()) + .await + .expect("sink error timed out") + .expect("sink error missing"); + assert!(matches!( + error, + ShutdownError::SinkAborted { error, .. } if error.contains(&expected_error) + )); + topology.stop().await; +} + #[tokio::test] async fn test_tcp_socket_bind_error_is_reported() { trace_init(); @@ -135,6 +171,34 @@ async fn test_udp_syslog_bind_error_is_reported() { topology.stop().await; } +#[cfg(feature = "sources-statsd")] +#[tokio::test] +async fn test_udp_statsd_bind_error_is_reported() { + trace_init(); + + let (_guard, address) = next_addr(); + let _socket = UdpSocket::bind(address).unwrap(); + let expected_error = UdpSocket::bind(address).unwrap_err().to_string(); + + let mut config = Config::builder(); + config.add_source( + "in", + StatsdConfig::Udp(StatsdUdpConfig::from_address(address.into())), + ); + config.add_sink("out", &["in"], basic_sink(1).1); + + let (topology, mut errors) = start_topology(config.build().unwrap(), false).await; + let error = timeout(Duration::from_secs(5), errors.recv()) + .await + .expect("source error timed out") + .expect("source error missing"); + assert!(matches!( + error, + ShutdownError::SourceAborted { error, .. } if error == expected_error + )); + topology.stop().await; +} + /// Ensures that an unrelated source completing immediately with an error does not prematurely terminate the topology. #[tokio::test] async fn test_source_error() { From 88099b2d61d80c5c2128c5164dd13d6861ef8649 Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Mon, 12 Oct 2026 16:50:32 +0200 Subject: [PATCH 07/18] refactor(topology): simplify component error handling --- src/sources/aws_kinesis_firehose/mod.rs | 5 +-- src/sources/datadog_agent/mod.rs | 5 +-- src/sources/splunk_hec/mod.rs | 5 +-- src/sources/util/http/prelude.rs | 10 +---- src/topology/builder.rs | 26 +++-------- src/topology/mod.rs | 13 +++--- src/topology/task.rs | 23 +++++++++- src/topology/test/crash.rs | 58 ++++++++----------------- 8 files changed, 57 insertions(+), 88 deletions(-) diff --git a/src/sources/aws_kinesis_firehose/mod.rs b/src/sources/aws_kinesis_firehose/mod.rs index c04d8cdc024d2..363ee7e322127 100644 --- a/src/sources/aws_kinesis_firehose/mod.rs +++ b/src/sources/aws_kinesis_firehose/mod.rs @@ -219,10 +219,7 @@ impl SourceConfig for AwsKinesisFirehoseConfig { .serve(make_svc) .with_graceful_shutdown(shutdown.map(|_| ())) .await - .map_err(|err| { - error!("An error occurred: {err:?}."); - err - })?; + .inspect_err(|err| error!("An error occurred: {err:?}."))?; Ok(()) })) diff --git a/src/sources/datadog_agent/mod.rs b/src/sources/datadog_agent/mod.rs index f53bae9a5ab8e..cb3c67b38404e 100644 --- a/src/sources/datadog_agent/mod.rs +++ b/src/sources/datadog_agent/mod.rs @@ -254,10 +254,7 @@ impl SourceConfig for DatadogAgentConfig { .serve(make_svc) .with_graceful_shutdown(shutdown.map(|_| ())) .await - .map_err(|err| { - error!("An error occurred: {err:?}."); - err - })?; + .inspect_err(|err| error!("An error occurred: {err:?}."))?; Ok(()) })) diff --git a/src/sources/splunk_hec/mod.rs b/src/sources/splunk_hec/mod.rs index 68101787f1800..43cbedcf65f75 100644 --- a/src/sources/splunk_hec/mod.rs +++ b/src/sources/splunk_hec/mod.rs @@ -293,10 +293,7 @@ impl SplunkConfig { .serve(make_svc) .with_graceful_shutdown(shutdown.map(|_| ())) .await - .map_err(|err| { - error!("An error occurred: {err:?}."); - err - })?; + .inspect_err(|err| error!("An error occurred: {err:?}."))?; Ok(()) })) diff --git a/src/sources/util/http/prelude.rs b/src/sources/util/http/prelude.rs index 5c2cc3a901c82..a4c4e3bad1384 100644 --- a/src/sources/util/http/prelude.rs +++ b/src/sources/util/http/prelude.rs @@ -272,20 +272,14 @@ pub trait HttpSource: Clone + Send + Sync + 'static { let listener = tls .bind(&address) .await - .map_err(|err| { - error!("An error occurred: {err:?}."); - err - })? + .inspect_err(|err| error!("An error occurred: {err:?}."))? .with_keepalive(keepalive_settings.tcp_keepalive); Server::builder(hyper::server::accept::from_stream(listener.accept_stream())) .serve(make_svc) .with_graceful_shutdown(cx.shutdown.map(|_| ())) .await - .map_err(|err| { - error!("An error occurred: {err:?}."); - err - })?; + .inspect_err(|err| error!("An error occurred: {err:?}."))?; Ok(()) })) diff --git a/src/topology/builder.rs b/src/topology/builder.rs index 798e56978a45c..1d46234fc0e20 100644 --- a/src/topology/builder.rs +++ b/src/topology/builder.rs @@ -35,7 +35,6 @@ use vector_lib::{ internal_event::{self, CountByteSize, EventsSent, InternalEventHandle as _, Registered}, latency::LatencyRecorder, schema::Definition, - source::SourceError, source_sender::{DEFAULT_CHUNK_SIZE_EVENTS, SourceSenderItem}, transform::update_runtime_schema_definition, }; @@ -458,20 +457,12 @@ impl<'a> Builder<'a> { Ok(e) = &mut pump_error_rx => Err(e), // The source finished normally. - result = server => result.map_err(|error| match error { - SourceError::Opaque => TaskError::Opaque, - SourceError::Detailed(error) => TaskError::wrapped(error), - }), + result = server => result.map_err(Into::into), }; - // Even though we already tried to receive any pump task error above, we may have exited - // on the source itself returning an error due to task scheduling, where the pump task - // encountered an error, sent it over the oneshot, but we were polling the source - // already and hit an error trying to send to the now-shutdown pump task. - // - // Since the error from the source is opaque at the moment (i.e. `()`), we try a final - // time to see if the pump task encountered an error, using _that_ instead if so, to - // propagate the true error that caused the source to have to stop. + // The pump may fail while the source is being polled, before its error is received + // above. Prefer the pump error if both tasks fail: a source send error can be a + // consequence of the pump stopping, even when the source reports its cause. if let Ok(e) = pump_error_rx.try_recv() { result = Err(e); } @@ -765,13 +756,8 @@ impl<'a> Builder<'a> { debug!("Sink finished normally."); TaskOutput::Sink(rx) }) - .map_err(|error| { - debug!("Sink finished with an error."); - match error { - vector_lib::sink::SinkError::Opaque => TaskError::Opaque, - vector_lib::sink::SinkError::Detailed(error) => TaskError::wrapped(error), - } - }) + .inspect_err(|_| debug!("Sink finished with an error.")) + .map_err(Into::into) }; let task = Task::new(key.clone(), typetag, sink); diff --git a/src/topology/mod.rs b/src/topology/mod.rs index a6a560c47bd89..adcf8ec57e331 100644 --- a/src/topology/mod.rs +++ b/src/topology/mod.rs @@ -68,14 +68,11 @@ async fn handle_errors( .map_err(TaskError::panicked) .and_then(|res| res) .map_err(|e| { - if let TaskError::Panicked { panic } = &e { - error!( - panic = panic.as_str(), - "An error occurred that Vector couldn't handle: {e}." - ); - } else { - error!("An error occurred that Vector couldn't handle: {e}."); - } + let panic = match &e { + TaskError::Panicked { panic } => Some(panic.as_str()), + _ => None, + }; + error!(panic, "An error occurred that Vector couldn't handle: {e}."); _ = abort_tx.send(error(e.to_string())); e }) diff --git a/src/topology/task.rs b/src/topology/task.rs index 6e118ff9e7ccc..cd6e022302f40 100644 --- a/src/topology/task.rs +++ b/src/topology/task.rs @@ -10,7 +10,10 @@ use futures::{FutureExt, future::BoxFuture}; use pin_project::pin_project; use snafu::Snafu; use tokio::task::JoinError; -use vector_lib::{buffers::topology::channel::BufferReceiverStream, event::EventArray}; +use vector_lib::{ + buffers::topology::channel::BufferReceiverStream, event::EventArray, sink::SinkError, + source::SourceError, +}; use crate::{config::ComponentKey, utilization::Utilization}; @@ -51,6 +54,24 @@ impl TaskError { } } +impl From for TaskError { + fn from(error: SourceError) -> Self { + match error { + SourceError::Opaque => Self::Opaque, + SourceError::Detailed(error) => Self::wrapped(error), + } + } +} + +impl From for TaskError { + fn from(error: SinkError) -> Self { + match error { + SinkError::Opaque => Self::Opaque, + SinkError::Detailed(error) => Self::wrapped(error), + } + } +} + impl From for TaskError { fn from(e: JoinError) -> Self { if e.is_cancelled() { diff --git a/src/topology/test/crash.rs b/src/topology/test/crash.rs index 7c9665344b20f..e09705826498d 100644 --- a/src/topology/test/crash.rs +++ b/src/topology/test/crash.rs @@ -25,9 +25,19 @@ use crate::sinks::websocket_server::WebSocketListenerSinkConfig; use crate::sources::statsd::{StatsdConfig, UdpConfig as StatsdUdpConfig}; #[cfg(feature = "sources-syslog")] use crate::sources::syslog::{Mode as SyslogMode, SyslogConfig}; -#[cfg(feature = "sinks-prometheus")] +#[cfg(any(feature = "sinks-prometheus", feature = "sinks-websocket_server"))] use crate::test_util::mock::basic_source; +async fn first_shutdown_error(config: Config) -> ShutdownError { + let (topology, mut errors) = start_topology(config, false).await; + let error = timeout(Duration::from_secs(5), errors.recv()) + .await + .expect("component error timed out") + .expect("component error missing"); + topology.stop().await; + error +} + #[cfg(feature = "sinks-websocket_server")] #[tokio::test] async fn test_websocket_sink_bind_error_is_reported() { @@ -36,26 +46,21 @@ async fn test_websocket_sink_bind_error_is_reported() { let (_guard, address) = next_addr(); let _listener = TcpListener::bind(address).unwrap(); let expected_error = TcpListener::bind(address).unwrap_err().to_string(); - let (_source_guard, source_address) = next_addr(); + let (_input, source) = basic_source(); let sink = WebSocketListenerSinkConfig { address, ..Default::default() }; let mut config = Config::builder(); - config.add_source("in", SocketConfig::make_basic_tcp_config(source_address)); + config.add_source("in", source); config.add_sink("out", &["in"], sink); - let (topology, mut errors) = start_topology(config.build().unwrap(), false).await; - let error = timeout(Duration::from_secs(5), errors.recv()) - .await - .expect("sink error timed out") - .expect("sink error missing"); + let error = first_shutdown_error(config.build().unwrap()).await; assert!(matches!( error, ShutdownError::SinkAborted { error, .. } if error.contains(&expected_error) )); - topology.stop().await; } #[cfg(feature = "sinks-prometheus")] @@ -76,16 +81,11 @@ async fn test_prometheus_exporter_bind_error_is_reported() { config.add_source("in", source); config.add_sink("out", &["in"], sink); - let (topology, mut errors) = start_topology(config.build().unwrap(), false).await; - let error = timeout(Duration::from_secs(5), errors.recv()) - .await - .expect("sink error timed out") - .expect("sink error missing"); + let error = first_shutdown_error(config.build().unwrap()).await; assert!(matches!( error, ShutdownError::SinkAborted { error, .. } if error.contains(&expected_error) )); - topology.stop().await; } #[tokio::test] @@ -100,17 +100,12 @@ async fn test_tcp_socket_bind_error_is_reported() { config.add_source("in", SocketConfig::make_basic_tcp_config(address)); config.add_sink("out", &["in"], basic_sink(1).1); - let (topology, mut errors) = start_topology(config.build().unwrap(), false).await; - let error = timeout(Duration::from_secs(5), errors.recv()) - .await - .expect("source error timed out") - .expect("source error missing"); + let error = first_shutdown_error(config.build().unwrap()).await; assert!(matches!( error, ShutdownError::SourceAborted { error, .. } if error.contains("TCP bind failed") && error.contains(&expected_error) )); - topology.stop().await; } #[tokio::test] @@ -128,16 +123,11 @@ async fn test_udp_socket_bind_error_is_reported() { ); config.add_sink("out", &["in"], basic_sink(1).1); - let (topology, mut errors) = start_topology(config.build().unwrap(), false).await; - let error = timeout(Duration::from_secs(5), errors.recv()) - .await - .expect("source error timed out") - .expect("source error missing"); + let error = first_shutdown_error(config.build().unwrap()).await; assert!(matches!( error, ShutdownError::SourceAborted { error, .. } if error == expected_error )); - topology.stop().await; } #[cfg(feature = "sources-syslog")] @@ -159,16 +149,11 @@ async fn test_udp_syslog_bind_error_is_reported() { ); config.add_sink("out", &["in"], basic_sink(1).1); - let (topology, mut errors) = start_topology(config.build().unwrap(), false).await; - let error = timeout(Duration::from_secs(5), errors.recv()) - .await - .expect("source error timed out") - .expect("source error missing"); + let error = first_shutdown_error(config.build().unwrap()).await; assert!(matches!( error, ShutdownError::SourceAborted { error, .. } if error == expected_error )); - topology.stop().await; } #[cfg(feature = "sources-statsd")] @@ -187,16 +172,11 @@ async fn test_udp_statsd_bind_error_is_reported() { ); config.add_sink("out", &["in"], basic_sink(1).1); - let (topology, mut errors) = start_topology(config.build().unwrap(), false).await; - let error = timeout(Duration::from_secs(5), errors.recv()) - .await - .expect("source error timed out") - .expect("source error missing"); + let error = first_shutdown_error(config.build().unwrap()).await; assert!(matches!( error, ShutdownError::SourceAborted { error, .. } if error == expected_error )); - topology.stop().await; } /// Ensures that an unrelated source completing immediately with an error does not prematurely terminate the topology. From a67669370e414aeb46926121d4e7ba607b4ae2e2 Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Mon, 12 Oct 2026 17:01:35 +0200 Subject: [PATCH 08/18] fix(sink): propagate Humio metrics runtime errors --- src/sinks/humio/metrics.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sinks/humio/metrics.rs b/src/sinks/humio/metrics.rs index 9cf0d20ec295b..2bf40ef4d857d 100644 --- a/src/sinks/humio/metrics.rs +++ b/src/sinks/humio/metrics.rs @@ -8,7 +8,7 @@ use vector_lib::{ lookup, lookup::lookup_v2::{ConfigValuePath, OptionalTargetPath, OptionalValuePath}, sensitive_string::SensitiveString, - sink::StreamSink, + sink::{SinkError, StreamSink}, }; use super::{ @@ -207,7 +207,7 @@ impl ValidatedSink for HumioMetricsConfig { inner: sink, transform, }; - Ok((VectorSink::Stream(Box::new(sink)), healthcheck)) + Ok((VectorSink::DetailedStream(Box::new(sink)), healthcheck)) } } @@ -247,8 +247,8 @@ pub struct HumioMetricsSink { } #[async_trait] -impl StreamSink for HumioMetricsSink { - async fn run(self: Box, input: BoxStream<'_, EventArray>) -> Result<(), ()> { +impl StreamSink for HumioMetricsSink { + async fn run(self: Box, input: BoxStream<'_, EventArray>) -> Result<(), SinkError> { let mut transform = self.transform; self.inner .run(input.map(move |events| { From 0486b9ac770a1e90f9ae4381709b4ffd6d954f47 Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Tue, 13 Oct 2026 14:58:09 +0200 Subject: [PATCH 09/18] refactor(sinks): use one stream variant for runtime errors --- lib/vector-core/src/sink.rs | 39 +++++++++++++++++++++++------ src/sinks/blackhole/config.rs | 2 +- src/sinks/blackhole/mod.rs | 2 +- src/sinks/humio/metrics.rs | 2 +- src/sinks/sematext/logs.rs | 10 +++++--- src/test_util/components.rs | 7 ------ src/test_util/mock/sinks/oneshot.rs | 4 +-- 7 files changed, 42 insertions(+), 24 deletions(-) diff --git a/lib/vector-core/src/sink.rs b/lib/vector-core/src/sink.rs index 844692b1921b9..8d77436dc84c3 100644 --- a/lib/vector-core/src/sink.rs +++ b/lib/vector-core/src/sink.rs @@ -27,8 +27,7 @@ where pub enum VectorSink { Sink(Box + Send + Unpin>), - Stream(Box + Send>), - DetailedStream(Box + Send>), + Stream(Box + Send>), } impl VectorSink { @@ -44,8 +43,7 @@ impl VectorSink { .forward(sink) .await .map_err(|()| SinkError::Opaque), - Self::Stream(s) => s.run(Box::pin(input)).await.map_err(|()| SinkError::Opaque), - Self::DetailedStream(s) => s.run(Box::pin(input)).await, + Self::Stream(s) => s.run(Box::pin(input)).await, } } @@ -79,7 +77,7 @@ impl VectorSink { /// # Panics /// /// This function will panic if the self instance is not `VectorSink::Stream`. - pub fn into_stream(self) -> Box + Send> { + pub fn into_stream(self) -> Box + Send> { match self { Self::Stream(stream) => stream, _ => panic!("Failed type coercion, {self:?} is not a Stream"), @@ -99,8 +97,16 @@ impl VectorSink { /// Converts an event stream into a `VectorSink` pub fn from_event_streamsink(sink: impl StreamSink + Send + 'static) -> Self { - let sink = Box::new(sink); - VectorSink::Stream(Box::new(EventStream { sink })) + Self::from_streamsink(EventStream { + sink: Box::new(sink), + }) + } + + /// Converts a stream sink with opaque runtime errors into a `VectorSink`. + pub fn from_streamsink(sink: impl StreamSink + Send + 'static) -> Self { + Self::Stream(Box::new(OpaqueStream { + sink: Box::new(sink), + })) } /// Converts a sink that retains its runtime errors into a `VectorSink`. @@ -108,7 +114,7 @@ impl VectorSink { sink: impl StreamSink + Send + 'static, ) -> Self { let sink = Box::new(sink); - VectorSink::DetailedStream(Box::new(EventStream { sink })) + VectorSink::Stream(Box::new(EventStream { sink })) } } @@ -125,6 +131,23 @@ pub trait StreamSink { async fn run(self: Box, input: stream::BoxStream<'_, T>) -> Result<(), E>; } +struct OpaqueStream { + sink: Box, +} + +#[async_trait::async_trait] +impl StreamSink for OpaqueStream +where + T: StreamSink + Send, +{ + async fn run( + self: Box, + input: stream::BoxStream<'_, EventArray>, + ) -> Result<(), SinkError> { + self.sink.run(input).await.map_err(|()| SinkError::Opaque) + } +} + /// Wrapper for sinks implementing `Sink` to implement /// `Sink`. This stores an iterator over the incoming /// `EventArray` to be pushed into the wrapped sink one at a time. diff --git a/src/sinks/blackhole/config.rs b/src/sinks/blackhole/config.rs index 81c4374293c59..b9243ca100b5a 100644 --- a/src/sinks/blackhole/config.rs +++ b/src/sinks/blackhole/config.rs @@ -81,7 +81,7 @@ impl ValidatedSink for BlackholeConfig { let sink = BlackholeSink::new(self.clone()); let healthcheck = future::ok(()).boxed(); - Ok((VectorSink::Stream(Box::new(sink)), healthcheck)) + Ok((VectorSink::from_streamsink(sink), healthcheck)) } } diff --git a/src/sinks/blackhole/mod.rs b/src/sinks/blackhole/mod.rs index 2fddea593fe28..f909f7b02a2ce 100644 --- a/src/sinks/blackhole/mod.rs +++ b/src/sinks/blackhole/mod.rs @@ -30,7 +30,7 @@ mod tests { acknowledgements: AcknowledgementsConfig::default(), }; let sink = BlackholeSink::new(config); - let sink = VectorSink::Stream(Box::new(sink)); + let sink = VectorSink::from_streamsink(sink); let (_input_lines, events) = random_events_with_stream(100, 10, None); run_and_assert_sink_compliance(sink, events, &SINK_TAGS).await; diff --git a/src/sinks/humio/metrics.rs b/src/sinks/humio/metrics.rs index 2bf40ef4d857d..00c7d240969eb 100644 --- a/src/sinks/humio/metrics.rs +++ b/src/sinks/humio/metrics.rs @@ -207,7 +207,7 @@ impl ValidatedSink for HumioMetricsConfig { inner: sink, transform, }; - Ok((VectorSink::DetailedStream(Box::new(sink)), healthcheck)) + Ok((VectorSink::Stream(Box::new(sink)), healthcheck)) } } diff --git a/src/sinks/sematext/logs.rs b/src/sinks/sematext/logs.rs index b1d419a5ad85f..7290922ae79c6 100644 --- a/src/sinks/sematext/logs.rs +++ b/src/sinks/sematext/logs.rs @@ -1,7 +1,9 @@ use async_trait::async_trait; use futures::stream::{BoxStream, StreamExt}; use indoc::indoc; -use vector_lib::{configurable::configurable_component, sensitive_string::SensitiveString}; +use vector_lib::{ + configurable::configurable_component, sensitive_string::SensitiveString, sink::SinkError, +}; use vrl::event_path; use super::Region; @@ -161,12 +163,12 @@ impl SematextLogsConfig { } struct MapTimestampStream { - inner: Box + Send>, + inner: Box + Send>, } #[async_trait] -impl StreamSink for MapTimestampStream { - async fn run(self: Box, input: BoxStream<'_, EventArray>) -> Result<(), ()> { +impl StreamSink for MapTimestampStream { + async fn run(self: Box, input: BoxStream<'_, EventArray>) -> Result<(), SinkError> { let mapped_input = input.map(map_timestamp).boxed(); self.inner.run(mapped_input).await } diff --git a/src/test_util/components.rs b/src/test_util/components.rs index 3db1ebc11e4b0..a5f339c0fc70d 100644 --- a/src/test_util/components.rs +++ b/src/test_util/components.rs @@ -711,13 +711,6 @@ where .await .expect("Sending event stream to sink failed"); } - VectorSink::DetailedStream(stream) => { - let events = events.filter_map(|x| async move { x.ok() }).boxed(); - stream - .run(events) - .await - .expect("Sending event stream to sink failed"); - } } SINK_TESTS.assert(tags); } diff --git a/src/test_util/mock/sinks/oneshot.rs b/src/test_util/mock/sinks/oneshot.rs index e8510c1122abe..943d4cdc9a2b9 100644 --- a/src/test_util/mock/sinks/oneshot.rs +++ b/src/test_util/mock/sinks/oneshot.rs @@ -62,11 +62,11 @@ impl ValidatedSink for OneshotSinkConfig { let mut guard = self.tx.lock().expect("who cares if the lock is poisoned"); guard.take() }; - let sink = Box::new(OneshotSink { tx }); + let sink = OneshotSink { tx }; let healthcheck = Box::pin(async { Ok(()) }); - Ok((VectorSink::Stream(sink), healthcheck)) + Ok((VectorSink::from_streamsink(sink), healthcheck)) } } From 48bb414f9b41e6b09d0848d89e7a3335e2c86d26 Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Wed, 14 Oct 2026 11:20:11 +0200 Subject: [PATCH 10/18] refactor(topology): use boxed errors for component failures --- lib/vector-core/src/sink.rs | 24 ++++++------------------ lib/vector-core/src/source.rs | 23 ++++++----------------- src/sinks/console/sink.rs | 17 +++++++---------- src/sinks/prometheus/exporter.rs | 2 +- src/sources/amqp.rs | 4 +--- src/sources/file.rs | 4 +++- src/sources/gcp_pubsub.rs | 2 +- src/sources/kubernetes_logs/mod.rs | 2 +- src/sources/syslog.rs | 2 +- src/sources/util/framestream.rs | 2 +- src/sources/util/net/tcp/mod.rs | 2 +- src/sources/util/unix_datagram.rs | 2 +- src/sources/vector/mod.rs | 2 +- src/test_util/mock/sources/error.rs | 2 +- src/test_util/mock/sources/tripwire.rs | 2 +- src/topology/task.rs | 25 ++++--------------------- 16 files changed, 37 insertions(+), 80 deletions(-) diff --git a/lib/vector-core/src/sink.rs b/lib/vector-core/src/sink.rs index 8d77436dc84c3..9e26d23e7a8a4 100644 --- a/lib/vector-core/src/sink.rs +++ b/lib/vector-core/src/sink.rs @@ -8,22 +8,7 @@ use futures::{ use crate::event::{Event, EventArray, EventContainer, into_event_stream}; /// Error returned by a running sink. -#[derive(Debug)] -pub enum SinkError { - /// The sink did not retain its failure reason. - Opaque, - /// The error that stopped the sink. - Detailed(vector_common::Error), -} - -impl From for SinkError -where - E: std::error::Error + Send + Sync + 'static, -{ - fn from(error: E) -> Self { - Self::Detailed(Box::new(error)) - } -} +pub type SinkError = vector_common::Error; pub enum VectorSink { Sink(Box + Send + Unpin>), @@ -42,7 +27,7 @@ impl VectorSink { .map(Ok) .forward(sink) .await - .map_err(|()| SinkError::Opaque), + .map_err(|()| "the task completed with an error".into()), Self::Stream(s) => s.run(Box::pin(input)).await, } } @@ -144,7 +129,10 @@ where self: Box, input: stream::BoxStream<'_, EventArray>, ) -> Result<(), SinkError> { - self.sink.run(input).await.map_err(|()| SinkError::Opaque) + self.sink + .run(input) + .await + .map_err(|()| "the task completed with an error".into()) } } diff --git a/lib/vector-core/src/source.rs b/lib/vector-core/src/source.rs index 5fb1b394b82ab..fdd8172eeac49 100644 --- a/lib/vector-core/src/source.rs +++ b/lib/vector-core/src/source.rs @@ -3,22 +3,7 @@ use std::future::Future; use futures::future::BoxFuture; /// Error returned by a running source. -#[derive(Debug)] -pub enum SourceError { - /// The source did not retain its failure reason. - Opaque, - /// The error that stopped the source. - Detailed(vector_common::Error), -} - -impl From for SourceError -where - E: std::error::Error + Send + Sync + 'static, -{ - fn from(error: E) -> Self { - Self::Detailed(Box::new(error)) - } -} +pub type SourceError = vector_common::Error; /// Future that runs a source until shutdown or failure. pub type Source = BoxFuture<'static, Result<(), SourceError>>; @@ -28,5 +13,9 @@ pub fn opaque_source(future: F) -> Source where F: Future> + Send + 'static, { - Box::pin(async move { future.await.map_err(|()| SourceError::Opaque) }) + Box::pin(async move { + future + .await + .map_err(|()| "the task completed with an error".into()) + }) } diff --git a/src/sinks/console/sink.rs b/src/sinks/console/sink.rs index 66e1d56dec0ab..5b50daba6ecf1 100644 --- a/src/sinks/console/sink.rs +++ b/src/sinks/console/sink.rs @@ -118,15 +118,12 @@ mod test { .await .expect_err("writing to a closed pipe should fail"); - match error { - SinkError::Detailed(error) => assert_eq!( - error - .downcast_ref::() - .expect("the sink should retain the I/O error") - .kind(), - io::ErrorKind::BrokenPipe - ), - SinkError::Opaque => panic!("the sink should retain the I/O error"), - } + assert_eq!( + error + .downcast_ref::() + .expect("the sink should retain the I/O error") + .kind(), + io::ErrorKind::BrokenPipe + ); } } diff --git a/src/sinks/prometheus/exporter.rs b/src/sinks/prometheus/exporter.rs index ac347e18bcd8f..7e456fa81aaf8 100644 --- a/src/sinks/prometheus/exporter.rs +++ b/src/sinks/prometheus/exporter.rs @@ -548,7 +548,7 @@ impl StreamSink for PrometheusExporter { async fn run(mut self: Box, mut input: BoxStream<'_, Event>) -> Result<(), SinkError> { self.start_server_if_needed().await.map_err(|error| { error!("Failed to start Prometheus exporter: {error}."); - SinkError::Detailed(error) + SinkError::from(error) })?; let mut last_flush = Instant::now(); diff --git a/src/sources/amqp.rs b/src/sources/amqp.rs index 165e99887c53b..1d67983cf7aca 100644 --- a/src/sources/amqp.rs +++ b/src/sources/amqp.rs @@ -320,9 +320,7 @@ async fn receive_event( msg: Delivery, ) -> Result<(), crate::sources::SourceError> { let payload = Cursor::new(Bytes::copy_from_slice(&msg.data)); - let decoder = config - .decoder(log_namespace) - .map_err(crate::sources::SourceError::Detailed)?; + let decoder = config.decoder(log_namespace)?; let mut stream = DecoderFramedRead::new(payload, decoder); // Extract timestamp from AMQP message diff --git a/src/sources/file.rs b/src/sources/file.rs index 3c7b336ba8dec..bda3b0cef4e91 100644 --- a/src/sources/file.rs +++ b/src/sources/file.rs @@ -484,7 +484,9 @@ pub fn file_source( message = "`include` configuration option must contain at least one file pattern.", internal_log_rate_limit = false ); - return Box::pin(future::ready(Err(super::SourceError::Opaque))); + return Box::pin(future::ready( + Err("the task completed with an error".into()), + )); } let exclude_patterns = config diff --git a/src/sources/gcp_pubsub.rs b/src/sources/gcp_pubsub.rs index 1d8021faf77ac..8bf3f07fe4f27 100644 --- a/src/sources/gcp_pubsub.rs +++ b/src/sources/gcp_pubsub.rs @@ -329,7 +329,7 @@ impl SourceConfig for PubsubConfig { .run_all(self.max_concurrency, self.poll_time_seconds) .map_err(|error| { error!(message = "Source failed.", %error); - crate::sources::SourceError::Detailed(error) + crate::sources::SourceError::from(error) }); Ok(Box::pin(source)) } diff --git a/src/sources/kubernetes_logs/mod.rs b/src/sources/kubernetes_logs/mod.rs index b372535e50d7c..dfb6871f53af9 100644 --- a/src/sources/kubernetes_logs/mod.rs +++ b/src/sources/kubernetes_logs/mod.rs @@ -364,7 +364,7 @@ impl SourceConfig for Config { .map(|result| { result.map_err(|error| { error!(message = "Source future failed.", %error); - crate::sources::SourceError::Detailed(error) + crate::sources::SourceError::from(error) }) }), )) diff --git a/src/sources/syslog.rs b/src/sources/syslog.rs index ea4cb65b515de..4b56b812fae4d 100644 --- a/src/sources/syslog.rs +++ b/src/sources/syslog.rs @@ -419,7 +419,7 @@ pub fn udp( Err(_) => { let (count, _) = stream.size_hint(); emit!(StreamClosedError { count }); - Err(super::SourceError::Opaque) + Err("the task completed with an error".into()) } } }) diff --git a/src/sources/util/framestream.rs b/src/sources/util/framestream.rs index 6c872de365617..f52a4c8c5eb2d 100644 --- a/src/sources/util/framestream.rs +++ b/src/sources/util/framestream.rs @@ -428,7 +428,7 @@ pub fn build_framestream_tcp_source( mode: SocketMode::Tcp, error: &error, }); - crate::sources::SourceError::Detailed(error) + crate::sources::SourceError::from(error) })?; info!( diff --git a/src/sources/util/net/tcp/mod.rs b/src/sources/util/net/tcp/mod.rs index b10c3f2eef539..10aff19893d12 100644 --- a/src/sources/util/net/tcp/mod.rs +++ b/src/sources/util/net/tcp/mod.rs @@ -149,7 +149,7 @@ where mode: SocketMode::Tcp, error: &error, }); - SourceError::Detailed(error) + SourceError::from(error) })?; info!( diff --git a/src/sources/util/unix_datagram.rs b/src/sources/util/unix_datagram.rs index d6390d24884fe..bd510ae6f9ae6 100644 --- a/src/sources/util/unix_datagram.rs +++ b/src/sources/util/unix_datagram.rs @@ -55,7 +55,7 @@ pub fn build_unix_datagram_source( }); } - result.map_err(|_| crate::sources::SourceError::Opaque) + result.map_err(|_| "the task completed with an error".into()) })) } diff --git a/src/sources/vector/mod.rs b/src/sources/vector/mod.rs index 9c25953b323a6..4cbaecd0af1e9 100644 --- a/src/sources/vector/mod.rs +++ b/src/sources/vector/mod.rs @@ -234,7 +234,7 @@ impl SourceConfig for VectorConfig { ) .map_err(|error| { error!(message = "Source future failed.", %error); - crate::sources::SourceError::Detailed(error) + crate::sources::SourceError::from(error) }); Ok(Box::pin(source)) diff --git a/src/test_util/mock/sources/error.rs b/src/test_util/mock/sources/error.rs index 90eefc60a0bdc..351d27472150d 100644 --- a/src/test_util/mock/sources/error.rs +++ b/src/test_util/mock/sources/error.rs @@ -23,7 +23,7 @@ impl_generate_config_from_default!(ErrorSourceConfig); #[typetag::serde(name = "test_error")] impl SourceConfig for ErrorSourceConfig { async fn build(&self, _cx: SourceContext) -> crate::Result { - Ok(err(crate::sources::SourceError::Opaque).boxed()) + Ok(err("the task completed with an error".into()).boxed()) } fn outputs(&self, _global_log_namespace: LogNamespace) -> Vec { diff --git a/src/test_util/mock/sources/tripwire.rs b/src/test_util/mock/sources/tripwire.rs index b2842a30f97f7..35f8b6b1c8d5f 100644 --- a/src/test_util/mock/sources/tripwire.rs +++ b/src/test_util/mock/sources/tripwire.rs @@ -62,7 +62,7 @@ impl SourceConfig for TripwireSourceConfig { ) .map(|_| drop(out)) .unit_error() - .map(|result| result.map_err(|_| crate::sources::SourceError::Opaque)), + .map(|result| result.map_err(|_| "the task completed with an error".into())), )) } diff --git a/src/topology/task.rs b/src/topology/task.rs index cd6e022302f40..13f36d789a4e7 100644 --- a/src/topology/task.rs +++ b/src/topology/task.rs @@ -10,10 +10,7 @@ use futures::{FutureExt, future::BoxFuture}; use pin_project::pin_project; use snafu::Snafu; use tokio::task::JoinError; -use vector_lib::{ - buffers::topology::channel::BufferReceiverStream, event::EventArray, sink::SinkError, - source::SourceError, -}; +use vector_lib::{buffers::topology::channel::BufferReceiverStream, event::EventArray}; use crate::{config::ComponentKey, utilization::Utilization}; @@ -32,8 +29,6 @@ pub(crate) enum TaskError { Cancelled, #[snafu(display("the task panicked and was aborted"))] Panicked { panic: String }, - #[snafu(display("the task completed with an error"))] - Opaque, #[snafu(display("{}", source))] Wrapped { source: crate::Error }, } @@ -54,21 +49,9 @@ impl TaskError { } } -impl From for TaskError { - fn from(error: SourceError) -> Self { - match error { - SourceError::Opaque => Self::Opaque, - SourceError::Detailed(error) => Self::wrapped(error), - } - } -} - -impl From for TaskError { - fn from(error: SinkError) -> Self { - match error { - SinkError::Opaque => Self::Opaque, - SinkError::Detailed(error) => Self::wrapped(error), - } +impl From for TaskError { + fn from(error: crate::Error) -> Self { + Self::wrapped(error) } } From 8fc1fac6b0bae2d2b6d650f33a93030ba2b14b88 Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Wed, 14 Oct 2026 12:31:48 +0200 Subject: [PATCH 11/18] fix(topology): retain available source errors --- lib/vector-core/src/lib.rs | 5 ++++ lib/vector-core/src/sink.rs | 4 ++-- lib/vector-core/src/source.rs | 6 +---- lib/vector-lib/src/lib.rs | 4 ++-- src/sources/file.rs | 10 +++----- src/sources/syslog.rs | 32 ++++++++++++++++++++++++-- src/sources/util/unix_datagram.rs | 7 +++--- src/test_util/mock/sources/error.rs | 2 +- src/test_util/mock/sources/tripwire.rs | 7 +++--- 9 files changed, 52 insertions(+), 25 deletions(-) diff --git a/lib/vector-core/src/lib.rs b/lib/vector-core/src/lib.rs index 87b562a3ca6f7..0759b6b4140b8 100644 --- a/lib/vector-core/src/lib.rs +++ b/lib/vector-core/src/lib.rs @@ -59,6 +59,11 @@ pub fn default_data_dir() -> Option { Some(PathBuf::from("/var/lib/vector/")) } +/// Error for a component that reports failure without a cause. +pub fn default_task_error() -> vector_common::Error { + "the task completed with an error".into() +} + pub(crate) use vector_common::{Error, Result}; pub(crate) fn float_eq(l_value: f64, r_value: f64) -> bool { diff --git a/lib/vector-core/src/sink.rs b/lib/vector-core/src/sink.rs index 9e26d23e7a8a4..c3d47190b9618 100644 --- a/lib/vector-core/src/sink.rs +++ b/lib/vector-core/src/sink.rs @@ -27,7 +27,7 @@ impl VectorSink { .map(Ok) .forward(sink) .await - .map_err(|()| "the task completed with an error".into()), + .map_err(|()| crate::default_task_error()), Self::Stream(s) => s.run(Box::pin(input)).await, } } @@ -132,7 +132,7 @@ where self.sink .run(input) .await - .map_err(|()| "the task completed with an error".into()) + .map_err(|()| crate::default_task_error()) } } diff --git a/lib/vector-core/src/source.rs b/lib/vector-core/src/source.rs index fdd8172eeac49..c01011f84d452 100644 --- a/lib/vector-core/src/source.rs +++ b/lib/vector-core/src/source.rs @@ -13,9 +13,5 @@ pub fn opaque_source(future: F) -> Source where F: Future> + Send + 'static, { - Box::pin(async move { - future - .await - .map_err(|()| "the task completed with an error".into()) - }) + Box::pin(async move { future.await.map_err(|()| crate::default_task_error()) }) } diff --git a/lib/vector-lib/src/lib.rs b/lib/vector-lib/src/lib.rs index 7fb4e0371b8b7..a51659b5dd467 100644 --- a/lib/vector-lib/src/lib.rs +++ b/lib/vector-lib/src/lib.rs @@ -22,8 +22,8 @@ pub use vector_config as configurable; pub use vector_config::impl_generate_config_from_default; pub use vector_core::compile_vrl; pub use vector_core::{ - EstimatedJsonEncodedSizeOf, SpanField, buckets, default_data_dir, emit, event, fanout, - ipallowlist, latency, metric_tags, metrics, partition, quantiles, register, + EstimatedJsonEncodedSizeOf, SpanField, buckets, default_data_dir, default_task_error, emit, + event, fanout, ipallowlist, latency, metric_tags, metrics, partition, quantiles, register, register_extra_span_field, samples, schema, serde, sink, source, source_sender, tcp, tls, transform, }; diff --git a/src/sources/file.rs b/src/sources/file.rs index bda3b0cef4e91..c73c69e591ccd 100644 --- a/src/sources/file.rs +++ b/src/sources/file.rs @@ -480,13 +480,9 @@ pub fn file_source( ) -> super::Source { // the include option must be specified but also must contain at least one entry. if config.include.is_empty() { - error!( - message = "`include` configuration option must contain at least one file pattern.", - internal_log_rate_limit = false - ); - return Box::pin(future::ready( - Err("the task completed with an error".into()), - )); + let error = "`include` configuration option must contain at least one file pattern."; + error!(message = error, internal_log_rate_limit = false); + return Box::pin(future::ready(Err(error.into()))); } let exclude_patterns = config diff --git a/src/sources/syslog.rs b/src/sources/syslog.rs index 4b56b812fae4d..048e7029ef2b5 100644 --- a/src/sources/syslog.rs +++ b/src/sources/syslog.rs @@ -416,10 +416,10 @@ pub fn udp( debug!("Finished sending."); Ok(()) } - Err(_) => { + Err(error) => { let (count, _) = stream.size_hint(); emit!(StreamClosedError { count }); - Err("the task completed with an error".into()) + Err(error.into()) } } }) @@ -1235,6 +1235,34 @@ mod test { .await; } + #[tokio::test] + async fn test_udp_syslog_send_error_retains_cause() { + let (_guard, in_addr) = next_addr(); + let config = SyslogConfig::from_mode(Mode::Udp { + address: in_addr.into(), + receive_buffer_bytes: None, + }); + + let key = ComponentKey::from("in"); + let (tx, rx) = SourceSender::new_test(); + drop(rx); + let (context, _shutdown) = SourceContext::new_shutdown(&key, tx); + let source = config.build(context).await.unwrap(); + let source = tokio::spawn(source); + + sleep(Duration::from_millis(150)).await; + let socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let message = SyslogMessageRfc5424::random(0, 30, 4, 3, 3).to_string(); + socket.send_to(message.as_bytes(), in_addr).await.unwrap(); + + let error = tokio::time::timeout(Duration::from_secs(5), source) + .await + .expect("source error timed out") + .unwrap() + .expect_err("source should fail when its output is closed"); + assert_eq!(error.to_string(), "Sender is closed."); + } + #[tokio::test] async fn test_udp_syslog() { assert_source_compliance(&SOCKET_PUSH_SOURCE_TAGS, async { diff --git a/src/sources/util/unix_datagram.rs b/src/sources/util/unix_datagram.rs index bd510ae6f9ae6..cb657d9da7ae3 100644 --- a/src/sources/util/unix_datagram.rs +++ b/src/sources/util/unix_datagram.rs @@ -55,7 +55,7 @@ pub fn build_unix_datagram_source( }); } - result.map_err(|_| "the task completed with an error".into()) + result })) } @@ -66,7 +66,7 @@ async fn listen( mut shutdown: ShutdownSignal, handle_events: impl Fn(&mut [Event], Option) + Clone + Send + Sync + 'static, mut out: SourceSender, -) -> Result<(), ()> { +) -> Result<(), crate::sources::SourceError> { let mut buf = BytesMut::with_capacity(max_length); let bytes_received = register!(BytesReceived::from(Protocol::UNIX)); loop { @@ -78,7 +78,8 @@ async fn listen( emit!(SocketReceiveError { mode: SocketMode::Unix, error: &error - }) + }); + crate::sources::SourceError::from(error) })?; let span = info_span!("datagram"); diff --git a/src/test_util/mock/sources/error.rs b/src/test_util/mock/sources/error.rs index 351d27472150d..cc5836598ff89 100644 --- a/src/test_util/mock/sources/error.rs +++ b/src/test_util/mock/sources/error.rs @@ -23,7 +23,7 @@ impl_generate_config_from_default!(ErrorSourceConfig); #[typetag::serde(name = "test_error")] impl SourceConfig for ErrorSourceConfig { async fn build(&self, _cx: SourceContext) -> crate::Result { - Ok(err("the task completed with an error".into()).boxed()) + Ok(err(vector_lib::default_task_error()).boxed()) } fn outputs(&self, _global_log_namespace: LogNamespace) -> Vec { diff --git a/src/test_util/mock/sources/tripwire.rs b/src/test_util/mock/sources/tripwire.rs index 35f8b6b1c8d5f..46cebe19cc3e8 100644 --- a/src/test_util/mock/sources/tripwire.rs +++ b/src/test_util/mock/sources/tripwire.rs @@ -60,9 +60,10 @@ impl SourceConfig for TripwireSourceConfig { .then(crate::shutdown::tripwire_handler) .boxed(), ) - .map(|_| drop(out)) - .unit_error() - .map(|result| result.map_err(|_| "the task completed with an error".into())), + .map(|_| { + drop(out); + Ok(()) + }), )) } From 500956e7357dc7c3b2981edcac90f13ba5accd0a Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Wed, 14 Oct 2026 15:34:01 +0200 Subject: [PATCH 12/18] refactor(topology): give sink errors a default wrapper --- lib/vector-core/src/sink.rs | 52 +++++++++++++++++++++++++++--- src/sinks/console/sink.rs | 4 +-- src/sinks/prometheus/exporter.rs | 2 +- src/sinks/websocket_server/sink.rs | 6 +--- src/topology/task.rs | 10 +++++- 5 files changed, 61 insertions(+), 13 deletions(-) diff --git a/lib/vector-core/src/sink.rs b/lib/vector-core/src/sink.rs index c3d47190b9618..ad8ba3915469b 100644 --- a/lib/vector-core/src/sink.rs +++ b/lib/vector-core/src/sink.rs @@ -1,4 +1,4 @@ -use std::{fmt, iter::IntoIterator, pin::Pin}; +use std::{fmt, iter::IntoIterator, ops::Deref, pin::Pin}; use futures::{ Sink, SinkExt, Stream, StreamExt, stream, @@ -8,7 +8,51 @@ use futures::{ use crate::event::{Event, EventArray, EventContainer, into_event_stream}; /// Error returned by a running sink. -pub type SinkError = vector_common::Error; +#[repr(transparent)] +#[derive(Debug)] +pub struct SinkError(vector_common::Error); + +impl SinkError { + pub fn new(error: impl Into) -> Self { + Self(error.into()) + } + + pub fn into_inner(self) -> vector_common::Error { + self.0 + } +} + +impl Default for SinkError { + fn default() -> Self { + Self(crate::default_task_error()) + } +} + +impl From for SinkError { + fn from(error: vector_common::Error) -> Self { + Self(error) + } +} + +impl fmt::Display for SinkError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl std::error::Error for SinkError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(self.0.as_ref()) + } +} + +impl Deref for SinkError { + type Target = vector_common::Error; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} pub enum VectorSink { Sink(Box + Send + Unpin>), @@ -27,7 +71,7 @@ impl VectorSink { .map(Ok) .forward(sink) .await - .map_err(|()| crate::default_task_error()), + .map_err(|()| SinkError::default()), Self::Stream(s) => s.run(Box::pin(input)).await, } } @@ -132,7 +176,7 @@ where self.sink .run(input) .await - .map_err(|()| crate::default_task_error()) + .map_err(|()| SinkError::default()) } } diff --git a/src/sinks/console/sink.rs b/src/sinks/console/sink.rs index 5b50daba6ecf1..5d5d990d065c5 100644 --- a/src/sinks/console/sink.rs +++ b/src/sinks/console/sink.rs @@ -40,7 +40,7 @@ where let mut bytes = BytesMut::new(); self.encoder.encode(event, &mut bytes).map_err(|error| { finalizers.update_status(EventStatus::Errored); - SinkError::from(error) + SinkError::new(error) })?; if let Err(error) = self.output.write_all(&bytes).await { @@ -48,7 +48,7 @@ where // so stop the sink. error!(message = "Error writing to output. Stopping sink.", %error, internal_log_rate_limit = false); finalizers.update_status(EventStatus::Errored); - return Err(SinkError::from(error)); + return Err(SinkError::new(error)); } finalizers.update_status(EventStatus::Delivered); diff --git a/src/sinks/prometheus/exporter.rs b/src/sinks/prometheus/exporter.rs index 7e456fa81aaf8..ddb1e12181fcf 100644 --- a/src/sinks/prometheus/exporter.rs +++ b/src/sinks/prometheus/exporter.rs @@ -548,7 +548,7 @@ impl StreamSink for PrometheusExporter { async fn run(mut self: Box, mut input: BoxStream<'_, Event>) -> Result<(), SinkError> { self.start_server_if_needed().await.map_err(|error| { error!("Failed to start Prometheus exporter: {error}."); - SinkError::from(error) + SinkError::new(error) })?; let mut last_flush = Instant::now(); diff --git a/src/sinks/websocket_server/sink.rs b/src/sinks/websocket_server/sink.rs index 43d2ad88ad6dc..6aa288e9f281c 100644 --- a/src/sinks/websocket_server/sink.rs +++ b/src/sinks/websocket_server/sink.rs @@ -364,11 +364,7 @@ impl StreamSink for WebSocketListenerSink { let events_sent = register!(EventsSent::from(Output(None))); let encode_as_binary = self.encoder.serializer().is_binary(); - let listener = self - .tls - .bind(&self.address) - .await - .map_err(SinkError::from)?; + let listener = self.tls.bind(&self.address).await.map_err(SinkError::new)?; let peers = Arc::new(Mutex::new(HashMap::default())); let message_buffer = Arc::new(Mutex::new(VecDeque::with_capacity( diff --git a/src/topology/task.rs b/src/topology/task.rs index 13f36d789a4e7..effa4238abaf0 100644 --- a/src/topology/task.rs +++ b/src/topology/task.rs @@ -10,7 +10,9 @@ use futures::{FutureExt, future::BoxFuture}; use pin_project::pin_project; use snafu::Snafu; use tokio::task::JoinError; -use vector_lib::{buffers::topology::channel::BufferReceiverStream, event::EventArray}; +use vector_lib::{ + buffers::topology::channel::BufferReceiverStream, event::EventArray, sink::SinkError, +}; use crate::{config::ComponentKey, utilization::Utilization}; @@ -55,6 +57,12 @@ impl From for TaskError { } } +impl From for TaskError { + fn from(error: SinkError) -> Self { + Self::wrapped(error.into_inner()) + } +} + impl From for TaskError { fn from(e: JoinError) -> Self { if e.is_cancelled() { From 7721fa074d9923823267e676b8dfb53cd215f8ba Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Wed, 14 Oct 2026 15:50:31 +0200 Subject: [PATCH 13/18] fix(sources): retain output send error causes --- lib/vector-core/src/source.rs | 9 +++++++++ src/sources/apache_metrics/mod.rs | 6 +++--- src/sources/aws_ecs_metrics/mod.rs | 8 ++++---- src/sources/demo_logs.rs | 6 +++--- src/sources/file_descriptors/mod.rs | 8 ++++---- src/sources/host_metrics/mod.rs | 12 ++++++++---- src/sources/http_client/client.rs | 2 +- src/sources/internal_logs.rs | 8 ++++---- src/sources/internal_metrics.rs | 8 ++++---- src/sources/mod.rs | 2 +- src/sources/mongodb_metrics/mod.rs | 6 +++--- src/sources/nats/config.rs | 4 ++-- src/sources/nats/source.rs | 16 +++++++-------- src/sources/nginx_metrics/mod.rs | 6 +++--- src/sources/okta/client.rs | 14 ++++++++------ src/sources/postgresql_metrics.rs | 6 +++--- src/sources/prometheus/scrape.rs | 2 +- src/sources/static_metrics.rs | 30 +++++++++++++++++++++++++---- src/sources/util/http_client.rs | 6 +++--- 19 files changed, 98 insertions(+), 61 deletions(-) diff --git a/lib/vector-core/src/source.rs b/lib/vector-core/src/source.rs index c01011f84d452..312400f98adab 100644 --- a/lib/vector-core/src/source.rs +++ b/lib/vector-core/src/source.rs @@ -15,3 +15,12 @@ where { Box::pin(async move { future.await.map_err(|()| crate::default_task_error()) }) } + +/// Adapt a source that returns an error with a cause. +pub fn source_with_error(future: F) -> Source +where + F: Future> + Send + 'static, + E: Into, +{ + Box::pin(async move { future.await.map_err(Into::into) }) +} diff --git a/src/sources/apache_metrics/mod.rs b/src/sources/apache_metrics/mod.rs index f396641bb12bd..2de3d013f96d2 100644 --- a/src/sources/apache_metrics/mod.rs +++ b/src/sources/apache_metrics/mod.rs @@ -149,7 +149,7 @@ fn apache_metrics( mut out: SourceSender, proxy: ProxyConfig, ) -> super::Source { - crate::sources::opaque_source(async move { + crate::sources::source_with_error(async move { let mut stream = IntervalStream::new(tokio::time::interval(interval)) .take_until(shutdown) .map(move |_| stream::iter(urls.clone())) @@ -267,10 +267,10 @@ fn apache_metrics( debug!("Finished sending."); Ok(()) } - Err(_) => { + Err(error) => { let (count, _) = stream.size_hint(); emit!(StreamClosedError { count }); - Err(()) + Err(error) } } }) diff --git a/src/sources/aws_ecs_metrics/mod.rs b/src/sources/aws_ecs_metrics/mod.rs index 05de242d84fab..ab62b01205506 100644 --- a/src/sources/aws_ecs_metrics/mod.rs +++ b/src/sources/aws_ecs_metrics/mod.rs @@ -157,7 +157,7 @@ impl SourceConfig for AwsEcsMetricsSourceConfig { let namespace = Some(self.namespace.clone()).filter(|namespace| !namespace.is_empty()); let http_client = HttpClient::new(None, &cx.proxy)?; - Ok(crate::sources::opaque_source(aws_ecs_metrics( + Ok(crate::sources::source_with_error(aws_ecs_metrics( http_client, self.stats_endpoint(), self.scrape_interval_secs, @@ -183,7 +183,7 @@ async fn aws_ecs_metrics( namespace: Option, mut out: SourceSender, shutdown: ShutdownSignal, -) -> Result<(), ()> { +) -> Result<(), vector_lib::source_sender::SendError> { let mut interval = IntervalStream::new(time::interval(interval)).take_until(shutdown); let bytes_received = register!(BytesReceived::from(Protocol::HTTP)); while interval.next().await.is_some() { @@ -210,9 +210,9 @@ async fn aws_ecs_metrics( endpoint: uri.path(), }); - if (out.send_batch(metrics).await).is_err() { + if let Err(error) = out.send_batch(metrics).await { emit!(StreamClosedError { count }); - return Err(()); + return Err(error); } } Err(error) => { diff --git a/src/sources/demo_logs.rs b/src/sources/demo_logs.rs index 394c2d4b891a2..6114cad645adb 100644 --- a/src/sources/demo_logs.rs +++ b/src/sources/demo_logs.rs @@ -211,7 +211,7 @@ async fn demo_logs_source( mut shutdown: ShutdownSignal, mut out: SourceSender, log_namespace: LogNamespace, -) -> Result<(), ()> { +) -> Result<(), vector_lib::source_sender::SendError> { let interval: Option = (interval != Duration::ZERO).then_some(interval); let mut interval = interval.map(time::interval); @@ -263,7 +263,7 @@ async fn demo_logs_source( event }); - out.send_batch(events).await.map_err(|_| { + out.send_batch(events).await.inspect_err(|_| { emit!(StreamClosedError { count }); })?; } @@ -293,7 +293,7 @@ impl SourceConfig for DemoLogsConfig { let decoder = DecodingConfig::new(self.framing.clone(), self.decoding.clone(), log_namespace) .build()?; - Ok(crate::sources::opaque_source(demo_logs_source( + Ok(crate::sources::source_with_error(demo_logs_source( self.interval, self.count, self.format.clone(), diff --git a/src/sources/file_descriptors/mod.rs b/src/sources/file_descriptors/mod.rs index cf4f6799f6983..071229b551372 100644 --- a/src/sources/file_descriptors/mod.rs +++ b/src/sources/file_descriptors/mod.rs @@ -74,7 +74,7 @@ pub trait FileDescriptorConfig: NamedComponent { read_from_fd(reader, sender); }); - Ok(crate::sources::opaque_source(process_stream( + Ok(crate::sources::source_with_error(process_stream( receiver, decoder, out, @@ -122,7 +122,7 @@ async fn process_stream( source_type: &'static str, hostname: Option, log_namespace: LogNamespace, -) -> Result<(), ()> { +) -> Result<(), vector_lib::source_sender::SendError> { let bytes_received = register!(BytesReceived::from(Protocol::NONE)); let events_received = register!(EventsReceived); let stream = receiver.inspect(|result| { @@ -190,10 +190,10 @@ async fn process_stream( debug!("Finished sending."); Ok(()) } - Err(_) => { + Err(error) => { let (count, _) = stream.size_hint(); emit!(StreamClosedError { count }); - Err(()) + Err(error) } } } diff --git a/src/sources/host_metrics/mod.rs b/src/sources/host_metrics/mod.rs index 7fa7c959e4603..b48bee5e5415c 100644 --- a/src/sources/host_metrics/mod.rs +++ b/src/sources/host_metrics/mod.rs @@ -304,7 +304,7 @@ impl SourceConfig for HostMetricsConfig { let mut config = self.clone(); config.namespace = config.namespace.filter(|namespace| !namespace.is_empty()); - Ok(crate::sources::opaque_source( + Ok(crate::sources::source_with_error( config.run(cx.out, cx.shutdown), )) } @@ -324,7 +324,11 @@ impl HostMetricsConfig { self.scrape_interval_secs = Duration::from_secs_f64(value); } - async fn run(self, mut out: SourceSender, shutdown: ShutdownSignal) -> Result<(), ()> { + async fn run( + self, + mut out: SourceSender, + shutdown: ShutdownSignal, + ) -> Result<(), vector_lib::source_sender::SendError> { let duration = self.scrape_interval_secs; let mut interval = IntervalStream::new(time::interval(duration)).take_until(shutdown); @@ -336,9 +340,9 @@ impl HostMetricsConfig { bytes_received.emit(ByteSize(0)); let metrics = generator.capture_metrics().await; let count = metrics.len(); - if (out.send_batch(metrics).await).is_err() { + if let Err(error) = out.send_batch(metrics).await { emit!(StreamClosedError { count }); - return Err(()); + return Err(error); } } diff --git a/src/sources/http_client/client.rs b/src/sources/http_client/client.rs index ec82406885dbd..d8eaf3290b169 100644 --- a/src/sources/http_client/client.rs +++ b/src/sources/http_client/client.rs @@ -377,7 +377,7 @@ impl SourceConfig for HttpClientConfig { shutdown: cx.shutdown, }; - Ok(crate::sources::opaque_source(call( + Ok(crate::sources::source_with_error(call( inputs, context, cx.out, diff --git a/src/sources/internal_logs.rs b/src/sources/internal_logs.rs index 77756544a85a4..210459e30430d 100644 --- a/src/sources/internal_logs.rs +++ b/src/sources/internal_logs.rs @@ -113,7 +113,7 @@ impl SourceConfig for InternalLogsConfig { let log_namespace = cx.log_namespace(self.log_namespace); - Ok(crate::sources::opaque_source(run( + Ok(crate::sources::source_with_error(run( host_key, pid_key, subscription, @@ -145,7 +145,7 @@ async fn run( mut out: SourceSender, shutdown: ShutdownSignal, log_namespace: LogNamespace, -) -> Result<(), ()> { +) -> Result<(), vector_lib::source_sender::SendError> { let hostname = crate::get_hostname(); let pid = std::process::id(); @@ -196,10 +196,10 @@ async fn run( Utc::now(), ); - if (out.send_event(Event::from(log)).await).is_err() { + if let Err(error) = out.send_event(Event::from(log)).await { // this wont trigger any infinite loop considering it stops the component emit!(StreamClosedError { count: 1 }); - return Err(()); + return Err(error); } } diff --git a/src/sources/internal_metrics.rs b/src/sources/internal_metrics.rs index c8d0e8f030f0a..3b356d175245d 100644 --- a/src/sources/internal_metrics.rs +++ b/src/sources/internal_metrics.rs @@ -112,7 +112,7 @@ impl SourceConfig for InternalMetricsConfig { .as_deref() .and_then(|tag| (!tag.is_empty()).then(|| tag.to_owned())); - Ok(crate::sources::opaque_source( + Ok(crate::sources::source_with_error( InternalMetrics { namespace, host_key, @@ -146,7 +146,7 @@ struct InternalMetrics<'a> { } impl InternalMetrics<'_> { - async fn run(mut self) -> Result<(), ()> { + async fn run(mut self) -> Result<(), vector_lib::source_sender::SendError> { let events_received = register!(EventsReceived); let bytes_received = register!(BytesReceived::from(Protocol::INTERNAL)); let mut interval = @@ -181,9 +181,9 @@ impl InternalMetrics<'_> { metric }); - if (self.out.send_batch(batch).await).is_err() { + if let Err(error) = self.out.send_batch(batch).await { emit!(StreamClosedError { count }); - return Err(()); + return Err(error); } } diff --git a/src/sources/mod.rs b/src/sources/mod.rs index accafb2aa747c..3f4fd97bed6bb 100644 --- a/src/sources/mod.rs +++ b/src/sources/mod.rs @@ -99,7 +99,7 @@ pub mod windows_event_log; pub mod util; -pub use vector_lib::source::{Source, SourceError, opaque_source}; +pub use vector_lib::source::{Source, SourceError, opaque_source, source_with_error}; #[allow(dead_code)] // Easier than listing out all the features that use this /// Common build errors diff --git a/src/sources/mongodb_metrics/mod.rs b/src/sources/mongodb_metrics/mod.rs index f3e6e48bd7d77..c6ad3bc3d58c3 100644 --- a/src/sources/mongodb_metrics/mod.rs +++ b/src/sources/mongodb_metrics/mod.rs @@ -135,7 +135,7 @@ impl SourceConfig for MongoDbMetricsConfig { let duration = self.scrape_interval_secs; let shutdown = cx.shutdown; - Ok(crate::sources::opaque_source(async move { + Ok(crate::sources::source_with_error(async move { let mut interval = IntervalStream::new(time::interval(duration)).take_until(shutdown); while interval.next().await.is_some() { let start = Instant::now(); @@ -148,9 +148,9 @@ impl SourceConfig for MongoDbMetricsConfig { let metrics: Vec = metrics.into_iter().flatten().collect(); let count = metrics.len(); - if (cx.out.send_batch(metrics).await).is_err() { + if let Err(error) = cx.out.send_batch(metrics).await { emit!(StreamClosedError { count }); - return Err(()); + return Err(error); } } diff --git a/src/sources/nats/config.rs b/src/sources/nats/config.rs index 33cb5d7be69ea..fb4b3733086c0 100644 --- a/src/sources/nats/config.rs +++ b/src/sources/nats/config.rs @@ -202,7 +202,7 @@ impl SourceConfig for NatsSourceConfig { let connection = self.connect().await?; let messages = create_consumer_stream(&connection, js_config).await?; - Ok(crate::sources::opaque_source(run_nats_jetstream( + Ok(crate::sources::source_with_error(run_nats_jetstream( self.clone(), connection, messages, @@ -215,7 +215,7 @@ impl SourceConfig for NatsSourceConfig { NatsMode::Core => { let (connection, subscription) = create_subscription(self).await?; - Ok(crate::sources::opaque_source(run_nats_core( + Ok(crate::sources::source_with_error(run_nats_core( self.clone(), connection, subscription, diff --git a/src/sources/nats/source.rs b/src/sources/nats/source.rs index d22a4ca37cba7..29030ffc07480 100644 --- a/src/sources/nats/source.rs +++ b/src/sources/nats/source.rs @@ -33,7 +33,7 @@ pub enum ProcessingStatus { /// A non-recoverable error occurred while decoding the payload. Failed, /// The downstream channel is closed, and the source should shut down. - ChannelClosed, + ChannelClosed(vector_lib::source_sender::SendError), } /// Processes a single NATS message, sending decoded events downstream. @@ -84,9 +84,9 @@ pub async fn process_message( event }); - if out.send_batch(events).await.is_err() { + if let Err(error) = out.send_batch(events).await { emit!(StreamClosedError { count }); - return ProcessingStatus::ChannelClosed; + return ProcessingStatus::ChannelClosed(error); } } Err(error) => { @@ -137,7 +137,7 @@ pub async fn run_nats_jetstream( log_namespace: LogNamespace, mut shutdown: ShutdownSignal, mut out: SourceSender, -) -> Result<(), ()> { +) -> Result<(), vector_lib::source_sender::SendError> { let events_received = register!(EventsReceived); let bytes_received = register!(BytesReceived::from(Protocol::TCP)); let mut backoff = ExponentialBackoff::default().max_delay(std::time::Duration::from_secs(30)); @@ -179,7 +179,7 @@ pub async fn run_nats_jetstream( error!(message = "Failed to acknowledge JetStream message.", %err); } } - ProcessingStatus::ChannelClosed => return Err(()), + ProcessingStatus::ChannelClosed(error) => return Err(error), // Do not acknowledge on failure; the message will be redelivered. ProcessingStatus::Failed => {} } @@ -232,7 +232,7 @@ pub async fn run_nats_core( log_namespace: LogNamespace, mut shutdown: ShutdownSignal, mut out: SourceSender, -) -> Result<(), ()> { +) -> Result<(), vector_lib::source_sender::SendError> { let events_received = register!(EventsReceived); let bytes_received = register!(BytesReceived::from(Protocol::TCP)); @@ -261,8 +261,8 @@ pub async fn run_nats_core( ) .await; - if let ProcessingStatus::ChannelClosed = status { - return Err(()); + if let ProcessingStatus::ChannelClosed(error) = status { + return Err(error); } }, None => { diff --git a/src/sources/nginx_metrics/mod.rs b/src/sources/nginx_metrics/mod.rs index 142186558d34c..7f1d4c2c2d59f 100644 --- a/src/sources/nginx_metrics/mod.rs +++ b/src/sources/nginx_metrics/mod.rs @@ -120,7 +120,7 @@ impl SourceConfig for NginxMetricsConfig { let duration = self.scrape_interval_secs; let shutdown = cx.shutdown; - Ok(crate::sources::opaque_source(async move { + Ok(crate::sources::source_with_error(async move { let mut interval = IntervalStream::new(time::interval(duration)).take_until(shutdown); while interval.next().await.is_some() { let start = Instant::now(); @@ -133,9 +133,9 @@ impl SourceConfig for NginxMetricsConfig { let metrics: Vec = metrics.into_iter().flatten().collect(); let count = metrics.len(); - if (cx.out.send_batch(metrics).await).is_err() { + if let Err(error) = cx.out.send_batch(metrics).await { emit!(StreamClosedError { count }); - return Err(()); + return Err(error); } } diff --git a/src/sources/okta/client.rs b/src/sources/okta/client.rs index 3ccfa8af0d9f0..8073a3515605e 100644 --- a/src/sources/okta/client.rs +++ b/src/sources/okta/client.rs @@ -153,7 +153,7 @@ impl SourceConfig for OktaConfig { warn_if_interval_too_low(self.timeout, self.interval); - Ok(crate::sources::opaque_source(run( + Ok(crate::sources::source_with_error(run( url, tls, cx.proxy, @@ -293,7 +293,7 @@ async fn run( log_namespace: LogNamespace, shutdown: ShutdownSignal, mut out: SourceSender, -) -> Result<(), ()> { +) -> Result<(), crate::Error> { let url_mutex = Arc::new(Mutex::new(url.clone())); let decoder = DecodingConfig::new( FramingConfig::Bytes, @@ -301,15 +301,17 @@ async fn run( log_namespace, ) .build() - .map_err(|ref e| { + .inspect_err(|e| { emit!(DecoderDeserializeError { error: e }); })?; let client = HttpClient::new(tls, &proxy).map_err(|e| { + let error = Arc::new(e); emit!(HttpClientHttpError { - error: Box::new(e), + error: Box::new(Arc::clone(&error)), url: url.to_string() }); + crate::Error::from(error) })?; let mut stream = IntervalStream::new(tokio::time::interval(interval)) @@ -389,10 +391,10 @@ async fn run( debug!("Finished sending."); Ok(()) } - Err(_) => { + Err(error) => { let (count, _) = stream.size_hint(); emit!(StreamClosedError { count }); - Err(()) + Err(error.into()) } } } diff --git a/src/sources/postgresql_metrics.rs b/src/sources/postgresql_metrics.rs index 4d18ec8ae8a08..6da7742286f12 100644 --- a/src/sources/postgresql_metrics.rs +++ b/src/sources/postgresql_metrics.rs @@ -218,7 +218,7 @@ impl SourceConfig for PostgresqlMetricsConfig { let duration = self.scrape_interval_secs; let shutdown = cx.shutdown; - Ok(crate::sources::opaque_source(async move { + Ok(crate::sources::source_with_error(async move { let mut interval = IntervalStream::new(time::interval(duration)).take_until(shutdown); while interval.next().await.is_some() { let start = Instant::now(); @@ -231,9 +231,9 @@ impl SourceConfig for PostgresqlMetricsConfig { let metrics: Vec = metrics.into_iter().flatten().collect(); let count = metrics.len(); - if (cx.out.send_batch(metrics).await).is_err() { + if let Err(error) = cx.out.send_batch(metrics).await { emit!(StreamClosedError { count }); - return Err(()); + return Err(error); } } diff --git a/src/sources/prometheus/scrape.rs b/src/sources/prometheus/scrape.rs index 4e7a02484f72f..c50afc6c4462b 100644 --- a/src/sources/prometheus/scrape.rs +++ b/src/sources/prometheus/scrape.rs @@ -171,7 +171,7 @@ impl SourceConfig for PrometheusScrapeConfig { shutdown: cx.shutdown, }; - Ok(crate::sources::opaque_source(call( + Ok(crate::sources::source_with_error(call( inputs, builder, cx.out, diff --git a/src/sources/static_metrics.rs b/src/sources/static_metrics.rs index d7fe5a934eada..7e8a7477a00e7 100644 --- a/src/sources/static_metrics.rs +++ b/src/sources/static_metrics.rs @@ -103,7 +103,7 @@ impl SourceConfig for StaticMetricsConfig { let metrics = self.metrics.clone(); - Ok(crate::sources::opaque_source( + Ok(crate::sources::source_with_error( StaticMetrics { namespace, metrics, @@ -133,7 +133,7 @@ struct StaticMetrics { } impl StaticMetrics { - async fn run(mut self) -> Result<(), ()> { + async fn run(mut self) -> Result<(), vector_lib::source_sender::SendError> { let events_received = register!(EventsReceived); let bytes_received = register!(BytesReceived::from(Protocol::STATIC)); let mut interval = @@ -185,9 +185,9 @@ impl StaticMetrics { .into_iter() .map(|metric| metric.with_timestamp(Some(Utc::now()))); - if (self.out.send_batch(batch).await).is_err() { + if let Err(error) = self.out.send_batch(batch).await { emit!(StreamClosedError { count }); - return Err(()); + return Err(error); } } @@ -225,6 +225,28 @@ mod tests { } } + #[tokio::test] + async fn send_error_retains_cause() { + let (out, receiver) = SourceSender::new_test(); + drop(receiver); + + let source = StaticMetrics { + namespace: "static".to_owned(), + metrics: vec![default_metric()], + interval: time::Duration::from_secs(1), + out, + shutdown: ShutdownSignal::noop(), + }; + + let error = crate::sources::source_with_error(source.run()) + .await + .expect_err("sending to a closed output should fail"); + assert_eq!( + error.downcast_ref::(), + Some(&vector_lib::source_sender::SendError::Closed) + ); + } + #[tokio::test] async fn default_empty() { let events = events_from_config(StaticMetricsConfig::default()).await; diff --git a/src/sources/util/http_client.rs b/src/sources/util/http_client.rs index b0730ebe04861..73258f340e3ae 100644 --- a/src/sources/util/http_client.rs +++ b/src/sources/util/http_client.rs @@ -198,7 +198,7 @@ pub(crate) async fn call< context_builder: B, mut out: SourceSender, http_method: HttpMethod, -) -> Result<(), ()> { +) -> Result<(), vector_lib::source_sender::SendError> { // Building the HttpClient should not fail as it is just setting up the client with the // proxy and tls settings. let client = @@ -341,10 +341,10 @@ pub(crate) async fn call< debug!("Finished sending."); Ok(()) } - Err(_) => { + Err(error) => { let (count, _) = stream.size_hint(); emit!(StreamClosedError { count }); - Err(()) + Err(error) } } } From f16b88027fcdba1ff5328fbe1e1ab03b7df57b00 Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Wed, 14 Oct 2026 16:33:09 +0200 Subject: [PATCH 14/18] misc simplification --- lib/vector-core/src/source.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/vector-core/src/source.rs b/lib/vector-core/src/source.rs index 312400f98adab..1e127d80eaff0 100644 --- a/lib/vector-core/src/source.rs +++ b/lib/vector-core/src/source.rs @@ -1,6 +1,6 @@ use std::future::Future; -use futures::future::BoxFuture; +use futures::{TryFutureExt, future::BoxFuture}; /// Error returned by a running source. pub type SourceError = vector_common::Error; @@ -13,7 +13,7 @@ pub fn opaque_source(future: F) -> Source where F: Future> + Send + 'static, { - Box::pin(async move { future.await.map_err(|()| crate::default_task_error()) }) + Box::pin(future.map_err(|()| crate::default_task_error())) } /// Adapt a source that returns an error with a cause. @@ -22,5 +22,5 @@ where F: Future> + Send + 'static, E: Into, { - Box::pin(async move { future.await.map_err(Into::into) }) + Box::pin(future.err_into()) } From af8235d31be174984d4c02acda774e0b4cfcb56b Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Fri, 16 Oct 2026 10:48:01 +0200 Subject: [PATCH 15/18] refactor(topology): simplify runtime error adapters --- lib/vector-core/src/sink.rs | 13 +++---------- src/sources/mqtt/source.rs | 8 ++------ src/topology/builder.rs | 4 ++-- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/lib/vector-core/src/sink.rs b/lib/vector-core/src/sink.rs index ad8ba3915469b..7769f923d9e20 100644 --- a/lib/vector-core/src/sink.rs +++ b/lib/vector-core/src/sink.rs @@ -133,9 +133,7 @@ impl VectorSink { /// Converts a stream sink with opaque runtime errors into a `VectorSink`. pub fn from_streamsink(sink: impl StreamSink + Send + 'static) -> Self { - Self::Stream(Box::new(OpaqueStream { - sink: Box::new(sink), - })) + Self::Stream(Box::new(OpaqueStream(Box::new(sink)))) } /// Converts a sink that retains its runtime errors into a `VectorSink`. @@ -160,9 +158,7 @@ pub trait StreamSink { async fn run(self: Box, input: stream::BoxStream<'_, T>) -> Result<(), E>; } -struct OpaqueStream { - sink: Box, -} +struct OpaqueStream(Box); #[async_trait::async_trait] impl StreamSink for OpaqueStream @@ -173,10 +169,7 @@ where self: Box, input: stream::BoxStream<'_, EventArray>, ) -> Result<(), SinkError> { - self.sink - .run(input) - .await - .map_err(|()| SinkError::default()) + self.0.run(input).await.map_err(|()| SinkError::default()) } } diff --git a/src/sources/mqtt/source.rs b/src/sources/mqtt/source.rs index 088c4c69c9b2d..03a8bc1cac23d 100644 --- a/src/sources/mqtt/source.rs +++ b/src/sources/mqtt/source.rs @@ -48,10 +48,7 @@ impl MqttSource { match &self.config.topic { OneOrMany::One(topic) => { - client - .subscribe(topic, QoS::AtLeastOnce) - .await - .map_err(SourceError::from)?; + client.subscribe(topic, QoS::AtLeastOnce).await?; } OneOrMany::Many(topics) => { client @@ -61,8 +58,7 @@ impl MqttSource { .cloned() .map(|topic| SubscribeFilter::new(topic, QoS::AtLeastOnce)), ) - .await - .map_err(SourceError::from)?; + .await?; } } diff --git a/src/topology/builder.rs b/src/topology/builder.rs index 1d46234fc0e20..fb4a07ff7e38b 100644 --- a/src/topology/builder.rs +++ b/src/topology/builder.rs @@ -473,7 +473,7 @@ impl<'a> Builder<'a> { Ok(TaskOutput::Source) } Err(e) => { - debug!("Source finished with an error."); + debug!(error = %e, "Source finished with an error."); Err(e) } } @@ -756,7 +756,7 @@ impl<'a> Builder<'a> { debug!("Sink finished normally."); TaskOutput::Sink(rx) }) - .inspect_err(|_| debug!("Sink finished with an error.")) + .inspect_err(|error| debug!(%error, "Sink finished with an error.")) .map_err(Into::into) }; From 13e9b588759e0d208447956c8aa10066e5d2be0c Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Fri, 16 Oct 2026 11:12:22 +0200 Subject: [PATCH 16/18] refactor(sources): alias downstream send results --- lib/vector-core/src/source_sender/mod.rs | 2 ++ src/sources/aws_ecs_metrics/mod.rs | 2 +- src/sources/demo_logs.rs | 2 +- src/sources/file_descriptors/mod.rs | 2 +- src/sources/host_metrics/mod.rs | 2 +- src/sources/internal_logs.rs | 2 +- src/sources/internal_metrics.rs | 2 +- src/sources/mongodb_metrics/mod.rs | 11 ++++++----- src/sources/nats/source.rs | 4 ++-- src/sources/static_metrics.rs | 2 +- src/sources/util/http_client.rs | 2 +- 11 files changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/vector-core/src/source_sender/mod.rs b/lib/vector-core/src/source_sender/mod.rs index fb06b237cb4ea..9ab0e73c8918b 100644 --- a/lib/vector-core/src/source_sender/mod.rs +++ b/lib/vector-core/src/source_sender/mod.rs @@ -14,6 +14,8 @@ mod tests; pub use builder::Builder; pub use errors::SendError; +/// Result of sending a source's events downstream. +pub type SendResult = Result<(), SendError>; use output::{Output, OutputMetrics}; pub use sender::{SourceSender, SourceSenderItem}; diff --git a/src/sources/aws_ecs_metrics/mod.rs b/src/sources/aws_ecs_metrics/mod.rs index ab62b01205506..4cc0d3a18bdd2 100644 --- a/src/sources/aws_ecs_metrics/mod.rs +++ b/src/sources/aws_ecs_metrics/mod.rs @@ -183,7 +183,7 @@ async fn aws_ecs_metrics( namespace: Option, mut out: SourceSender, shutdown: ShutdownSignal, -) -> Result<(), vector_lib::source_sender::SendError> { +) -> vector_lib::source_sender::SendResult { let mut interval = IntervalStream::new(time::interval(interval)).take_until(shutdown); let bytes_received = register!(BytesReceived::from(Protocol::HTTP)); while interval.next().await.is_some() { diff --git a/src/sources/demo_logs.rs b/src/sources/demo_logs.rs index 6114cad645adb..34f9ef4783323 100644 --- a/src/sources/demo_logs.rs +++ b/src/sources/demo_logs.rs @@ -211,7 +211,7 @@ async fn demo_logs_source( mut shutdown: ShutdownSignal, mut out: SourceSender, log_namespace: LogNamespace, -) -> Result<(), vector_lib::source_sender::SendError> { +) -> vector_lib::source_sender::SendResult { let interval: Option = (interval != Duration::ZERO).then_some(interval); let mut interval = interval.map(time::interval); diff --git a/src/sources/file_descriptors/mod.rs b/src/sources/file_descriptors/mod.rs index 071229b551372..f818729d0e6e5 100644 --- a/src/sources/file_descriptors/mod.rs +++ b/src/sources/file_descriptors/mod.rs @@ -122,7 +122,7 @@ async fn process_stream( source_type: &'static str, hostname: Option, log_namespace: LogNamespace, -) -> Result<(), vector_lib::source_sender::SendError> { +) -> vector_lib::source_sender::SendResult { let bytes_received = register!(BytesReceived::from(Protocol::NONE)); let events_received = register!(EventsReceived); let stream = receiver.inspect(|result| { diff --git a/src/sources/host_metrics/mod.rs b/src/sources/host_metrics/mod.rs index b48bee5e5415c..c60b3a32d7c52 100644 --- a/src/sources/host_metrics/mod.rs +++ b/src/sources/host_metrics/mod.rs @@ -328,7 +328,7 @@ impl HostMetricsConfig { self, mut out: SourceSender, shutdown: ShutdownSignal, - ) -> Result<(), vector_lib::source_sender::SendError> { + ) -> vector_lib::source_sender::SendResult { let duration = self.scrape_interval_secs; let mut interval = IntervalStream::new(time::interval(duration)).take_until(shutdown); diff --git a/src/sources/internal_logs.rs b/src/sources/internal_logs.rs index 210459e30430d..6c79b625ddf12 100644 --- a/src/sources/internal_logs.rs +++ b/src/sources/internal_logs.rs @@ -145,7 +145,7 @@ async fn run( mut out: SourceSender, shutdown: ShutdownSignal, log_namespace: LogNamespace, -) -> Result<(), vector_lib::source_sender::SendError> { +) -> vector_lib::source_sender::SendResult { let hostname = crate::get_hostname(); let pid = std::process::id(); diff --git a/src/sources/internal_metrics.rs b/src/sources/internal_metrics.rs index 3b356d175245d..c18ffc2b26b33 100644 --- a/src/sources/internal_metrics.rs +++ b/src/sources/internal_metrics.rs @@ -146,7 +146,7 @@ struct InternalMetrics<'a> { } impl InternalMetrics<'_> { - async fn run(mut self) -> Result<(), vector_lib::source_sender::SendError> { + async fn run(mut self) -> vector_lib::source_sender::SendResult { let events_received = register!(EventsReceived); let bytes_received = register!(BytesReceived::from(Protocol::INTERNAL)); let mut interval = diff --git a/src/sources/mongodb_metrics/mod.rs b/src/sources/mongodb_metrics/mod.rs index c6ad3bc3d58c3..5568d1224bd3d 100644 --- a/src/sources/mongodb_metrics/mod.rs +++ b/src/sources/mongodb_metrics/mod.rs @@ -17,6 +17,7 @@ use tokio::time; use tokio_stream::wrappers::IntervalStream; use vector_lib::{ ByteSizeOf, EstimatedJsonEncodedSizeOf, configurable::configurable_component, metric_tags, + source_sender::SendError, }; use crate::{ @@ -148,13 +149,13 @@ impl SourceConfig for MongoDbMetricsConfig { let metrics: Vec = metrics.into_iter().flatten().collect(); let count = metrics.len(); - if let Err(error) = cx.out.send_batch(metrics).await { - emit!(StreamClosedError { count }); - return Err(error); - } + cx.out + .send_batch(metrics) + .await + .inspect_err(|_| emit!(StreamClosedError { count }))?; } - Ok(()) + Ok::<(), SendError>(()) })) } diff --git a/src/sources/nats/source.rs b/src/sources/nats/source.rs index 29030ffc07480..84687065d3b59 100644 --- a/src/sources/nats/source.rs +++ b/src/sources/nats/source.rs @@ -137,7 +137,7 @@ pub async fn run_nats_jetstream( log_namespace: LogNamespace, mut shutdown: ShutdownSignal, mut out: SourceSender, -) -> Result<(), vector_lib::source_sender::SendError> { +) -> vector_lib::source_sender::SendResult { let events_received = register!(EventsReceived); let bytes_received = register!(BytesReceived::from(Protocol::TCP)); let mut backoff = ExponentialBackoff::default().max_delay(std::time::Duration::from_secs(30)); @@ -232,7 +232,7 @@ pub async fn run_nats_core( log_namespace: LogNamespace, mut shutdown: ShutdownSignal, mut out: SourceSender, -) -> Result<(), vector_lib::source_sender::SendError> { +) -> vector_lib::source_sender::SendResult { let events_received = register!(EventsReceived); let bytes_received = register!(BytesReceived::from(Protocol::TCP)); diff --git a/src/sources/static_metrics.rs b/src/sources/static_metrics.rs index 7e8a7477a00e7..da55c2d19a554 100644 --- a/src/sources/static_metrics.rs +++ b/src/sources/static_metrics.rs @@ -133,7 +133,7 @@ struct StaticMetrics { } impl StaticMetrics { - async fn run(mut self) -> Result<(), vector_lib::source_sender::SendError> { + async fn run(mut self) -> vector_lib::source_sender::SendResult { let events_received = register!(EventsReceived); let bytes_received = register!(BytesReceived::from(Protocol::STATIC)); let mut interval = diff --git a/src/sources/util/http_client.rs b/src/sources/util/http_client.rs index 73258f340e3ae..a412ea85dcb78 100644 --- a/src/sources/util/http_client.rs +++ b/src/sources/util/http_client.rs @@ -198,7 +198,7 @@ pub(crate) async fn call< context_builder: B, mut out: SourceSender, http_method: HttpMethod, -) -> Result<(), vector_lib::source_sender::SendError> { +) -> vector_lib::source_sender::SendResult { // Building the HttpClient should not fail as it is just setting up the client with the // proxy and tls settings. let client = From 2896529ec369bc1c1cd0441e0f3850aef5592231 Mon Sep 17 00:00:00 2001 From: Yoenn Burban Date: Fri, 16 Oct 2026 11:26:01 +0200 Subject: [PATCH 17/18] fix(topology): identify opaque source and sink failures --- lib/vector-core/src/lib.rs | 5 ----- lib/vector-core/src/sink.rs | 2 +- lib/vector-core/src/source.rs | 6 +++++- lib/vector-lib/src/lib.rs | 4 ++-- src/test_util/mock/sources/error.rs | 4 ++-- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/vector-core/src/lib.rs b/lib/vector-core/src/lib.rs index 0759b6b4140b8..87b562a3ca6f7 100644 --- a/lib/vector-core/src/lib.rs +++ b/lib/vector-core/src/lib.rs @@ -59,11 +59,6 @@ pub fn default_data_dir() -> Option { Some(PathBuf::from("/var/lib/vector/")) } -/// Error for a component that reports failure without a cause. -pub fn default_task_error() -> vector_common::Error { - "the task completed with an error".into() -} - pub(crate) use vector_common::{Error, Result}; pub(crate) fn float_eq(l_value: f64, r_value: f64) -> bool { diff --git a/lib/vector-core/src/sink.rs b/lib/vector-core/src/sink.rs index 7769f923d9e20..e3e76d0d684e2 100644 --- a/lib/vector-core/src/sink.rs +++ b/lib/vector-core/src/sink.rs @@ -24,7 +24,7 @@ impl SinkError { impl Default for SinkError { fn default() -> Self { - Self(crate::default_task_error()) + Self("the sink task completed with an error".into()) } } diff --git a/lib/vector-core/src/source.rs b/lib/vector-core/src/source.rs index 1e127d80eaff0..f3aa4ce90c8de 100644 --- a/lib/vector-core/src/source.rs +++ b/lib/vector-core/src/source.rs @@ -8,12 +8,16 @@ pub type SourceError = vector_common::Error; /// Future that runs a source until shutdown or failure. pub type Source = BoxFuture<'static, Result<(), SourceError>>; +fn default_source_error() -> SourceError { + "the source task completed with an error".into() +} + /// Adapt a source that only reports whether it failed. pub fn opaque_source(future: F) -> Source where F: Future> + Send + 'static, { - Box::pin(future.map_err(|()| crate::default_task_error())) + Box::pin(future.map_err(|()| default_source_error())) } /// Adapt a source that returns an error with a cause. diff --git a/lib/vector-lib/src/lib.rs b/lib/vector-lib/src/lib.rs index a51659b5dd467..7fb4e0371b8b7 100644 --- a/lib/vector-lib/src/lib.rs +++ b/lib/vector-lib/src/lib.rs @@ -22,8 +22,8 @@ pub use vector_config as configurable; pub use vector_config::impl_generate_config_from_default; pub use vector_core::compile_vrl; pub use vector_core::{ - EstimatedJsonEncodedSizeOf, SpanField, buckets, default_data_dir, default_task_error, emit, - event, fanout, ipallowlist, latency, metric_tags, metrics, partition, quantiles, register, + EstimatedJsonEncodedSizeOf, SpanField, buckets, default_data_dir, emit, event, fanout, + ipallowlist, latency, metric_tags, metrics, partition, quantiles, register, register_extra_span_field, samples, schema, serde, sink, source, source_sender, tcp, tls, transform, }; diff --git a/src/test_util/mock/sources/error.rs b/src/test_util/mock/sources/error.rs index cc5836598ff89..05f4029fb060a 100644 --- a/src/test_util/mock/sources/error.rs +++ b/src/test_util/mock/sources/error.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use futures_util::{FutureExt, future::err}; +use futures_util::future::err; use vector_lib::{ config::{DataType, LogNamespace, SourceOutput}, configurable::configurable_component, @@ -23,7 +23,7 @@ impl_generate_config_from_default!(ErrorSourceConfig); #[typetag::serde(name = "test_error")] impl SourceConfig for ErrorSourceConfig { async fn build(&self, _cx: SourceContext) -> crate::Result { - Ok(err(vector_lib::default_task_error()).boxed()) + Ok(vector_lib::source::opaque_source(err(()))) } fn outputs(&self, _global_log_namespace: LogNamespace) -> Vec { From 0231edc5277d2bb7c000e3d100a0943e6839e0bf Mon Sep 17 00:00:00 2001 From: Yoenn Burban <62966490+gwenaskell@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:59:27 +0200 Subject: [PATCH 18/18] Update changelog.d/socket_bind_error_context.fix.md Co-authored-by: Thomas --- changelog.d/socket_bind_error_context.fix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/socket_bind_error_context.fix.md b/changelog.d/socket_bind_error_context.fix.md index ce6229e43036b..bb2127116cd6f 100644 --- a/changelog.d/socket_bind_error_context.fix.md +++ b/changelog.d/socket_bind_error_context.fix.md @@ -1,3 +1,3 @@ -Retain underlying errors from several sources and sinks in topology task failures, including bind, subscription, checkpoint, AMQP consumer, and console errors. Include component panic payloads in a structured `panic` field. +Vector now reports the underlying error when a source or sink task fails at runtime; previously, it only said `the task completed with an error`. That includes failures to bind a listening socket (for example `Address already in use (os error 48)`), broker subscribe and consume errors, checkpoint file errors, sink runtime errors, and sources that stop because their downstream channel closed. Vector now includes the panic payload in its logs. authors: gwenaskell