1use std::{convert::Infallible, fmt, net::SocketAddr, time::Duration};
2
3use futures::FutureExt;
4use hyper::{Server, service::make_service_fn};
5use tokio::net::TcpStream;
6use tower::ServiceBuilder;
7use tracing::Span;
8use vector_lib::{
9 codecs::decoding::{DeserializerConfig, FramingConfig},
10 config::{LegacyKey, LogNamespace},
11 configurable::configurable_component,
12 lookup::owned_value_path,
13 sensitive_string::SensitiveString,
14 tls::MaybeTlsIncomingStream,
15};
16use vrl::value::{Kind, kind::Collection};
17
18use crate::{
19 codecs::DecodingConfig,
20 config::{
21 GenerateConfig, Resource, SourceAcknowledgementsConfig, SourceConfig, SourceContext,
22 SourceOutput,
23 },
24 http::{KeepaliveConfig, MaxConnectionAgeLayer, build_http_trace_layer},
25 serde::{bool_or_struct, default_decoding, default_framing_message_based},
26 sources::http_server::{build_param_matcher, remove_duplicates},
27 tls::{MaybeTlsSettings, TlsEnableableConfig},
28};
29
30pub mod errors;
31mod filters;
32mod handlers;
33mod models;
34
35#[configurable_component(source(
37 "aws_kinesis_firehose",
38 "Collect logs from AWS Kinesis Firehose."
39))]
40#[derive(Clone, Debug)]
41pub struct AwsKinesisFirehoseConfig {
42 #[configurable(metadata(docs::examples = "0.0.0.0:443"))]
44 #[configurable(metadata(docs::examples = "localhost:443"))]
45 address: SocketAddr,
46
47 #[configurable(deprecated = "This option has been deprecated, use `access_keys` instead.")]
52 #[configurable(metadata(docs::examples = "A94A8FE5CCB19BA61C4C08"))]
53 access_key: Option<SensitiveString>,
54
55 #[configurable(metadata(docs::examples = "access_keys_example()"))]
60 access_keys: Option<Vec<SensitiveString>>,
61
62 #[configurable(derived)]
67 store_access_key: bool,
68
69 #[serde(default)]
81 record_compression: Compression,
82
83 #[configurable(derived)]
84 tls: Option<TlsEnableableConfig>,
85
86 #[configurable(derived)]
87 #[configurable(metadata(docs::advanced))]
88 #[serde(default = "default_framing_message_based")]
89 framing: FramingConfig,
90
91 #[configurable(derived)]
92 #[configurable(metadata(docs::advanced))]
93 #[serde(default = "default_decoding")]
94 decoding: DeserializerConfig,
95
96 #[configurable(derived)]
97 #[serde(default, deserialize_with = "bool_or_struct")]
98 acknowledgements: SourceAcknowledgementsConfig,
99
100 #[configurable(metadata(docs::hidden))]
102 #[serde(default)]
103 log_namespace: Option<bool>,
104
105 #[configurable(derived)]
106 #[serde(default)]
107 keepalive: KeepaliveConfig,
108
109 #[serde(default)]
118 #[configurable(metadata(docs::examples = "environment"))]
119 #[configurable(metadata(docs::examples = "application_group"))]
120 #[configurable(metadata(docs::examples = "application_*"))]
121 #[configurable(metadata(docs::examples = "*"))]
122 common_attributes: Vec<String>,
123}
124
125const fn access_keys_example() -> [&'static str; 2] {
126 ["A94A8FE5CCB19BA61C4C08", "B94B8FE5CCB19BA61C4C12"]
127}
128
129#[configurable_component]
131#[configurable(metadata(docs::advanced))]
132#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
133#[serde(rename_all = "lowercase")]
134pub enum Compression {
135 #[default]
146 Auto,
147
148 None,
150
151 Gzip,
153}
154
155impl fmt::Display for Compression {
156 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
157 match self {
158 Compression::Auto => write!(fmt, "auto"),
159 Compression::None => write!(fmt, "none"),
160 Compression::Gzip => write!(fmt, "gzip"),
161 }
162 }
163}
164
165#[async_trait::async_trait]
166#[typetag::serde(name = "aws_kinesis_firehose")]
167impl SourceConfig for AwsKinesisFirehoseConfig {
168 async fn build(&self, cx: SourceContext) -> crate::Result<super::Source> {
169 let log_namespace = cx.log_namespace(self.log_namespace);
170 let decoder =
171 DecodingConfig::new(self.framing.clone(), self.decoding.clone(), log_namespace)
172 .build()?;
173
174 let acknowledgements = cx.do_acknowledgements(self.acknowledgements);
175
176 if self.access_key.is_some() {
177 warn!("DEPRECATION `access_key`, use `access_keys` instead.")
178 }
179
180 let access_keys = self
182 .access_keys
183 .iter()
184 .flatten()
185 .chain(self.access_key.iter());
186
187 let common_attributes = build_param_matcher(&remove_duplicates(
188 self.common_attributes.clone(),
189 "common_attributes",
190 ))?;
191
192 let svc = filters::firehose(
193 access_keys.map(|key| key.inner().to_string()).collect(),
194 self.store_access_key,
195 self.record_compression,
196 decoder,
197 acknowledgements,
198 cx.out,
199 log_namespace,
200 common_attributes,
201 );
202
203 let tls = MaybeTlsSettings::from_config(self.tls.as_ref(), true)?;
204 let listener = tls.bind(&self.address).await?;
205
206 let keepalive_settings = self.keepalive.clone();
207 let shutdown = cx.shutdown;
208 Ok(Box::pin(async move {
209 let span = Span::current();
210 let make_svc = make_service_fn(move |conn: &MaybeTlsIncomingStream<TcpStream>| {
211 let svc = ServiceBuilder::new()
212 .layer(build_http_trace_layer(span.clone()))
213 .option_layer(keepalive_settings.max_connection_age_secs.map(|secs| {
214 MaxConnectionAgeLayer::new(
215 Duration::from_secs(secs),
216 keepalive_settings.max_connection_age_jitter_factor,
217 conn.peer_addr(),
218 )
219 }))
220 .service(warp::service(svc.clone()));
221 futures_util::future::ok::<_, Infallible>(svc)
222 });
223
224 Server::builder(hyper::server::accept::from_stream(listener.accept_stream()))
225 .serve(make_svc)
226 .with_graceful_shutdown(shutdown.map(|_| ()))
227 .await
228 .map_err(|err| {
229 error!("An error occurred: {:?}.", err);
230 })?;
231
232 Ok(())
233 }))
234 }
235
236 fn outputs(&self, global_log_namespace: LogNamespace) -> Vec<SourceOutput> {
237 let common_attributes_path = (!self.common_attributes.is_empty()).then_some(
238 LegacyKey::InsertIfEmpty(owned_value_path!("common_attributes")),
239 );
240 let schema_definition = self
241 .decoding
242 .schema_definition(global_log_namespace.merge(self.log_namespace))
243 .with_standard_vector_source_metadata()
244 .with_source_metadata(
245 Self::NAME,
246 Some(LegacyKey::InsertIfEmpty(owned_value_path!("request_id"))),
247 &owned_value_path!("request_id"),
248 Kind::bytes(),
249 None,
250 )
251 .with_source_metadata(
252 Self::NAME,
253 Some(LegacyKey::InsertIfEmpty(owned_value_path!("source_arn"))),
254 &owned_value_path!("source_arn"),
255 Kind::bytes(),
256 None,
257 )
258 .with_source_metadata(
260 Self::NAME,
261 common_attributes_path,
262 &owned_value_path!("common_attributes"),
263 Kind::object(Collection::from_unknown(
264 Kind::bytes().or_null().or_undefined(),
265 ))
266 .or_undefined(),
267 None,
268 );
269
270 vec![SourceOutput::new_maybe_logs(
271 self.decoding.output_type(),
272 schema_definition,
273 )]
274 }
275
276 fn resources(&self) -> Vec<Resource> {
277 vec![Resource::tcp(self.address)]
278 }
279
280 fn can_acknowledge(&self) -> bool {
281 true
282 }
283}
284
285impl GenerateConfig for AwsKinesisFirehoseConfig {
286 fn generate_config() -> toml::Value {
287 toml::Value::try_from(Self {
288 address: "0.0.0.0:443".parse().unwrap(),
289 access_key: None,
290 access_keys: None,
291 store_access_key: false,
292 tls: None,
293 record_compression: Default::default(),
294 framing: default_framing_message_based(),
295 decoding: default_decoding(),
296 acknowledgements: Default::default(),
297 log_namespace: None,
298 keepalive: Default::default(),
299 common_attributes: vec![],
300 })
301 .unwrap()
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 #![allow(clippy::print_stdout)] use std::{
310 io::{Cursor, Read},
311 net::SocketAddr,
312 sync::LazyLock,
313 };
314
315 use base64::prelude::{BASE64_STANDARD, Engine as _};
316 use bytes::Bytes;
317 use chrono::{DateTime, SubsecRound, Utc};
318 use flate2::read::GzEncoder;
319 use futures::Stream;
320 use similar_asserts::assert_eq;
321 use vector_lib::{assert_event_data_eq, lookup::path};
322 use vrl::{value, value::KeyString, value::ObjectMap, value::Value};
323
324 use super::*;
325 use crate::{
326 SourceSender,
327 event::{Event, EventStatus},
328 log_event,
329 test_util::{
330 addr::{PortGuard, next_addr},
331 collect_n,
332 components::{SOURCE_TAGS, assert_source_compliance},
333 wait_for_tcp,
334 },
335 };
336
337 const SOURCE_ARN: &str = "arn:aws:firehose:us-east-1:111111111111:deliverystream/test";
338 const REQUEST_ID: &str = "e17265d6-97af-4938-982e-90d5614c4242";
339 const RECORD: &str = r#"
341 {
342 "messageType": "DATA_MESSAGE",
343 "owner": "071959437513",
344 "logGroup": "/jesse/test",
345 "logStream": "test",
346 "subscriptionFilters": ["Destination"],
347 "logEvents": [
348 {
349 "id": "35683658089614582423604394983260738922885519999578275840",
350 "timestamp": 1600110569039,
351 "message": "{\"bytes\":26780,\"datetime\":\"14/Sep/2020:11:45:41 -0400\",\"host\":\"157.130.216.193\",\"method\":\"PUT\",\"protocol\":\"HTTP/1.0\",\"referer\":\"https://www.principalcross-platform.io/markets/ubiquitous\",\"request\":\"/expedite/convergence\",\"source_type\":\"stdin\",\"status\":301,\"user-identifier\":\"-\"}"
352 },
353 {
354 "id": "35683658089659183914001456229543810359430816722590236673",
355 "timestamp": 1600110569041,
356 "message": "{\"bytes\":17707,\"datetime\":\"14/Sep/2020:11:45:41 -0400\",\"host\":\"109.81.244.252\",\"method\":\"GET\",\"protocol\":\"HTTP/2.0\",\"referer\":\"http://www.investormission-critical.io/24/7/vortals\",\"request\":\"/scale/functionalities/optimize\",\"source_type\":\"stdin\",\"status\":502,\"user-identifier\":\"feeney1708\"}"
357 }
358 ]
359 }
360 "#;
361
362 const COMMON_ATTRIBUTES: &str = r#"{ "commonAttributes": { "environment": "testing", "application_group": "tymur_test" } }"#;
363
364 static COMMON_ATTRIBUTES_MAP: LazyLock<ObjectMap> = LazyLock::new(|| {
365 ObjectMap::from_iter([
366 (
367 KeyString::from("environment"),
368 Value::Bytes("testing".into()),
369 ),
370 (
371 KeyString::from("application_group"),
372 Value::Bytes("tymur_test".into()),
373 ),
374 ])
375 });
376
377 #[test]
378 fn generate_config() {
379 crate::test_util::test_generate_config::<AwsKinesisFirehoseConfig>();
380 }
381
382 async fn source(
383 access_key: Option<SensitiveString>,
384 access_keys: Option<Vec<SensitiveString>>,
385 store_access_key: bool,
386 record_compression: Compression,
387 delivered: bool,
388 log_namespace: bool,
389 common_attributes: Vec<String>,
390 ) -> (impl Stream<Item = Event> + Unpin, SocketAddr, PortGuard) {
391 use EventStatus::*;
392 let status = if delivered { Delivered } else { Rejected };
393 let (sender, recv) = SourceSender::new_test_finalize(status);
394 let (_guard, address) = next_addr();
395 let cx = SourceContext::new_test(sender, None);
396 tokio::spawn(async move {
397 AwsKinesisFirehoseConfig {
398 address,
399 tls: None,
400 access_key,
401 access_keys,
402 store_access_key,
403 record_compression,
404 framing: default_framing_message_based(),
405 decoding: default_decoding(),
406 acknowledgements: true.into(),
407 log_namespace: Some(log_namespace),
408 keepalive: Default::default(),
409 common_attributes,
410 }
411 .build(cx)
412 .await
413 .unwrap()
414 .await
415 .unwrap()
416 });
417 wait_for_tcp(address).await;
419 (recv, address, _guard)
420 }
421
422 async fn send(
426 address: SocketAddr,
427 timestamp: DateTime<Utc>,
428 records: Vec<&[u8]>,
429 key: Option<&str>,
430 gzip: bool,
431 record_compression: Compression,
432 common_attributes: Option<&str>,
433 ) -> reqwest::Result<reqwest::Response> {
434 let request = models::FirehoseRequest {
435 access_key: key.map(|s| s.to_string()),
436 request_id: REQUEST_ID.to_string(),
437 timestamp,
438 records: records
439 .into_iter()
440 .map(|record| models::EncodedFirehoseRecord {
441 data: encode_record(record, record_compression).unwrap(),
442 })
443 .collect(),
444 };
445
446 let mut builder = reqwest::Client::new()
447 .post(format!("http://{address}"))
448 .header("host", address.to_string())
449 .header(
450 "x-amzn-trace-id",
451 "Root=1-5f5fbf1c-877c68cace58bea222ddbeec",
452 )
453 .header("x-amz-firehose-protocol-version", "1.0")
454 .header("x-amz-firehose-request-id", REQUEST_ID.to_string())
455 .header("x-amz-firehose-source-arn", SOURCE_ARN.to_string())
456 .header("user-agent", "Amazon Kinesis Data Firehose Agent/1.0")
457 .header("content-type", "application/json");
458
459 if let Some(key) = key {
460 builder = builder.header("x-amz-firehose-access-key", key);
461 }
462
463 if let Some(common_attributes) = common_attributes {
464 builder = builder.header("x-amz-firehose-common-attributes", common_attributes)
465 }
466
467 if gzip {
468 let mut gz = GzEncoder::new(
469 Cursor::new(serde_json::to_vec(&request).unwrap()),
470 flate2::Compression::fast(),
471 );
472 let mut buffer = Vec::new();
473 gz.read_to_end(&mut buffer).unwrap();
474 builder = builder.header("content-encoding", "gzip").body(buffer);
475 } else {
476 builder = builder.json(&request);
477 }
478
479 builder.send().await
480 }
481
482 async fn spawn_send(
483 address: SocketAddr,
484 timestamp: DateTime<Utc>,
485 records: Vec<&'static [u8]>,
486 key: Option<&'static str>,
487 gzip: bool,
488 record_compression: Compression,
489 common_attributes: Option<&'static str>,
490 ) -> tokio::task::JoinHandle<reqwest::Result<reqwest::Response>> {
491 tokio::spawn(async move {
492 send(
493 address,
494 timestamp,
495 records,
496 key,
497 gzip,
498 record_compression,
499 common_attributes,
500 )
501 .await
502 })
503 }
504
505 fn encode_record(record: &[u8], compression: Compression) -> std::io::Result<String> {
508 let compressed = match compression {
509 Compression::Auto => panic!("cannot encode records as Auto"),
510 Compression::Gzip => {
511 let mut buffer = Vec::new();
512 if !record.is_empty() {
513 let mut gz = GzEncoder::new(record, flate2::Compression::fast());
514 gz.read_to_end(&mut buffer)?;
515 }
516 buffer
517 }
518 Compression::None => record.to_vec(),
519 };
520
521 Ok(BASE64_STANDARD.encode(compressed))
522 }
523
524 #[tokio::test]
525 async fn aws_kinesis_firehose_forwards_events_legacy_namespace() {
526 let gzipped_record = {
527 let mut buf = Vec::new();
528 let mut gz = GzEncoder::new(RECORD.as_bytes(), flate2::Compression::fast());
529 gz.read_to_end(&mut buf).unwrap();
530 buf
531 };
532
533 for (source_record_compression, record_compression, success, record, expected) in [
534 (
535 Compression::Auto,
536 Compression::Gzip,
537 true,
538 RECORD.as_bytes(),
539 RECORD.as_bytes().to_owned(),
540 ),
541 (
542 Compression::Auto,
543 Compression::None,
544 true,
545 RECORD.as_bytes(),
546 RECORD.as_bytes().to_owned(),
547 ),
548 (
549 Compression::None,
550 Compression::Gzip,
551 true,
552 RECORD.as_bytes(),
553 gzipped_record,
554 ),
555 (
556 Compression::None,
557 Compression::None,
558 true,
559 RECORD.as_bytes(),
560 RECORD.as_bytes().to_owned(),
561 ),
562 (
563 Compression::Gzip,
564 Compression::Gzip,
565 true,
566 RECORD.as_bytes(),
567 RECORD.as_bytes().to_owned(),
568 ),
569 (
570 Compression::Gzip,
571 Compression::None,
572 false,
573 RECORD.as_bytes(),
574 RECORD.as_bytes().to_owned(),
575 ),
576 (
577 Compression::Gzip,
578 Compression::Gzip,
579 true,
580 "".as_bytes(),
581 Vec::new(),
582 ),
583 ] {
584 let (rx, addr, _guard) = source(
585 None,
586 None,
587 false,
588 source_record_compression,
589 true,
590 false,
591 vec![],
592 )
593 .await;
594
595 let timestamp: DateTime<Utc> = Utc::now();
596
597 let res = spawn_send(
598 addr,
599 timestamp,
600 vec![record],
601 None,
602 false,
603 record_compression,
604 None,
605 )
606 .await;
607
608 if success {
609 let events = collect_n(rx, 1).await;
610
611 let res = res.await.unwrap().unwrap();
612 assert_eq!(200, res.status().as_u16());
613
614 assert_event_data_eq!(
615 events,
616 vec![log_event! {
617 "source_type" => Bytes::from("aws_kinesis_firehose"),
618 "timestamp" => timestamp.trunc_subsecs(3), "message" => Bytes::from(expected),
620 "request_id" => REQUEST_ID,
621 "source_arn" => SOURCE_ARN,
622 },]
623 );
624
625 let response: models::FirehoseResponse = res.json().await.unwrap();
626 assert_eq!(response.request_id, REQUEST_ID);
627 } else {
628 let res = res.await.unwrap().unwrap();
629 assert_eq!(400, res.status().as_u16());
630 }
631 }
632 }
633
634 #[tokio::test]
635 async fn aws_kinesis_firehose_forwards_events_vector_namespace() {
636 let gzipped_record = {
637 let mut buf = Vec::new();
638 let mut gz = GzEncoder::new(RECORD.as_bytes(), flate2::Compression::fast());
639 gz.read_to_end(&mut buf).unwrap();
640 buf
641 };
642
643 for (source_record_compression, record_compression, success, record, expected) in [
644 (
645 Compression::Auto,
646 Compression::Gzip,
647 true,
648 RECORD.as_bytes(),
649 RECORD.as_bytes().to_owned(),
650 ),
651 (
652 Compression::Auto,
653 Compression::None,
654 true,
655 RECORD.as_bytes(),
656 RECORD.as_bytes().to_owned(),
657 ),
658 (
659 Compression::None,
660 Compression::Gzip,
661 true,
662 RECORD.as_bytes(),
663 gzipped_record,
664 ),
665 (
666 Compression::None,
667 Compression::None,
668 true,
669 RECORD.as_bytes(),
670 RECORD.as_bytes().to_owned(),
671 ),
672 (
673 Compression::Gzip,
674 Compression::Gzip,
675 true,
676 RECORD.as_bytes(),
677 RECORD.as_bytes().to_owned(),
678 ),
679 (
680 Compression::Gzip,
681 Compression::None,
682 false,
683 RECORD.as_bytes(),
684 RECORD.as_bytes().to_owned(),
685 ),
686 (
687 Compression::Gzip,
688 Compression::Gzip,
689 true,
690 "".as_bytes(),
691 Vec::new(),
692 ),
693 ] {
694 let (rx, addr, _guard) = source(
695 None,
696 None,
697 false,
698 source_record_compression,
699 true,
700 true,
701 vec![],
702 )
703 .await;
704
705 let timestamp: DateTime<Utc> = Utc::now();
706
707 let res = spawn_send(
708 addr,
709 timestamp,
710 vec![record],
711 None,
712 false,
713 record_compression,
714 None,
715 )
716 .await;
717
718 if success {
719 let events = collect_n(rx, 1).await;
720
721 let res = res.await.unwrap().unwrap();
722 assert_eq!(200, res.status().as_u16());
723
724 for event in events {
725 let log = event.as_log();
726 let meta = log.metadata();
727
728 assert_eq!(log.value(), &value!(Bytes::from(expected.to_owned())));
730
731 assert_eq!(
733 meta.value().get(path!("vector", "source_type")).unwrap(),
734 &value!("aws_kinesis_firehose")
735 );
736 assert!(
737 meta.value()
738 .get(path!("vector", "ingest_timestamp"))
739 .unwrap()
740 .is_timestamp()
741 );
742
743 assert_eq!(
745 meta.value()
746 .get(path!("aws_kinesis_firehose", "request_id"))
747 .unwrap(),
748 &value!(REQUEST_ID)
749 );
750 assert_eq!(
751 meta.value()
752 .get(path!("aws_kinesis_firehose", "source_arn"))
753 .unwrap(),
754 &value!(SOURCE_ARN)
755 );
756 assert_eq!(
757 meta.value()
758 .get(path!("aws_kinesis_firehose", "timestamp"))
759 .unwrap(),
760 &value!(timestamp.trunc_subsecs(3))
761 );
762 assert!(
763 meta.value()
764 .get(path!("aws_kinesis_firehose", "common_attributes"))
765 .is_none()
766 );
767 }
768
769 let response: models::FirehoseResponse = res.json().await.unwrap();
770 assert_eq!(response.request_id, REQUEST_ID);
771 } else {
772 let res = res.await.unwrap().unwrap();
773 assert_eq!(400, res.status().as_u16());
774 }
775 }
776 }
777
778 #[tokio::test]
779 async fn aws_kinesis_firehose_forwards_events_gzip_request() {
780 assert_source_compliance(&SOURCE_TAGS, async move {
781 let (rx, addr, _guard) =
782 source(None, None, false, Default::default(), true, false, vec![]).await;
783
784 let timestamp: DateTime<Utc> = Utc::now();
785
786 let res = spawn_send(
787 addr,
788 timestamp,
789 vec![RECORD.as_bytes()],
790 None,
791 true,
792 Compression::None,
793 None,
794 )
795 .await;
796
797 let events = collect_n(rx, 1).await;
798 let res = res.await.unwrap().unwrap();
799 assert_eq!(200, res.status().as_u16());
800
801 assert_event_data_eq!(
802 events,
803 vec![log_event! {
804 "source_type" => Bytes::from("aws_kinesis_firehose"),
805 "timestamp" => timestamp.trunc_subsecs(3), "message"=> RECORD,
807 "request_id" => REQUEST_ID,
808 "source_arn" => SOURCE_ARN,
809 },]
810 );
811
812 let response: models::FirehoseResponse = res.json().await.unwrap();
813 assert_eq!(response.request_id, REQUEST_ID);
814 })
815 .await;
816 }
817
818 #[tokio::test]
819 async fn aws_kinesis_firehose_forwards_events_wildcard_common_attributes_legacy_namespace() {
820 assert_source_compliance(&SOURCE_TAGS, async move {
821 let (rx, addr, _guard) = source(
822 None,
823 None,
824 false,
825 Default::default(),
826 true,
827 false,
828 vec!["*".to_string()],
829 )
830 .await;
831
832 let timestamp: DateTime<Utc> = Utc::now();
833
834 let res = spawn_send(
835 addr,
836 timestamp,
837 vec![RECORD.as_bytes()],
838 None,
839 true,
840 Compression::None,
841 Some(COMMON_ATTRIBUTES),
842 )
843 .await;
844
845 let events = collect_n(rx, 1).await;
846 let res = res.await.unwrap().unwrap();
847 assert_eq!(200, res.status().as_u16());
848
849 assert_event_data_eq!(
850 events,
851 vec![log_event! {
852 "source_type" => Bytes::from("aws_kinesis_firehose"),
853 "timestamp" => timestamp.trunc_subsecs(3), "message"=> RECORD,
855 "request_id" => REQUEST_ID,
856 "source_arn" => SOURCE_ARN,
857 "common_attributes" => COMMON_ATTRIBUTES_MAP.clone(),
858 },]
859 );
860
861 let response: models::FirehoseResponse = res.json().await.unwrap();
862 assert_eq!(response.request_id, REQUEST_ID);
863 })
864 .await;
865 }
866
867 #[tokio::test]
868 async fn aws_kinesis_firehose_forwards_events_wildcard_common_attributes_vector_namespace() {
869 assert_source_compliance(&SOURCE_TAGS, async move {
870 let (rx, addr, _guard) = source(
871 None,
872 None,
873 false,
874 Default::default(),
875 true,
876 true,
877 vec!["*".to_string()],
878 )
879 .await;
880
881 let timestamp: DateTime<Utc> = Utc::now();
882
883 let res = spawn_send(
884 addr,
885 timestamp,
886 vec![RECORD.as_bytes()],
887 None,
888 true,
889 Compression::None,
890 Some(COMMON_ATTRIBUTES),
891 )
892 .await;
893
894 let mut events = collect_n(rx, 1).await;
895 let res = res.await.unwrap().unwrap();
896 assert_eq!(200, res.status().as_u16());
897
898 let event = events.pop().unwrap();
899 let log = event.as_log();
900 let meta = log.metadata();
901
902 assert_eq!(log.value(), &value!(Bytes::from(RECORD.to_owned())));
904
905 assert_eq!(
907 meta.value().get(path!("vector", "source_type")).unwrap(),
908 &value!("aws_kinesis_firehose")
909 );
910 assert!(
911 meta.value()
912 .get(path!("vector", "ingest_timestamp"))
913 .unwrap()
914 .is_timestamp()
915 );
916
917 assert_eq!(
919 meta.value()
920 .get(path!("aws_kinesis_firehose", "request_id"))
921 .unwrap(),
922 &value!(REQUEST_ID)
923 );
924 assert_eq!(
925 meta.value()
926 .get(path!("aws_kinesis_firehose", "source_arn"))
927 .unwrap(),
928 &value!(SOURCE_ARN)
929 );
930 assert_eq!(
931 meta.value()
932 .get(path!("aws_kinesis_firehose", "timestamp"))
933 .unwrap(),
934 &value!(timestamp.trunc_subsecs(3))
935 );
936 assert_eq!(
937 meta.value()
938 .get(path!("aws_kinesis_firehose", "common_attributes"))
939 .unwrap(),
940 &value!(COMMON_ATTRIBUTES_MAP.clone())
941 );
942
943 let response: models::FirehoseResponse = res.json().await.unwrap();
944 assert_eq!(response.request_id, REQUEST_ID);
945 })
946 .await;
947 }
948
949 #[tokio::test]
950 async fn aws_kinesis_firehose_forwards_events_common_attributes_legacy_namespace() {
951 assert_source_compliance(&SOURCE_TAGS, async move {
952 let mut expected_common_attributes = ObjectMap::new();
953 expected_common_attributes.insert(
954 KeyString::from("environment"),
955 COMMON_ATTRIBUTES_MAP["environment"].clone(),
956 );
957 expected_common_attributes.insert(KeyString::from("absent_attribute"), Value::Null);
958
959 let (rx, addr, _guard) = source(
960 None,
961 None,
962 false,
963 Default::default(),
964 true,
965 false,
966 vec!["environment".to_string(), "absent_attribute".to_string()],
967 )
968 .await;
969
970 let timestamp: DateTime<Utc> = Utc::now();
971
972 let res = spawn_send(
973 addr,
974 timestamp,
975 vec![RECORD.as_bytes()],
976 None,
977 true,
978 Compression::None,
979 Some(COMMON_ATTRIBUTES),
980 )
981 .await;
982
983 let events = collect_n(rx, 1).await;
984 let res = res.await.unwrap().unwrap();
985 assert_eq!(200, res.status().as_u16());
986
987 assert_event_data_eq!(
988 events,
989 vec![log_event! {
990 "source_type" => Bytes::from("aws_kinesis_firehose"),
991 "timestamp" => timestamp.trunc_subsecs(3), "message"=> RECORD,
993 "request_id" => REQUEST_ID,
994 "source_arn" => SOURCE_ARN,
995 "common_attributes" => expected_common_attributes,
996 },]
997 );
998
999 let response: models::FirehoseResponse = res.json().await.unwrap();
1000 assert_eq!(response.request_id, REQUEST_ID);
1001 })
1002 .await;
1003 }
1004
1005 #[tokio::test]
1006 async fn aws_kinesis_firehose_forwards_events_common_attributes_vector_namespace() {
1007 assert_source_compliance(&SOURCE_TAGS, async move {
1008 let mut expected_common_attributes = ObjectMap::new();
1009 expected_common_attributes.insert(
1010 KeyString::from("environment"),
1011 COMMON_ATTRIBUTES_MAP["environment"].clone(),
1012 );
1013 expected_common_attributes.insert(KeyString::from("absent_attribute"), Value::Null);
1014
1015 let (rx, addr, _guard) = source(
1016 None,
1017 None,
1018 false,
1019 Default::default(),
1020 true,
1021 true,
1022 vec!["environment".to_string(), "absent_attribute".to_string()],
1023 )
1024 .await;
1025
1026 let timestamp: DateTime<Utc> = Utc::now();
1027
1028 let res = spawn_send(
1029 addr,
1030 timestamp,
1031 vec![RECORD.as_bytes()],
1032 None,
1033 true,
1034 Compression::None,
1035 Some(COMMON_ATTRIBUTES),
1036 )
1037 .await;
1038
1039 let mut events = collect_n(rx, 1).await;
1040 let res = res.await.unwrap().unwrap();
1041 assert_eq!(200, res.status().as_u16());
1042
1043 let event = events.pop().unwrap();
1044 let log = event.as_log();
1045 let meta = log.metadata();
1046
1047 assert_eq!(log.value(), &value!(Bytes::from(RECORD.to_owned())));
1049
1050 assert_eq!(
1052 meta.value().get(path!("vector", "source_type")).unwrap(),
1053 &value!("aws_kinesis_firehose")
1054 );
1055 assert!(
1056 meta.value()
1057 .get(path!("vector", "ingest_timestamp"))
1058 .unwrap()
1059 .is_timestamp()
1060 );
1061
1062 assert_eq!(
1064 meta.value()
1065 .get(path!("aws_kinesis_firehose", "request_id"))
1066 .unwrap(),
1067 &value!(REQUEST_ID)
1068 );
1069 assert_eq!(
1070 meta.value()
1071 .get(path!("aws_kinesis_firehose", "source_arn"))
1072 .unwrap(),
1073 &value!(SOURCE_ARN)
1074 );
1075 assert_eq!(
1076 meta.value()
1077 .get(path!("aws_kinesis_firehose", "timestamp"))
1078 .unwrap(),
1079 &value!(timestamp.trunc_subsecs(3))
1080 );
1081 assert_eq!(
1082 meta.value()
1083 .get(path!("aws_kinesis_firehose", "common_attributes"))
1084 .unwrap(),
1085 &value!(expected_common_attributes)
1086 );
1087
1088 let response: models::FirehoseResponse = res.json().await.unwrap();
1089 assert_eq!(response.request_id, REQUEST_ID);
1090 })
1091 .await;
1092 }
1093
1094 #[tokio::test]
1098 async fn aws_kinesis_firehose_ignores_malformed_common_attributes_if_none_configured() {
1099 assert_source_compliance(&SOURCE_TAGS, async move {
1100 let (rx, addr, _guard) =
1101 source(None, None, false, Default::default(), true, true, vec![]).await;
1102
1103 let timestamp: DateTime<Utc> = Utc::now();
1104
1105 let res = spawn_send(
1106 addr,
1107 timestamp,
1108 vec![RECORD.as_bytes()],
1109 None,
1110 true,
1111 Compression::None,
1112 Some("malformed-common-attributes"),
1113 )
1114 .await;
1115
1116 let mut events = collect_n(rx, 1).await;
1117 let res = res.await.unwrap().unwrap();
1118 assert_eq!(200, res.status().as_u16());
1119
1120 let event = events.pop().unwrap();
1121 let log = event.as_log();
1122 let meta = log.metadata();
1123
1124 assert_eq!(log.value(), &value!(Bytes::from(RECORD.to_owned())));
1126
1127 assert_eq!(
1129 meta.value().get(path!("vector", "source_type")).unwrap(),
1130 &value!("aws_kinesis_firehose")
1131 );
1132 assert!(
1133 meta.value()
1134 .get(path!("vector", "ingest_timestamp"))
1135 .unwrap()
1136 .is_timestamp()
1137 );
1138
1139 assert_eq!(
1141 meta.value()
1142 .get(path!("aws_kinesis_firehose", "request_id"))
1143 .unwrap(),
1144 &value!(REQUEST_ID)
1145 );
1146 assert_eq!(
1147 meta.value()
1148 .get(path!("aws_kinesis_firehose", "source_arn"))
1149 .unwrap(),
1150 &value!(SOURCE_ARN)
1151 );
1152 assert_eq!(
1153 meta.value()
1154 .get(path!("aws_kinesis_firehose", "timestamp"))
1155 .unwrap(),
1156 &value!(timestamp.trunc_subsecs(3))
1157 );
1158 assert!(
1159 meta.value()
1160 .get(path!("aws_kinesis_firehose", "common_attributes"))
1161 .is_none()
1162 );
1163
1164 let response: models::FirehoseResponse = res.json().await.unwrap();
1165 assert_eq!(response.request_id, REQUEST_ID);
1166 })
1167 .await;
1168 }
1169
1170 #[tokio::test]
1171 async fn aws_kinesis_firehose_rejects_bad_access_key() {
1172 let (_rx, addr, _guard) = source(
1173 Some("an access key".to_string().into()),
1174 Some(vec!["an access key in list".to_string().into()]),
1175 Default::default(),
1176 Default::default(),
1177 true,
1178 false,
1179 vec![],
1180 )
1181 .await;
1182
1183 let res = send(
1184 addr,
1185 Utc::now(),
1186 vec![],
1187 Some("bad access key"),
1188 false,
1189 Compression::None,
1190 None,
1191 )
1192 .await
1193 .unwrap();
1194 assert_eq!(401, res.status().as_u16());
1195
1196 let response: models::FirehoseResponse = res.json().await.unwrap();
1197 assert_eq!(response.request_id, REQUEST_ID);
1198 }
1199
1200 #[tokio::test]
1201 async fn aws_kinesis_firehose_rejects_bad_access_key_from_list() {
1202 let (_rx, addr, _guard) = source(
1203 None,
1204 Some(vec!["an access key in list".to_string().into()]),
1205 Default::default(),
1206 Default::default(),
1207 true,
1208 false,
1209 vec![],
1210 )
1211 .await;
1212
1213 let res = send(
1214 addr,
1215 Utc::now(),
1216 vec![],
1217 Some("bad access key"),
1218 false,
1219 Compression::None,
1220 None,
1221 )
1222 .await
1223 .unwrap();
1224 assert_eq!(401, res.status().as_u16());
1225
1226 let response: models::FirehoseResponse = res.json().await.unwrap();
1227 assert_eq!(response.request_id, REQUEST_ID);
1228 }
1229
1230 #[tokio::test]
1231 async fn aws_kinesis_firehose_accepts_merged_access_keys() {
1232 let valid_access_key = SensitiveString::from(String::from("an access key in list"));
1233
1234 let (_rx, addr, _guard) = source(
1235 Some(valid_access_key.clone()),
1236 Some(vec!["valid access key 2".to_string().into()]),
1237 Default::default(),
1238 Default::default(),
1239 true,
1240 false,
1241 vec![],
1242 )
1243 .await;
1244
1245 let res = send(
1246 addr,
1247 Utc::now(),
1248 vec![],
1249 Some(valid_access_key.clone().inner()),
1250 false,
1251 Compression::None,
1252 None,
1253 )
1254 .await
1255 .unwrap();
1256
1257 assert_eq!(200, res.status().as_u16());
1258
1259 let response: models::FirehoseResponse = res.json().await.unwrap();
1260 assert_eq!(response.request_id, REQUEST_ID);
1261 }
1262
1263 #[tokio::test]
1264 async fn aws_kinesis_firehose_accepts_access_keys_from_list() {
1265 let valid_access_key = "an access key in list".to_string();
1266
1267 let (_rx, addr, _guard) = source(
1268 None,
1269 Some(vec![
1270 valid_access_key.clone().into(),
1271 "valid access key 2".to_string().into(),
1272 ]),
1273 Default::default(),
1274 Default::default(),
1275 true,
1276 false,
1277 vec![],
1278 )
1279 .await;
1280
1281 let res = send(
1282 addr,
1283 Utc::now(),
1284 vec![],
1285 Some(&valid_access_key),
1286 false,
1287 Compression::None,
1288 None,
1289 )
1290 .await
1291 .unwrap();
1292
1293 assert_eq!(200, res.status().as_u16());
1294
1295 let response: models::FirehoseResponse = res.json().await.unwrap();
1296 assert_eq!(response.request_id, REQUEST_ID);
1297 }
1298
1299 #[tokio::test]
1300 async fn handles_acknowledgement_failure() {
1301 let expected = RECORD.as_bytes().to_owned();
1302
1303 let (rx, addr, _guard) =
1304 source(None, None, false, Compression::None, false, false, vec![]).await;
1305
1306 let timestamp: DateTime<Utc> = Utc::now();
1307
1308 let res = spawn_send(
1309 addr,
1310 timestamp,
1311 vec![RECORD.as_bytes()],
1312 None,
1313 false,
1314 Compression::None,
1315 None,
1316 )
1317 .await;
1318
1319 let events = collect_n(rx, 1).await;
1320
1321 let res = res.await.unwrap().unwrap();
1322 assert_eq!(406, res.status().as_u16());
1323
1324 assert_event_data_eq!(
1325 events,
1326 vec![log_event! {
1327 "source_type" => Bytes::from("aws_kinesis_firehose"),
1328 "timestamp" => timestamp.trunc_subsecs(3), "message"=> Bytes::from(expected),
1330 "request_id" => REQUEST_ID,
1331 "source_arn" => SOURCE_ARN,
1332 },]
1333 );
1334
1335 let response: models::FirehoseResponse = res.json().await.unwrap();
1336 assert_eq!(response.request_id, REQUEST_ID);
1337 }
1338
1339 #[tokio::test]
1340 async fn event_access_key_passthrough_enabled() {
1341 let (rx, address, _guard) = source(
1342 None,
1343 Some(vec!["an access key".to_string().into()]),
1344 true,
1345 Default::default(),
1346 true,
1347 true,
1348 vec![],
1349 )
1350 .await;
1351
1352 let timestamp: DateTime<Utc> = Utc::now();
1353
1354 spawn_send(
1355 address,
1356 timestamp,
1357 vec![RECORD.as_bytes()],
1358 Some("an access key"),
1359 false,
1360 Compression::None,
1361 None,
1362 )
1363 .await;
1364
1365 let events = collect_n(rx, 1).await;
1366 let access_key = events[0]
1367 .metadata()
1368 .secrets()
1369 .get("aws_kinesis_firehose_access_key")
1370 .unwrap();
1371 assert_eq!(access_key.to_string(), "an access key".to_string());
1372 }
1373
1374 #[tokio::test]
1375 async fn no_authorization_access_key_passthrough_enabled() {
1376 let (rx, address, _guard) =
1377 source(None, None, true, Default::default(), true, true, vec![]).await;
1378
1379 let timestamp: DateTime<Utc> = Utc::now();
1380
1381 spawn_send(
1382 address,
1383 timestamp,
1384 vec![RECORD.as_bytes()],
1385 None,
1386 false,
1387 Compression::None,
1388 None,
1389 )
1390 .await;
1391
1392 let events = collect_n(rx, 1).await;
1393
1394 assert!(
1395 events[0]
1396 .metadata()
1397 .secrets()
1398 .get("aws_kinesis_firehose_access_key")
1399 .is_none()
1400 );
1401 }
1402}