Skip to content

Map JSON properties as MQTT labels #172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
bin/
.git
systemd/
systemd/
hack
8 changes: 7 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
FROM golang:1.23 as builder
FROM golang:1.24 AS builder

# enable cross-platform builds with CGO_ENABLED
# I had to first compile without buildx for buildx to then work
ENV CGO_ENABLED=1
ENV GOOS=linux
ENV GOARCH=amd64

COPY . /build/mqtt2prometheus
WORKDIR /build/mqtt2prometheus
Expand Down
2 changes: 2 additions & 0 deletions config.yaml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ metrics:
# A map of string to string for constant labels. This labels will be attached to every prometheus metric
const_labels:
sensor_type: dht22
inherit_labels:
- serialNumber
# The name of the metric in prometheus
- prom_name: humidity
# The name of the metric in a MQTT JSON message
Expand Down
7 changes: 4 additions & 3 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,10 @@ type MetricConfig struct {
ForceMonotonicy bool `yaml:"force_monotonicy"`
ConstantLabels map[string]string `yaml:"const_labels"`
DynamicLabels map[string]string `yaml:"dynamic_labels"`
InheritLabels []string `yaml:"inherit_labels"`
StringValueMapping *StringValueMappingConfig `yaml:"string_value_mapping"`
MQTTValueScale float64 `yaml:"mqtt_value_scale"`
// ErrorValue is used while error during value parsing
ErrorValue *float64 `yaml:"error_value"`
ErrorValue *float64 `yaml:"error_value"` // ErrorValue is used while error during value parsing
}

// StringValueMappingConfig defines the mapping from string to float
Expand All @@ -162,6 +162,7 @@ type StringValueMappingConfig struct {

func (mc *MetricConfig) PrometheusDescription() *prometheus.Desc {
labels := append([]string{"sensor", "topic"}, mc.DynamicLabelsKeys()...)
labels = append(labels, mc.InheritLabels...)
return prometheus.NewDesc(
mc.PrometheusName, mc.Help, labels, mc.ConstantLabels,
)
Expand Down Expand Up @@ -257,7 +258,7 @@ func LoadConfig(configFile string, logger *zap.Logger) (Config, error) {
logger.Warn("string_value_mapping.error_value is deprecated: please use error_value at the metric level.", zap.String("prometheusName", m.PrometheusName), zap.String("MQTTName", m.MQTTName))
}

if m.Expression != "" && m.RawExpression != "" {
if m.Expression != "" && m.RawExpression != "" {
return Config{}, fmt.Errorf("metric %s/%s: expression and raw_expression are mutually exclusive.", m.MQTTName, m.PrometheusName)
}
}
Expand Down
10 changes: 9 additions & 1 deletion pkg/metrics/extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func NewJSONObjectExtractor(p Parser) Extractor {
return func(topic string, payload []byte, deviceID string) (MetricCollection, error) {
var mc MetricCollection
parsed := gojsonq.New(gojsonq.SetSeparator(p.separator)).FromString(string(payload))
rawPayload := gojsonq.New(gojsonq.SetSeparator(p.separator)).FromString(string(payload))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than parse the raw data again, can we avoid cloning altogether or at least clone the object without deserializing again?


for path := range p.config() {
rawValue := parsed.Find(path)
Expand All @@ -33,12 +34,19 @@ func NewJSONObjectExtractor(p Parser) Extractor {
}

// Find all valid metric configs
// var labels map[string]string
for _, config := range p.findMetricConfigs(path, deviceID) {
id := metricID(topic, path, deviceID, config.PrometheusName)
m, err := p.parseMetric(config, id, rawValue)
if err != nil {
return nil, fmt.Errorf("failed to parse valid value from '%v' for metric %q: %w", rawValue, config.PrometheusName, err)
return nil, fmt.Errorf("failed to parse valid json value from '%v' for metric %q: %w", rawValue, config.PrometheusName, err)
}
labels, err := p.parseInheritedLabels(config, m, rawPayload)
if err != nil {
return nil, fmt.Errorf("failed to parse valid json labels from '%v' for metric %q: %w", rawPayload, config.PrometheusName, err)
}
m.Labels = labels
m.LabelsKeys = append(m.LabelsKeys, config.InheritLabels...)
m.Topic = topic
mc = append(mc, m)
}
Expand Down
34 changes: 32 additions & 2 deletions pkg/metrics/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
"github.com/hikhvar/mqtt2prometheus/pkg/config"
"github.com/thedevsaddam/gojsonq/v2"
"gopkg.in/yaml.v2"
)

Expand Down Expand Up @@ -178,6 +179,30 @@ func (p *Parser) findMetricConfigs(metric string, deviceID string) []*config.Met
return configs
}

// parseInheritedLabels parses the given JSON data and extracts the listed labels
// to append them to the Prometheus Metric
// this function returns a map of labels
func (p *Parser) parseInheritedLabels(cfg *config.MetricConfig, m Metric, payloadJson *gojsonq.JSONQ) (map[string]string, error) {

// includes already-defined labels that are provided by parseMetric()
labels := m.Labels

// inherit labels
if len(cfg.InheritLabels) > 0 {
var jsonCopy *gojsonq.JSONQ
for _, v := range cfg.InheritLabels {
jsonCopy = payloadJson.Copy()
result, err := jsonCopy.From(v).GetR()
if err != nil {
return labels, fmt.Errorf("failed to parse labels from '%v' for label %q: %w", jsonCopy, v, err)
}
this_label, _ := result.String()
labels[v] = this_label
}
}
return labels, nil
}

// parseMetric parses the given value according to the given deviceID and metricPath. The config allows to
// parse a metric value according to the device ID.
func (p *Parser) parseMetric(cfg *config.MetricConfig, metricID string, value interface{}) (Metric, error) {
Expand Down Expand Up @@ -209,7 +234,7 @@ func (p *Parser) parseMetric(cfg *config.MetricConfig, metricID string, value in
if ok {
metricValue = floatValue

// deprecated, replaced by ErrorValue from the upper level
// deprecated, replaced by ErrorValue from the upper level
} else if cfg.StringValueMapping.ErrorValue != nil {
metricValue = *cfg.StringValueMapping.ErrorValue
} else if cfg.ErrorValue != nil {
Expand Down Expand Up @@ -272,8 +297,13 @@ func (p *Parser) parseMetric(cfg *config.MetricConfig, metricID string, value in
ingestTime = now()
}

// generate dynamic labels
// build labels
var labels map[string]string
if len(cfg.DynamicLabels) > 0 || len(cfg.InheritLabels) > 0 {
labels = make(map[string]string, len(cfg.DynamicLabels)+len(cfg.InheritLabels))
}

// generate dynamic labels
if len(cfg.DynamicLabels) > 0 {
labels = make(map[string]string, len(cfg.DynamicLabels))
for k, v := range cfg.DynamicLabels {
Expand Down