github.com/Jeffail/benthos/v3@v3.65.0/lib/input/reader/amqp.go (about)

     1  package reader
     2  
     3  import (
     4  	"crypto/tls"
     5  	"fmt"
     6  	"net/url"
     7  	"strconv"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/Jeffail/benthos/v3/lib/log"
    13  	"github.com/Jeffail/benthos/v3/lib/message"
    14  	"github.com/Jeffail/benthos/v3/lib/metrics"
    15  	"github.com/Jeffail/benthos/v3/lib/types"
    16  	btls "github.com/Jeffail/benthos/v3/lib/util/tls"
    17  	amqp "github.com/rabbitmq/amqp091-go"
    18  )
    19  
    20  //------------------------------------------------------------------------------
    21  
    22  // AMQPQueueDeclareConfig contains fields indicating whether the target AMQP
    23  // queue needs to be declared and bound to an exchange, as well as any fields
    24  // specifying how to accomplish that.
    25  type AMQPQueueDeclareConfig struct {
    26  	Enabled bool `json:"enabled" yaml:"enabled"`
    27  	Durable bool `json:"durable" yaml:"durable"`
    28  }
    29  
    30  // AMQPBindingConfig contains fields describing a queue binding to be declared.
    31  type AMQPBindingConfig struct {
    32  	Exchange   string `json:"exchange" yaml:"exchange"`
    33  	RoutingKey string `json:"key" yaml:"key"`
    34  }
    35  
    36  // AMQPConfig contains configuration for the AMQP input type.
    37  type AMQPConfig struct {
    38  	URL             string                 `json:"url" yaml:"url"`
    39  	Queue           string                 `json:"queue" yaml:"queue"`
    40  	QueueDeclare    AMQPQueueDeclareConfig `json:"queue_declare" yaml:"queue_declare"`
    41  	BindingsDeclare []AMQPBindingConfig    `json:"bindings_declare" yaml:"bindings_declare"`
    42  	ConsumerTag     string                 `json:"consumer_tag" yaml:"consumer_tag"`
    43  	PrefetchCount   int                    `json:"prefetch_count" yaml:"prefetch_count"`
    44  	PrefetchSize    int                    `json:"prefetch_size" yaml:"prefetch_size"`
    45  	MaxBatchCount   int                    `json:"max_batch_count" yaml:"max_batch_count"`
    46  	TLS             btls.Config            `json:"tls" yaml:"tls"`
    47  }
    48  
    49  // NewAMQPConfig creates a new AMQPConfig with default values.
    50  func NewAMQPConfig() AMQPConfig {
    51  	return AMQPConfig{
    52  		URL:   "amqp://guest:guest@localhost:5672/",
    53  		Queue: "benthos-queue",
    54  		QueueDeclare: AMQPQueueDeclareConfig{
    55  			Enabled: false,
    56  			Durable: true,
    57  		},
    58  		ConsumerTag:     "benthos-consumer",
    59  		PrefetchCount:   10,
    60  		PrefetchSize:    0,
    61  		TLS:             btls.NewConfig(),
    62  		MaxBatchCount:   1,
    63  		BindingsDeclare: []AMQPBindingConfig{},
    64  	}
    65  }
    66  
    67  //------------------------------------------------------------------------------
    68  
    69  // AMQP is an input type that reads messages via the AMQP 0.91 protocol.
    70  type AMQP struct {
    71  	conn         *amqp.Connection
    72  	amqpChan     *amqp.Channel
    73  	consumerChan <-chan amqp.Delivery
    74  
    75  	ackTag  uint64
    76  	tlsConf *tls.Config
    77  
    78  	conf  AMQPConfig
    79  	stats metrics.Type
    80  	log   log.Modular
    81  
    82  	m sync.RWMutex
    83  }
    84  
    85  // NewAMQP creates a new AMQP input type.
    86  func NewAMQP(conf AMQPConfig, log log.Modular, stats metrics.Type) (Type, error) {
    87  	a := AMQP{
    88  		conf:  conf,
    89  		stats: stats,
    90  		log:   log,
    91  	}
    92  	if conf.TLS.Enabled {
    93  		var err error
    94  		if a.tlsConf, err = conf.TLS.Get(); err != nil {
    95  			return nil, err
    96  		}
    97  	}
    98  	return &a, nil
    99  }
   100  
   101  //------------------------------------------------------------------------------
   102  
   103  // Connect establishes a connection to an AMQP server.
   104  func (a *AMQP) Connect() (err error) {
   105  	a.m.Lock()
   106  	defer a.m.Unlock()
   107  
   108  	if a.conn != nil {
   109  		return nil
   110  	}
   111  
   112  	var conn *amqp.Connection
   113  	var amqpChan *amqp.Channel
   114  	var consumerChan <-chan amqp.Delivery
   115  
   116  	u, err := url.Parse(a.conf.URL)
   117  	if err != nil {
   118  		return fmt.Errorf("invalid amqp URL: %v", err)
   119  	}
   120  
   121  	if a.conf.TLS.Enabled {
   122  		if u.User != nil {
   123  			conn, err = amqp.DialTLS(a.conf.URL, a.tlsConf)
   124  			if err != nil {
   125  				return fmt.Errorf("AMQP Connect: %s", err)
   126  			}
   127  		} else {
   128  			conn, err = amqp.DialTLS_ExternalAuth(a.conf.URL, a.tlsConf)
   129  			if err != nil {
   130  				return fmt.Errorf("AMQP Connect: %s", err)
   131  			}
   132  		}
   133  	} else {
   134  		conn, err = amqp.Dial(a.conf.URL)
   135  		if err != nil {
   136  			return fmt.Errorf("AMQP Connect: %s", err)
   137  		}
   138  	}
   139  
   140  	amqpChan, err = conn.Channel()
   141  	if err != nil {
   142  		return fmt.Errorf("AMQP Channel: %s", err)
   143  	}
   144  
   145  	if a.conf.QueueDeclare.Enabled {
   146  		if _, err = amqpChan.QueueDeclare(
   147  			a.conf.Queue,                // name of the queue
   148  			a.conf.QueueDeclare.Durable, // durable
   149  			false,                       // delete when unused
   150  			false,                       // exclusive
   151  			false,                       // noWait
   152  			nil,                         // arguments
   153  		); err != nil {
   154  			return fmt.Errorf("queue Declare: %s", err)
   155  		}
   156  	}
   157  
   158  	for _, bConf := range a.conf.BindingsDeclare {
   159  		if err = amqpChan.QueueBind(
   160  			a.conf.Queue,     // name of the queue
   161  			bConf.RoutingKey, // bindingKey
   162  			bConf.Exchange,   // sourceExchange
   163  			false,            // noWait
   164  			nil,              // arguments
   165  		); err != nil {
   166  			return fmt.Errorf("queue Bind: %s", err)
   167  		}
   168  	}
   169  
   170  	if err = amqpChan.Qos(
   171  		a.conf.PrefetchCount, a.conf.PrefetchSize, false,
   172  	); err != nil {
   173  		return fmt.Errorf("qos: %s", err)
   174  	}
   175  
   176  	if consumerChan, err = amqpChan.Consume(
   177  		a.conf.Queue,       // name
   178  		a.conf.ConsumerTag, // consumerTag,
   179  		false,              // noAck
   180  		false,              // exclusive
   181  		false,              // noLocal
   182  		false,              // noWait
   183  		nil,                // arguments
   184  	); err != nil {
   185  		return fmt.Errorf("queue Consume: %s", err)
   186  	}
   187  
   188  	a.conn = conn
   189  	a.amqpChan = amqpChan
   190  	a.consumerChan = consumerChan
   191  
   192  	a.log.Infof("Receiving AMQP messages from queue: %v\n", a.conf.Queue)
   193  	return
   194  }
   195  
   196  // disconnect safely closes a connection to an AMQP server.
   197  func (a *AMQP) disconnect() error {
   198  	a.m.Lock()
   199  	defer a.m.Unlock()
   200  
   201  	if a.amqpChan != nil {
   202  		err := a.amqpChan.Cancel(a.conf.ConsumerTag, true)
   203  		a.amqpChan = nil
   204  		if err != nil {
   205  			return fmt.Errorf("consumer cancel failed: %s", err)
   206  		}
   207  	}
   208  	if a.conn != nil {
   209  		err := a.conn.Close()
   210  		a.conn = nil
   211  		if err != nil {
   212  			return fmt.Errorf("AMQP connection close error: %s", err)
   213  		}
   214  	}
   215  
   216  	return nil
   217  }
   218  
   219  //------------------------------------------------------------------------------
   220  
   221  // Determine the type of the value and set the metadata.
   222  func setMetadata(p types.Part, k string, v interface{}) {
   223  	var metaValue string
   224  	var metaKey = strings.ReplaceAll(k, "-", "_")
   225  
   226  	switch v := v.(type) {
   227  	case bool:
   228  		metaValue = strconv.FormatBool(v)
   229  	case float32:
   230  		metaValue = strconv.FormatFloat(float64(v), 'f', -1, 32)
   231  	case float64:
   232  		metaValue = strconv.FormatFloat(v, 'f', -1, 64)
   233  	case byte:
   234  		metaValue = strconv.Itoa(int(v))
   235  	case int16:
   236  		metaValue = strconv.Itoa(int(v))
   237  	case int32:
   238  		metaValue = strconv.Itoa(int(v))
   239  	case int64:
   240  		metaValue = strconv.Itoa(int(v))
   241  	case nil:
   242  		metaValue = ""
   243  	case string:
   244  		metaValue = v
   245  	case []byte:
   246  		metaValue = string(v)
   247  	case time.Time:
   248  		metaValue = v.Format(time.RFC3339)
   249  	case amqp.Decimal:
   250  		dec := strconv.Itoa(int(v.Value))
   251  		index := len(dec) - int(v.Scale)
   252  		metaValue = dec[:index] + "." + dec[index:]
   253  	case amqp.Table:
   254  		for key, value := range v {
   255  			setMetadata(p, metaKey+"_"+key, value)
   256  		}
   257  		return
   258  	default:
   259  		metaValue = ""
   260  	}
   261  
   262  	if metaValue != "" {
   263  		p.Metadata().Set(metaKey, metaValue)
   264  	}
   265  }
   266  
   267  //------------------------------------------------------------------------------
   268  
   269  // Read a new AMQP message.
   270  func (a *AMQP) Read() (types.Message, error) {
   271  	var c <-chan amqp.Delivery
   272  
   273  	a.m.RLock()
   274  	if a.conn != nil {
   275  		c = a.consumerChan
   276  	}
   277  	a.m.RUnlock()
   278  
   279  	if c == nil {
   280  		return nil, types.ErrNotConnected
   281  	}
   282  
   283  	msg := message.New(nil)
   284  	addPart := func(data amqp.Delivery) {
   285  		// Only store the latest delivery tag, but always Ack multiple.
   286  		a.ackTag = data.DeliveryTag
   287  
   288  		part := message.NewPart(data.Body)
   289  
   290  		for k, v := range data.Headers {
   291  			setMetadata(part, k, v)
   292  		}
   293  
   294  		setMetadata(part, "amqp_content_type", data.ContentType)
   295  		setMetadata(part, "amqp_content_encoding", data.ContentEncoding)
   296  
   297  		if data.DeliveryMode != 0 {
   298  			setMetadata(part, "amqp_delivery_mode", data.DeliveryMode)
   299  		}
   300  
   301  		setMetadata(part, "amqp_priority", data.Priority)
   302  		setMetadata(part, "amqp_correlation_id", data.CorrelationId)
   303  		setMetadata(part, "amqp_reply_to", data.ReplyTo)
   304  		setMetadata(part, "amqp_expiration", data.Expiration)
   305  		setMetadata(part, "amqp_message_id", data.MessageId)
   306  
   307  		if !data.Timestamp.IsZero() {
   308  			setMetadata(part, "amqp_timestamp", data.Timestamp.Unix())
   309  		}
   310  
   311  		setMetadata(part, "amqp_type", data.Type)
   312  		setMetadata(part, "amqp_user_id", data.UserId)
   313  		setMetadata(part, "amqp_app_id", data.AppId)
   314  		setMetadata(part, "amqp_consumer_tag", data.ConsumerTag)
   315  		setMetadata(part, "amqp_delivery_tag", data.DeliveryTag)
   316  		setMetadata(part, "amqp_redelivered", data.Redelivered)
   317  		setMetadata(part, "amqp_exchange", data.Exchange)
   318  		setMetadata(part, "amqp_routing_key", data.RoutingKey)
   319  
   320  		msg.Append(part)
   321  	}
   322  
   323  	data, open := <-c
   324  	if !open {
   325  		a.disconnect()
   326  		return nil, types.ErrNotConnected
   327  	}
   328  	addPart(data)
   329  
   330  batchLoop:
   331  	for i := 1; i < a.conf.MaxBatchCount; i++ {
   332  		select {
   333  		case data, open = <-c:
   334  			if !open {
   335  				return nil, types.ErrTypeClosed
   336  			}
   337  			addPart(data)
   338  		default:
   339  			// Drained the buffer
   340  			break batchLoop
   341  		}
   342  	}
   343  
   344  	if msg.Len() == 0 {
   345  		return nil, types.ErrTimeout
   346  	}
   347  	return msg, nil
   348  }
   349  
   350  // Acknowledge instructs whether unacknowledged messages have been successfully
   351  // propagated.
   352  func (a *AMQP) Acknowledge(err error) error {
   353  	a.m.RLock()
   354  	defer a.m.RUnlock()
   355  	if a.conn == nil {
   356  		return types.ErrNotConnected
   357  	}
   358  	if err != nil {
   359  		return a.amqpChan.Reject(a.ackTag, true)
   360  	}
   361  	return a.amqpChan.Ack(a.ackTag, true)
   362  }
   363  
   364  // CloseAsync shuts down the AMQP input and stops processing requests.
   365  func (a *AMQP) CloseAsync() {
   366  	a.disconnect()
   367  }
   368  
   369  // WaitForClose blocks until the AMQP input has closed down.
   370  func (a *AMQP) WaitForClose(timeout time.Duration) error {
   371  	return nil
   372  }
   373  
   374  //------------------------------------------------------------------------------