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  //------------------------------------------------------------------------------