pax_global_header00006660000000000000000000000064137545215370014526gustar00rootroot0000000000000052 comment=a5e6c59d28a9acf503206d6b7ea859a2b3359feb prometheus-mqtt-exporter-0.1.4/000077500000000000000000000000001375452153700165545ustar00rootroot00000000000000prometheus-mqtt-exporter-0.1.4/.dockerignore000066400000000000000000000000221375452153700212220ustar00rootroot00000000000000bin/ .git systemd/prometheus-mqtt-exporter-0.1.4/.github/000077500000000000000000000000001375452153700201145ustar00rootroot00000000000000prometheus-mqtt-exporter-0.1.4/.github/workflows/000077500000000000000000000000001375452153700221515ustar00rootroot00000000000000prometheus-mqtt-exporter-0.1.4/.github/workflows/dockerimage.yml000066400000000000000000000003601375452153700251450ustar00rootroot00000000000000name: docker on: pull_request: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Build the Docker image run: docker build . --file Dockerfile --tag mqtt2prometheus:${GITHUB_REF##*/} prometheus-mqtt-exporter-0.1.4/.github/workflows/linting.yml000066400000000000000000000004071375452153700243410ustar00rootroot00000000000000on: push: branches: ["master"] pull_request: name: linting jobs: linting: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Run golangci-lint uses: actions-contrib/golangci-lint@v1 prometheus-mqtt-exporter-0.1.4/.github/workflows/release.yml000066400000000000000000000022261375452153700243160ustar00rootroot00000000000000name: release on: push: tags: - '*' jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Unshallow run: git fetch --prune --unshallow - name: Set up Go uses: actions/setup-go@v1 with: go-version: 1.15.x - name: Test run: go test ./... - name: Vet run: go vet ./... # Login to the registries - name: Login to Github Packages run: echo ${{ secrets.GITHUB_TOKEN }} | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin - name: Login to Dockerhub run: echo ${{ secrets.DOCKERHUB_ACCESS_KEY }} | docker login -u hikhvar --password-stdin # Github Registry does not support the github actions token - name: Login to Github Registry run: echo ${{secrets.PERSONAL_ACCESS_TOKEN }} | docker login ghcr.io -u hikhvar --password-stdin - name: Run GoReleaser uses: goreleaser/goreleaser-action@v1 with: version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}prometheus-mqtt-exporter-0.1.4/.github/workflows/tests.yml000066400000000000000000000007731375452153700240450ustar00rootroot00000000000000on: push: branches: ["master"] pull_request: name: tests jobs: test: strategy: matrix: go-version: [1.14.x, 1.15.x] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - name: Install Go uses: actions/setup-go@v1 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 - name: Test run: go test ./... - name: Vet run: go vet ./... prometheus-mqtt-exporter-0.1.4/.gitignore000066400000000000000000000000351375452153700205420ustar00rootroot00000000000000config.yaml bin/ vendor dist prometheus-mqtt-exporter-0.1.4/.goreleaser.yml000066400000000000000000000035351375452153700215130ustar00rootroot00000000000000# This is an example goreleaser.yaml file with some sane defaults. # Make sure to check the documentation at http://goreleaser.com before: hooks: # You may remove this if you don't use go modules. - go mod download # you may remove this if you don't need go generate - go generate ./... release: prerelease: auto builds: - env: - CGO_ENABLED=0 main: cmd/mqtt2prometheus.go # GOOS list to build for. # For more info refer to: https://golang.org/doc/install/source#environment # Defaults are darwin and linux. goos: - linux - darwin - freebsd - windows # GOARCH to build for. # For more info refer to: https://golang.org/doc/install/source#environment # Defaults are 386 and amd64. goarch: - amd64 - arm - arm64 # GOARM to build for when GOARCH is arm. # For more info refer to: https://golang.org/doc/install/source#environment # Default is only 6. goarm: - 5 - 6 - 7 # GOMIPS and GOMIPS64 to build when GOARCH is mips, mips64, mipsle or mips64le. # For more info refer to: https://golang.org/doc/install/source#environment # Default is empty. gomips: - hardfloat - softfloat archives: - replacements: darwin: Darwin linux: Linux windows: Windows 386: i386 amd64: x86_64 checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ .Tag }}-next" changelog: sort: asc filters: exclude: - '^docs:' - '^test:' dockers: - dockerfile: release/Dockerfile.scratch image_templates: - "hikhvar/mqtt2prometheus:{{ .Tag }}" - "hikhvar/mqtt2prometheus:latest" - "docker.pkg.github.com/hikhvar/mqtt2prometheus/mqtt2prometheus:{{ .Tag }}" - "docker.pkg.github.com/hikhvar/mqtt2prometheus/mqtt2prometheus:latest" - "ghcr.io/hikhvar/mqtt2prometheus:{{ .Tag }}" - "ghcr.io/hikhvar/mqtt2prometheus:latest"prometheus-mqtt-exporter-0.1.4/Dockerfile000066400000000000000000000003641375452153700205510ustar00rootroot00000000000000FROM golang:1.15.2 as builder COPY . /build/mqtt2prometheus WORKDIR /build/mqtt2prometheus RUN make static_build TARGET_FILE=/bin/mqtt2prometheus FROM scratch COPY --from=builder /bin/mqtt2prometheus /mqtt2prometheus CMD ["/mqtt2prometheus"] prometheus-mqtt-exporter-0.1.4/LICENSE000066400000000000000000000020641375452153700175630ustar00rootroot00000000000000MIT License Copyright (c) 2018 Christoph Petrausch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. prometheus-mqtt-exporter-0.1.4/Makefile000066400000000000000000000013751375452153700202220ustar00rootroot00000000000000ifndef GOPATH GOPATH:=$(shell go env GOPATH) endif ifndef GOBIN GOBIN:=$(GOPATH)/bin endif ifndef GOARCH GOARCH:=$(shell go env GOARCH) endif ifndef GOOS GOOS:=$(shell go env GOOS) endif ifndef GOARM GOARM:=$(shell go env GOARM) endif ifndef TARGET_FILE TARGET_FILE:=bin/mqtt2prometheus.$(GOOS)_$(GOARCH)$(GOARM) endif all: build GO111MODULE=on lint: golangci-lint run test: go test ./... go vet ./... build: GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(TARGET_FILE) ./cmd static_build: 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 . test_release: goreleaser --rm-dist --skip-validate --skip-publish prometheus-mqtt-exporter-0.1.4/Readme.md000066400000000000000000000143641375452153700203030ustar00rootroot00000000000000# MQTT2Prometheus ![](https://github.com/hikhvar/mqtt2prometheus/workflows/tests/badge.svg) ![](https://github.com/hikhvar/mqtt2prometheus/workflows/release/badge.svg) This exporter translates from MQTT topics to prometheus metrics. The core design is that clients send arbitrary JSON messages on the topics. The translation is programmed into the mqtt2prometheus since we often can not change the IoT devices sending the messages. 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. The used arduino scetch can be found in the [dht22tomqtt](https://github.com/hikhvar/dht22tomqtt) repository. A local hacking environment with mqtt2prometheus, a MQTT broker and a prometheus server is in the [hack](https://github.com/hikhvar/mqtt2prometheus/tree/master/hack) directory. ## Assumptions about Messages and Topics This exporter makes some assumptions about the MQTT topics. This exporter assumes that each client publish the metrics into a dedicated topic. The regular expression ìn the configuration field `mqtt.device_id_regex` defines how to extract the device ID from the MQTT topic. This allow an arbitrary place of the device ID in the mqtt topic. For example the [tasmota](https://github.com/arendst/Tasmota) firmware pushes the telemetry data to the topics `tele//SENSOR`. Let us assume the default configuration from [#ConfigFile]. A sensor publishes the following message ```json {"temperature":23.20,"humidity":51.60, "computed": {"heat_index":22.92} } ``` to the MQTT topic `devices/me/livingroom`. This message becomes the following prometheus metrics: ```text temperature{sensor="livingroom",topic="devices/me/livingroom"} 23.2 heat_index{sensor="livingroom",topic="devices/me/livingroom"} 22.92 humidity{sensor="livingroom",topic="devices/me/livingroom"} 51.6 ``` ### Tasmota An example configuration for the tasmota based Gosund SP111 device is given in [examples/gosund_sp111.yaml](examples/gosund_sp111.yaml). ## Build To build the exporter run: ```bash make build ``` Only the latest two Go major versions are tested and supported. ### Docker #### Use Public Image To start the public available image run: ```bash docker run -it -v "$(pwd)/config.yaml:/config.yaml" -p 8002:8002 docker.pkg.github.com/hikhvar/mqtt2prometheus/mqtt2prometheus:latest ``` #### Build The Image locally 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 9641:9641 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 (default "0.0.0.0") -listen-port string HTTP port used to expose metrics (default "9641") -log-format string set the desired log output format. Valid values are 'console' and 'json' (default "console") -log-level value sets the default loglevel (default: "info") -version show the builds version, date and commit ``` The logging is implemented via [zap](https://github.com/uber-go/zap). The logs are printed to `stderr` and valid log levels are those supported by zap. ### 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 subscribe to. Be aware that you have to specify the wildcard. topic_path: v1/devices/me/+ # Optional: Regular expression to extract the device ID from the topic path. The default regular expression, assumes # that the last "element" of the topic_path is the device id. # The regular expression must contain a named capture group with the name deviceid # For example the expression for tasamota based sensors is "tele/(?P.*)/.*" device_id_regex: "(.*/)?(?P.*)" # 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: 24h # 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_celsius # The name of the metric in a MQTT JSON message. This can be an arbitrary gojsonq path. 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. Here a nested field. mqtt_name: computed.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% ``` ## Best Practices The exporter can only listen to one topic_path per instance. If you have to listen to two different topic_paths it is recommended to run two instances of the mqtt2prometheus exporter. You can run both on the same host or if you run in Kubernetes, even in the same pod. prometheus-mqtt-exporter-0.1.4/cmd/000077500000000000000000000000001375452153700173175ustar00rootroot00000000000000prometheus-mqtt-exporter-0.1.4/cmd/mqtt2prometheus.go000066400000000000000000000075421375452153700230410ustar00rootroot00000000000000package main import ( "encoding/json" "flag" "fmt" "go.uber.org/zap" "go.uber.org/zap/zapcore" "net/http" "os" "time" "github.com/eclipse/paho.mqtt.golang" "github.com/hikhvar/mqtt2prometheus/pkg/config" "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" ) // These variables are set by goreleaser at linking time. var ( version string commit string date string ) var ( configFlag = flag.String( "config", "config.yaml", "config file", ) portFlag = flag.String( "listen-port", "9641", "HTTP port used to expose metrics", ) addressFlag = flag.String( "listen-address", "0.0.0.0", "listen address for HTTP server used to expose metrics", ) versionFlag = flag.Bool( "version", false, "show the builds version, date and commit", ) logLevelFlag = zap.LevelFlag("log-level", zap.InfoLevel, "sets the default loglevel (default: \"info\")") logEncodingFlag = flag.String( "log-format", "console", "set the desired log output format. Valid values are 'console' and 'json'", ) ) func main() { flag.Parse() if *versionFlag { mustShowVersion() os.Exit(0) } logger := mustSetupLogger() defer logger.Sync() //nolint:errcheck c := make(chan os.Signal, 1) hostName, err := os.Hostname() if err != nil { logger.Fatal("Could not get hostname", zap.Error(err)) } cfg, err := config.LoadConfig(*configFlag) if err != nil { logger.Fatal("Could not load config", zap.Error(err)) } mqttClientOptions := mqtt.NewClientOptions() mqttClientOptions.AddBroker(cfg.MQTT.Server).SetClientID(hostName).SetCleanSession(true) mqttClientOptions.SetUsername(cfg.MQTT.User) mqttClientOptions.SetPassword(cfg.MQTT.Password) mqttClientOptions.SetClientID(mustMQTTClientID()) collector := metrics.NewCollector(cfg.Cache.Timeout, cfg.Metrics) ingest := metrics.NewIngest(collector, cfg.Metrics, cfg.MQTT.DeviceIDRegex) errorChan := make(chan error, 1) for { err = mqttclient.Subscribe(mqttClientOptions, mqttclient.SubscribeOptions{ Topic: cfg.MQTT.TopicPath, QoS: cfg.MQTT.QoS, OnMessageReceived: ingest.SetupSubscriptionHandler(errorChan), Logger: logger, }) if err == nil { // connected, break loop break } logger.Warn("could not connect to mqtt broker %s, sleep 10 second", zap.Error(err)) time.Sleep(10 * time.Second) } prometheus.MustRegister(ingest.MessageMetric) prometheus.MustRegister(collector) http.Handle("/metrics", promhttp.Handler()) go func() { err = http.ListenAndServe(getListenAddress(), nil) if err != nil { logger.Fatal("Error while serving http", zap.Error(err)) } }() for { select { case <-c: logger.Info("Terminated via Signal. Stop.") os.Exit(0) case err = <-errorChan: logger.Error("Error while processing message", zap.Error(err)) } } } func getListenAddress() string { return fmt.Sprintf("%s:%s", *addressFlag, *portFlag) } func mustShowVersion() { versionInfo := struct { Version string Commit string Date string }{ Version: version, Commit: commit, Date: date, } err := json.NewEncoder(os.Stdout).Encode(versionInfo) if err != nil { panic(err) } } func mustMQTTClientID() string { host, err := os.Hostname() if err != nil { panic(fmt.Sprintf("failed to get hostname: %v", err)) } pid := os.Getpid() return fmt.Sprintf("%s-%d", host, pid) } func mustSetupLogger() *zap.Logger { cfg := zap.NewProductionConfig() cfg.Level = zap.NewAtomicLevelAt(*logLevelFlag) cfg.Encoding = *logEncodingFlag if cfg.Encoding == "console" { cfg.EncoderConfig.EncodeTime = zapcore.RFC3339TimeEncoder } logger, err := cfg.Build() if err != nil { panic(fmt.Sprintf("failed to build logger: %v", err)) } config.SetProcessContext(logger) return logger } prometheus-mqtt-exporter-0.1.4/config.yaml.dist000066400000000000000000000066521375452153700216600ustar00rootroot00000000000000# 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 # Optional: Username and Password for authenticating with the MQTT Server # user: bob # password: happylittleclouds # The Topic path to subscribe to. Be aware that you have to specify the wildcard. topic_path: v1/devices/me/+ # Optional: Regular expression to extract the device ID from the topic path. The default regular expression, assumes # that the last "element" of the topic_path is the device id. # The regular expression must contain a named capture group with the name deviceid # For example the expression for tasamota based sensors is "tele/(?P.*)/.*" # device_id_regex: "(.*/)?(?P.*)" # The MQTT QoS level qos: 0 cache: # Timeout. Each received metric will be presented for this time if no update is send via MQTT. # Set the timeout to -1 to disable the deletion of metrics from the cache. The exporter presents the ingest timestamp # to prometheus. timeout: 24h # 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 # The name of the metric in prometheus - prom_name: state # The name of the metric in a MQTT JSON message mqtt_name: state # Regular expression to only match sensors with the given name pattern sensor_name_filter: "^.*-light$" # The prometheus help text for this metric help: Light state # 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: ikea # When specified, enables mapping between string values to metric values. string_value_mapping: # A map of string to metric value. map: off: 0 low: 0 # Metric value to use if a match cannot be found in the map above. # If not specified, parsing error will occur. error_value: 1 prometheus-mqtt-exporter-0.1.4/examples/000077500000000000000000000000001375452153700203725ustar00rootroot00000000000000prometheus-mqtt-exporter-0.1.4/examples/gosund_sp111.yaml000066400000000000000000000036371375452153700235130ustar00rootroot00000000000000# Settings for the MQTT Client. Currently only these three are supported mqtt: # The MQTT broker to connect to server: tcp://192.168.1.11:1883 # Optional: Username and Password for authenticating with the MQTT Server # user: bob # password: happylittleclouds # The Topic path to subscribe to. Be aware that you have to specify the wildcard. topic_path: tele/+/SENSOR # Optional: Regular expression to extract the device ID from the topic path. The default regular expression, assumes # that the last "element" of the topic_path is the device id. # The regular expression must contain a named capture group with the name deviceid # For example the expression for tasamota based sensors is "tele/(?P.*)/.*" device_id_regex: "tele/(?P.*)/SENSOR" # The MQTT QoS level qos: 0 cache: # Timeout. Each received metric will be presented for this time if no update is send via MQTT. # Set the timeout to -1 to disable the deletion of metrics from the cache. The exporter presents the ingest timestamp # to prometheus. timeout: 24h # This is a list of valid metrics. Only metrics listed here will be exported metrics: # The name of the metric in prometheus - prom_name: consumed_energy_kilowatthours_total mqtt_name: "ENERGY.Total" help: "total measured kilowatthours since flash" type: counter - prom_name: voltage_volts mqtt_name: "ENERGY.Voltage" help: "Currently measured voltage" type: gauge - prom_name: current_amperes mqtt_name: "ENERGY.Current" help: "Currently measured current" type: gauge - prom_name: power_watts mqtt_name: "ENERGY.Power" help: "Currently measured power" type: gauge - prom_name: apparent_power_watt mqtt_name: "ENERGY.ApparentPower" help: "Currently apparent power" type: gauge - prom_name: reactive_power_watt mqtt_name: "ENERGY.ReactivePower" help: "Currently reactive power" type: gaugeprometheus-mqtt-exporter-0.1.4/go.mod000066400000000000000000000012201375452153700176550ustar00rootroot00000000000000module github.com/hikhvar/mqtt2prometheus go 1.14 require ( github.com/beorn7/perks v1.0.1 github.com/eclipse/paho.mqtt.golang v1.1.1 github.com/golang/protobuf v1.4.0 github.com/matttproud/golang_protobuf_extensions v1.0.1 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.6.0 github.com/prometheus/client_model v0.2.0 github.com/prometheus/common v0.9.1 github.com/prometheus/procfs v0.0.11 github.com/thedevsaddam/gojsonq v2.3.0+incompatible // indirect github.com/thedevsaddam/gojsonq/v2 v2.5.2 go.uber.org/zap v1.16.0 golang.org/x/net v0.0.0-20190620200207-3b0461eec859 gopkg.in/yaml.v2 v2.2.5 ) prometheus-mqtt-exporter-0.1.4/go.sum000066400000000000000000000347161375452153700177220ustar00rootroot00000000000000github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eclipse/paho.mqtt.golang v1.1.1 h1:iPJYXJLaViCshRTW/PSqImSS6HJ2Rf671WR0bXZ2GIU= github.com/eclipse/paho.mqtt.golang v1.1.1/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.8.0 h1:1921Yw9Gc3iSc4VQh3PIoOqgPCZS7G/4xQNVUp8Mda8= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mglnMrWfyrw6A= github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181218105931-67670fe90761 h1:z6tvbDJ5OLJ48FFmnksv04a78maSTRBUIhkdHYV5Y98= github.com/prometheus/common v0.0.0-20181218105931-67670fe90761/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/thedevsaddam/gojsonq v1.9.1 h1:zQulEP43nwmq5EKrNWyIgJVbqDeMdC1qzXM/f5O15a0= github.com/thedevsaddam/gojsonq v2.3.0+incompatible h1:i2lFTvGY4LvoZ2VUzedsFlRiyaWcJm3Uh6cQ9+HyQA8= github.com/thedevsaddam/gojsonq v2.3.0+incompatible/go.mod h1:RBcQaITThgJAAYKH7FNp2onYodRz8URfsuEGpAch0NA= github.com/thedevsaddam/gojsonq/v2 v2.5.2 h1:CoMVaYyKFsVj6TjU6APqAhAvC07hTI6IQen8PHzHYY0= github.com/thedevsaddam/gojsonq/v2 v2.5.2/go.mod h1:bv6Xa7kWy82uT0LnXPE2SzGqTj33TAEeR560MdJkiXs= go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= prometheus-mqtt-exporter-0.1.4/hack/000077500000000000000000000000001375452153700174625ustar00rootroot00000000000000prometheus-mqtt-exporter-0.1.4/hack/docker-compose.yml000066400000000000000000000007771375452153700231320ustar00rootroot00000000000000version: "3.8" services: mqtt2prometheus: build: context: ../ dockerfile: Dockerfile ports: - 9641:9641 volumes: - type: bind source: ./mqtt2prometheus.yaml target: /config.yaml mosquitto: image: eclipse-mosquitto:1.6.9 ports: - 1883:1883 - 9001:9001 prometheus: image: prom/prometheus:v2.18.1 ports: - 9090:9090 volumes: - type: bind source: ./prometheus.yml target: /etc/prometheus/prometheus.yml prometheus-mqtt-exporter-0.1.4/hack/mqtt2prometheus.yaml000066400000000000000000000053571375452153700235430ustar00rootroot00000000000000# Settings for the MQTT Client. Currently only these three are supported mqtt: # The MQTT broker to connect to server: tcp://mosquitto:1883 # Optional: Username and Password for authenticating with the MQTT Server # user: bob # password: happylittleclouds # The Topic path to subscribe to. Be aware that you have to specify the wildcard. 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: 60m # 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 - prom_name: state # The name of the metric in a MQTT JSON message mqtt_name: state # The prometheus help text for this metric help: Light state # 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: ikea # When specified, enables mapping between string values to metric values. string_value_mapping: # A map of string to metric value. map: off: 0 low: 0 # Metric value to use if a match cannot be found in the map above. # If not specified, parsing error will occur. error_value: 1 prometheus-mqtt-exporter-0.1.4/hack/prometheus.yml000066400000000000000000000012201375452153700223730ustar00rootroot00000000000000global: scrape_interval: 15s scrape_timeout: 10s evaluation_interval: 15s alerting: alertmanagers: - static_configs: - targets: [] scheme: http timeout: 10s api_version: v1 scrape_configs: - job_name: prometheus honor_timestamps: true scrape_interval: 15s scrape_timeout: 10s metrics_path: /metrics scheme: http static_configs: - targets: - localhost:9090 - job_name: mqtt2prometheus honor_timestamps: true scrape_interval: 15s scrape_timeout: 10s metrics_path: /metrics scheme: http static_configs: - targets: - mqtt2prometheus:9641 prometheus-mqtt-exporter-0.1.4/pkg/000077500000000000000000000000001375452153700173355ustar00rootroot00000000000000prometheus-mqtt-exporter-0.1.4/pkg/config/000077500000000000000000000000001375452153700206025ustar00rootroot00000000000000prometheus-mqtt-exporter-0.1.4/pkg/config/config.go000066400000000000000000000101431375452153700223750ustar00rootroot00000000000000package config import ( "fmt" "io/ioutil" "regexp" "time" "github.com/prometheus/client_golang/prometheus" "gopkg.in/yaml.v2" ) const GaugeValueType = "gauge" const CounterValueType = "counter" const DeviceIDRegexGroup = "deviceid" var MQTTConfigDefaults = MQTTConfig{ Server: "tcp://127.0.0.1:1883", TopicPath: "v1/devices/me", DeviceIDRegex: mustNewRegexp(fmt.Sprintf("(.*/)?(?P<%s>.*)", DeviceIDRegexGroup)), QoS: 0, } var CacheConfigDefaults = CacheConfig{ Timeout: 2 * time.Minute, } type Regexp struct { r *regexp.Regexp pattern string } func (rf *Regexp) UnmarshalYAML(unmarshal func(interface{}) error) error { var pattern string if err := unmarshal(&pattern); err != nil { return err } r, err := regexp.Compile(pattern) if err != nil { return err } rf.r = r rf.pattern = pattern return nil } func (rf *Regexp) MarshalYAML() (interface{}, error) { return rf.pattern, nil } func (rf *Regexp) Match(s string) bool { return rf.r == nil || rf.r.MatchString(s) } // GroupValue returns the value of the given group. If the group is not part of the underlying regexp, returns the empty string. func (rf *Regexp) GroupValue(s string, groupName string) string { match := rf.r.FindStringSubmatch(s) groupValues := make(map[string]string) for i, name := range rf.r.SubexpNames() { if name != "" { groupValues[name] = match[i] } } return groupValues[groupName] } func (rf *Regexp) RegEx() *regexp.Regexp { return rf.r } func mustNewRegexp(pattern string) *Regexp { return &Regexp{ pattern: pattern, r: regexp.MustCompile(pattern), } } type Config struct { Metrics []MetricConfig `yaml:"metrics"` MQTT *MQTTConfig `yaml:"mqtt,omitempty"` Cache *CacheConfig `yaml:"cache,omitempty"` } type CacheConfig struct { Timeout time.Duration `yaml:"timeout"` } type MQTTConfig struct { Server string `yaml:"server"` TopicPath string `yaml:"topic_path"` DeviceIDRegex *Regexp `yaml:"device_id_regex"` User string `yaml:"user"` Password string `yaml:"password"` 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"` SensorNameFilter Regexp `yaml:"sensor_name_filter"` Help string `yaml:"help"` ValueType string `yaml:"type"` ConstantLabels map[string]string `yaml:"const_labels"` StringValueMapping *StringValueMappingConfig `yaml:"string_value_mapping"` } // StringValueMappingConfig defines the mapping from string to float type StringValueMappingConfig struct { // ErrorValue is used when no mapping is found in Map ErrorValue *float64 `yaml:"error_value"` Map map[string]float64 `yaml:"map"` } func (mc *MetricConfig) PrometheusDescription() *prometheus.Desc { return prometheus.NewDesc( mc.PrometheusName, mc.Help, []string{"sensor", "topic"}, 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 } if cfg.MQTT.DeviceIDRegex == nil { cfg.MQTT.DeviceIDRegex = MQTTConfigDefaults.DeviceIDRegex } var validRegex bool for _, name := range cfg.MQTT.DeviceIDRegex.RegEx().SubexpNames() { if name == DeviceIDRegexGroup { validRegex = true } } if !validRegex { return Config{}, fmt.Errorf("device id regex %q does not contain required regex group %q", cfg.MQTT.DeviceIDRegex.pattern, DeviceIDRegexGroup) } return cfg, nil } prometheus-mqtt-exporter-0.1.4/pkg/config/runtime.go000066400000000000000000000005571375452153700226230ustar00rootroot00000000000000package config import "go.uber.org/zap" // runtimeContext contains process global settings like the logger, type runtimeContext struct { logger *zap.Logger } func (r *runtimeContext) Logger() *zap.Logger { return r.logger } var ProcessContext runtimeContext func SetProcessContext(logger *zap.Logger) { ProcessContext = runtimeContext{ logger: logger, } } prometheus-mqtt-exporter-0.1.4/pkg/metrics/000077500000000000000000000000001375452153700210035ustar00rootroot00000000000000prometheus-mqtt-exporter-0.1.4/pkg/metrics/collector.go000066400000000000000000000031461375452153700233240ustar00rootroot00000000000000package 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 IngestTime time.Time Topic string } 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 := prometheus.MustNewConstMetric( metric.Description, metric.ValueType, metric.Value, device, metric.Topic, ) mc <- prometheus.NewMetricWithTimestamp(metric.IngestTime, m) } } } prometheus-mqtt-exporter-0.1.4/pkg/metrics/ingest.go000066400000000000000000000100371375452153700226240ustar00rootroot00000000000000package metrics import ( "fmt" "go.uber.org/zap" "strconv" "time" gojsonq "github.com/thedevsaddam/gojsonq/v2" "github.com/eclipse/paho.mqtt.golang" "github.com/hikhvar/mqtt2prometheus/pkg/config" "github.com/prometheus/client_golang/prometheus" ) type Ingest struct { metricConfigs map[string][]config.MetricConfig deviceIDRegex *config.Regexp collector Collector MessageMetric *prometheus.CounterVec logger *zap.Logger } func NewIngest(collector Collector, metrics []config.MetricConfig, deviceIDRegex *config.Regexp) *Ingest { cfgs := make(map[string][]config.MetricConfig) for i := range metrics { key := metrics[i].MQTTName cfgs[key] = append(cfgs[key], metrics[i]) } return &Ingest{ metricConfigs: cfgs, deviceIDRegex: deviceIDRegex, collector: collector, MessageMetric: prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "received_messages", Help: "received messages per topic and status", }, []string{"status", "topic"}, ), logger: config.ProcessContext.Logger(), } } // validMetric returns config matching the metric and deviceID // Second return value indicates if config was found. func (i *Ingest) validMetric(metric string, deviceID string) (config.MetricConfig, bool) { for _, c := range i.metricConfigs[metric] { if c.SensorNameFilter.Match(deviceID) { return c, true } } return config.MetricConfig{}, false } func (i *Ingest) store(topic string, payload []byte) error { var mc MetricCollection deviceID := i.deviceID(topic) parsed := gojsonq.New().FromString(string(payload)) for path := range i.metricConfigs { rawValue := parsed.Find(path) parsed.Reset() if rawValue == nil { continue } m, err := i.parseMetric(path, deviceID, rawValue) if err != nil { return fmt.Errorf("failed to parse valid metric value: %w", err) } m.Topic = topic mc = append(mc, m) } i.collector.Observe(deviceID, mc) return nil } func (i *Ingest) parseMetric(metricPath string, deviceID string, value interface{}) (Metric, error) { cfg, cfgFound := i.validMetric(metricPath, deviceID) if !cfgFound { return Metric{}, nil } var metricValue float64 if boolValue, ok := value.(bool); ok { if boolValue { metricValue = 1 } else { metricValue = 0 } } else if strValue, ok := value.(string); ok { // If string value mapping is defined, use that if cfg.StringValueMapping != nil { floatValue, ok := cfg.StringValueMapping.Map[strValue] if ok { metricValue = floatValue } else if cfg.StringValueMapping.ErrorValue != nil { metricValue = *cfg.StringValueMapping.ErrorValue } else { return Metric{}, fmt.Errorf("got unexpected string data '%s'", strValue) } } else { // otherwise try to parse float floatValue, err := strconv.ParseFloat(strValue, 64) if err != nil { return Metric{}, fmt.Errorf("got data with unexpectd type: %T ('%s') and failed to parse to float", value, value) } metricValue = floatValue } } else if floatValue, ok := value.(float64); ok { metricValue = floatValue } else { return Metric{}, fmt.Errorf("got data with unexpectd type: %T ('%s')", value, value) } return Metric{ Description: cfg.PrometheusDescription(), Value: metricValue, ValueType: cfg.PrometheusValueType(), IngestTime: time.Now(), }, nil } func (i *Ingest) SetupSubscriptionHandler(errChan chan<- error) mqtt.MessageHandler { return func(c mqtt.Client, m mqtt.Message) { i.logger.Debug("Got message", zap.String("topic", m.Topic()), zap.String("payload", string(m.Payload()))) err := i.store(m.Topic(), m.Payload()) 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() return } i.MessageMetric.WithLabelValues("success", m.Topic()).Inc() } } // deviceID uses the configured DeviceIDRegex to extract the device ID from the given mqtt topic path. func (i *Ingest) deviceID(topic string) string { return i.deviceIDRegex.GroupValue(topic, config.DeviceIDRegexGroup) } prometheus-mqtt-exporter-0.1.4/pkg/mqttclient/000077500000000000000000000000001375452153700215215ustar00rootroot00000000000000prometheus-mqtt-exporter-0.1.4/pkg/mqttclient/mqttClient.go000066400000000000000000000016601375452153700241770ustar00rootroot00000000000000package mqttclient import ( "github.com/eclipse/paho.mqtt.golang" "go.uber.org/zap" ) type SubscribeOptions struct { Topic string QoS byte OnMessageReceived mqtt.MessageHandler Logger *zap.Logger } func Subscribe(connectionOptions *mqtt.ClientOptions, subscribeOptions SubscribeOptions) error { connectionOptions.OnConnect = func(client mqtt.Client) { logger := subscribeOptions.Logger logger.Info("Connected to MQTT Broker") logger.Info("Will subscribe to topic", zap.String("topic", subscribeOptions.Topic)) if token := client.Subscribe(subscribeOptions.Topic, subscribeOptions.QoS, subscribeOptions.OnMessageReceived); token.Wait() && token.Error() != nil { logger.Error("Could not subscribe", zap.Error(token.Error())) } } client := mqtt.NewClient(connectionOptions) if token := client.Connect(); token.Wait() && token.Error() != nil { return token.Error() } return nil } prometheus-mqtt-exporter-0.1.4/release/000077500000000000000000000000001375452153700201745ustar00rootroot00000000000000prometheus-mqtt-exporter-0.1.4/release/Dockerfile.scratch000066400000000000000000000001141375452153700236100ustar00rootroot00000000000000FROM scratch COPY mqtt2prometheus /mqtt2prometheus CMD ["/mqtt2prometheus"]prometheus-mqtt-exporter-0.1.4/systemd/000077500000000000000000000000001375452153700202445ustar00rootroot00000000000000prometheus-mqtt-exporter-0.1.4/systemd/mqtt2prometheus.service000066400000000000000000000004131375452153700250070ustar00rootroot00000000000000[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