1#![allow(clippy::let_underscore_must_use)]
2
3use std::{any::Any, cell::RefCell, path::PathBuf, sync::Arc, time::Duration};
4
5use async_trait::async_trait;
6use derivative::Derivative;
7use dyn_clone::DynClone;
8use serde::Serialize;
9use serde_with::serde_as;
10use vector_lib::{
11 buffers::{BufferConfig, BufferType},
12 config::{AcknowledgementsConfig, GlobalOptions, Input},
13 configurable::{
14 Configurable, GenerateError, Metadata, NamedComponent,
15 attributes::CustomAttribute,
16 configurable_component,
17 schema::{SchemaGenerator, SchemaObject},
18 },
19 id::Inputs,
20 sink::VectorSink,
21};
22use vector_vrl_metrics::MetricsStorage;
23
24use super::{
25 ComponentKey, DynValidatedSink, ProxyConfig, Resource, dot_graph::GraphConfig, schema,
26};
27use crate::{
28 extra_context::ExtraContext,
29 sinks::{Healthcheck, util::UriSerde},
30};
31
32pub type BoxedSink = Box<dyn SinkConfig>;
33
34impl Configurable for BoxedSink {
35 fn referenceable_name() -> Option<&'static str> {
36 Some("vector::sinks::Sinks")
37 }
38
39 fn metadata() -> Metadata {
40 let mut metadata = Metadata::default();
41 metadata.set_description("Configurable sinks in Vector.");
42 metadata.add_custom_attribute(CustomAttribute::kv("docs::enum_tagging", "internal"));
43 metadata.add_custom_attribute(CustomAttribute::kv("docs::enum_tag_field", "type"));
44 metadata
45 }
46
47 fn generate_schema(
48 generator: &RefCell<SchemaGenerator>,
49 ) -> Result<SchemaObject, GenerateError> {
50 vector_lib::configurable::component::SinkDescription::generate_schemas(generator)
51 }
52}
53
54impl<T: SinkConfig + 'static> From<T> for BoxedSink {
55 fn from(value: T) -> Self {
56 Box::new(value)
57 }
58}
59
60#[configurable_component]
62#[configurable(metadata(docs::component_base_type = "sink"))]
63#[derive(Clone, Derivative)]
64#[derivative(Debug)]
65pub struct SinkOuter<T>
66where
67 T: Configurable + Serialize + 'static,
68{
69 #[configurable(derived)]
70 #[serde(default, skip_serializing_if = "vector_lib::serde::is_default")]
71 pub graph: GraphConfig,
72
73 #[configurable(derived)]
74 pub inputs: Inputs<T>,
75
76 #[configurable(deprecated, metadata(docs::hidden), validation(format = "uri"))]
81 pub healthcheck_uri: Option<UriSerde>,
82
83 #[configurable(derived)]
84 #[serde(default, deserialize_with = "crate::serde::bool_or_struct")]
85 pub healthcheck: SinkHealthcheckOptions,
86
87 #[configurable(derived)]
88 #[serde(default, skip_serializing_if = "vector_lib::serde::is_default")]
89 pub buffer: BufferConfig,
90
91 #[configurable(derived)]
92 #[serde(default, skip_serializing_if = "vector_lib::serde::is_default")]
93 pub proxy: ProxyConfig,
94
95 #[serde(flatten)]
96 #[configurable(metadata(docs::hidden))]
97 pub inner: BoxedSink,
98
99 #[serde(skip)]
106 #[derivative(Debug = "ignore")]
107 pub(crate) validated: Option<Arc<dyn Any + Send + Sync>>,
108}
109
110impl<T> SinkOuter<T>
111where
112 T: Configurable + Serialize,
113{
114 pub async fn build(&self, cx: SinkContext) -> crate::Result<(VectorSink, Healthcheck)> {
117 match self.inner.as_dyn_validated() {
118 Some(p) => {
119 let validated = self
120 .validated
121 .as_ref()
122 .expect("validated state missing for migrated sink");
123 p.build_dyn(validated.as_ref(), cx).await
124 }
125 None => self.inner.build(cx).await,
126 }
127 }
128 pub fn new<I, IS>(inputs: I, inner: IS) -> SinkOuter<T>
129 where
130 I: IntoIterator<Item = T>,
131 IS: Into<BoxedSink>,
132 {
133 SinkOuter {
134 inputs: Inputs::from_iter(inputs),
135 buffer: Default::default(),
136 healthcheck: SinkHealthcheckOptions::default(),
137 healthcheck_uri: None,
138 inner: inner.into(),
139 proxy: Default::default(),
140 graph: Default::default(),
141 validated: None,
142 }
143 }
144
145 pub fn resources(&self, id: &ComponentKey) -> Vec<Resource> {
146 let mut resources = self.inner.resources();
147 for stage in self.buffer.stages() {
148 match stage {
149 BufferType::Memory { .. } => {}
150 BufferType::DiskV2 { .. } => resources.push(Resource::DiskBuffer(id.to_string())),
151 }
152 }
153 resources
154 }
155
156 pub fn healthcheck(&self) -> SinkHealthcheckOptions {
157 if self.healthcheck_uri.is_some() && self.healthcheck.uri.is_some() {
158 warn!(
159 "Both `healthcheck.uri` and `healthcheck_uri` options are specified. Using value of `healthcheck.uri`."
160 )
161 } else if self.healthcheck_uri.is_some() {
162 warn!(
163 "The `healthcheck_uri` option has been deprecated, use `healthcheck.uri` instead."
164 )
165 }
166 SinkHealthcheckOptions {
167 uri: self
168 .healthcheck
169 .uri
170 .clone()
171 .or_else(|| self.healthcheck_uri.clone()),
172 ..self.healthcheck.clone()
173 }
174 }
175
176 pub const fn proxy(&self) -> &ProxyConfig {
177 &self.proxy
178 }
179
180 pub(super) fn map_inputs<U>(self, f: impl Fn(&T) -> U) -> SinkOuter<U>
181 where
182 U: Configurable + Serialize,
183 {
184 let inputs = self.inputs.iter().map(f).collect::<Vec<_>>();
185 self.with_inputs(inputs)
186 }
187
188 pub(crate) fn with_inputs<I, U>(self, inputs: I) -> SinkOuter<U>
189 where
190 I: IntoIterator<Item = U>,
191 U: Configurable + Serialize,
192 {
193 SinkOuter {
194 inputs: Inputs::from_iter(inputs),
195 inner: self.inner,
196 buffer: self.buffer,
197 healthcheck: self.healthcheck,
198 healthcheck_uri: self.healthcheck_uri,
199 proxy: self.proxy,
200 graph: self.graph,
201 validated: self.validated,
202 }
203 }
204}
205
206#[serde_as]
208#[configurable_component]
209#[derive(Clone, Debug)]
210#[serde(default)]
211pub struct SinkHealthcheckOptions {
212 pub enabled: bool,
214
215 #[serde_as(as = "serde_with::DurationSecondsWithFrac<f64>")]
217 #[serde(
218 default = "default_healthcheck_timeout",
219 skip_serializing_if = "is_default_healthcheck_timeout"
220 )]
221 pub timeout: Duration,
222
223 #[configurable(validation(format = "uri"))]
228 pub uri: Option<UriSerde>,
229}
230
231const fn default_healthcheck_timeout() -> Duration {
232 Duration::from_secs(10)
233}
234
235fn is_default_healthcheck_timeout(timeout: &Duration) -> bool {
236 timeout == &default_healthcheck_timeout()
237}
238
239impl Default for SinkHealthcheckOptions {
240 fn default() -> Self {
241 Self {
242 enabled: true,
243 uri: None,
244 timeout: default_healthcheck_timeout(),
245 }
246 }
247}
248
249impl From<bool> for SinkHealthcheckOptions {
250 fn from(enabled: bool) -> Self {
251 Self {
252 enabled,
253 ..Default::default()
254 }
255 }
256}
257
258impl From<UriSerde> for SinkHealthcheckOptions {
259 fn from(uri: UriSerde) -> Self {
260 Self {
261 uri: Some(uri),
262 ..Default::default()
263 }
264 }
265}
266
267#[async_trait]
269#[typetag::serde(tag = "type")]
270pub trait SinkConfig: DynClone + NamedComponent + core::fmt::Debug + Send + Sync {
271 async fn build(&self, cx: SinkContext) -> crate::Result<(VectorSink, Healthcheck)> {
282 match self.as_dyn_validated() {
283 Some(dyn_sink) => {
284 let validated = dyn_sink.validate_dyn()?;
285 dyn_sink.build_dyn(&*validated, cx).await
286 }
287 None => Err("sink does not implement a build method".into()),
288 }
289 }
290
291 fn input(&self) -> Input;
293
294 fn files_to_watch(&self) -> Vec<&PathBuf> {
296 Vec::new()
297 }
298
299 fn resources(&self) -> Vec<Resource> {
307 Vec::new()
308 }
309
310 fn confinement_config(&self) -> Option<&crate::template::ConfinementConfig> {
316 None
317 }
318
319 fn acknowledgements(&self) -> &AcknowledgementsConfig;
321
322 fn as_dyn_validated(&self) -> Option<&dyn DynValidatedSink> {
329 None
330 }
331}
332
333dyn_clone::clone_trait_object!(SinkConfig);
334
335#[derive(Clone, Debug)]
336pub struct SinkContext {
337 pub healthcheck: SinkHealthcheckOptions,
338 pub globals: GlobalOptions,
339 pub enrichment_tables: vector_lib::enrichment::TableRegistry,
340 pub metrics_storage: MetricsStorage,
341 pub proxy: ProxyConfig,
342 pub schema: schema::Options,
343 pub app_name: String,
344 pub app_name_slug: String,
345
346 pub extra_context: ExtraContext,
349}
350
351impl Default for SinkContext {
352 fn default() -> Self {
353 Self {
354 healthcheck: Default::default(),
355 globals: Default::default(),
356 enrichment_tables: Default::default(),
357 metrics_storage: Default::default(),
358 proxy: Default::default(),
359 schema: Default::default(),
360 app_name: crate::get_app_name().to_string(),
361 app_name_slug: crate::get_slugified_app_name(),
362 extra_context: Default::default(),
363 }
364 }
365}
366
367impl SinkContext {
368 pub const fn globals(&self) -> &GlobalOptions {
369 &self.globals
370 }
371
372 pub const fn proxy(&self) -> &ProxyConfig {
373 &self.proxy
374 }
375}