vector/sources/aws_kinesis_firehose/
handlers.rs1use std::collections::HashMap;
2
3use base64::prelude::{BASE64_STANDARD, Engine as _};
4use bytes::Bytes;
5use chrono::Utc;
6use futures::StreamExt;
7use snafu::{ResultExt, Snafu};
8use vector_common::constants::GZIP_MAGIC;
9use vector_lib::{
10 EstimatedJsonEncodedSizeOf,
11 codecs::{DecoderFramedRead, StreamDecodingError},
12 config::{LegacyKey, LogNamespace},
13 event::BatchNotifier,
14 finalization::AddBatchNotifier,
15 internal_event::{
16 ByteSize, BytesReceived, CountByteSize, InternalEventHandle as _, Registered,
17 },
18 lookup::{PathPrefix, metadata_path, path},
19 source_sender::SendError,
20};
21use vrl::{
22 compiler::SecretTarget,
23 value::{KeyString, ObjectMap, Value},
24};
25use warp::reject;
26
27use super::{
28 Compression,
29 errors::{ParseRecordsSnafu, RequestError},
30 models::{EncodedFirehoseRecord, FirehoseRequest, FirehoseResponse},
31};
32use crate::{
33 SourceSender,
34 codecs::Decoder,
35 config::log_schema,
36 event::{BatchStatus, Event},
37 internal_events::{
38 AwsKinesisFirehoseAutomaticRecordDecodeError, EventsReceived, StreamClosedError,
39 },
40 sources::{
41 aws_kinesis_firehose::AwsKinesisFirehoseConfig,
42 http_server::HttpConfigParamKind,
43 util::decompression::{CappedDecoder, is_decompressed_size_limit_error},
44 },
45};
46
47#[derive(Clone)]
48pub(super) struct Context {
49 pub(super) compression: Compression,
50 pub(super) store_access_key: bool,
51 pub(super) decoder: Decoder,
52 pub(super) acknowledgements: bool,
53 pub(super) bytes_received: Registered<BytesReceived>,
54 pub(super) out: SourceSender,
55 pub(super) log_namespace: LogNamespace,
56 pub(super) common_attributes: Vec<HttpConfigParamKind>,
57}
58
59pub(super) async fn firehose(
61 request_id: String,
62 source_arn: String,
63 common_attributes: HashMap<String, String>,
64 request: FirehoseRequest,
65 mut context: Context,
66) -> Result<impl warp::Reply, reject::Rejection> {
67 let log_namespace = context.log_namespace;
68 let events_received = register!(EventsReceived);
69 let common_attributes_map =
70 build_common_attributes_map(&context.common_attributes, &common_attributes);
71
72 for record in request.records {
73 let bytes = decode_record(&record, context.compression)
74 .with_context(|_| ParseRecordsSnafu {
75 request_id: request_id.clone(),
76 })
77 .map_err(reject::custom)?;
78 context.bytes_received.emit(ByteSize(bytes.len()));
79
80 let mut stream = DecoderFramedRead::new(bytes.as_ref(), context.decoder.clone());
81 loop {
82 match stream.next().await {
83 Some(Ok((mut events, _byte_size))) => {
84 events_received.emit(CountByteSize(
85 events.len(),
86 events.estimated_json_encoded_size_of(),
87 ));
88
89 let (batch, receiver) = if context.acknowledgements {
90 {
91 let (batch, receiver) = BatchNotifier::new_with_receiver();
92 (Some(batch), Some(receiver))
93 }
94 } else {
95 (None, None)
96 };
97
98 let now = Utc::now();
99 for event in &mut events {
100 if let Some(batch) = &batch {
101 event.add_batch_notifier(batch.clone());
102 }
103 if let Event::Log(log) = event {
104 log_namespace.insert_vector_metadata(
105 log,
106 log_schema().source_type_key(),
107 path!("source_type"),
108 Bytes::from_static(AwsKinesisFirehoseConfig::NAME.as_bytes()),
109 );
110 match log_namespace {
113 LogNamespace::Vector => {
114 log.insert(metadata_path!("vector", "ingest_timestamp"), now);
115 log.insert(
116 metadata_path!(AwsKinesisFirehoseConfig::NAME, "timestamp"),
117 request.timestamp,
118 );
119 }
120 LogNamespace::Legacy => {
121 if let Some(timestamp_key) = log_schema().timestamp_key() {
122 log.try_insert(
123 (PathPrefix::Event, timestamp_key),
124 request.timestamp,
125 );
126 }
127 }
128 };
129
130 log_namespace.insert_source_metadata(
131 AwsKinesisFirehoseConfig::NAME,
132 log,
133 Some(LegacyKey::InsertIfEmpty(path!("request_id"))),
134 path!("request_id"),
135 request_id.to_owned(),
136 );
137 log_namespace.insert_source_metadata(
138 AwsKinesisFirehoseConfig::NAME,
139 log,
140 Some(LegacyKey::InsertIfEmpty(path!("source_arn"))),
141 path!("source_arn"),
142 source_arn.to_owned(),
143 );
144
145 if !common_attributes_map.is_empty() {
146 log_namespace.insert_source_metadata(
147 AwsKinesisFirehoseConfig::NAME,
148 log,
149 Some(LegacyKey::InsertIfEmpty(path!("common_attributes"))),
150 path!("common_attributes"),
151 common_attributes_map.clone(),
152 );
153 }
154
155 if context.store_access_key
156 && let Some(access_key) = &request.access_key
157 {
158 log.metadata_mut()
159 .secrets_mut()
160 .insert_secret("aws_kinesis_firehose_access_key", access_key);
161 }
162 }
163 }
164
165 let count = events.len();
166 match context.out.send_batch(events).await {
167 Ok(()) => (),
168 Err(SendError::Closed) => {
169 emit!(StreamClosedError { count });
170 let error = RequestError::ShuttingDown {
171 request_id: request_id.clone(),
172 };
173 warp::reject::custom(error);
174 }
175 Err(SendError::Timeout) => unreachable!("No timeout is configured here"),
176 }
177
178 drop(batch);
179 if let Some(receiver) = receiver {
180 match receiver.await {
181 BatchStatus::Delivered => Ok(()),
182 BatchStatus::Rejected => {
183 Err(warp::reject::custom(RequestError::DeliveryFailed {
184 request_id: request_id.clone(),
185 }))
186 }
187 BatchStatus::Errored => {
188 Err(warp::reject::custom(RequestError::DeliveryErrored {
189 request_id: request_id.clone(),
190 }))
191 }
192 }?;
193 }
194 }
195 Some(Err(error)) => {
196 if !error.can_continue() {
199 break;
200 }
201 }
202 None => break,
203 }
204 }
205 }
206
207 Ok(warp::reply::json(&FirehoseResponse {
208 request_id: request_id.clone(),
209 timestamp: Utc::now(),
210 error_message: None,
211 }))
212}
213
214#[derive(Debug, Snafu)]
215pub enum RecordDecodeError {
216 #[snafu(display("Could not base64 decode request data: {}", source))]
217 Base64 { source: base64::DecodeError },
218 #[snafu(display("Could not decompress request data as {}: {}", compression, source))]
219 Decompression {
220 source: std::io::Error,
221 compression: Compression,
222 },
223}
224
225fn decode_record(
227 record: &EncodedFirehoseRecord,
228 compression: Compression,
229) -> Result<Bytes, RecordDecodeError> {
230 let buf = BASE64_STANDARD
231 .decode(record.data.as_bytes())
232 .context(Base64Snafu {})?;
233
234 if buf.is_empty() {
235 return Ok(Bytes::default());
236 }
237
238 match compression {
239 Compression::None => Ok(Bytes::from(buf)),
240 Compression::Gzip => decode_gzip(&buf[..]).with_context(|_| DecompressionSnafu {
241 compression: compression.to_owned(),
242 }),
243 Compression::Auto => {
244 if is_gzip(&buf) {
245 decode_gzip(&buf[..]).or_else(|error| {
246 if is_decompressed_size_limit_error(&error) {
250 return Err(error).with_context(|_| DecompressionSnafu {
251 compression: Compression::Gzip,
252 });
253 }
254 emit!(AwsKinesisFirehoseAutomaticRecordDecodeError {
255 compression: Compression::Gzip,
256 error
257 });
258 Ok(Bytes::from(buf))
259 })
260 } else {
261 Ok(Bytes::from(buf))
263 }
264 }
265 }
266}
267
268fn is_gzip(data: &[u8]) -> bool {
269 data.starts_with(GZIP_MAGIC)
276}
277
278fn decode_gzip(data: &[u8]) -> std::io::Result<Bytes> {
279 CappedDecoder::gzip(data).decompress().map(Bytes::from)
281}
282
283fn build_common_attributes_map(
284 common_attributes_config: &[HttpConfigParamKind],
285 common_attributes: &HashMap<String, String>,
286) -> ObjectMap {
287 let mut common_attributes_map = ObjectMap::new();
288
289 for common_attribute_config in common_attributes_config {
290 match common_attribute_config {
291 HttpConfigParamKind::Exact(common_attribute_name) => {
292 let value = common_attributes
293 .get(common_attribute_name)
294 .map(String::as_bytes);
295 common_attributes_map.insert(
296 KeyString::from(common_attribute_name.to_owned()),
297 Value::from(value.map(Bytes::copy_from_slice)),
298 );
299 }
300 HttpConfigParamKind::Glob(common_attribute_pattern) => {
301 for common_attribute_name in common_attributes.keys() {
302 if common_attribute_pattern.matches(common_attribute_name) {
303 let value = common_attributes
304 .get(common_attribute_name)
305 .map(String::as_bytes);
306 common_attributes_map.insert(
307 KeyString::from(common_attribute_name.to_owned()),
308 Value::from(value.map(Bytes::copy_from_slice)),
309 );
310 }
311 }
312 }
313 }
314 }
315
316 common_attributes_map
317}
318
319#[cfg(test)]
320mod tests {
321 use std::io::Write as _;
322
323 use flate2::{Compression, write::GzEncoder};
324
325 use super::*;
326
327 const CONTENT: &[u8] = b"Example";
328
329 #[test]
330 fn correctly_detects_gzipped_content() {
331 assert!(!is_gzip(CONTENT));
332 let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
333 encoder.write_all(CONTENT).unwrap();
334 let compressed = encoder.finish().unwrap();
335 assert!(is_gzip(&compressed));
336 }
337}