├── .circleci └── config.yml ├── .gitignore ├── .promu.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── Makefile.common ├── README.md ├── VERSION ├── exporter.go ├── go.mod ├── go.sum ├── logger.go └── main.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | 4 | orbs: 5 | prometheus: prometheus/prometheus@0.4 6 | 7 | executors: 8 | # This must match .promu.yml. 9 | golang: 10 | docker: 11 | - image: circleci/golang:1.14 12 | 13 | jobs: 14 | test: 15 | executor: golang 16 | 17 | steps: 18 | - prometheus/setup_environment 19 | - run: make 20 | - prometheus/store_artifact: 21 | file: mqttgateway 22 | 23 | publish_master: 24 | executor: golang 25 | 26 | steps: 27 | - prometheus/setup_build_environment 28 | - prometheus/publish_images: 29 | registry: docker.io 30 | organization: mqttgateway 31 | login_variable: DOCKER_LOGIN 32 | password_variable: DOCKER_PASSWORD 33 | 34 | publish_release: 35 | executor: golang 36 | 37 | steps: 38 | - prometheus/setup_build_environment 39 | - run: promu crossbuild tarballs 40 | - run: promu checksum .tarballs 41 | - run: promu release .tarballs 42 | - store_artifacts: 43 | path: .tarballs 44 | destination: releases 45 | - prometheus/publish_release_images: 46 | registry: docker.io 47 | organization: mqttgateway 48 | login_variable: DOCKER_LOGIN 49 | password_variable: DOCKER_PASSWORD 50 | 51 | workflows: 52 | version: 2 53 | mqttgateway: 54 | jobs: 55 | - test: 56 | filters: 57 | tags: 58 | only: /.*/ 59 | - prometheus/build: 60 | name: build 61 | filters: 62 | tags: 63 | only: /.*/ 64 | - publish_master: 65 | requires: 66 | - test 67 | - build 68 | filters: 69 | branches: 70 | only: master 71 | - publish_release: 72 | requires: 73 | - test 74 | - build 75 | filters: 76 | tags: 77 | only: /^v.*/ 78 | branches: 79 | ignore: /.*/ 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | mqttgateway 3 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | go: 2 | # Whenever the Go version is updated here, 3 | # .circle/config.yml should also be updated. 4 | version: 1.14 5 | repository: 6 | path: github.com/inuits/mqttgateway 7 | build: 8 | binaries: 9 | - name: mqttgateway 10 | path: . 11 | flags: -a -tags netgo,builtinassets 12 | ldflags: | 13 | -X github.com/prometheus/common/version.Version={{.Version}} 14 | -X github.com/prometheus/common/version.Revision={{.Revision}} 15 | -X github.com/prometheus/common/version.Branch={{.Branch}} 16 | -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} 17 | -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} 18 | tarball: 19 | files: 20 | - LICENSE 21 | crossbuild: 22 | platforms: 23 | - linux/amd64 24 | - linux/386 25 | - linux/arm 26 | - linux/arm64 27 | - linux/mips 28 | - linux/mipsle 29 | - linux/mips64 30 | - linux/mips64le 31 | - linux/ppc64 32 | - linux/ppc64le 33 | - linux/s390x 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 / 2020-06-12 2 | 3 | * Initial release 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ARCH="amd64" 2 | ARG OS="linux" 3 | FROM quay.io/prometheus/busybox-${OS}-${ARCH}:glibc 4 | LABEL maintainer="Julien Pivotto " 5 | 6 | ARG ARCH="amd64" 7 | ARG OS="linux" 8 | COPY .build/${OS}-${ARCH}/mqttgateway /bin/mqttgateway 9 | 10 | EXPOSE 7979 11 | USER nobody 12 | ENTRYPOINT [ "/bin/mqttgateway" ] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Julien Pivotto 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_ARCHS ?= amd64 armv7 arm64 ppc64le s390x 2 | 3 | include Makefile.common 4 | 5 | DOCKER_IMAGE_NAME ?= mqttgateway 6 | MACH ?= $(shell uname -m) 7 | -------------------------------------------------------------------------------- /Makefile.common: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | 15 | # A common Makefile that includes rules to be reused in different prometheus projects. 16 | # !!! Open PRs only against the prometheus/prometheus/Makefile.common repository! 17 | 18 | # Example usage : 19 | # Create the main Makefile in the root project directory. 20 | # include Makefile.common 21 | # customTarget: 22 | # @echo ">> Running customTarget" 23 | # 24 | 25 | # Ensure GOBIN is not set during build so that promu is installed to the correct path 26 | unexport GOBIN 27 | 28 | GO ?= go 29 | GOFMT ?= $(GO)fmt 30 | FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) 31 | GOOPTS ?= 32 | GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) 33 | GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH) 34 | 35 | GO_VERSION ?= $(shell $(GO) version) 36 | GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) 37 | PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') 38 | 39 | GOVENDOR := 40 | GO111MODULE := 41 | ifeq (, $(PRE_GO_111)) 42 | ifneq (,$(wildcard go.mod)) 43 | # Enforce Go modules support just in case the directory is inside GOPATH (and for Travis CI). 44 | GO111MODULE := on 45 | 46 | ifneq (,$(wildcard vendor)) 47 | # Always use the local vendor/ directory to satisfy the dependencies. 48 | GOOPTS := $(GOOPTS) -mod=vendor 49 | endif 50 | endif 51 | else 52 | ifneq (,$(wildcard go.mod)) 53 | ifneq (,$(wildcard vendor)) 54 | $(warning This repository requires Go >= 1.11 because of Go modules) 55 | $(warning Some recipes may not work as expected as the current Go runtime is '$(GO_VERSION_NUMBER)') 56 | endif 57 | else 58 | # This repository isn't using Go modules (yet). 59 | GOVENDOR := $(FIRST_GOPATH)/bin/govendor 60 | endif 61 | endif 62 | PROMU := $(FIRST_GOPATH)/bin/promu 63 | pkgs = ./... 64 | 65 | ifeq (arm, $(GOHOSTARCH)) 66 | GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM) 67 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM) 68 | else 69 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH) 70 | endif 71 | 72 | GOTEST := $(GO) test 73 | GOTEST_DIR := 74 | ifneq ($(CIRCLE_JOB),) 75 | ifneq ($(shell which gotestsum),) 76 | GOTEST_DIR := test-results 77 | GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml -- 78 | endif 79 | endif 80 | 81 | PROMU_VERSION ?= 0.5.0 82 | PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz 83 | 84 | GOLANGCI_LINT := 85 | GOLANGCI_LINT_OPTS ?= 86 | GOLANGCI_LINT_VERSION ?= v1.18.0 87 | # golangci-lint only supports linux, darwin and windows platforms on i386/amd64. 88 | # windows isn't included here because of the path separator being different. 89 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) 90 | ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386)) 91 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 92 | endif 93 | endif 94 | 95 | PREFIX ?= $(shell pwd) 96 | BIN_DIR ?= $(shell pwd) 97 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) 98 | DOCKERFILE_PATH ?= ./Dockerfile 99 | DOCKERBUILD_CONTEXT ?= ./ 100 | DOCKER_REPO ?= prom 101 | 102 | DOCKER_ARCHS ?= amd64 103 | 104 | BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) 105 | PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) 106 | TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) 107 | 108 | ifeq ($(GOHOSTARCH),amd64) 109 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) 110 | # Only supported on amd64 111 | test-flags := -race 112 | endif 113 | endif 114 | 115 | # This rule is used to forward a target like "build" to "common-build". This 116 | # allows a new "build" target to be defined in a Makefile which includes this 117 | # one and override "common-build" without override warnings. 118 | %: common-% ; 119 | 120 | .PHONY: common-all 121 | common-all: precheck style check_license lint unused build test 122 | 123 | .PHONY: common-style 124 | common-style: 125 | @echo ">> checking code style" 126 | @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ 127 | if [ -n "$${fmtRes}" ]; then \ 128 | echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ 129 | echo "Please ensure you are using $$($(GO) version) for formatting code."; \ 130 | exit 1; \ 131 | fi 132 | 133 | .PHONY: common-check_license 134 | common-check_license: 135 | @echo ">> checking license header" 136 | @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ 137 | awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ 138 | done); \ 139 | if [ -n "$${licRes}" ]; then \ 140 | echo "license header checking failed:"; echo "$${licRes}"; \ 141 | exit 1; \ 142 | fi 143 | 144 | .PHONY: common-deps 145 | common-deps: 146 | @echo ">> getting dependencies" 147 | ifdef GO111MODULE 148 | GO111MODULE=$(GO111MODULE) $(GO) mod download 149 | else 150 | $(GO) get $(GOOPTS) -t ./... 151 | endif 152 | 153 | .PHONY: update-go-deps 154 | update-go-deps: 155 | @echo ">> updating Go dependencies" 156 | @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ 157 | $(GO) get $$m; \ 158 | done 159 | GO111MODULE=$(GO111MODULE) $(GO) mod tidy 160 | ifneq (,$(wildcard vendor)) 161 | GO111MODULE=$(GO111MODULE) $(GO) mod vendor 162 | endif 163 | 164 | .PHONY: common-test-short 165 | common-test-short: $(GOTEST_DIR) 166 | @echo ">> running short tests" 167 | GO111MODULE=$(GO111MODULE) $(GOTEST) -short $(GOOPTS) $(pkgs) 168 | 169 | .PHONY: common-test 170 | common-test: $(GOTEST_DIR) 171 | @echo ">> running all tests" 172 | GO111MODULE=$(GO111MODULE) $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) 173 | 174 | $(GOTEST_DIR): 175 | @mkdir -p $@ 176 | 177 | .PHONY: common-format 178 | common-format: 179 | @echo ">> formatting code" 180 | GO111MODULE=$(GO111MODULE) $(GO) fmt $(pkgs) 181 | 182 | .PHONY: common-vet 183 | common-vet: 184 | @echo ">> vetting code" 185 | GO111MODULE=$(GO111MODULE) $(GO) vet $(GOOPTS) $(pkgs) 186 | 187 | .PHONY: common-lint 188 | common-lint: $(GOLANGCI_LINT) 189 | ifdef GOLANGCI_LINT 190 | @echo ">> running golangci-lint" 191 | ifdef GO111MODULE 192 | # 'go list' needs to be executed before staticcheck to prepopulate the modules cache. 193 | # Otherwise staticcheck might fail randomly for some reason not yet explained. 194 | GO111MODULE=$(GO111MODULE) $(GO) list -e -compiled -test=true -export=false -deps=true -find=false -tags= -- ./... > /dev/null 195 | GO111MODULE=$(GO111MODULE) $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) 196 | else 197 | $(GOLANGCI_LINT) run $(pkgs) 198 | endif 199 | endif 200 | 201 | # For backward-compatibility. 202 | .PHONY: common-staticcheck 203 | common-staticcheck: lint 204 | 205 | .PHONY: common-unused 206 | common-unused: $(GOVENDOR) 207 | ifdef GOVENDOR 208 | @echo ">> running check for unused packages" 209 | @$(GOVENDOR) list +unused | grep . && exit 1 || echo 'No unused packages' 210 | else 211 | ifdef GO111MODULE 212 | @echo ">> running check for unused/missing packages in go.mod" 213 | GO111MODULE=$(GO111MODULE) $(GO) mod tidy 214 | ifeq (,$(wildcard vendor)) 215 | @git diff --exit-code -- go.sum go.mod 216 | else 217 | @echo ">> running check for unused packages in vendor/" 218 | GO111MODULE=$(GO111MODULE) $(GO) mod vendor 219 | @git diff --exit-code -- go.sum go.mod vendor/ 220 | endif 221 | endif 222 | endif 223 | 224 | .PHONY: common-build 225 | common-build: promu 226 | @echo ">> building binaries" 227 | GO111MODULE=$(GO111MODULE) $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) 228 | 229 | .PHONY: common-tarball 230 | common-tarball: promu 231 | @echo ">> building release tarball" 232 | $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) 233 | 234 | .PHONY: common-docker $(BUILD_DOCKER_ARCHS) 235 | common-docker: $(BUILD_DOCKER_ARCHS) 236 | $(BUILD_DOCKER_ARCHS): common-docker-%: 237 | docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" \ 238 | -f $(DOCKERFILE_PATH) \ 239 | --build-arg ARCH="$*" \ 240 | --build-arg OS="linux" \ 241 | $(DOCKERBUILD_CONTEXT) 242 | 243 | .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) 244 | common-docker-publish: $(PUBLISH_DOCKER_ARCHS) 245 | $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: 246 | docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" 247 | 248 | .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) 249 | common-docker-tag-latest: $(TAG_DOCKER_ARCHS) 250 | $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: 251 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" 252 | 253 | .PHONY: common-docker-manifest 254 | common-docker-manifest: 255 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(DOCKER_IMAGE_TAG)) 256 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" 257 | 258 | .PHONY: promu 259 | promu: $(PROMU) 260 | 261 | $(PROMU): 262 | $(eval PROMU_TMP := $(shell mktemp -d)) 263 | curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) 264 | mkdir -p $(FIRST_GOPATH)/bin 265 | cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu 266 | rm -r $(PROMU_TMP) 267 | 268 | .PHONY: proto 269 | proto: 270 | @echo ">> generating code from proto files" 271 | @./scripts/genproto.sh 272 | 273 | ifdef GOLANGCI_LINT 274 | $(GOLANGCI_LINT): 275 | mkdir -p $(FIRST_GOPATH)/bin 276 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ 277 | | sed -e '/install -d/d' \ 278 | | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) 279 | endif 280 | 281 | ifdef GOVENDOR 282 | .PHONY: $(GOVENDOR) 283 | $(GOVENDOR): 284 | GOOS= GOARCH= $(GO) get -u github.com/kardianos/govendor 285 | endif 286 | 287 | .PHONY: precheck 288 | precheck:: 289 | 290 | define PRECHECK_COMMAND_template = 291 | precheck:: $(1)_precheck 292 | 293 | PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) 294 | .PHONY: $(1)_precheck 295 | $(1)_precheck: 296 | @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ 297 | echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ 298 | exit 1; \ 299 | fi 300 | endef 301 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MQTTGateway for Prometheus 2 | 3 | [![circleci](https://circleci.com/gh/inuits/mqttgateway/tree/master.svg?style=shield)][circleci] 4 | [![go report card](https://goreportcard.com/badge/github.com/inuits/mqttgateway)][goreportcard] 5 | 6 | A project that subscribes to MQTT queues and published prometheus metrics. 7 | 8 | ``` 9 | usage: mqttgateway [] 10 | 11 | Flags: 12 | --help Show context-sensitive help (also try --help-long and --help-man). 13 | --web.listen-address=":9337" Address on which to expose metrics and web interface. 14 | --web.telemetry-path="/metrics" 15 | Path under which to expose metrics. 16 | --mqtt.broker-address="tcp://localhost:1883" 17 | Address of the MQTT broker. 18 | The default is taken from $MQTT_BROKER_ADDRESS if it is set. 19 | --mqtt.topic="prometheus/#" MQTT topic to subscribe to. 20 | The default is taken from $MQTT_TOPIC if it is set. 21 | --mqtt.prefix="prometheus" MQTT topic prefix to remove when creating metrics. 22 | The default is taken from $MQTT_PREFIX if it is set. 23 | --mqtt.username="" MQTT username. 24 | The default is taken from $MQTT_USERNAME if it is set. 25 | --mqtt.password="" MQTT password. 26 | The default is taken from $MQTT_PASSWORD if it is set. 27 | --mqtt.clientid="" MQTT client ID. 28 | The default is taken from $MQTT_CLIENT_ID if it is set. 29 | --log.level="info" Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal] 30 | --log.format="logger:stderr" Set the log target and format. Example: "logger:syslog?appname=bob&local=7" or "logger:stdout?json=true" 31 | ``` 32 | 33 | ## Installation from source 34 | 35 | Requires go 36 | 37 | ``` 38 | go get -u github.com/inuits/mqttgateway 39 | ``` 40 | 41 | ## Installation from binaries 42 | 43 | Pre-built binaries are available on the [releases 44 | page](https://github.com/inuits/mqttgateway/releases). 45 | 46 | ## Installation with Docker 47 | 48 | [![dockeri.co](https://dockeri.co/image/mqttgateway/mqttgateway)](https://hub.docker.com/r/mqttgateway/mqttgateway) 49 | 50 | Use a MQTT broker which runs locally: 51 | 52 | ``` 53 | $ docker run --network="host" -p 9337:9337 mqttgateway/mqttgateway 54 | ``` 55 | 56 | Use a MQTT broker which runs elsewhere: 57 | 58 | ``` 59 | $ docker run -p 9337:9337 mqttgateway/mqttgateway --mqtt.broker-address="tcp://mosquitto.local:1883" 60 | ``` 61 | 62 | 63 | ## How does it work? 64 | 65 | mqttgateway will connect to the MQTT broker at `--mqtt.broker-address` and 66 | listen to the topics specified by `--mqtt.topic`. 67 | 68 | By default, it will listen to `prometheus/#`. 69 | 70 | The format for the topics is as follow: 71 | 72 | `prefix/LABEL1/VALUE1/LABEL2/VALUE2/NAME` 73 | 74 | A topic `prometheus/job/ESP8266/instance/livingroom/temperature_celsius` would 75 | be converted to a metric 76 | `temperature_celsius{job="ESP8266",instance="livingroom"}`. 77 | 78 | If labelnames differ for a same metric, then we invalidate existing metrics and 79 | only keep new ones. Then we issue a warning in the logs. You should avoid it. 80 | 81 | Two other metrics are published, for each metric: 82 | 83 | - `mqtt_NAME_last_pushed_timestamp`, the last time NAME metric has been pushed 84 | (unix time, in seconds) 85 | - `mqtt_NAME_push_total`, the number of times a metric has been pushed 86 | 87 | ## Security 88 | 89 | This project does not support authentication yet but that is planned. 90 | 91 | ## Example 92 | 93 | ``` 94 | $ mosquitto_pub -m 20.2 -t prometheus/job/ESP8266/instance/livingroom/temperature_celsius 95 | $ curl -s http://127.0.0.1:9337/metrics|grep temperature_celsius|grep -v '#' 96 | mqtt_temperature_celsius_last_pushed_timestamp{instance="livingroom",job="ESP8266"} 1.525185129171293e+09 97 | mqtt_temperature_celsius_push_total{instance="livingroom",job="ESP8266"} 1 98 | temperature_celsius{instance="livingroom",job="ESP8266"} 20.2 99 | ``` 100 | 101 | ## A note about the prometheus config 102 | 103 | If you use `job` and `instance` labels, please refer to the [pushgateway 104 | exporter 105 | documentation](https://github.com/prometheus/pushgateway#about-the-job-and-instance-labels). 106 | 107 | TL;DR: you should set `honor_labels: true` in the scrape config. 108 | 109 | [circleci]:https://circleci.com/gh/inuits/mqttgateway 110 | [goreportcard]:https://goreportcard.com/report/github.com/inuits/mqttgateway 111 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.1 2 | -------------------------------------------------------------------------------- /exporter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MQTTGateway authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "strconv" 19 | "strings" 20 | "sync" 21 | 22 | mqtt "github.com/eclipse/paho.mqtt.golang" 23 | "github.com/prometheus/client_golang/prometheus" 24 | "github.com/prometheus/common/log" 25 | ) 26 | 27 | var mutex sync.RWMutex 28 | 29 | type mqttExporter struct { 30 | client mqtt.Client 31 | connectDesc *prometheus.Desc 32 | metrics map[string]*prometheus.GaugeVec // hold the metrics collected 33 | counterMetrics map[string]*prometheus.CounterVec // hold the metrics collected 34 | metricsLabels map[string][]string // holds the labels set for each metric to be able to invalidate them 35 | } 36 | 37 | func newMQTTExporter() *mqttExporter { 38 | // create a MQTT client 39 | options := mqtt.NewClientOptions() 40 | log.Infof("Connecting to %v", *brokerAddress) 41 | options.AddBroker(*brokerAddress) 42 | if *username != "" { 43 | options.SetUsername(*username) 44 | } 45 | if *password != "" { 46 | options.SetPassword(*password) 47 | } 48 | if *clientID != "" { 49 | options.SetClientID(*clientID) 50 | } 51 | m := mqtt.NewClient(options) 52 | if token := m.Connect(); token.Wait() && token.Error() != nil { 53 | log.Fatal(token.Error()) 54 | } 55 | 56 | // create an exporter 57 | c := &mqttExporter{ 58 | client: m, 59 | connectDesc: prometheus.NewDesc( 60 | prometheus.BuildFQName(progname, "mqtt", "connected"), 61 | "Is the exporter connected to mqtt broker", 62 | nil, 63 | nil), 64 | } 65 | 66 | c.metrics = make(map[string]*prometheus.GaugeVec) 67 | c.counterMetrics = make(map[string]*prometheus.CounterVec) 68 | c.metricsLabels = make(map[string][]string) 69 | 70 | m.Subscribe(*topic, 2, c.receiveMessage()) 71 | 72 | return c 73 | } 74 | 75 | func (c *mqttExporter) Describe(ch chan<- *prometheus.Desc) { 76 | mutex.RLock() 77 | defer mutex.RUnlock() 78 | ch <- c.connectDesc 79 | for _, m := range c.counterMetrics { 80 | m.Describe(ch) 81 | } 82 | for _, m := range c.metrics { 83 | m.Describe(ch) 84 | } 85 | } 86 | 87 | func (c *mqttExporter) Collect(ch chan<- prometheus.Metric) { 88 | mutex.RLock() 89 | defer mutex.RUnlock() 90 | connected := 0. 91 | if c.client.IsConnectionOpen() { 92 | connected = 1. 93 | } 94 | ch <- prometheus.MustNewConstMetric( 95 | c.connectDesc, 96 | prometheus.GaugeValue, 97 | connected, 98 | ) 99 | for _, m := range c.counterMetrics { 100 | m.Collect(ch) 101 | } 102 | for _, m := range c.metrics { 103 | m.Collect(ch) 104 | } 105 | } 106 | 107 | func (e *mqttExporter) receiveMessage() func(mqtt.Client, mqtt.Message) { 108 | return func(c mqtt.Client, m mqtt.Message) { 109 | mutex.Lock() 110 | defer mutex.Unlock() 111 | t := strings.TrimPrefix(m.Topic(), *prefix) 112 | t = strings.TrimPrefix(t, "/") 113 | parts := strings.Split(t, "/") 114 | if len(parts)%2 == 0 { 115 | log.Warnf("Invalid topic: %s: odd number of levels, ignoring", t) 116 | return 117 | } 118 | metric_name := parts[len(parts)-1] 119 | pushed_metric_name := fmt.Sprintf("mqtt_%s_last_pushed_timestamp", metric_name) 120 | count_metric_name := fmt.Sprintf("mqtt_%s_push_total", metric_name) 121 | metric_labels := parts[:len(parts)-1] 122 | var labels []string 123 | labelValues := prometheus.Labels{} 124 | log.Debugf("Metric name: %v", metric_name) 125 | for i, l := range metric_labels { 126 | if i%2 == 1 { 127 | continue 128 | } 129 | labels = append(labels, l) 130 | labelValues[l] = metric_labels[i+1] 131 | } 132 | 133 | invalidate := false 134 | if _, ok := e.metricsLabels[metric_name]; ok { 135 | l := e.metricsLabels[metric_name] 136 | if !compareLabels(l, labels) { 137 | log.Warnf("Label names are different: %v and %v, invalidating existing metric", l, labels) 138 | prometheus.Unregister(e.metrics[metric_name]) 139 | invalidate = true 140 | } 141 | } 142 | e.metricsLabels[metric_name] = labels 143 | if _, ok := e.metrics[metric_name]; ok && !invalidate { 144 | log.Debugf("Metric already exists") 145 | } else { 146 | log.Debugf("Creating new metric: %s %v", metric_name, labels) 147 | e.metrics[metric_name] = prometheus.NewGaugeVec( 148 | prometheus.GaugeOpts{ 149 | Name: metric_name, 150 | Help: "Metric pushed via MQTT", 151 | }, 152 | labels, 153 | ) 154 | e.counterMetrics[count_metric_name] = prometheus.NewCounterVec( 155 | prometheus.CounterOpts{ 156 | Name: count_metric_name, 157 | Help: fmt.Sprintf("Number of times %s was pushed via MQTT", metric_name), 158 | }, 159 | labels, 160 | ) 161 | e.metrics[pushed_metric_name] = prometheus.NewGaugeVec( 162 | prometheus.GaugeOpts{ 163 | Name: pushed_metric_name, 164 | Help: fmt.Sprintf("Last time %s was pushed via MQTT", metric_name), 165 | }, 166 | labels, 167 | ) 168 | } 169 | if s, err := strconv.ParseFloat(string(m.Payload()), 64); err == nil { 170 | e.metrics[metric_name].With(labelValues).Set(s) 171 | e.metrics[pushed_metric_name].With(labelValues).SetToCurrentTime() 172 | e.counterMetrics[count_metric_name].With(labelValues).Inc() 173 | } 174 | } 175 | } 176 | 177 | func compareLabels(a, b []string) bool { 178 | if len(a) != len(b) { 179 | return false 180 | } 181 | for i, v := range a { 182 | if v != b[i] { 183 | return false 184 | } 185 | } 186 | return true 187 | } 188 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/inuits/mqttgateway 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/eclipse/paho.mqtt.golang v1.2.0 7 | github.com/prometheus/client_golang v1.6.0 8 | github.com/prometheus/common v0.10.0 9 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= 2 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 4 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= 8 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 9 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= 10 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 11 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 12 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 13 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 14 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 15 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 16 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0= 21 | github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= 22 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 23 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 24 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 25 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 26 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 27 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 28 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 30 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 31 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 32 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 33 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 34 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 35 | github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= 36 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 37 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 38 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 39 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 40 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 42 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 43 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 44 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 45 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 46 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 47 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 48 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 49 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 50 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 51 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 52 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 53 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 54 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 55 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 57 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 58 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 59 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 60 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 61 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 62 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 63 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 64 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 65 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 66 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 67 | github.com/prometheus/client_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mglnMrWfyrw6A= 68 | github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4= 69 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 70 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 71 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 72 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 73 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 74 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 75 | github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= 76 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 77 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 78 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 79 | github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= 80 | github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 81 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 82 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 83 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 84 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 85 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 86 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 87 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 88 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 89 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 90 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 91 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 92 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 93 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 94 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= 95 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 96 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 98 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 99 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 100 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 101 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 102 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 103 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 104 | golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= 105 | golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 107 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 108 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 109 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 110 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 111 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 112 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 113 | google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= 114 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 115 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 116 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 117 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 118 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 119 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 120 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 121 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 122 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 123 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 124 | gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= 125 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 126 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MQTTGateway authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | mqtt "github.com/eclipse/paho.mqtt.golang" 18 | "github.com/prometheus/common/log" 19 | ) 20 | 21 | func init() { 22 | mqtt.ERROR = &errorLogger{} 23 | mqtt.CRITICAL = &errorLogger{} 24 | mqtt.WARN = &warnLogger{} 25 | mqtt.DEBUG = &debugLogger{} 26 | } 27 | 28 | type debugLogger struct{} 29 | 30 | func (*debugLogger) Println(v ...interface{}) { 31 | log.Debug(v...) 32 | } 33 | 34 | func (*debugLogger) Printf(s string, v ...interface{}) { 35 | log.Debugf(s, v...) 36 | } 37 | 38 | type warnLogger struct{} 39 | 40 | func (*warnLogger) Println(v ...interface{}) { 41 | log.Warn(v...) 42 | } 43 | 44 | func (*warnLogger) Printf(s string, v ...interface{}) { 45 | log.Warnf(s, v...) 46 | } 47 | 48 | type errorLogger struct{} 49 | 50 | func (*errorLogger) Println(v ...interface{}) { 51 | log.Error(v...) 52 | } 53 | 54 | func (*errorLogger) Printf(s string, v ...interface{}) { 55 | log.Errorf(s, v...) 56 | } 57 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MQTTGateway authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "net/http" 18 | 19 | "github.com/prometheus/client_golang/prometheus" 20 | "github.com/prometheus/client_golang/prometheus/promhttp" 21 | "github.com/prometheus/common/log" 22 | "github.com/prometheus/common/version" 23 | "gopkg.in/alecthomas/kingpin.v2" 24 | ) 25 | 26 | var ( 27 | listenAddress = kingpin.Flag("web.listen-address", "Address on which to expose metrics and web interface.").Default(":9337").String() 28 | metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String() 29 | brokerAddress = kingpin.Flag("mqtt.broker-address", "Address of the MQTT broker.").Envar("MQTT_BROKER_ADDRESS").Default("tcp://localhost:1883").String() 30 | topic = kingpin.Flag("mqtt.topic", "MQTT topic to subscribe to").Envar("MQTT_TOPIC").Default("prometheus/#").String() 31 | prefix = kingpin.Flag("mqtt.prefix", "MQTT topic prefix to remove when creating metrics").Envar("MQTT_PREFIX").Default("prometheus").String() 32 | username = kingpin.Flag("mqtt.username", "MQTT username").Envar("MQTT_USERNAME").String() 33 | password = kingpin.Flag("mqtt.password", "MQTT password").Envar("MQTT_PASSWORD").String() 34 | clientID = kingpin.Flag("mqtt.clientid", "MQTT client ID").Envar("MQTT_CLIENT_ID").String() 35 | progname = "mqttgateway" 36 | ) 37 | 38 | func main() { 39 | log.AddFlags(kingpin.CommandLine) 40 | kingpin.Parse() 41 | 42 | prometheus.MustRegister(version.NewCollector("mqttgateway")) 43 | prometheus.MustRegister(newMQTTExporter()) 44 | 45 | http.Handle(*metricsPath, promhttp.Handler()) 46 | log.Infoln("Listening on", *listenAddress) 47 | err := http.ListenAndServe(*listenAddress, nil) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | } 52 | --------------------------------------------------------------------------------