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