github.com/Jeffail/benthos/v3@v3.65.0/lib/output/writer/amqp.go (about)

     1  package writer
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"errors"
     7  	"fmt"
     8  	"net/url"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/Jeffail/benthos/v3/internal/bloblang/field"
    15  	"github.com/Jeffail/benthos/v3/internal/interop"
    16  	"github.com/Jeffail/benthos/v3/internal/metadata"
    17  	"github.com/Jeffail/benthos/v3/lib/log"
    18  	"github.com/Jeffail/benthos/v3/lib/metrics"
    19  	"github.com/Jeffail/benthos/v3/lib/types"
    20  	btls "github.com/Jeffail/benthos/v3/lib/util/tls"
    21  	amqp "github.com/rabbitmq/amqp091-go"
    22  )
    23  
    24  const defaultDeprecatedAMQP09URL = "amqp://guest:guest@localhost:5672/"
    25  
    26  var errAMQP09Connect = errors.New("AMQP 0.9 Connect")
    27  
    28  //------------------------------------------------------------------------------
    29  
    30  // AMQPExchangeDeclareConfig contains fields indicating whether the target AMQP
    31  // exchange needs to be declared, as well as any fields specifying how to
    32  // accomplish that.
    33  type AMQPExchangeDeclareConfig struct {
    34  	Enabled bool   `json:"enabled" yaml:"enabled"`
    35  	Type    string `json:"type" yaml:"type"`
    36  	Durable bool   `json:"durable" yaml:"durable"`
    37  }
    38  
    39  // AMQPConfig contains configuration fields for the AMQP output type.
    40  type AMQPConfig struct {
    41  	URL             string                       `json:"url" yaml:"url"`
    42  	URLs            []string                     `json:"urls" yaml:"urls"`
    43  	MaxInFlight     int                          `json:"max_in_flight" yaml:"max_in_flight"`
    44  	Exchange        string                       `json:"exchange" yaml:"exchange"`
    45  	ExchangeDeclare AMQPExchangeDeclareConfig    `json:"exchange_declare" yaml:"exchange_declare"`
    46  	BindingKey      string                       `json:"key" yaml:"key"`
    47  	Type            string                       `json:"type" yaml:"type"`
    48  	ContentType     string                       `json:"content_type" yaml:"content_type"`
    49  	ContentEncoding string                       `json:"content_encoding" yaml:"content_encoding"`
    50  	Metadata        metadata.ExcludeFilterConfig `json:"metadata" yaml:"metadata"`
    51  	Priority        string                       `json:"priority" yaml:"priority"`
    52  	Persistent      bool                         `json:"persistent" yaml:"persistent"`
    53  	Mandatory       bool                         `json:"mandatory" yaml:"mandatory"`
    54  	Immediate       bool                         `json:"immediate" yaml:"immediate"`
    55  	TLS             btls.Config                  `json:"tls" yaml:"tls"`
    56  }
    57  
    58  // NewAMQPConfig creates a new AMQPConfig with default values.
    59  func NewAMQPConfig() AMQPConfig {
    60  	return AMQPConfig{
    61  		URL:         defaultDeprecatedAMQP09URL,
    62  		URLs:        []string{},
    63  		MaxInFlight: 1,
    64  		Exchange:    "benthos-exchange",
    65  		ExchangeDeclare: AMQPExchangeDeclareConfig{
    66  			Enabled: false,
    67  			Type:    "direct",
    68  			Durable: true,
    69  		},
    70  		BindingKey:      "benthos-key",
    71  		Type:            "",
    72  		ContentType:     "application/octet-stream",
    73  		ContentEncoding: "",
    74  		Metadata:        metadata.NewExcludeFilterConfig(),
    75  		Priority:        "",
    76  		Persistent:      false,
    77  		Mandatory:       false,
    78  		Immediate:       false,
    79  		TLS:             btls.NewConfig(),
    80  	}
    81  }
    82  
    83  //------------------------------------------------------------------------------
    84  
    85  // AMQP is an output type that serves AMQP messages.
    86  type AMQP struct {
    87  	key             *field.Expression
    88  	msgType         *field.Expression
    89  	contentType     *field.Expression
    90  	contentEncoding *field.Expression
    91  	priority        *field.Expression
    92  	metaFilter      *metadata.ExcludeFilter
    93  
    94  	log   log.Modular
    95  	stats metrics.Type
    96  
    97  	conf    AMQPConfig
    98  	urls    []string
    99  	tlsConf *tls.Config
   100  
   101  	conn        *amqp.Connection
   102  	amqpChan    *amqp.Channel
   103  	confirmChan <-chan amqp.Confirmation
   104  	returnChan  <-chan amqp.Return
   105  
   106  	deliveryMode uint8
   107  
   108  	connLock sync.RWMutex
   109  }
   110  
   111  // NewAMQP creates a new AMQP writer type.
   112  //
   113  // Deprecated: use the V2 API instead.
   114  func NewAMQP(conf AMQPConfig, log log.Modular, stats metrics.Type) (*AMQP, error) {
   115  	return NewAMQPV2(types.NoopMgr(), conf, log, stats)
   116  }
   117  
   118  // NewAMQPV2 creates a new AMQP writer type.
   119  func NewAMQPV2(mgr types.Manager, conf AMQPConfig, log log.Modular, stats metrics.Type) (*AMQP, error) {
   120  	a := AMQP{
   121  		log:          log,
   122  		stats:        stats,
   123  		conf:         conf,
   124  		deliveryMode: amqp.Transient,
   125  	}
   126  	var err error
   127  	if a.metaFilter, err = conf.Metadata.Filter(); err != nil {
   128  		return nil, fmt.Errorf("failed to construct metadata filter: %w", err)
   129  	}
   130  	if a.key, err = interop.NewBloblangField(mgr, conf.BindingKey); err != nil {
   131  		return nil, fmt.Errorf("failed to parse binding key expression: %v", err)
   132  	}
   133  	if a.msgType, err = interop.NewBloblangField(mgr, conf.Type); err != nil {
   134  		return nil, fmt.Errorf("failed to parse type property expression: %v", err)
   135  	}
   136  	if a.contentType, err = interop.NewBloblangField(mgr, conf.ContentType); err != nil {
   137  		return nil, fmt.Errorf("failed to parse content_type property expression: %v", err)
   138  	}
   139  	if a.contentEncoding, err = interop.NewBloblangField(mgr, conf.ContentEncoding); err != nil {
   140  		return nil, fmt.Errorf("failed to parse content_encoding property expression: %v", err)
   141  	}
   142  	if a.priority, err = interop.NewBloblangField(mgr, conf.Priority); err != nil {
   143  		return nil, fmt.Errorf("failed to parse priority property expression: %w", err)
   144  	}
   145  	if conf.Persistent {
   146  		a.deliveryMode = amqp.Persistent
   147  	}
   148  
   149  	if conf.URL != defaultDeprecatedAMQP09URL && len(conf.URLs) > 0 {
   150  		return nil, errors.New("cannot mix both the field `url` and `urls`")
   151  	}
   152  
   153  	if len(conf.URLs) == 0 {
   154  		a.urls = append(a.urls, conf.URL)
   155  	}
   156  
   157  	for _, u := range conf.URLs {
   158  		for _, splitURL := range strings.Split(u, ",") {
   159  			if trimmed := strings.TrimSpace(splitURL); len(trimmed) > 0 {
   160  				a.urls = append(a.urls, trimmed)
   161  			}
   162  		}
   163  	}
   164  
   165  	if conf.TLS.Enabled {
   166  		if a.tlsConf, err = conf.TLS.Get(); err != nil {
   167  			return nil, err
   168  		}
   169  	}
   170  	return &a, nil
   171  }
   172  
   173  //------------------------------------------------------------------------------
   174  
   175  // ConnectWithContext establishes a connection to an AMQP server.
   176  func (a *AMQP) ConnectWithContext(ctx context.Context) error {
   177  	return a.Connect()
   178  }
   179  
   180  // Connect establishes a connection to an AMQP server.
   181  func (a *AMQP) Connect() error {
   182  	a.connLock.Lock()
   183  	defer a.connLock.Unlock()
   184  
   185  	conn, err := a.reDial(a.urls)
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	var amqpChan *amqp.Channel
   191  	if amqpChan, err = conn.Channel(); err != nil {
   192  		conn.Close()
   193  		return fmt.Errorf("amqp failed to create channel: %v", err)
   194  	}
   195  
   196  	if a.conf.ExchangeDeclare.Enabled {
   197  		if err = amqpChan.ExchangeDeclare(
   198  			a.conf.Exchange,                // name of the exchange
   199  			a.conf.ExchangeDeclare.Type,    // type
   200  			a.conf.ExchangeDeclare.Durable, // durable
   201  			false,                          // delete when complete
   202  			false,                          // internal
   203  			false,                          // noWait
   204  			nil,                            // arguments
   205  		); err != nil {
   206  			conn.Close()
   207  			return fmt.Errorf("amqp failed to declare exchange: %v", err)
   208  		}
   209  	}
   210  
   211  	if err = amqpChan.Confirm(false); err != nil {
   212  		conn.Close()
   213  		return fmt.Errorf("amqp channel could not be put into confirm mode: %v", err)
   214  	}
   215  
   216  	a.conn = conn
   217  	a.amqpChan = amqpChan
   218  	a.confirmChan = amqpChan.NotifyPublish(make(chan amqp.Confirmation, a.conf.MaxInFlight))
   219  	if a.conf.Mandatory || a.conf.Immediate {
   220  		a.returnChan = amqpChan.NotifyReturn(make(chan amqp.Return, 1))
   221  	}
   222  
   223  	a.log.Infof("Sending AMQP messages to exchange: %v\n", a.conf.Exchange)
   224  	return nil
   225  }
   226  
   227  // disconnect safely closes a connection to an AMQP server.
   228  func (a *AMQP) disconnect() error {
   229  	a.connLock.Lock()
   230  	defer a.connLock.Unlock()
   231  
   232  	if a.amqpChan != nil {
   233  		a.amqpChan = nil
   234  	}
   235  	if a.conn != nil {
   236  		if err := a.conn.Close(); err != nil {
   237  			a.log.Errorf("Failed to close connection cleanly: %v\n", err)
   238  		}
   239  		a.conn = nil
   240  	}
   241  	return nil
   242  }
   243  
   244  //------------------------------------------------------------------------------
   245  
   246  // WriteWithContext will attempt to write a message over AMQP, wait for
   247  // acknowledgement, and returns an error if applicable.
   248  func (a *AMQP) WriteWithContext(ctx context.Context, msg types.Message) error {
   249  	return a.Write(msg)
   250  }
   251  
   252  // Write will attempt to write a message over AMQP, wait for acknowledgement,
   253  // and returns an error if applicable.
   254  func (a *AMQP) Write(msg types.Message) error {
   255  	a.connLock.RLock()
   256  	conn := a.conn
   257  	amqpChan := a.amqpChan
   258  	confirmChan := a.confirmChan
   259  	returnChan := a.returnChan
   260  	a.connLock.RUnlock()
   261  
   262  	if conn == nil {
   263  		return types.ErrNotConnected
   264  	}
   265  
   266  	return IterateBatchedSend(msg, func(i int, p types.Part) error {
   267  		bindingKey := strings.ReplaceAll(a.key.String(i, msg), "/", ".")
   268  		msgType := strings.ReplaceAll(a.msgType.String(i, msg), "/", ".")
   269  		contentType := a.contentType.String(i, msg)
   270  		contentEncoding := a.contentEncoding.String(i, msg)
   271  
   272  		var priority uint8
   273  		if priorityString := a.priority.String(i, msg); priorityString != "" {
   274  			priorityInt, err := strconv.Atoi(priorityString)
   275  			if err != nil {
   276  				return fmt.Errorf("failed to parse valid integer from priority expression: %w", err)
   277  			}
   278  			if priorityInt > 9 || priorityInt < 0 {
   279  				return fmt.Errorf("invalid priority parsed from expression, must be <= 9 and >= 0, got %v", priorityInt)
   280  			}
   281  			priority = uint8(priorityInt)
   282  		}
   283  
   284  		headers := amqp.Table{}
   285  		a.metaFilter.Iter(p.Metadata(), func(k, v string) error {
   286  			headers[strings.ReplaceAll(k, "_", "-")] = v
   287  			return nil
   288  		})
   289  
   290  		err := amqpChan.Publish(
   291  			a.conf.Exchange,  // publish to an exchange
   292  			bindingKey,       // routing to 0 or more queues
   293  			a.conf.Mandatory, // mandatory
   294  			a.conf.Immediate, // immediate
   295  			amqp.Publishing{
   296  				Headers:         headers,
   297  				ContentType:     contentType,
   298  				ContentEncoding: contentEncoding,
   299  				Body:            p.Get(),
   300  				DeliveryMode:    a.deliveryMode, // 1=non-persistent, 2=persistent
   301  				Priority:        priority,       // 0-9
   302  				Type:            msgType,
   303  				// a bunch of application/implementation-specific fields
   304  			},
   305  		)
   306  		if err != nil {
   307  			a.disconnect()
   308  			a.log.Errorf("Failed to send message: %v\n", err)
   309  			return types.ErrNotConnected
   310  		}
   311  		select {
   312  		case confirm, open := <-confirmChan:
   313  			if !open {
   314  				a.log.Errorln("Failed to send message, ensure your target exchange exists.")
   315  				return types.ErrNotConnected
   316  			}
   317  			if !confirm.Ack {
   318  				a.log.Errorln("Failed to acknowledge message.")
   319  				return types.ErrNoAck
   320  			}
   321  		case _, open := <-returnChan:
   322  			if !open {
   323  				return fmt.Errorf("acknowledgement not supported, ensure server supports immediate and mandatory flags")
   324  			}
   325  			return types.ErrNoAck
   326  		}
   327  		return nil
   328  	})
   329  }
   330  
   331  // CloseAsync shuts down the AMQP output and stops processing messages.
   332  func (a *AMQP) CloseAsync() {
   333  	a.disconnect()
   334  }
   335  
   336  // WaitForClose blocks until the AMQP output has closed down.
   337  func (a *AMQP) WaitForClose(timeout time.Duration) error {
   338  	return nil
   339  }
   340  
   341  //------------------------------------------------------------------------------
   342  
   343  // reDial connection to amqp with one or more fallback URLs
   344  func (a *AMQP) reDial(urls []string) (conn *amqp.Connection, err error) {
   345  	for _, u := range urls {
   346  		conn, err = a.dial(u)
   347  		if err != nil {
   348  			if errors.Is(err, errAMQP09Connect) {
   349  				continue
   350  			}
   351  			break
   352  		}
   353  		return conn, nil
   354  	}
   355  	return nil, err
   356  }
   357  
   358  // dial attempts to connect to amqp URL
   359  func (a *AMQP) dial(amqpURL string) (conn *amqp.Connection, err error) {
   360  	u, err := url.Parse(amqpURL)
   361  	if err != nil {
   362  		return nil, fmt.Errorf("invalid AMQP URL: %w", err)
   363  	}
   364  
   365  	if a.conf.TLS.Enabled {
   366  		if u.User != nil {
   367  			conn, err = amqp.DialTLS(amqpURL, a.tlsConf)
   368  			if err != nil {
   369  				return nil, fmt.Errorf("%w: %s", errAMQP09Connect, err)
   370  			}
   371  		} else {
   372  			conn, err = amqp.DialTLS_ExternalAuth(amqpURL, a.tlsConf)
   373  			if err != nil {
   374  				return nil, fmt.Errorf("%w: %s", errAMQP09Connect, err)
   375  			}
   376  		}
   377  	} else {
   378  		conn, err = amqp.Dial(amqpURL)
   379  		if err != nil {
   380  			return nil, fmt.Errorf("%w: %s", errAMQP09Connect, err)
   381  		}
   382  	}
   383  
   384  	return conn, nil
   385  }