vector/sinks/aws_kinesis/streams/
config.rs1use aws_sdk_kinesis::operation::{
2 describe_stream::DescribeStreamError, put_records::PutRecordsError,
3};
4use aws_smithy_runtime_api::client::{orchestrator::HttpResponse, result::SdkError};
5use futures::FutureExt;
6use snafu::Snafu;
7use vector_lib::configurable::{component::GenerateConfig, configurable_component};
8
9use super::{
10 KinesisClient, KinesisError, KinesisRecord, KinesisResponse, KinesisSinkBaseConfig, build_sink,
11 record::{KinesisStreamClient, KinesisStreamRecord},
12 sink::BatchKinesisRequest,
13};
14use crate::{
15 aws::{ClientBuilder, create_client, is_retriable_error},
16 config::{
17 AcknowledgementsConfig, DynValidatedSink, Input, ProxyConfig, SinkConfig, SinkContext,
18 ValidatedSink,
19 },
20 sinks::{
21 Healthcheck, VectorSink,
22 prelude::*,
23 util::{
24 BatchConfig, SinkBatchSettings,
25 retries::{RetryAction, RetryLogic},
26 },
27 },
28};
29
30#[allow(clippy::large_enum_variant)]
31#[derive(Debug, Snafu)]
32enum HealthcheckError {
33 #[snafu(display("DescribeStream failed: {}", source))]
34 DescribeStreamFailed {
35 source: SdkError<DescribeStreamError, HttpResponse>,
36 },
37 #[snafu(display("Stream names do not match, got {}, expected {}", name, stream_name))]
38 StreamNamesMismatch { name: String, stream_name: String },
39}
40
41pub struct KinesisClientBuilder;
42
43impl ClientBuilder for KinesisClientBuilder {
44 type Client = KinesisClient;
45
46 fn build(&self, config: &aws_types::SdkConfig) -> Self::Client {
47 KinesisClient::new(config)
48 }
49}
50
51pub const MAX_PAYLOAD_SIZE: usize = 5_000_000;
52pub const MAX_PAYLOAD_EVENTS: usize = 500;
53
54#[derive(Clone, Copy, Debug, Default)]
55pub struct KinesisDefaultBatchSettings;
56
57impl SinkBatchSettings for KinesisDefaultBatchSettings {
58 const MAX_EVENTS: Option<usize> = Some(MAX_PAYLOAD_EVENTS);
59 const MAX_BYTES: Option<usize> = Some(MAX_PAYLOAD_SIZE);
60 const TIMEOUT_SECS: f64 = 1.0;
61}
62
63#[configurable_component(sink(
65 "aws_kinesis_streams",
66 "Publish logs to AWS Kinesis Streams topics."
67))]
68#[derive(Clone, Debug)]
69pub struct KinesisStreamsSinkConfig {
70 #[serde(flatten)]
71 pub base: KinesisSinkBaseConfig,
72
73 #[configurable(derived)]
74 #[serde(default)]
75 pub batch: BatchConfig<KinesisDefaultBatchSettings>,
76}
77
78impl KinesisStreamsSinkConfig {
79 async fn healthcheck(self, client: KinesisClient) -> crate::Result<()> {
80 let stream_name = self.base.stream_name;
81
82 let describe_result = client
83 .describe_stream()
84 .stream_name(stream_name.clone())
85 .set_exclusive_start_shard_id(None)
86 .limit(1)
87 .send()
88 .await;
89
90 match describe_result {
91 Ok(resp) => {
92 let name = resp
93 .stream_description
94 .map(|x| x.stream_name)
95 .unwrap_or_default();
96 if name == stream_name {
97 Ok(())
98 } else {
99 Err(HealthcheckError::StreamNamesMismatch { name, stream_name }.into())
100 }
101 }
102 Err(source) => Err(HealthcheckError::DescribeStreamFailed { source }.into()),
103 }
104 }
105
106 pub async fn create_client(&self, proxy: &ProxyConfig) -> crate::Result<KinesisClient> {
107 create_client::<KinesisClientBuilder>(
108 &KinesisClientBuilder {},
109 &self.base.auth,
110 self.base.region.region(),
111 self.base.region.endpoint(),
112 proxy,
113 self.base.tls.as_ref(),
114 None,
115 )
116 .await
117 }
118}
119
120#[async_trait::async_trait]
121#[typetag::serde(name = "aws_kinesis_streams")]
122impl SinkConfig for KinesisStreamsSinkConfig {
123 fn input(&self) -> Input {
124 self.base.input()
125 }
126
127 fn acknowledgements(&self) -> &AcknowledgementsConfig {
128 self.base.acknowledgements()
129 }
130
131 fn as_dyn_validated(&self) -> Option<&dyn DynValidatedSink> {
132 Some(self)
133 }
134}
135
136#[derive(Clone, Debug)]
137pub struct ValidatedKinesisStreams {
138 batch_settings: BatcherSettings,
139}
140
141#[async_trait::async_trait]
142impl ValidatedSink for KinesisStreamsSinkConfig {
143 type Validated = ValidatedKinesisStreams;
144
145 fn validate(&self) -> crate::Result<ValidatedKinesisStreams> {
146 let batch_settings = self
147 .batch
148 .validate()?
149 .limit_max_bytes(MAX_PAYLOAD_SIZE)?
150 .limit_max_events(MAX_PAYLOAD_EVENTS)?
151 .into_batcher_settings()?;
152
153 Ok(ValidatedKinesisStreams { batch_settings })
154 }
155
156 async fn build(
157 &self,
158 validated: &ValidatedKinesisStreams,
159 cx: SinkContext,
160 ) -> crate::Result<(VectorSink, Healthcheck)> {
161 let client = self.create_client(&cx.proxy).await?;
162 let healthcheck = self.clone().healthcheck(client.clone()).boxed();
163
164 let sink = build_sink::<
165 KinesisStreamClient,
166 KinesisRecord,
167 KinesisStreamRecord,
168 KinesisError,
169 KinesisRetryLogic,
170 >(
171 &self.base,
172 self.base.partition_key_field.clone(),
173 validated.batch_settings,
174 KinesisStreamClient { client },
175 KinesisRetryLogic {
176 retry_partial: self.base.request_retry_partial,
177 },
178 )?;
179
180 Ok((sink, healthcheck))
181 }
182}
183
184impl GenerateConfig for KinesisStreamsSinkConfig {
185 fn generate_config() -> serde_json::Value {
186 serde_yaml::from_str(indoc::indoc! {
187 r#"partition_key_field: foo
188 stream_name: my-stream
189 encoding:
190 codec: json"#,
191 })
192 .unwrap()
193 }
194}
195#[derive(Default, Clone)]
196struct KinesisRetryLogic {
197 retry_partial: bool,
198}
199
200impl RetryLogic for KinesisRetryLogic {
201 type Error = SdkError<KinesisError, HttpResponse>;
202 type Request = BatchKinesisRequest<KinesisStreamRecord>;
203 type Response = KinesisResponse;
204
205 fn is_retriable_error(&self, error: &Self::Error) -> bool {
206 if let SdkError::ServiceError(inner) = error {
207 if matches!(
214 inner.err(),
215 PutRecordsError::ProvisionedThroughputExceededException(_)
216 ) {
217 return true;
218 }
219 }
220 is_retriable_error(error)
221 }
222
223 fn should_retry_response(&self, response: &Self::Response) -> RetryAction<Self::Request> {
224 if response.failure_count > 0 && self.retry_partial && !response.failed_records.is_empty() {
225 let failed_records = response.failed_records.clone();
226 RetryAction::RetryPartial(Box::new(move |original_request| {
227 let failed_events: Vec<_> = failed_records
228 .iter()
229 .filter_map(|r| original_request.events.get(r.index).cloned())
230 .collect();
231
232 let metadata = RequestMetadata::from_batch(
233 failed_events.iter().map(|req| req.get_metadata().clone()),
234 );
235
236 BatchKinesisRequest {
237 events: failed_events,
238 metadata,
239 }
240 }))
241 } else {
242 RetryAction::Successful
243 }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn generate_config() {
253 crate::test_util::test_generate_config::<KinesisStreamsSinkConfig>();
254 }
255
256 #[test]
257 fn validate_produces_batch_settings() {
258 let config = KinesisStreamsSinkConfig {
259 batch: BatchConfig::<KinesisDefaultBatchSettings>::default(),
260 base: KinesisSinkBaseConfig {
261 stream_name: String::from("test"),
262 region: crate::aws::RegionOrEndpoint::with_both(
263 "us-east-1",
264 "http://localhost:4566",
265 ),
266 encoding: vector_lib::codecs::JsonSerializerConfig::default().into(),
267 compression: Compression::None,
268 request: Default::default(),
269 tls: None,
270 auth: Default::default(),
271 request_retry_partial: false,
272 acknowledgements: Default::default(),
273 partition_key_field: None,
274 },
275 };
276
277 let validated = config.validate().expect("validation should succeed");
278 assert_eq!(validated.batch_settings.item_limit, MAX_PAYLOAD_EVENTS);
279 assert_eq!(validated.batch_settings.size_limit, MAX_PAYLOAD_SIZE);
280 }
281}