feat(outputs.mqtt): add support for MQTT 5 publish properties (#12678)

Co-authored-by: Jose Luis Ordiales <jose.luis@evergen.energy>
Co-authored-by: Joshua Powers <powersj@fastmail.com>
This commit is contained in:
Jose Luis Ordiales Coscia 2023-02-16 06:55:24 -03:00 committed by GitHub
parent 9c79277405
commit 8896538f5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 131 additions and 22 deletions

View File

@ -105,4 +105,19 @@ to use them.
## more about them here: ## more about them here:
## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_OUTPUT.md ## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_OUTPUT.md
data_format = "influx" data_format = "influx"
## Optional MQTT 5 publish properties
## These setting only apply if the "protocol" property is set to 5. This must
## be defined at the end of the plugin settings, otherwise TOML will assume
## anything else is part of this table. For more details on publish properties
## see the spec:
## https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901109
# [outputs.mqtt.v5]
# content_type = ""
# response_topic = ""
# message_expiry = "0s"
# topic_alias = 0
# [outputs.mqtt.v5.user_properties]
# "key1" = "value 1"
# "key2" = "value 2"
``` ```

View File

@ -35,10 +35,11 @@ type MQTT struct {
QoS int `toml:"qos"` QoS int `toml:"qos"`
ClientID string `toml:"client_id"` ClientID string `toml:"client_id"`
tls.ClientConfig tls.ClientConfig
BatchMessage bool `toml:"batch"` BatchMessage bool `toml:"batch"`
Retain bool `toml:"retain"` Retain bool `toml:"retain"`
KeepAlive int64 `toml:"keep_alive"` KeepAlive int64 `toml:"keep_alive"`
Log telegraf.Logger `toml:"-"` V5PublishProperties *mqttv5PublishProperties `toml:"v5"`
Log telegraf.Logger `toml:"-"`
client Client client Client
serializer serializers.Serializer serializer serializers.Serializer

View File

@ -2,7 +2,6 @@ package mqtt
import ( import (
"fmt" "fmt"
"github.com/influxdata/telegraf/metric"
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
@ -10,6 +9,8 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go/wait" "github.com/testcontainers/testcontainers-go/wait"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/metric"
"github.com/influxdata/telegraf/plugins/serializers" "github.com/influxdata/telegraf/plugins/serializers"
"github.com/influxdata/telegraf/testutil" "github.com/influxdata/telegraf/testutil"
) )
@ -98,20 +99,55 @@ func TestConnectAndWriteIntegrationMQTTv5(t *testing.T) {
var url = fmt.Sprintf("%s:%s", container.Address, container.Ports[servicePort]) var url = fmt.Sprintf("%s:%s", container.Address, container.Ports[servicePort])
s := serializers.NewInfluxSerializer() s := serializers.NewInfluxSerializer()
m := &MQTT{
Servers: []string{url}, tests := []struct {
Protocol: "5", name string
serializer: s, properties *mqttv5PublishProperties
KeepAlive: 30, }{
Log: testutil.Logger{Name: "mqttv5-integration-test"}, {
name: "no publish properties",
properties: nil,
},
{
name: "content type set",
properties: &mqttv5PublishProperties{ContentType: "text/plain"},
},
{
name: "response topic set",
properties: &mqttv5PublishProperties{ResponseTopic: "test/topic"},
},
{
name: "message expiry set",
properties: &mqttv5PublishProperties{MessageExpiry: config.Duration(10 * time.Minute)},
},
{
name: "topic alias set",
properties: &mqttv5PublishProperties{TopicAlias: new(uint16)},
},
{
name: "user properties set",
properties: &mqttv5PublishProperties{UserProperties: map[string]string{"key": "value"}},
},
} }
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &MQTT{
Servers: []string{url},
Protocol: "5",
serializer: s,
KeepAlive: 30,
Log: testutil.Logger{Name: "mqttv5-integration-test"},
V5PublishProperties: tt.properties,
}
// Verify that we can connect to the MQTT broker // Verify that we can connect to the MQTT broker
require.NoError(t, m.Init()) require.NoError(t, m.Init())
require.NoError(t, m.Connect()) require.NoError(t, m.Connect())
// Verify that we can successfully write data to the mqtt broker // Verify that we can successfully write data to the mqtt broker
require.NoError(t, m.Write(testutil.MockMetrics())) require.NoError(t, m.Write(testutil.MockMetrics()))
})
}
} }
func TestMQTTTopicGenerationTemplateIsValid(t *testing.T) { func TestMQTTTopicGenerationTemplateIsValid(t *testing.T) {

View File

@ -13,13 +13,54 @@ import (
"github.com/influxdata/telegraf/internal" "github.com/influxdata/telegraf/internal"
) )
// mqtt v5-specific publish properties.
// See https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901109
type mqttv5PublishProperties struct {
ContentType string `toml:"content_type"`
ResponseTopic string `toml:"response_topic"`
MessageExpiry config.Duration `toml:"message_expiry"`
TopicAlias *uint16 `toml:"topic_alias"`
UserProperties map[string]string `toml:"user_properties"`
}
type mqttv5Client struct { type mqttv5Client struct {
*MQTT *MQTT
client *mqttv5auto.ConnectionManager client *mqttv5auto.ConnectionManager
publishProperties *mqttv5.PublishProperties
} }
func newMQTTv5Client(cfg *MQTT) *mqttv5Client { func newMQTTv5Client(cfg *MQTT) *mqttv5Client {
return &mqttv5Client{MQTT: cfg} return &mqttv5Client{
MQTT: cfg,
publishProperties: buildPublishProperties(cfg),
}
}
// Build the v5 specific publish properties if they are present in the
// config.
// These should not change during the lifecycle of the client.
func buildPublishProperties(cfg *MQTT) *mqttv5.PublishProperties {
if cfg.V5PublishProperties == nil {
return nil
}
publishProperties := &mqttv5.PublishProperties{
ContentType: cfg.V5PublishProperties.ContentType,
ResponseTopic: cfg.V5PublishProperties.ResponseTopic,
TopicAlias: cfg.V5PublishProperties.TopicAlias,
User: make([]mqttv5.UserProperty, 0, len(cfg.V5PublishProperties.UserProperties)),
}
messageExpiry := time.Duration(cfg.V5PublishProperties.MessageExpiry)
if expirySeconds := uint32(messageExpiry.Seconds()); expirySeconds > 0 {
publishProperties.MessageExpiry = &expirySeconds
}
for k, v := range cfg.V5PublishProperties.UserProperties {
publishProperties.User.Add(k, v)
}
return publishProperties
} }
func (m *mqttv5Client) Connect() error { func (m *mqttv5Client) Connect() error {
@ -92,10 +133,11 @@ func (m *mqttv5Client) Publish(topic string, body []byte) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(m.Timeout)) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(m.Timeout))
defer cancel() defer cancel()
_, err := m.client.Publish(ctx, &mqttv5.Publish{ _, err := m.client.Publish(ctx, &mqttv5.Publish{
Topic: topic, Topic: topic,
QoS: byte(m.QoS), QoS: byte(m.QoS),
Retain: m.Retain, Retain: m.Retain,
Payload: body, Payload: body,
Properties: m.publishProperties,
}) })
if err != nil { if err != nil {
return err return err

View File

@ -70,3 +70,18 @@
## more about them here: ## more about them here:
## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_OUTPUT.md ## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_OUTPUT.md
data_format = "influx" data_format = "influx"
## Optional MQTT 5 publish properties
## These setting only apply if the "protocol" property is set to 5. This must
## be defined at the end of the plugin settings, otherwise TOML will assume
## anything else is part of this table. For more details on publish properties
## see the spec:
## https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901109
# [outputs.mqtt.v5]
# content_type = ""
# response_topic = ""
# message_expiry = "0s"
# topic_alias = 0
# [outputs.mqtt.v5.user_properties]
# "key1" = "value 1"
# "key2" = "value 2"