11//! AWS SNS sink for publishing events to an Amazon SNS topic.
22//!
3- //! Publishes each event as a JSON message to the configured topic ARN.
4- //! The sink uses the AWS SDK for async message delivery.
3+ //! Publishes each event's payload as JSON to a topic ARN determined by:
4+ //! 1. `topic` key in event metadata (from subscription's metadata/metadata_extensions)
5+ //! 2. Fallback to `topic_arn` in sink config
56//!
6- //! # Payload Extensions
7+ //! # Dynamic Routing
78//!
8- //! This sink does not require any specific payload extensions. However, you can
9- //! use payload extensions to add SNS message attributes:
9+ //! The target topic ARN can be configured per-event using metadata_extensions:
1010//!
1111//! ```sql
12- //! payload_extensions = '[
13- //! {"key": "message_group_id", "value": "orders"},
14- //! {"key": "subject", "value": "New Order"}
12+ //! metadata_extensions = '[
13+ //! {"json_path": "topic", "expression": "''arn:aws:sns:us-east-1:123456789:'' || topic_name"}
1514//! ]'
1615//! ```
1716
1817use aws_sdk_sns:: Client ;
1918use etl:: error:: EtlResult ;
2019use serde:: { Deserialize , Serialize } ;
2120use std:: sync:: Arc ;
22- use tracing:: info;
2321
2422use crate :: sink:: Sink ;
2523use crate :: types:: TriggeredEvent ;
@@ -30,8 +28,8 @@ use crate::types::TriggeredEvent;
3028/// leaking secrets (AWS credentials, endpoint URLs) in serialized forms.
3129#[ derive( Clone , Debug , Deserialize ) ]
3230pub struct SnsSinkConfig {
33- /// SNS topic ARN to publish messages to.
34- pub topic_arn : String ,
31+ /// SNS topic ARN to publish messages to. Optional if provided via event metadata.
32+ pub topic_arn : Option < String > ,
3533
3634 /// AWS region (e.g., "us-east-1").
3735 pub region : String ,
@@ -54,8 +52,8 @@ pub struct SnsSinkConfig {
5452/// Safe to serialize and log. Use this for debugging and metrics.
5553#[ derive( Clone , Debug , Serialize , Deserialize ) ]
5654pub struct SnsSinkConfigWithoutSecrets {
57- /// SNS topic ARN to publish messages to.
58- pub topic_arn : String ,
55+ /// SNS topic ARN to publish messages to (if configured) .
56+ pub topic_arn : Option < String > ,
5957
6058 /// AWS region.
6159 pub region : String ,
@@ -91,15 +89,15 @@ impl From<&SnsSinkConfig> for SnsSinkConfigWithoutSecrets {
9189
9290/// Sink that publishes events to an AWS SNS topic.
9391///
94- /// Each event is serialized as JSON and published to the configured topic.
92+ /// Each event's payload is serialized as JSON and published to the configured topic.
9593/// The sink uses the AWS SDK with automatic retry handling.
9694#[ derive( Clone ) ]
9795pub struct SnsSink {
9896 /// AWS SNS client.
9997 client : Arc < Client > ,
10098
101- /// Topic ARN to publish messages to .
102- topic_arn : String ,
99+ /// Default topic ARN. Can be overridden per-event via metadata .
100+ topic_arn : Option < String > ,
103101}
104102
105103impl SnsSink {
@@ -144,6 +142,18 @@ impl SnsSink {
144142 topic_arn : config. topic_arn ,
145143 } )
146144 }
145+
146+ /// Resolves the topic ARN for an event from metadata or config.
147+ fn resolve_topic_arn < ' a > ( & ' a self , event : & ' a TriggeredEvent ) -> Option < & ' a str > {
148+ // First check event metadata for dynamic topic
149+ if let Some ( ref metadata) = event. metadata {
150+ if let Some ( topic) = metadata. get ( "topic" ) . and_then ( |v| v. as_str ( ) ) {
151+ return Some ( topic) ;
152+ }
153+ }
154+ // Fall back to config topic ARN
155+ self . topic_arn . as_deref ( )
156+ }
147157}
148158
149159impl Sink for SnsSink {
@@ -156,47 +166,35 @@ impl Sink for SnsSink {
156166 return Ok ( ( ) ) ;
157167 }
158168
159- info ! (
160- "publishing {} events to SNS topic '{}'" ,
161- events. len( ) ,
162- self . topic_arn
163- ) ;
164-
165169 for event in & events {
166- // Build JSON object manually since TriggeredEvent doesn't implement Serialize.
167- let mut json_obj = serde_json:: json!( {
168- "id" : event. id. id,
169- "created_at" : event. id. created_at. to_rfc3339_opts( chrono:: SecondsFormat :: Millis , true ) ,
170- "payload" : event. payload,
171- "stream_id" : format!( "{:?}" , event. stream_id) ,
172- } ) ;
173-
174- // Add optional fields.
175- if let Some ( ref metadata) = event. metadata {
176- json_obj[ "metadata" ] = metadata. clone ( ) ;
177- }
178- if let Some ( lsn) = event. lsn {
179- json_obj[ "lsn" ] = serde_json:: json!( lsn. to_string( ) ) ;
180- }
170+ // Resolve topic ARN from event metadata or config.
171+ let topic_arn = self . resolve_topic_arn ( event) . ok_or_else ( || {
172+ etl:: etl_error!(
173+ etl:: error:: ErrorKind :: ConfigError ,
174+ "No topic ARN configured" ,
175+ "Topic ARN must be provided in sink config or event metadata"
176+ )
177+ } ) ?;
181178
182- let message = serde_json:: to_string ( & json_obj) . map_err ( |e| {
179+ // Serialize payload to JSON.
180+ let message = serde_json:: to_string ( & event. payload ) . map_err ( |e| {
183181 etl:: etl_error!(
184182 etl:: error:: ErrorKind :: InvalidData ,
185- "Failed to serialize event to JSON" ,
183+ "Failed to serialize payload to JSON" ,
186184 e. to_string( )
187185 )
188186 } ) ?;
189187
190188 // Publish message to SNS topic.
191189 self . client
192190 . publish ( )
193- . topic_arn ( & self . topic_arn )
191+ . topic_arn ( topic_arn)
194192 . message ( & message)
195193 . send ( )
196194 . await
197195 . map_err ( |e| {
198196 etl:: etl_error!(
199- etl:: error:: ErrorKind :: InvalidData ,
197+ etl:: error:: ErrorKind :: DestinationError ,
200198 "Failed to publish message to SNS" ,
201199 e. to_string( )
202200 )
@@ -219,7 +217,7 @@ mod tests {
219217 #[ test]
220218 fn test_config_without_secrets ( ) {
221219 let config = SnsSinkConfig {
222- topic_arn : "arn:aws:sns:us-east-1:123456789:my-topic" . to_string ( ) ,
220+ topic_arn : Some ( "arn:aws:sns:us-east-1:123456789:my-topic" . to_string ( ) ) ,
223221 region : "us-east-1" . to_string ( ) ,
224222 endpoint_url : Some ( "http://localhost:4566" . to_string ( ) ) ,
225223 access_key_id : Some ( "AKIAIOSFODNN7EXAMPLE" . to_string ( ) ) ,
@@ -230,7 +228,7 @@ mod tests {
230228
231229 assert_eq ! (
232230 without_secrets. topic_arn,
233- "arn:aws:sns:us-east-1:123456789:my-topic"
231+ Some ( "arn:aws:sns:us-east-1:123456789:my-topic" . to_string ( ) )
234232 ) ;
235233 assert_eq ! ( without_secrets. region, "us-east-1" ) ;
236234 assert ! ( without_secrets. has_custom_endpoint) ;
0 commit comments