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

     1  package writer
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  	"sync"
     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/mqttconf"
    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  	"github.com/Jeffail/benthos/v3/lib/util/tls"
    18  	mqtt "github.com/eclipse/paho.mqtt.golang"
    19  	gonanoid "github.com/matoous/go-nanoid/v2"
    20  )
    21  
    22  //------------------------------------------------------------------------------
    23  
    24  // MQTTConfig contains configuration fields for the MQTT output type.
    25  type MQTTConfig struct {
    26  	URLs                  []string      `json:"urls" yaml:"urls"`
    27  	QoS                   uint8         `json:"qos" yaml:"qos"`
    28  	Retained              bool          `json:"retained" yaml:"retained"`
    29  	RetainedInterpolated  string        `json:"retained_interpolated" yaml:"retained_interpolated"`
    30  	Topic                 string        `json:"topic" yaml:"topic"`
    31  	ClientID              string        `json:"client_id" yaml:"client_id"`
    32  	DynamicClientIDSuffix string        `json:"dynamic_client_id_suffix" yaml:"dynamic_client_id_suffix"`
    33  	Will                  mqttconf.Will `json:"will" yaml:"will"`
    34  	User                  string        `json:"user" yaml:"user"`
    35  	Password              string        `json:"password" yaml:"password"`
    36  	ConnectTimeout        string        `json:"connect_timeout" yaml:"connect_timeout"`
    37  	WriteTimeout          string        `json:"write_timeout" yaml:"write_timeout"`
    38  	KeepAlive             int64         `json:"keepalive" yaml:"keepalive"`
    39  	MaxInFlight           int           `json:"max_in_flight" yaml:"max_in_flight"`
    40  	TLS                   tls.Config    `json:"tls" yaml:"tls"`
    41  }
    42  
    43  // NewMQTTConfig creates a new MQTTConfig with default values.
    44  func NewMQTTConfig() MQTTConfig {
    45  	return MQTTConfig{
    46  		URLs:           []string{"tcp://localhost:1883"},
    47  		QoS:            1,
    48  		Topic:          "benthos_topic",
    49  		ClientID:       "benthos_output",
    50  		Will:           mqttconf.EmptyWill(),
    51  		User:           "",
    52  		Password:       "",
    53  		ConnectTimeout: "30s",
    54  		WriteTimeout:   "3s",
    55  		MaxInFlight:    1,
    56  		KeepAlive:      30,
    57  		TLS:            tls.NewConfig(),
    58  	}
    59  }
    60  
    61  //------------------------------------------------------------------------------
    62  
    63  // MQTT is an output type that serves MQTT messages.
    64  type MQTT struct {
    65  	log   log.Modular
    66  	stats metrics.Type
    67  
    68  	connectTimeout time.Duration
    69  	writeTimeout   time.Duration
    70  
    71  	urls     []string
    72  	conf     MQTTConfig
    73  	topic    *field.Expression
    74  	retained *field.Expression
    75  
    76  	client  mqtt.Client
    77  	connMut sync.RWMutex
    78  }
    79  
    80  // NewMQTT creates a new MQTT output type.
    81  //
    82  // Deprecated: use the V2 API instead.
    83  func NewMQTT(
    84  	conf MQTTConfig,
    85  	log log.Modular,
    86  	stats metrics.Type,
    87  ) (*MQTT, error) {
    88  	return NewMQTTV2(conf, types.NoopMgr(), log, stats)
    89  }
    90  
    91  // NewMQTTV2 creates a new MQTT output type.
    92  func NewMQTTV2(
    93  	conf MQTTConfig,
    94  	mgr types.Manager,
    95  	log log.Modular,
    96  	stats metrics.Type,
    97  ) (*MQTT, error) {
    98  	m := &MQTT{
    99  		log:   log,
   100  		stats: stats,
   101  		conf:  conf,
   102  	}
   103  
   104  	var err error
   105  	if m.connectTimeout, err = time.ParseDuration(conf.ConnectTimeout); err != nil {
   106  		return nil, fmt.Errorf("unable to parse connect timeout duration string: %w", err)
   107  	}
   108  	if m.writeTimeout, err = time.ParseDuration(conf.WriteTimeout); err != nil {
   109  		return nil, fmt.Errorf("unable to parse write timeout duration string: %w", err)
   110  	}
   111  
   112  	if m.topic, err = interop.NewBloblangField(mgr, conf.Topic); err != nil {
   113  		return nil, fmt.Errorf("failed to parse topic expression: %v", err)
   114  	}
   115  
   116  	if conf.RetainedInterpolated != "" {
   117  		if m.retained, err = interop.NewBloblangField(mgr, conf.RetainedInterpolated); err != nil {
   118  			return nil, fmt.Errorf("failed to parse retained expression: %v", err)
   119  		}
   120  	}
   121  
   122  	switch m.conf.DynamicClientIDSuffix {
   123  	case "nanoid":
   124  		nid, err := gonanoid.New()
   125  		if err != nil {
   126  			return nil, fmt.Errorf("failed to generate nanoid: %w", err)
   127  		}
   128  		m.conf.ClientID += nid
   129  	case "":
   130  	default:
   131  		return nil, fmt.Errorf("unknown dynamic_client_id_suffix: %v", m.conf.DynamicClientIDSuffix)
   132  	}
   133  
   134  	if err := m.conf.Will.Validate(); err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	for _, u := range conf.URLs {
   139  		for _, splitURL := range strings.Split(u, ",") {
   140  			if len(splitURL) > 0 {
   141  				m.urls = append(m.urls, splitURL)
   142  			}
   143  		}
   144  	}
   145  
   146  	return m, nil
   147  }
   148  
   149  //------------------------------------------------------------------------------
   150  
   151  // ConnectWithContext establishes a connection to an MQTT server.
   152  func (m *MQTT) ConnectWithContext(ctx context.Context) error {
   153  	return m.Connect()
   154  }
   155  
   156  // Connect establishes a connection to an MQTT server.
   157  func (m *MQTT) Connect() error {
   158  	m.connMut.Lock()
   159  	defer m.connMut.Unlock()
   160  
   161  	if m.client != nil {
   162  		return nil
   163  	}
   164  
   165  	conf := mqtt.NewClientOptions().
   166  		SetAutoReconnect(false).
   167  		SetConnectionLostHandler(func(client mqtt.Client, reason error) {
   168  			client.Disconnect(0)
   169  			m.log.Errorf("Connection lost due to: %v\n", reason)
   170  		}).
   171  		SetConnectTimeout(m.connectTimeout).
   172  		SetWriteTimeout(m.writeTimeout).
   173  		SetKeepAlive(time.Duration(m.conf.KeepAlive) * time.Second).
   174  		SetClientID(m.conf.ClientID)
   175  
   176  	for _, u := range m.urls {
   177  		conf = conf.AddBroker(u)
   178  	}
   179  
   180  	if m.conf.Will.Enabled {
   181  		conf = conf.SetWill(m.conf.Will.Topic, m.conf.Will.Payload, m.conf.Will.QoS, m.conf.Will.Retained)
   182  	}
   183  
   184  	if m.conf.TLS.Enabled {
   185  		tlsConf, err := m.conf.TLS.Get()
   186  		if err != nil {
   187  			return err
   188  		}
   189  		conf.SetTLSConfig(tlsConf)
   190  	}
   191  
   192  	if m.conf.User != "" {
   193  		conf.SetUsername(m.conf.User)
   194  	}
   195  
   196  	if m.conf.Password != "" {
   197  		conf.SetPassword(m.conf.Password)
   198  	}
   199  
   200  	client := mqtt.NewClient(conf)
   201  
   202  	tok := client.Connect()
   203  	tok.Wait()
   204  	if err := tok.Error(); err != nil {
   205  		return err
   206  	}
   207  
   208  	m.client = client
   209  	return nil
   210  }
   211  
   212  //------------------------------------------------------------------------------
   213  
   214  // WriteWithContext attempts to write a message by pushing it to an MQTT broker.
   215  func (m *MQTT) WriteWithContext(ctx context.Context, msg types.Message) error {
   216  	return m.Write(msg)
   217  }
   218  
   219  // Write attempts to write a message by pushing it to an MQTT broker.
   220  func (m *MQTT) Write(msg types.Message) error {
   221  	m.connMut.RLock()
   222  	client := m.client
   223  	m.connMut.RUnlock()
   224  
   225  	if client == nil {
   226  		return types.ErrNotConnected
   227  	}
   228  
   229  	return IterateBatchedSend(msg, func(i int, p types.Part) error {
   230  		retained := m.conf.Retained
   231  		if m.retained != nil {
   232  			var parseErr error
   233  			retained, parseErr = strconv.ParseBool(m.retained.String(i, msg))
   234  			if parseErr != nil {
   235  				m.log.Errorf("Error parsing boolean value from retained flag: %v \n", parseErr)
   236  			}
   237  		}
   238  		mtok := client.Publish(m.topic.String(i, msg), m.conf.QoS, retained, p.Get())
   239  		mtok.Wait()
   240  		sendErr := mtok.Error()
   241  		if sendErr == mqtt.ErrNotConnected {
   242  			m.connMut.RLock()
   243  			m.client = nil
   244  			m.connMut.RUnlock()
   245  			sendErr = types.ErrNotConnected
   246  		}
   247  		return sendErr
   248  	})
   249  }
   250  
   251  // CloseAsync shuts down the MQTT output and stops processing messages.
   252  func (m *MQTT) CloseAsync() {
   253  	go func() {
   254  		m.connMut.Lock()
   255  		if m.client != nil {
   256  			m.client.Disconnect(0)
   257  			m.client = nil
   258  		}
   259  		m.connMut.Unlock()
   260  	}()
   261  }
   262  
   263  // WaitForClose blocks until the MQTT output has closed down.
   264  func (m *MQTT) WaitForClose(timeout time.Duration) error {
   265  	return nil
   266  }
   267  
   268  //------------------------------------------------------------------------------