├── .gitignore ├── Dockerfile ├── README.md ├── go.mod ├── go.sum ├── logo.svg ├── main.go ├── prometheus.yml └── relay ├── config.go └── relay.go /.gitignore: -------------------------------------------------------------------------------- 1 | /promqtt 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 as promqtt 2 | WORKDIR /promqtt 3 | COPY go.mod go.sum . 4 | RUN go mod download 5 | COPY . . 6 | RUN CGO_ENABLED=0 go build -ldflags='-extldflags="-static"' . 7 | 8 | FROM alpine 9 | COPY --from=promqtt /promqtt/promqtt /usr/bin/promqtt 10 | ENTRYPOINT ["/usr/bin/promqtt"] 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./logo.svg) 2 | 3 | # Promqtt: Prometheus ⟷ MQTT Bridge 4 | 5 | Promqtt makes Prometheus MQTT capable in a truly generic way. 6 | 7 | It has no assumptions on message payloads or topic layout. 8 | 9 | ## Installation 10 | 11 | Promqtt is best used with Docker, an image is available at [`shorez/promqtt`](https://hub.docker.com/r/shorez/promqtt). 12 | 13 | If that is no option, Promqtt can also be installed from source using Go: 14 | 15 | ```sh 16 | $ go install github.com/sh0rez/promqtt@latest 17 | ``` 18 | 19 | Promqtt accepts very little configuration which is exposed as command line flags: 20 | 21 | ```bash 22 | promqtt [flags] 23 | ``` 24 | 25 | **Arguments**: 26 | 27 | - `broker`: Any MQTT broker (e.g. `myBroker:1883`) 28 | 29 | **Flags**: 30 | 31 | - `--client-id`: The MQTT Client ID to use. Defaults to `promqtt@HOSTNAME` 32 | - `--listen`: The address to bind the HTTP server to. Defaults to `:9337` 33 | 34 | Above list may be incomplete, consult `promqtt --help` for what your current build supports 35 | 36 | ## Usage 37 | 38 | In similar fashion to the `blackbox_exporter` and `snmp_exporter`, Promqtt acts 39 | as a general purpose relay, meaning you do not configure specific topics, etc. 40 | ahead of time. 41 | 42 | Instead, these must be provided by Prometheus while scraping. Using clever 43 | relabeling it makes Prometheus look like it supported MQTT itself: 44 | 45 | ### Basic 46 | 47 | The most basic use-case assumes a sender literally publishes `float64` 48 | compatible values (any kind of number really) to some MQTT topic: 49 | 50 | > **Topic**: `diox/06FE2A3/co2` 51 | > **Payload**: `42.0` 52 | 53 | A possible `scrape_configs` entry for looks like this: 54 | 55 | ```yaml 56 | - job_name: diox 57 | metrics_path: /mqtt # Promqtt serves MQTT metrics under this path 58 | static_configs: 59 | - targets: 60 | - diox/.+/.+ # regular expression matching some MQTT topic 61 | relabel_configs: 62 | # copy above address (diox/...) into the "topic" URL parameter 63 | - source_labels: [__address__] 64 | target_label: __param_topic 65 | # and also to the instance label 66 | - source_labels: [__param_topic] 67 | target_label: instance 68 | # make Prometheus scrape Promqtt at ADDRESS 69 | - target_label: __address__ 70 | replacement: ADDRESS:9337 71 | ``` 72 | 73 | Considering above MQTT message, this would yield the following series: 74 | 75 | ```bash 76 | # TYPE diox_06FE2A3_co2 gauge 77 | diox_06FE2A3_co2{topic="diox/06FE2A3/co2", instance="diox/.+/.+"} 42.0 78 | ``` 79 | 80 | ### Regex 81 | 82 | Obviously, not all MQTT devices publish perfect float values to distinct topics. 83 | To accompany for that, Promqtt lets you optionally a regular expression using 84 | named capture groups to extract parts of messages: 85 | 86 | > **Topic**: `tele/tv/SENSOR` 87 | > **Payload**: 88 | > 89 | > ```js 90 | > {"Time":"2021-06-27T17:38:56","ENERGY":{"TotalStartTime":"2021-01-16T13:12:08","Total":56.040,"Yesterday":0.923,"Today":0.536,"Period":1,"Power":10,"ApparentPower":32,"ReactivePower":30,"Factor":0.32,"Voltage":233,"Current":0.136}} 91 | > ``` 92 | 93 | We could use the following regular expression, to parse out the `Power`, 94 | `Voltage` and `Current` fields 95 | ([Explanation](https://regex101.com/r/igWHKh/1/)): 96 | 97 | ```regex 98 | "Power":(?P<_power>[\d.]+).*"Voltage":(?P<_voltage>[\d.]+).*"Current":(?P<_current>[\d.]+) 99 | ``` 100 | 101 | This gives us the following `scrape_configs` entry: 102 | 103 | ```yaml 104 | - job_name: tasmota 105 | metrics_path: /mqtt # Promqtt serves MQTT metrics under this path 106 | static_configs: 107 | - targets: 108 | - tele/.+/SENSOR # regular expression matching some MQTT topic 109 | params: 110 | regex: 111 | - '"Total": *(?P<_total_kWh>[\d.]+).*"Power": *(?P<_power_watt>[\d.]+).*"ApparentPower": *(?P<_apparent_power_VA>[\d.]+).*"ReactivePower": *(?P<_reactive_power_VAr>[\d.]+).*"Factor": *(?P<_factor>[\d.]+).*"Voltage": *(?P<_voltage>[\d.]+).*"Current": *(?P<_current>[\d.]+)' 112 | 113 | relabel_configs: 114 | # copy above address (tele/...) into the "topic" URL parameter 115 | - source_labels: [__address__] 116 | target_label: __param_topic 117 | # and also to the instance label 118 | - source_labels: [__param_topic] 119 | target_label: instance 120 | # make Prometheus scrape Promqtt at ADDRESS 121 | - target_label: __address__ 122 | replacement: ADDRESS:9337 123 | ``` 124 | 125 | The names of the capture groups (`_power`, `_voltage` and `_current`) are 126 | appended to the generated series name, yielding the following: 127 | 128 | ```bash 129 | # TYPE tele_tv_SENSOR_power gauge 130 | tele_tv_SENSOR_power{topic="tele/tv/SENSOR", instance="tele/.+/SENSOR"} 10 131 | 132 | # TYPE tele_tv_SENSOR_voltage gauge 133 | tele_tv_SENSOR_voltage{topic="tele/tv/SENSOR", instance="tele/.+/SENSOR"} 233 134 | 135 | # TYPE tele_tv_SENSOR_current gauge 136 | tele_tv_SENSOR_current{topic="tele/tv/SENSOR", instance="tele/.+/SENSOR"} 0.136 137 | ``` 138 | 139 | ### Series renaming 140 | 141 | Above series obviously don't quite comply with the [Prometheus naming 142 | conventions](https://prometheus.io/docs/practices/naming/). 143 | 144 | But again, this can be fixed by adding some `metric_relabel_configs`. Continuing above example: 145 | 146 | ```yaml 147 | - job_name: tasmota 148 | # ... 149 | metric_relabel_configs: 150 | # rename series to tasmota_ style 151 | - source_labels: [__name__] 152 | target_label: __name__ 153 | regex: tele.+_SENSOR_(.+) 154 | replacement: tasmota_$1 155 | # extract device from topic to its own label 156 | - source_labels: [topic] 157 | target_label: dev 158 | regex: tele/(.+)/.+ 159 | ``` 160 | 161 | Afterwards, we get the following: 162 | 163 | ```bash 164 | # TYPE tele_tv_SENSOR_power gauge 165 | tasmota_power{topic="tele/tv/SENSOR", instance="tele/.+/SENSOR", dev="tv"} 10 166 | 167 | # TYPE tele_tv_SENSOR_voltage gauge 168 | tasmota_voltage{topic="tele/tv/SENSOR", instance="tele/.+/SENSOR", dev="tv"} 233 169 | 170 | # TYPE tele_tv_SENSOR_current gauge 171 | tasmota_current{topic="tele/tv/SENSOR", instance="tele/.+/SENSOR", dev="tv"} 0.136 172 | ``` 173 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sh0rez/promqtt 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/eclipse/paho.mqtt.golang v1.3.5 7 | github.com/prometheus/client_golang v1.11.0 8 | github.com/spf13/pflag v1.0.5 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 4 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 7 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 8 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 9 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 10 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 11 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 12 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y= 16 | github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= 17 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 18 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 19 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 20 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 21 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 22 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 23 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 24 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 25 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 28 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 29 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 30 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 31 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 32 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 33 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 34 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 35 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 36 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 37 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 38 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 39 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 40 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 41 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 42 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 43 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 44 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 45 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 46 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 47 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 48 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 49 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 50 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 51 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 52 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 53 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 54 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 55 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 56 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 57 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 58 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 59 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 60 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 61 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 62 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 63 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 64 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 65 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 66 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 67 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 69 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 70 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 71 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 72 | github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= 73 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 74 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 75 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 76 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 77 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 78 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 79 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 80 | github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= 81 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 82 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 83 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 84 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 85 | github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= 86 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 87 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 88 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 89 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 90 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 91 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 92 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 93 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 94 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 95 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 96 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 97 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 98 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 99 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 100 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 101 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 102 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 103 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 104 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 105 | golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 106 | golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= 107 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 108 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 109 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 110 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 111 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 112 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 113 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 114 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 115 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 116 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= 124 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 125 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 126 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 127 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 128 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 129 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 130 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 131 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 132 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 133 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 134 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 135 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 136 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 137 | google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ= 138 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 139 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 140 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 141 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 142 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 143 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 144 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 145 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 146 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 147 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/prometheus/client_golang/prometheus/promhttp" 10 | "github.com/sh0rez/promqtt/relay" 11 | "github.com/spf13/pflag" 12 | ) 13 | 14 | func main() { 15 | cfg := relay.DefaultConfig() 16 | pflag.StringVar(&cfg.ClientID, "client-id", cfg.ClientID, "mqtt client id") 17 | pflag.StringVar(&cfg.Listen, "listen", cfg.Listen, "address to listen on") 18 | pflag.BoolVarP(&cfg.Verbose, "verbose", "v", cfg.Verbose, "verbose logging") 19 | 20 | pflag.StringVarP(&cfg.Username, "username", "u", cfg.Username, "mqtt username") 21 | pflag.StringVarP(&cfg.Password, "password", "p", cfg.Password, "mqtt password") 22 | 23 | pflag.DurationVar(&cfg.PingTimeout, "ping-timeout", cfg.PingTimeout, "time to wait for PING response from broker") 24 | 25 | pflag.Usage = func() { 26 | fmt.Printf("Usage: %s [flags]\n", os.Args[0]) 27 | pflag.PrintDefaults() 28 | os.Exit(1) 29 | } 30 | 31 | pflag.Parse() 32 | if len(pflag.Args()) != 1 { 33 | pflag.Usage() 34 | } 35 | 36 | cfg.Broker = pflag.Arg(0) 37 | 38 | r, err := relay.New(cfg) 39 | if err != nil { 40 | log.Fatalln(err) 41 | } 42 | 43 | http.Handle("/mqtt", r) 44 | http.Handle("/metrics", promhttp.Handler()) 45 | 46 | log.Printf("listening at %s", cfg.Listen) 47 | if err := http.ListenAndServe(cfg.Listen, nil); err != nil { 48 | log.Fatalln(err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | scrape_configs: 4 | - job_name: "mqtt" 5 | metrics_path: /mqtt 6 | static_configs: 7 | - targets: 8 | - diox/+/+ 9 | relabel_configs: 10 | - source_labels: [__address__] 11 | target_label: __param_topic 12 | - source_labels: [__param_topic] 13 | target_label: instance 14 | - target_label: __address__ 15 | replacement: 127.0.0.1:9337 16 | metric_relabel_configs: 17 | - source_labels: [topic] 18 | target_label: dev 19 | regex: diox/(.+)/.+ 20 | - source_labels: [topic] 21 | target_label: __name__ 22 | regex: diox/.+/(.+) 23 | replacement: "diox_$1" 24 | - regex: topic 25 | action: labeldrop 26 | -------------------------------------------------------------------------------- /relay/config.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | mqtt "github.com/eclipse/paho.mqtt.golang" 8 | ) 9 | 10 | type Config struct { 11 | Broker string 12 | ClientID string 13 | 14 | Listen string 15 | Verbose bool 16 | 17 | Username string 18 | Password string 19 | 20 | PingTimeout time.Duration 21 | } 22 | 23 | func DefaultConfig() Config { 24 | opts := mqtt.NewClientOptions() 25 | 26 | clientID := "promqtt" 27 | if hostname, err := os.Hostname(); err == nil { 28 | clientID += "@" + hostname 29 | } 30 | 31 | return Config{ 32 | ClientID: clientID, 33 | 34 | Listen: ":9337", 35 | Verbose: false, 36 | 37 | PingTimeout: opts.PingTimeout, 38 | } 39 | } 40 | 41 | func (c Config) MQTT() *mqtt.ClientOptions { 42 | return mqtt.NewClientOptions(). 43 | AddBroker(c.Broker). 44 | SetClientID(c.ClientID). 45 | SetUsername(c.Username). 46 | SetPassword(c.Password). 47 | SetPingTimeout(c.PingTimeout) 48 | } 49 | -------------------------------------------------------------------------------- /relay/relay.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | 13 | mqtt "github.com/eclipse/paho.mqtt.golang" 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | ) 17 | 18 | func New(cfg Config) (*Relay, error) { 19 | mqtt.ERROR = log.New(os.Stderr, "", log.Flags()) 20 | if cfg.Verbose { 21 | mqtt.DEBUG = log.New(os.Stderr, "debug:", log.Flags()) 22 | } 23 | 24 | r := &Relay{ 25 | data: make(map[string]string), 26 | } 27 | 28 | opts := cfg.MQTT() 29 | if len(opts.Servers) == 0 { 30 | return nil, fmt.Errorf("must specify broker url") 31 | } 32 | 33 | opts.SetConnectionLostHandler(func(c mqtt.Client, e error) { 34 | log.Printf("connection lost: %s", e.Error()) 35 | }) 36 | 37 | opts.SetOnConnectHandler(func(c mqtt.Client) { 38 | tok := c.Subscribe("#", 0, r.HandleMQTT) 39 | 40 | if tok.Wait() && tok.Error() != nil { 41 | log.Fatalf("failed to subscribe to all topics: %s", tok.Error()) 42 | } 43 | log.Println("subscribed to '#'") 44 | }) 45 | 46 | if tok := mqtt.NewClient(opts).Connect(); tok.Wait() && tok.Error() != nil { 47 | return nil, tok.Error() 48 | } 49 | 50 | return r, nil 51 | } 52 | 53 | type Relay struct { 54 | mu sync.RWMutex 55 | data map[string]string 56 | } 57 | 58 | func (rl *Relay) ServeHTTP(w http.ResponseWriter, r *http.Request) { 59 | topic := r.URL.Query().Get("topic") 60 | if topic == "" { 61 | http.Error(w, "must pass topic", http.StatusBadRequest) 62 | return 63 | } 64 | topicExp, err := regexp.Compile(topic) 65 | if err != nil { 66 | http.Error(w, err.Error(), http.StatusBadRequest) 67 | return 68 | } 69 | 70 | regex := r.URL.Query().Get("regex") 71 | if regex == "" { 72 | regex = `(.*)` 73 | } 74 | 75 | regexExp, err := regexp.Compile(regex) 76 | if err != nil { 77 | http.Error(w, err.Error(), http.StatusBadRequest) 78 | return 79 | } 80 | 81 | reg := rl.metrics(topicExp, regexExp) 82 | promhttp.HandlerFor(reg, promhttp.HandlerOpts{}).ServeHTTP(w, r) 83 | } 84 | 85 | func (rl *Relay) metrics(topicExp, matcher *regexp.Regexp) *prometheus.Registry { 86 | reg := prometheus.NewRegistry() 87 | 88 | rl.mu.RLock() 89 | defer rl.mu.RUnlock() 90 | 91 | for topic, data := range rl.data { 92 | if !topicExp.MatchString(topic) { 93 | continue 94 | } 95 | 96 | matches := matcher.FindStringSubmatch(data) 97 | if len(matches) == 0 { 98 | continue 99 | } 100 | 101 | for i, name := range matcher.SubexpNames() { 102 | if i == 0 { 103 | continue 104 | } 105 | 106 | value, err := strconv.ParseFloat(matches[i], 64) 107 | if err != nil { 108 | log.Printf("failed to parse '%s' from '%s' as float64", data, topic) 109 | continue 110 | } 111 | 112 | g := prometheus.NewGauge(prometheus.GaugeOpts{ 113 | Name: metricName(topic) + name, 114 | ConstLabels: prometheus.Labels{"topic": topic}, 115 | }) 116 | g.Set(value) 117 | 118 | if err := reg.Register(g); err != nil { 119 | log.Println(err) 120 | } 121 | } 122 | } 123 | 124 | return reg 125 | } 126 | 127 | func (rl *Relay) HandleMQTT(c mqtt.Client, m mqtt.Message) { 128 | rl.mu.Lock() 129 | rl.data[m.Topic()] = string(m.Payload()) 130 | rl.mu.Unlock() 131 | } 132 | 133 | // metricName sanitizes a string so that it becomes a valid Prometheus metric 134 | // name by replacing all illegal characters with underscores (_) 135 | func metricName(s string) string { 136 | return strings.ReplaceAll(s, "/", "_") 137 | } 138 | --------------------------------------------------------------------------------