11//! AWS Kinesis sink for publishing events to a Kinesis data stream.
22//!
3- //! Publishes each event as a data record to the configured stream.
4- //! The sink uses the AWS SDK for async message delivery.
3+ //! Publishes each event's payload as a data record to a stream determined by:
4+ //! 1. `stream` key in event metadata (from subscription's metadata/metadata_extensions)
5+ //! 2. Fallback to `stream_name` in sink config
56//!
6- //! # Payload Extensions
7+ //! # Dynamic Routing
78//!
8- //! This sink supports the following optional payload extensions :
9+ //! The target stream can be configured per-event using metadata_extensions :
910//!
1011//! ```sql
11- //! payload_extensions = '[
12- //! {"key": "partition_key", "value": "user_123"},
13- //! {"key": "explicit_hash_key", "value": "abc123"}
12+ //! metadata_extensions = '[
13+ //! {"json_path": "stream", "expression": "''my-stream-'' || shard_key"}
1414//! ]'
1515//! ```
16- //!
17- //! - `partition_key`: Used for shard distribution. If not provided, the event ID is used.
18- //! - `explicit_hash_key`: Optional hash key to override partition key hashing.
1916
2017use aws_sdk_kinesis:: Client ;
2118use etl:: error:: EtlResult ;
2219use serde:: { Deserialize , Serialize } ;
2320use std:: sync:: Arc ;
24- use tracing:: info;
2521
2622use crate :: sink:: Sink ;
2723use crate :: types:: TriggeredEvent ;
@@ -32,8 +28,8 @@ use crate::types::TriggeredEvent;
3228/// leaking secrets (AWS credentials, endpoint URLs) in serialized forms.
3329#[ derive( Clone , Debug , Deserialize ) ]
3430pub struct KinesisSinkConfig {
35- /// Kinesis stream name to publish records to .
36- pub stream_name : String ,
31+ /// Kinesis stream name. Optional if provided via event metadata .
32+ pub stream_name : Option < String > ,
3733
3834 /// AWS region (e.g., "us-east-1").
3935 pub region : String ,
@@ -56,8 +52,8 @@ pub struct KinesisSinkConfig {
5652/// Safe to serialize and log. Use this for debugging and metrics.
5753#[ derive( Clone , Debug , Serialize , Deserialize ) ]
5854pub struct KinesisSinkConfigWithoutSecrets {
59- /// Kinesis stream name.
60- pub stream_name : String ,
55+ /// Kinesis stream name (if configured) .
56+ pub stream_name : Option < String > ,
6157
6258 /// AWS region.
6359 pub region : String ,
@@ -93,15 +89,15 @@ impl From<&KinesisSinkConfig> for KinesisSinkConfigWithoutSecrets {
9389
9490/// Sink that publishes events to an AWS Kinesis data stream.
9591///
96- /// Each event is serialized as JSON and published as a data record.
92+ /// Each event's payload is serialized as JSON and published as a data record.
9793/// The sink uses the AWS SDK with automatic retry handling.
9894#[ derive( Clone ) ]
9995pub struct KinesisSink {
10096 /// AWS Kinesis client.
10197 client : Arc < Client > ,
10298
103- /// Stream name to publish records to .
104- stream_name : String ,
99+ /// Default stream name. Can be overridden per-event via metadata .
100+ stream_name : Option < String > ,
105101}
106102
107103impl KinesisSink {
@@ -146,6 +142,18 @@ impl KinesisSink {
146142 stream_name : config. stream_name ,
147143 } )
148144 }
145+
146+ /// Resolves the stream name for an event from metadata or config.
147+ fn resolve_stream_name < ' a > ( & ' a self , event : & ' a TriggeredEvent ) -> Option < & ' a str > {
148+ // First check event metadata for dynamic stream
149+ if let Some ( ref metadata) = event. metadata {
150+ if let Some ( stream) = metadata. get ( "stream" ) . and_then ( |v| v. as_str ( ) ) {
151+ return Some ( stream) ;
152+ }
153+ }
154+ // Fall back to config stream name
155+ self . stream_name . as_deref ( )
156+ }
149157}
150158
151159impl Sink for KinesisSink {
@@ -158,33 +166,21 @@ impl Sink for KinesisSink {
158166 return Ok ( ( ) ) ;
159167 }
160168
161- info ! (
162- "publishing {} events to Kinesis stream '{}'" ,
163- events. len( ) ,
164- self . stream_name
165- ) ;
166-
167169 for event in & events {
168- // Build JSON object manually since TriggeredEvent doesn't implement Serialize.
169- let mut json_obj = serde_json:: json!( {
170- "id" : event. id. id,
171- "created_at" : event. id. created_at. to_rfc3339_opts( chrono:: SecondsFormat :: Millis , true ) ,
172- "payload" : event. payload,
173- "stream_id" : format!( "{:?}" , event. stream_id) ,
174- } ) ;
175-
176- // Add optional fields.
177- if let Some ( ref metadata) = event. metadata {
178- json_obj[ "metadata" ] = metadata. clone ( ) ;
179- }
180- if let Some ( lsn) = event. lsn {
181- json_obj[ "lsn" ] = serde_json:: json!( lsn. to_string( ) ) ;
182- }
170+ // Resolve stream name from event metadata or config.
171+ let stream_name = self . resolve_stream_name ( event) . ok_or_else ( || {
172+ etl:: etl_error!(
173+ etl:: error:: ErrorKind :: ConfigError ,
174+ "No stream name configured" ,
175+ "Stream name must be provided in sink config or event metadata"
176+ )
177+ } ) ?;
183178
184- let data = serde_json:: to_vec ( & json_obj) . map_err ( |e| {
179+ // Serialize payload to JSON.
180+ let data = serde_json:: to_vec ( & event. payload ) . map_err ( |e| {
185181 etl:: etl_error!(
186182 etl:: error:: ErrorKind :: InvalidData ,
187- "Failed to serialize event to JSON" ,
183+ "Failed to serialize payload to JSON" ,
188184 e. to_string( )
189185 )
190186 } ) ?;
@@ -195,14 +191,14 @@ impl Sink for KinesisSink {
195191 // Publish record to Kinesis stream.
196192 self . client
197193 . put_record ( )
198- . stream_name ( & self . stream_name )
194+ . stream_name ( stream_name)
199195 . partition_key ( & partition_key)
200196 . data ( aws_sdk_kinesis:: primitives:: Blob :: new ( data) )
201197 . send ( )
202198 . await
203199 . map_err ( |e| {
204200 etl:: etl_error!(
205- etl:: error:: ErrorKind :: InvalidData ,
201+ etl:: error:: ErrorKind :: DestinationError ,
206202 "Failed to publish record to Kinesis" ,
207203 e. to_string( )
208204 )
@@ -225,7 +221,7 @@ mod tests {
225221 #[ test]
226222 fn test_config_without_secrets ( ) {
227223 let config = KinesisSinkConfig {
228- stream_name : "my-stream" . to_string ( ) ,
224+ stream_name : Some ( "my-stream" . to_string ( ) ) ,
229225 region : "us-east-1" . to_string ( ) ,
230226 endpoint_url : Some ( "http://localhost:4566" . to_string ( ) ) ,
231227 access_key_id : Some ( "AKIAIOSFODNN7EXAMPLE" . to_string ( ) ) ,
@@ -234,7 +230,7 @@ mod tests {
234230
235231 let without_secrets: KinesisSinkConfigWithoutSecrets = ( & config) . into ( ) ;
236232
237- assert_eq ! ( without_secrets. stream_name, "my-stream" ) ;
233+ assert_eq ! ( without_secrets. stream_name, Some ( "my-stream" . to_string ( ) ) ) ;
238234 assert_eq ! ( without_secrets. region, "us-east-1" ) ;
239235 assert ! ( without_secrets. has_custom_endpoint) ;
240236 assert ! ( without_secrets. has_explicit_credentials) ;
0 commit comments