github.com/Jeffail/benthos/v3@v3.65.0/lib/output/writer/sns.go (about) 1 package writer 2 3 import ( 4 "context" 5 "fmt" 6 "regexp" 7 "sort" 8 "strings" 9 "time" 10 11 "github.com/Jeffail/benthos/v3/internal/bloblang/field" 12 "github.com/Jeffail/benthos/v3/internal/interop" 13 "github.com/Jeffail/benthos/v3/internal/metadata" 14 "github.com/Jeffail/benthos/v3/lib/log" 15 "github.com/Jeffail/benthos/v3/lib/metrics" 16 "github.com/Jeffail/benthos/v3/lib/types" 17 sess "github.com/Jeffail/benthos/v3/lib/util/aws/session" 18 "github.com/aws/aws-sdk-go/aws" 19 "github.com/aws/aws-sdk-go/aws/session" 20 "github.com/aws/aws-sdk-go/service/sns" 21 ) 22 23 //------------------------------------------------------------------------------ 24 25 // SNSConfig contains configuration fields for the output SNS type. 26 type SNSConfig struct { 27 TopicArn string `json:"topic_arn" yaml:"topic_arn"` 28 MessageGroupID string `json:"message_group_id" yaml:"message_group_id"` 29 MessageDeduplicationID string `json:"message_deduplication_id" yaml:"message_deduplication_id"` 30 Metadata metadata.ExcludeFilterConfig `json:"metadata" yaml:"metadata"` 31 sessionConfig `json:",inline" yaml:",inline"` 32 Timeout string `json:"timeout" yaml:"timeout"` 33 MaxInFlight int `json:"max_in_flight" yaml:"max_in_flight"` 34 } 35 36 // NewSNSConfig creates a new Config with default values. 37 func NewSNSConfig() SNSConfig { 38 return SNSConfig{ 39 sessionConfig: sessionConfig{ 40 Config: sess.NewConfig(), 41 }, 42 TopicArn: "", 43 MessageGroupID: "", 44 MessageDeduplicationID: "", 45 Metadata: metadata.NewExcludeFilterConfig(), 46 Timeout: "5s", 47 MaxInFlight: 1, 48 } 49 } 50 51 //------------------------------------------------------------------------------ 52 53 // SNS is a benthos writer.Type implementation that writes messages to an 54 // Amazon SNS queue. 55 type SNS struct { 56 conf SNSConfig 57 58 groupID *field.Expression 59 dedupeID *field.Expression 60 metaFilter *metadata.ExcludeFilter 61 62 session *session.Session 63 sns *sns.SNS 64 65 tout time.Duration 66 67 log log.Modular 68 stats metrics.Type 69 } 70 71 // NewSNS creates a new Amazon SNS writer.Type. 72 func NewSNS(conf SNSConfig, log log.Modular, stats metrics.Type) (*SNS, error) { 73 return NewSNSV2(conf, types.NoopMgr(), log, stats) 74 } 75 76 // NewSNSV2 creates a new AWS SNS writer. 77 func NewSNSV2(conf SNSConfig, mgr types.Manager, log log.Modular, stats metrics.Type) (*SNS, error) { 78 s := &SNS{ 79 conf: conf, 80 log: log, 81 stats: stats, 82 } 83 84 var err error 85 if id := conf.MessageGroupID; len(id) > 0 { 86 if s.groupID, err = interop.NewBloblangField(mgr, id); err != nil { 87 return nil, fmt.Errorf("failed to parse group ID expression: %v", err) 88 } 89 } 90 if id := conf.MessageDeduplicationID; len(id) > 0 { 91 if s.dedupeID, err = interop.NewBloblangField(mgr, id); err != nil { 92 return nil, fmt.Errorf("failed to parse dedupe ID expression: %v", err) 93 } 94 } 95 if s.metaFilter, err = conf.Metadata.Filter(); err != nil { 96 return nil, fmt.Errorf("failed to construct metadata filter: %w", err) 97 } 98 if tout := conf.Timeout; len(tout) > 0 { 99 if s.tout, err = time.ParseDuration(tout); err != nil { 100 return nil, fmt.Errorf("failed to parse timeout period string: %v", err) 101 } 102 } 103 return s, nil 104 } 105 106 // ConnectWithContext attempts to establish a connection to the target SNS queue. 107 func (a *SNS) ConnectWithContext(ctx context.Context) error { 108 return a.Connect() 109 } 110 111 // Connect attempts to establish a connection to the target SNS queue. 112 func (a *SNS) Connect() error { 113 if a.session != nil { 114 return nil 115 } 116 117 sess, err := a.conf.GetSession() 118 if err != nil { 119 return err 120 } 121 122 a.session = sess 123 a.sns = sns.New(sess) 124 125 a.log.Infof("Sending messages to Amazon SNS ARN: %v\n", a.conf.TopicArn) 126 return nil 127 } 128 129 type snsAttributes struct { 130 attrMap map[string]*sns.MessageAttributeValue 131 groupID *string 132 dedupeID *string 133 } 134 135 var snsAttributeKeyInvalidCharRegexp = regexp.MustCompile(`(^\.)|(\.\.)|(^aws\.)|(^amazon\.)|(\.$)|([^a-z0-9_\-.]+)`) 136 137 func isValidSNSAttribute(k, v string) bool { 138 return len(snsAttributeKeyInvalidCharRegexp.FindStringIndex(strings.ToLower(k))) == 0 139 } 140 141 func (a *SNS) getSNSAttributes(msg types.Message, i int) snsAttributes { 142 p := msg.Get(i) 143 keys := []string{} 144 a.metaFilter.Iter(p.Metadata(), func(k, v string) error { 145 if isValidSNSAttribute(k, v) { 146 keys = append(keys, k) 147 } else { 148 a.log.Debugf("Rejecting metadata key '%v' due to invalid characters\n", k) 149 } 150 return nil 151 }) 152 var values map[string]*sns.MessageAttributeValue 153 if len(keys) > 0 { 154 sort.Strings(keys) 155 values = map[string]*sns.MessageAttributeValue{} 156 157 for _, k := range keys { 158 values[k] = &sns.MessageAttributeValue{ 159 DataType: aws.String("String"), 160 StringValue: aws.String(p.Metadata().Get(k)), 161 } 162 } 163 } 164 165 var groupID, dedupeID *string 166 if a.groupID != nil { 167 groupID = aws.String(a.groupID.String(i, msg)) 168 } 169 if a.dedupeID != nil { 170 dedupeID = aws.String(a.dedupeID.String(i, msg)) 171 } 172 173 return snsAttributes{ 174 attrMap: values, 175 groupID: groupID, 176 dedupeID: dedupeID, 177 } 178 } 179 180 // Write attempts to write message contents to a target SNS. 181 func (a *SNS) Write(msg types.Message) error { 182 return a.WriteWithContext(context.Background(), msg) 183 } 184 185 // WriteWithContext attempts to write message contents to a target SNS. 186 func (a *SNS) WriteWithContext(wctx context.Context, msg types.Message) error { 187 if a.session == nil { 188 return types.ErrNotConnected 189 } 190 191 ctx, cancel := context.WithTimeout(wctx, a.tout) 192 defer cancel() 193 194 return IterateBatchedSend(msg, func(i int, p types.Part) error { 195 attrs := a.getSNSAttributes(msg, i) 196 message := &sns.PublishInput{ 197 TopicArn: aws.String(a.conf.TopicArn), 198 Message: aws.String(string(p.Get())), 199 MessageAttributes: attrs.attrMap, 200 MessageGroupId: attrs.groupID, 201 MessageDeduplicationId: attrs.dedupeID, 202 } 203 _, err := a.sns.PublishWithContext(ctx, message) 204 return err 205 }) 206 } 207 208 // CloseAsync begins cleaning up resources used by this reader asynchronously. 209 func (a *SNS) CloseAsync() { 210 } 211 212 // WaitForClose will block until either the reader is closed or a specified 213 // timeout occurs. 214 func (a *SNS) WaitForClose(time.Duration) error { 215 return nil 216 } 217 218 //------------------------------------------------------------------------------