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