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

     1  package writer
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"fmt"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/Azure/go-amqp"
    11  	"github.com/Jeffail/benthos/v3/internal/metadata"
    12  	"github.com/Jeffail/benthos/v3/lib/log"
    13  	"github.com/Jeffail/benthos/v3/lib/metrics"
    14  	"github.com/Jeffail/benthos/v3/lib/types"
    15  	"github.com/Jeffail/benthos/v3/lib/util/amqp/sasl"
    16  	btls "github.com/Jeffail/benthos/v3/lib/util/tls"
    17  )
    18  
    19  //------------------------------------------------------------------------------
    20  
    21  // AMQP1Config contains configuration fields for the AMQP1 output type.
    22  type AMQP1Config struct {
    23  	URL           string                       `json:"url" yaml:"url"`
    24  	TargetAddress string                       `json:"target_address" yaml:"target_address"`
    25  	MaxInFlight   int                          `json:"max_in_flight" yaml:"max_in_flight"`
    26  	TLS           btls.Config                  `json:"tls" yaml:"tls"`
    27  	SASL          sasl.Config                  `json:"sasl" yaml:"sasl"`
    28  	Metadata      metadata.ExcludeFilterConfig `json:"metadata" yaml:"metadata"`
    29  }
    30  
    31  // NewAMQP1Config creates a new AMQP1Config with default values.
    32  func NewAMQP1Config() AMQP1Config {
    33  	return AMQP1Config{
    34  		URL:           "",
    35  		TargetAddress: "",
    36  		MaxInFlight:   1,
    37  		TLS:           btls.NewConfig(),
    38  		SASL:          sasl.NewConfig(),
    39  		Metadata:      metadata.NewExcludeFilterConfig(),
    40  	}
    41  }
    42  
    43  //------------------------------------------------------------------------------
    44  
    45  // AMQP1 is an output type that serves AMQP1 messages.
    46  type AMQP1 struct {
    47  	client  *amqp.Client
    48  	session *amqp.Session
    49  	sender  *amqp.Sender
    50  
    51  	metaFilter *metadata.ExcludeFilter
    52  
    53  	log   log.Modular
    54  	stats metrics.Type
    55  
    56  	conf    AMQP1Config
    57  	tlsConf *tls.Config
    58  
    59  	connLock sync.RWMutex
    60  }
    61  
    62  // NewAMQP1 creates a new AMQP1 writer type.
    63  func NewAMQP1(conf AMQP1Config, log log.Modular, stats metrics.Type) (*AMQP1, error) {
    64  	a := AMQP1{
    65  		log:   log,
    66  		stats: stats,
    67  		conf:  conf,
    68  	}
    69  	var err error
    70  	if conf.TLS.Enabled {
    71  		if a.tlsConf, err = conf.TLS.Get(); err != nil {
    72  			return nil, err
    73  		}
    74  	}
    75  	if a.metaFilter, err = conf.Metadata.Filter(); err != nil {
    76  		return nil, fmt.Errorf("failed to construct metadata filter: %w", err)
    77  	}
    78  	return &a, nil
    79  }
    80  
    81  //------------------------------------------------------------------------------
    82  
    83  // Connect establishes a connection to an AMQP1 server.
    84  func (a *AMQP1) Connect() error {
    85  	return a.ConnectWithContext(context.Background())
    86  }
    87  
    88  // ConnectWithContext establishes a connection to an AMQP1 server.
    89  func (a *AMQP1) ConnectWithContext(ctx context.Context) error {
    90  	a.connLock.Lock()
    91  	defer a.connLock.Unlock()
    92  
    93  	if a.client != nil {
    94  		return nil
    95  	}
    96  
    97  	var (
    98  		client  *amqp.Client
    99  		session *amqp.Session
   100  		sender  *amqp.Sender
   101  		err     error
   102  	)
   103  
   104  	opts, err := a.conf.SASL.ToOptFns()
   105  	if err != nil {
   106  		return err
   107  	}
   108  	if a.conf.TLS.Enabled {
   109  		opts = append(opts, amqp.ConnTLS(true), amqp.ConnTLSConfig(a.tlsConf))
   110  	}
   111  
   112  	// Create client
   113  	if client, err = amqp.Dial(a.conf.URL, opts...); err != nil {
   114  		return err
   115  	}
   116  
   117  	// Open a session
   118  	if session, err = client.NewSession(); err != nil {
   119  		client.Close()
   120  		return err
   121  	}
   122  
   123  	// Create a sender
   124  	if sender, err = session.NewSender(
   125  		amqp.LinkTargetAddress(a.conf.TargetAddress),
   126  	); err != nil {
   127  		session.Close(context.Background())
   128  		client.Close()
   129  		return err
   130  	}
   131  
   132  	a.client = client
   133  	a.session = session
   134  	a.sender = sender
   135  
   136  	a.log.Infof("Sending AMQP 1.0 messages to target: %v\n", a.conf.TargetAddress)
   137  	return nil
   138  }
   139  
   140  // disconnect safely closes a connection to an AMQP1 server.
   141  func (a *AMQP1) disconnect(ctx context.Context) error {
   142  	a.connLock.Lock()
   143  	defer a.connLock.Unlock()
   144  
   145  	if a.client == nil {
   146  		return nil
   147  	}
   148  
   149  	if err := a.sender.Close(ctx); err != nil {
   150  		a.log.Errorf("Failed to cleanly close sender: %v\n", err)
   151  	}
   152  	if err := a.session.Close(ctx); err != nil {
   153  		a.log.Errorf("Failed to cleanly close session: %v\n", err)
   154  	}
   155  	if err := a.client.Close(); err != nil {
   156  		a.log.Errorf("Failed to cleanly close client: %v\n", err)
   157  	}
   158  	a.client = nil
   159  	a.session = nil
   160  	a.sender = nil
   161  
   162  	return nil
   163  }
   164  
   165  //------------------------------------------------------------------------------
   166  
   167  // Write will attempt to write a message over AMQP1, wait for acknowledgement,
   168  // and returns an error if applicable.
   169  func (a *AMQP1) Write(msg types.Message) error {
   170  	return a.WriteWithContext(context.Background(), msg)
   171  }
   172  
   173  // WriteWithContext will attempt to write a message over AMQP1, wait for
   174  // acknowledgement, and returns an error if applicable.
   175  func (a *AMQP1) WriteWithContext(ctx context.Context, msg types.Message) error {
   176  	var s *amqp.Sender
   177  	a.connLock.RLock()
   178  	if a.sender != nil {
   179  		s = a.sender
   180  	}
   181  	a.connLock.RUnlock()
   182  
   183  	if s == nil {
   184  		return types.ErrNotConnected
   185  	}
   186  
   187  	return IterateBatchedSend(msg, func(i int, p types.Part) error {
   188  		m := amqp.NewMessage(p.Get())
   189  		a.metaFilter.Iter(p.Metadata(), func(k, v string) error {
   190  			if m.Annotations == nil {
   191  				m.Annotations = amqp.Annotations{}
   192  			}
   193  			m.Annotations[k] = v
   194  			return nil
   195  		})
   196  		err := s.Send(ctx, m)
   197  		if err != nil {
   198  			if err == amqp.ErrTimeout {
   199  				err = types.ErrTimeout
   200  			} else {
   201  				if dErr, isDetachError := err.(*amqp.DetachError); isDetachError && dErr.RemoteError != nil {
   202  					a.log.Errorf("Lost connection due to: %v\n", dErr.RemoteError)
   203  				} else {
   204  					a.log.Errorf("Lost connection due to: %v\n", err)
   205  				}
   206  				a.disconnect(ctx)
   207  				err = types.ErrNotConnected
   208  			}
   209  		}
   210  		return err
   211  	})
   212  }
   213  
   214  // CloseAsync shuts down the AMQP1 output and stops processing messages.
   215  func (a *AMQP1) CloseAsync() {
   216  	a.disconnect(context.Background())
   217  }
   218  
   219  // WaitForClose blocks until the AMQP1 output has closed down.
   220  func (a *AMQP1) WaitForClose(timeout time.Duration) error {
   221  	return nil
   222  }
   223  
   224  //------------------------------------------------------------------------------