mirror of
https://github.com/hikhvar/mqtt2prometheus.git
synced 2026-02-14 09:59:52 +00:00
Initial commit
This commit is contained in:
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
bin/
|
||||
.git
|
||||
systemd/
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
config.yaml
|
||||
bin/
|
||||
vendor
|
||||
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM golang:1.10 as builder
|
||||
|
||||
COPY . /go/src/github.com/hikhvar/mqtt2prometheus
|
||||
WORKDIR /go/src/github.com/hikhvar/mqtt2prometheus
|
||||
RUN make static_build TARGET_FILE=/bin/mqtt2prometheus
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /bin/mqtt2prometheus /mqtt2prometheus
|
||||
CMD ["/mqtt2prometheus"]
|
||||
93
Gopkg.lock
generated
Normal file
93
Gopkg.lock
generated
Normal file
@@ -0,0 +1,93 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/beorn7/perks"
|
||||
packages = ["quantile"]
|
||||
revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/eclipse/paho.mqtt.golang"
|
||||
packages = [
|
||||
".",
|
||||
"packets"
|
||||
]
|
||||
revision = "aff15770515e3c57fc6109da73d42b0d46f7f483"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||
packages = ["pbutil"]
|
||||
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/patrickmn/go-cache"
|
||||
packages = ["."]
|
||||
revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0"
|
||||
version = "v2.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
packages = [
|
||||
"prometheus",
|
||||
"prometheus/promhttp"
|
||||
]
|
||||
revision = "c5b7fccd204277076155f10851dad72b76a49317"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/client_model"
|
||||
packages = ["go"]
|
||||
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/common"
|
||||
packages = [
|
||||
"expfmt",
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"model"
|
||||
]
|
||||
revision = "e4aa40a9169a88835b849a6efb71e05dc04b88f0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/procfs"
|
||||
packages = [
|
||||
".",
|
||||
"internal/util",
|
||||
"nfs",
|
||||
"xfs"
|
||||
]
|
||||
revision = "54d17b57dd7d4a3aa092476596b3f8a933bde349"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"proxy",
|
||||
"websocket"
|
||||
]
|
||||
revision = "24dd3780ca4f75fed9f321890729414a4b5d3f13"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5"
|
||||
version = "v2.1.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "bbe7d36dddf85170c7fa8504fb8d1c3dfd9cbcd1199debd977ec7a5b2cfa7930"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
46
Gopkg.toml
Normal file
46
Gopkg.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/eclipse/paho.mqtt.golang"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/patrickmn/go-cache"
|
||||
version = "2.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/yaml.v2"
|
||||
version = "2.1.1"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
42
Makefile
Normal file
42
Makefile
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
ifndef GOARCH
|
||||
GOARCH:=$(shell go env GOARCH)
|
||||
endif
|
||||
|
||||
ifndef GOOS
|
||||
GOOS:=$(shell go env GOOS)
|
||||
endif
|
||||
|
||||
ifndef TARGET_FILE
|
||||
TARGET_FILE:=bin/mqtt2prometheus.$(GOOS)_$(GOARCH)
|
||||
endif
|
||||
|
||||
install-dep:
|
||||
@which dep || go get -u github.com/golang/dep/cmd/dep
|
||||
|
||||
Gopkg.lock: | install-dep
|
||||
dep ensure --no-vendor
|
||||
|
||||
Gopkg.toml: | install-dep
|
||||
dep init
|
||||
|
||||
prepare-vendor: Gopkg.toml Gopkg.lock
|
||||
dep ensure -update --no-vendor
|
||||
dep status
|
||||
@echo "You can apply these locks via 'make apply-vendor-lock' or rollback via 'git checkout -- Gopkg.lock'"
|
||||
|
||||
vendor: Gopkg.toml Gopkg.lock
|
||||
dep ensure -vendor-only
|
||||
dep status
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
build: vendor
|
||||
GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(TARGET_FILE) ./cmd
|
||||
|
||||
static_build: vendor
|
||||
CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(TARGET_FILE) -a -tags netgo -ldflags '-w -extldflags "-static"' ./cmd
|
||||
|
||||
container:
|
||||
docker build -t mqtt2prometheus:latest .
|
||||
119
Readme.md
Normal file
119
Readme.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# MQTT2Prometheus
|
||||
This exporter is an analog to the [Prometheus Pushgateway](https://github.com/prometheus/pushgateway). Clients can push
|
||||
metrics via MQTT to an MQTT Broker. This exporter subscribes to the broker and
|
||||
publish the received messages as prometheus metrics. I wrote this exporter to publish
|
||||
metrics from small embedded sensors based on the NodeMCU to prometheus.
|
||||
|
||||
## Assumptions about Messages and Topics
|
||||
This exporter makes some assumptions about the message format and MQTT topics. This exporter assumes that each
|
||||
client publish the metrics into a dedicated topic. The last level topic becomes the `sensor` label in prometheus.
|
||||
This exporter assume that the message are JSON objects with only float fields. The golang type for the messages is:
|
||||
|
||||
```go
|
||||
type MQTTPayload map[string]float64
|
||||
```
|
||||
|
||||
For example the message
|
||||
|
||||
```json
|
||||
"temperature":23.20,"humidity":51.60,"heat_index":22.92}
|
||||
```
|
||||
|
||||
published to the MQTT topic `devices/me/livingroom` becomes the following prometheus metrics:
|
||||
|
||||
```text
|
||||
temperature{sensor="livingroom"} 23.2
|
||||
heat_index{sensor="livingroom"} 22.92
|
||||
humidity{sensor="livingroom"} 51.6
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
To build the exporter run:
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
To build a docker container with the mqtt2prometheus exporter included run:
|
||||
|
||||
```bash
|
||||
make container
|
||||
```
|
||||
|
||||
To run the container with a given config file:
|
||||
|
||||
```bash
|
||||
docker run -it -v "$(pwd)/config.yaml:/config.yaml" -p 8002:8002 mqtt2prometheus:latest
|
||||
```
|
||||
|
||||
## Configuration
|
||||
The exporter can be configured via command line and config file.
|
||||
|
||||
### Commandline
|
||||
Available command line flags:
|
||||
|
||||
```text
|
||||
Usage of ./mqtt2prometheus.linux_amd64:
|
||||
-config string
|
||||
config file (default "config.yaml")
|
||||
-listen-address string
|
||||
listen address for HTTP server used to expose metrics
|
||||
-listen-port string
|
||||
HTTP port used to expose metrics (default "8002")
|
||||
|
||||
```
|
||||
|
||||
### Config file
|
||||
The config file can look like this:
|
||||
|
||||
```yaml
|
||||
# Settings for the MQTT Client. Currently only these three are supported
|
||||
mqtt:
|
||||
# The MQTT broker to connect to
|
||||
server: tcp://127.0.0.1:1883
|
||||
# The Topic path to subscripe to. Actually this will become `$topic_path/+`
|
||||
topic_path: v1/devices/me
|
||||
# The MQTT QoS level
|
||||
qos: 0
|
||||
cache:
|
||||
# Timeout. Each received metric will be presented for this time if no update is send via MQTT
|
||||
timeout: 2min
|
||||
# This is a list of valid metrics. Only metrics listed here will be exported
|
||||
metrics:
|
||||
# The name of the metric in prometheus
|
||||
- prom_name: temperature
|
||||
# The name of the metric in a MQTT JSON message
|
||||
mqtt_name: temperature
|
||||
# The prometheus help text for this metric
|
||||
help: DHT22 temperature reading
|
||||
# The prometheus type for this metric. Valid values are: "gauge" and "counter"
|
||||
type: gauge
|
||||
# A map of string to string for constant labels. This labels will be attached to every prometheus metric
|
||||
const_labels:
|
||||
sensor_type: dht22
|
||||
# The name of the metric in prometheus
|
||||
- prom_name: humidity
|
||||
# The name of the metric in a MQTT JSON message
|
||||
mqtt_name: humidity
|
||||
# The prometheus help text for this metric
|
||||
help: DHT22 humidity reading
|
||||
# The prometheus type for this metric. Valid values are: "gauge" and "counter"
|
||||
type: gauge
|
||||
# A map of string to string for constant labels. This labels will be attached to every prometheus metric
|
||||
const_labels:
|
||||
sensor_type: dht22
|
||||
# The name of the metric in prometheus
|
||||
- prom_name: heat_index
|
||||
# The name of the metric in a MQTT JSON message
|
||||
mqtt_name: heat_index
|
||||
# The prometheus help text for this metric
|
||||
help: DHT22 heatIndex calculation
|
||||
# The prometheus type for this metric. Valid values are: "gauge" and "counter"
|
||||
type: gauge
|
||||
# A map of string to string for constant labels. This labels will be attached to every prometheus metric
|
||||
const_labels:
|
||||
sensor_type: dht22%
|
||||
```
|
||||
89
cmd/mqtt2prometheus.go
Normal file
89
cmd/mqtt2prometheus.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"os"
|
||||
|
||||
"time"
|
||||
|
||||
"flag"
|
||||
|
||||
"github.com/eclipse/paho.mqtt.golang"
|
||||
"github.com/hikhvar/mqtt2prometheus/pkg/metrics"
|
||||
"github.com/hikhvar/mqtt2prometheus/pkg/mqttclient"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/hikhvar/mqtt2prometheus/pkg/config"
|
||||
)
|
||||
|
||||
var (
|
||||
configFlag = flag.String(
|
||||
"config",
|
||||
"config.yaml",
|
||||
"config file",
|
||||
)
|
||||
portFlag = flag.String(
|
||||
"listen-port",
|
||||
"8002",
|
||||
"HTTP port used to expose metrics",
|
||||
)
|
||||
addressFlag = flag.String(
|
||||
"listen-address",
|
||||
"0.0.0.0",
|
||||
"listen address for HTTP server used to expose metrics",
|
||||
)
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
c := make(chan os.Signal, 1)
|
||||
hostName, err := os.Hostname()
|
||||
if err != nil {
|
||||
log.Fatalf("Could not get hostname. %s\n", err.Error())
|
||||
}
|
||||
cfg, err := config.LoadConfig(*configFlag)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not load config: %s\n", err.Error())
|
||||
}
|
||||
mqttClientOptions := mqtt.NewClientOptions()
|
||||
mqttClientOptions.AddBroker(cfg.MQTT.Server).SetClientID(hostName).SetCleanSession(true)
|
||||
collector := metrics.NewCollector(2*time.Minute, cfg.Metrics)
|
||||
ingest := metrics.NewIngest(collector, cfg.Metrics)
|
||||
|
||||
var errorChan chan error
|
||||
err = mqttclient.Subscribe(mqttClientOptions, mqttclient.SubscribeOptions{
|
||||
Topic: cfg.MQTT.TopicPath + "/+",
|
||||
QoS: cfg.MQTT.QoS,
|
||||
OnMessageReceived: ingest.SetupSubscriptionHandler(errorChan),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to mqtt broker %s", err.Error())
|
||||
}
|
||||
prometheus.MustRegister(ingest.MessageMetric)
|
||||
prometheus.MustRegister(collector)
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
go func() {
|
||||
err = http.ListenAndServe(getListenAddress(), nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Error while serving http: %s", err.Error())
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-c:
|
||||
log.Println("Terminated via Signal. Stop.")
|
||||
os.Exit(0)
|
||||
case err = <-errorChan:
|
||||
log.Printf("Error while processing message. %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getListenAddress() string {
|
||||
return fmt.Sprintf("%s:%s", *addressFlag, *portFlag)
|
||||
}
|
||||
46
config.yaml.dist
Normal file
46
config.yaml.dist
Normal file
@@ -0,0 +1,46 @@
|
||||
# Settings for the MQTT Client. Currently only these three are supported
|
||||
mqtt:
|
||||
# The MQTT broker to connect to
|
||||
server: tcp://127.0.0.1:1883
|
||||
# The Topic path to subscripe to. Actually this will become `$topic_path/+`
|
||||
topic_path: v1/devices/me
|
||||
# The MQTT QoS level
|
||||
qos: 0
|
||||
cache:
|
||||
# Timeout. Each received metric will be presented for this time if no update is send via MQTT
|
||||
timeout: 2min
|
||||
# This is a list of valid metrics. Only metrics listed here will be exported
|
||||
metrics:
|
||||
# The name of the metric in prometheus
|
||||
- prom_name: temperature
|
||||
# The name of the metric in a MQTT JSON message
|
||||
mqtt_name: temperature
|
||||
# The prometheus help text for this metric
|
||||
help: DHT22 temperature reading
|
||||
# The prometheus type for this metric. Valid values are: "gauge" and "counter"
|
||||
type: gauge
|
||||
# A map of string to string for constant labels. This labels will be attached to every prometheus metric
|
||||
const_labels:
|
||||
sensor_type: dht22
|
||||
# The name of the metric in prometheus
|
||||
- prom_name: humidity
|
||||
# The name of the metric in a MQTT JSON message
|
||||
mqtt_name: humidity
|
||||
# The prometheus help text for this metric
|
||||
help: DHT22 humidity reading
|
||||
# The prometheus type for this metric. Valid values are: "gauge" and "counter"
|
||||
type: gauge
|
||||
# A map of string to string for constant labels. This labels will be attached to every prometheus metric
|
||||
const_labels:
|
||||
sensor_type: dht22
|
||||
# The name of the metric in prometheus
|
||||
- prom_name: heat_index
|
||||
# The name of the metric in a MQTT JSON message
|
||||
mqtt_name: heat_index
|
||||
# The prometheus help text for this metric
|
||||
help: DHT22 heatIndex calculation
|
||||
# The prometheus type for this metric. Valid values are: "gauge" and "counter"
|
||||
type: gauge
|
||||
# A map of string to string for constant labels. This labels will be attached to every prometheus metric
|
||||
const_labels:
|
||||
sensor_type: dht22
|
||||
83
pkg/config/config.go
Normal file
83
pkg/config/config.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
const GaugeValueType = "gauge"
|
||||
const CounterValueType = "counter"
|
||||
|
||||
var MQTTConfigDefaults = MQTTConfig{
|
||||
Server: "tcp://127.0.0.1:1883",
|
||||
TopicPath: "v1/devices/me",
|
||||
QoS: 0,
|
||||
}
|
||||
|
||||
var CacheConfigDefaults = CacheConfig{
|
||||
Timeout: 2 + time.Minute,
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Metrics []MetricConfig `yaml:"metrics"`
|
||||
MQTT *MQTTConfig `yaml:"mqtt,omitempty"`
|
||||
Cache *CacheConfig `yaml:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
type CacheConfig struct {
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
}
|
||||
|
||||
type MQTTConfig struct {
|
||||
Server string `yaml:"server"`
|
||||
TopicPath string `yaml:"topic_path"`
|
||||
QoS byte `yaml:"qos"`
|
||||
}
|
||||
|
||||
// Metrics Config is a mapping between a metric send on mqtt to a prometheus metric
|
||||
type MetricConfig struct {
|
||||
PrometheusName string `yaml:"prom_name"`
|
||||
MQTTName string `yaml:"mqtt_name"`
|
||||
Help string `yaml:"help"`
|
||||
ValueType string `yaml:"type"`
|
||||
ConstantLabels map[string]string `yaml:"const_labels"`
|
||||
}
|
||||
|
||||
func (mc *MetricConfig) PrometheusDescription() *prometheus.Desc {
|
||||
return prometheus.NewDesc(
|
||||
mc.PrometheusName, mc.Help, []string{"sensor"}, mc.ConstantLabels,
|
||||
)
|
||||
}
|
||||
|
||||
func (mc *MetricConfig) PrometheusValueType() prometheus.ValueType {
|
||||
switch mc.ValueType {
|
||||
case GaugeValueType:
|
||||
return prometheus.GaugeValue
|
||||
case CounterValueType:
|
||||
return prometheus.CounterValue
|
||||
default:
|
||||
return prometheus.UntypedValue
|
||||
}
|
||||
}
|
||||
|
||||
func LoadConfig(configFile string) (Config, error) {
|
||||
configData, err := ioutil.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
var cfg Config
|
||||
if err = yaml.Unmarshal(configData, &cfg); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if cfg.MQTT == nil {
|
||||
cfg.MQTT = &MQTTConfigDefaults
|
||||
}
|
||||
if cfg.Cache == nil {
|
||||
cfg.Cache = &CacheConfigDefaults
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
68
pkg/metrics/collector.go
Normal file
68
pkg/metrics/collector.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hikhvar/mqtt2prometheus/pkg/config"
|
||||
gocache "github.com/patrickmn/go-cache"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const DefaultTimeout = 0
|
||||
|
||||
type Collector interface {
|
||||
prometheus.Collector
|
||||
Observe(deviceID string, collection MetricCollection)
|
||||
}
|
||||
|
||||
type MemoryCachedCollector struct {
|
||||
cache *gocache.Cache
|
||||
descriptions []*prometheus.Desc
|
||||
}
|
||||
|
||||
type Metric struct {
|
||||
Description *prometheus.Desc
|
||||
Value float64
|
||||
ValueType prometheus.ValueType
|
||||
}
|
||||
|
||||
type MetricCollection []Metric
|
||||
|
||||
func NewCollector(defaultTimeout time.Duration, possibleMetrics []config.MetricConfig) Collector {
|
||||
var descs []*prometheus.Desc
|
||||
for _, m := range possibleMetrics {
|
||||
descs = append(descs, m.PrometheusDescription())
|
||||
}
|
||||
return &MemoryCachedCollector{
|
||||
cache: gocache.New(defaultTimeout, defaultTimeout*10),
|
||||
descriptions: descs,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MemoryCachedCollector) Observe(deviceID string, collection MetricCollection) {
|
||||
c.cache.Set(deviceID, collection, DefaultTimeout)
|
||||
}
|
||||
|
||||
func (c *MemoryCachedCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
for i := range c.descriptions {
|
||||
ch <- c.descriptions[i]
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MemoryCachedCollector) Collect(mc chan<- prometheus.Metric) {
|
||||
for device, metricsRaw := range c.cache.Items() {
|
||||
metrics := metricsRaw.Object.(MetricCollection)
|
||||
for _, metric := range metrics {
|
||||
m, err := prometheus.NewConstMetric(
|
||||
metric.Description,
|
||||
metric.ValueType,
|
||||
metric.Value,
|
||||
device,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
mc <- m
|
||||
}
|
||||
}
|
||||
}
|
||||
80
pkg/metrics/ingest.go
Normal file
80
pkg/metrics/ingest.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"path/filepath"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"fmt"
|
||||
|
||||
"log"
|
||||
|
||||
"github.com/eclipse/paho.mqtt.golang"
|
||||
"github.com/hikhvar/mqtt2prometheus/pkg/config"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var NoValidPayload = errors.New("no valid MQTT payload")
|
||||
|
||||
type Ingest struct {
|
||||
validMetrics map[string]config.MetricConfig
|
||||
collector Collector
|
||||
MessageMetric *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func NewIngest(collector Collector, metrics []config.MetricConfig) *Ingest {
|
||||
valid := make(map[string]config.MetricConfig)
|
||||
for i := range metrics {
|
||||
key := metrics[i].MQTTName
|
||||
valid[key] = metrics[i]
|
||||
}
|
||||
return &Ingest{
|
||||
validMetrics: valid,
|
||||
collector: collector,
|
||||
MessageMetric: prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "received_messages",
|
||||
Help: "received messages per topic and status",
|
||||
}, []string{"status", "topic"},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type MQTTPayload map[string]float64
|
||||
|
||||
func (i *Ingest) store(deviceID string, rawMetrics MQTTPayload) error {
|
||||
var mc MetricCollection
|
||||
for metricName, value := range rawMetrics {
|
||||
if cfg, found := i.validMetrics[metricName]; found {
|
||||
mc = append(mc, Metric{
|
||||
Description: cfg.PrometheusDescription(),
|
||||
Value: value,
|
||||
ValueType: cfg.PrometheusValueType(),
|
||||
})
|
||||
}
|
||||
}
|
||||
i.collector.Observe(deviceID, mc)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Ingest) SetupSubscriptionHandler(errChan chan<- error) mqtt.MessageHandler {
|
||||
return func(c mqtt.Client, m mqtt.Message) {
|
||||
log.Printf("Got message '%s' on topic %s\n", string(m.Payload()), m.Topic())
|
||||
deviceId := filepath.Base(m.Topic())
|
||||
var rawMetrics MQTTPayload
|
||||
err := json.Unmarshal(m.Payload(), &rawMetrics)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("could not decode message '%s' on topic %s: %s", string(m.Payload()), m.Topic(), err.Error())
|
||||
i.MessageMetric.WithLabelValues("decodeError", m.Topic()).Desc()
|
||||
}
|
||||
err = i.store(deviceId, rawMetrics)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("could not store metrics '%s' on topic %s: %s", string(m.Payload()), m.Topic(), err.Error())
|
||||
i.MessageMetric.WithLabelValues("storeError", m.Topic()).Inc()
|
||||
}
|
||||
i.MessageMetric.WithLabelValues("success", m.Topic()).Inc()
|
||||
}
|
||||
|
||||
}
|
||||
29
pkg/mqttclient/mqttClient.go
Normal file
29
pkg/mqttclient/mqttClient.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package mqttclient
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
type SubscribeOptions struct {
|
||||
Topic string
|
||||
QoS byte
|
||||
OnMessageReceived mqtt.MessageHandler
|
||||
}
|
||||
|
||||
func Subscribe(connectionOptions *mqtt.ClientOptions, subscribeOptions SubscribeOptions) error {
|
||||
connectionOptions.OnConnect = func(client mqtt.Client) {
|
||||
log.Print("Connected to MQTT Broker.\n")
|
||||
log.Printf("Will subscribe to topic %s", subscribeOptions.Topic)
|
||||
if token := client.Subscribe(subscribeOptions.Topic, subscribeOptions.QoS, subscribeOptions.OnMessageReceived); token.Wait() && token.Error() != nil {
|
||||
log.Printf("Could not subscribe %s\n", token.Error().Error())
|
||||
}
|
||||
}
|
||||
client := mqtt.NewClient(connectionOptions)
|
||||
if token := client.Connect(); token.Wait() && token.Error() != nil {
|
||||
return token.Error()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
9
systemd/mqtt2prometheus.service
Normal file
9
systemd/mqtt2prometheus.service
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Simple translator from mqtt messages to prometheus. Analog to pushgateway
|
||||
Before=prometheus.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/opt/mqtt2prometheus/mqtt2prometheus -config /etc/mqtt2prometheus/config.yaml -port 8002
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user