Eerste versie

This commit is contained in:
2022-10-28 08:30:22 +02:00
parent de75a01de7
commit 4d8224d1cc
31 changed files with 1638 additions and 2 deletions

0
tests/__init__.py Normal file
View File

74
tests/readme.md Normal file
View File

@@ -0,0 +1,74 @@
# Tests with pytest
make sure pytest is installed `pip install -r requirements-dev.txt`
run `pytest -s -o log_cli=true -o log_cli_level="DEBUG"` from the repository root
## test_mqtt_explorer.py:test_update_metrics
This test loads test mqtt data from a file and feeds it into mqtt_exporter and check if expected results are recorded in the prometheus client.
### directory structure
Test data is loaded from following directory structure:
```
./tests/
./test_data/
./test1/
conf.yaml
mqtt_msg.csv
./test2/
conf.yaml
mqtt_msg.csv
./test_xyz/
conf.yaml
mqtt_msg.csv
./tmp_data
[metric_test...##,txt]
```
In `test-data` each subfolder (e.g. `test1`, `test_bla`) contains a separate set of test data. There is no naming convention for folders. The could be descriptive like `test_for_issue1234`. Avoid any special characters and white spaces.
Files:
- `conf.yaml`: a config like a config for mqtt_exporter itself, but only the `metrics` part and a new optional attribute `timescale` is read from it.
- `mqtt_msg.csv`: fake mqtt data, format description see below.
`tmp-data` contains prometheus scrape output from after each processed mqtt msg data. This folder will be cleaned before each test run.
### mqtt_msg.csv file format
`mqtt_msg.csv` is a CSV file with `;` as delimiter and `'` as quotation character.
Following Column looks are expected:
```
in_topic;in_payload;out_name;out_labels;out_value;delay;assert
```
- `in_topic`: topic as from mqtt server
- `in_payload`: payload from mqtt server as string (will be converted byte array)
- `out_name`: metric name without any suffix like `total`, `sum`, `bucket`, ...
- `out_labels`: labels notes as a JSON string including the topic.
- `out_value`: expected value for simple metrics like gauge it is a number. For other metrics is is a JSON string with expected values per suffix e.g. `{"_count": 10, "_sum": 85.55, "_bucket": 10}`
- `delay`: seconds delay until the next mqtt msg is processed. The `timescale` config attribute speed up/slow down the delay. A time scale of 0 means no default, a timescale of 1 means realtime. Default timescale = 0
- `assert`: `True/False`. Specify if the test should pass or not. In most cases this should be `True`
Metric type `Histogram` special handling here as it will log a `$(metric_name)_bucket` metric for each bucket with a reserved label `le` in the meaning of _less or equal_. Specify `le` for one bucket and set the expected count to the `bucket` attribute in the `out_value` JSON. See examples in `test1`.
For sample data see existing tests above.
### Gather test data from live environment
If logging level is set to `debug` the log will contain some lines that should be already correct formatted to be placed in a `mqtt_msq.csv`.
they look like this:
```
2021-08-08 22:24:36,996 DEBUG: TEST_DATA: fhem/Terrasse/TermPearl02/humidity; 21.0; fhem_humidity_percent; {"location": "paz", "topic": "fhem/paz/TermPearl01/humidity"}; 17.0; 0; True
2021-08-08 22:24:30,601 DEBUG: TEST_DATA: fhem/Terrasse/TerrasseWeiss/humidity; 20.0; fhem_humidity_percent; {"location": "paz", "topic": "fhem/paz/TermPearl01/humidity"}; 17.0; 0; True
2021-08-08 22:24:27,097 DEBUG: TEST_DATA: fhem/Garten/TermFetanten01/temperature; 16.7; fhem_temperature_celsius; {"location": "Terrasse", "topic": "fhem/Terrasse/TerrasseWeiss/temperature"}; 16.0; 0; True
2021-08-08 22:23:58,831 DEBUG: TEST_DATA: fhem/paz/TermPearl01/humidity; 17.0; fhem_humidity_percent; {"location": "paz", "topic": "fhem/paz/TermPearl01/humidity"}; 17.0; 0; True
```
Tips:
- remove `.* DEBUG: TEST_DATA: `.
- Make sure the `mqtt_msg.csv` contains as first line the headers given above.
- The captured data won't fit if the `payload/__value__` has been replaced by a label_config. Please set `in_payload` to the correct value manually. An example for this exception is the `- name: 'mqtt_broker_version'` metric from the example configurations.
- put the data in a new subfolder in the `test_data` dir. Copy also the config file from the live environment to this folder (you should remove the `mqtt` part from it. Make also sure the recorded data don't contain sensitive data).
- Create a PR and share the test data, as this will allow all developers to verify code changes.

View File

@@ -0,0 +1,76 @@
# Logging
logging:
# logfile: 'conf/mqttexperter.log' # Optional default '' (stdout)
level: 'debug' # Optional default 'info'
timescale: 0
# Metric definitions
metrics:
- name: 'ftp_transferred_bytes'
help: 'data transferred in bytes pe file'
type: 'summary'
topic: 'ftp/+/transferred'
label_configs:
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ["__msg_topic__"]
regex: "ftp/([^/]+).*"
target_label: "file"
replacement: '\1'
action: "replace"
- name: 'network_ping_ms'
help: 'ping response in ms'
type: 'histogram'
topic: 'network/+/+/ping'
buckets: '0.5,5,10'
label_configs:
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ["__msg_topic__"]
regex: "network/([^/]+).*"
target_label: "network"
replacement: '\1'
action: "replace"
- source_labels: ["__msg_topic__"]
regex: "network/[^/]+/([^/]+).*"
target_label: "server"
replacement: '\1'
action: "replace"
- name: "fhem_temperature_celsius"
help: "443 Mhz Sensors, Temperature in C"
type: "gauge"
topic: "fhem/+/+/temperature"
label_configs:
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ["__msg_topic__"]
regex: "fhem/([^/]+).*"
target_label: "location"
replacement: '\1'
action: "replace"
- name: "fhem_humidity_percent"
help: "443 Mhz Sensors, Humidity in %"
type: "gauge"
topic: "fhem/+/+/humidity"
label_configs:
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ["__msg_topic__"]
regex: "fhem/([^/]+).*"
target_label: "location"
replacement: '\1'
action: "replace"
- name: "fhem_rain_mm"
help: "443 Mhz Sensors, rain in mm/m2"
type: "counter"
topic: "fhem/+/+/rain_total"
label_configs:
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ["__msg_topic__"]
regex: "fhem/([^/]+).*"
target_label: "location"
replacement: '\1'
action: "replace"

View File

@@ -0,0 +1,26 @@
in_topic;in_payload;out_name;out_labels;out_value;delay;assert
fhem/Terrasse/TermPearl02/temperature;18;fhem_temperature_celsius;{"location": "Terrasse","topic": "fhem/Terrasse/TermPearl02/temperature"};18;1;True
fhem/Terrasse/TermPearl02/humidity;21;fhem_humidity_percent;{"location": "Terrasse","topic": "fhem/Terrasse/TermPearl02/humidity"};21;2;True
fhem/Garten/TermFetanten01/humidity;79;fhem_humidity_percent;{"location": "Garten","topic": "fhem/Garten/TermFetanten01/humidity"};79;2;True
fhem/Garten/rainmeter01/rain_total;134.8;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};134.8;4;True
fhem/Terrasse/TermPearl02/temperature;22;fhem_temperature_celsius;{"location": "Terrasse","topic": "fhem/Terrasse/TermPearl02/temperature"};22;5;True
fhem/Terrasse/TermPearl02/humidity;24.3;fhem_humidity_percent;{"location": "Terrasse","topic": "fhem/Terrasse/TermPearl02/humidity"};24.3;1;True
fhem/Garten/TermFetanten01/humidity;79;fhem_humidity_percent;{"location": "Garten","topic": "fhem/Garten/TermFetanten01/humidity"};79;2;True
fhem/Garten/rainmeter01/rain_total;11.1;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};145.9;3;True
fhem/Garten/rainmeter01/rain_total;10;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};155.9;5;True
network/vlan11/srv01.local/ping;2;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "5.0"};{"_count": 1, "_sum": 2, "_bucket": 1};2;True
network/vlan11/srv01.local/ping;4;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "5.0"};{"_count": 2, "_sum": 6, "_bucket": 2};6;True
network/vlan11/srv01.local/ping;7;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "10.0"};{"_count": 3, "_sum": 13, "_bucket": 3};1;True
network/vlan11/srv01.local/ping;0.4;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "0.5"};{"_count": 4, "_sum": 13.4, "_bucket": 1};4;True
network/vlan11/srv01.local/ping;20;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "+Inf"};{"_count": 5, "_sum": 33.4, "_bucket": 5};5;True
network/vlan11/srv01.local/ping;11.1;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "+Inf"};{"_count": 6, "_sum": 44.5, "_bucket": 6};2;True
network/vlan11/srv01.local/ping;5;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "5.0"};{"_count": 7, "_sum": 49.5, "_bucket": 4};4;True
network/vlan11/srv01.local/ping;6;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "10.0"};{"_count": 8, "_sum": 55.5, "_bucket": 6};1;True
network/vlan11/srv01.local/ping;0.05;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "0.5"};{"_count": 9, "_sum": 55.55, "_bucket": 2};4;True
network/vlan11/srv01.local/ping;30;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "+Inf"};{"_count": 10, "_sum": 85.55, "_bucket": 10};5;True
ftp/update.bin/transferred;123;ftp_transferred_bytes;{"file": "update.bin","topic": "ftp/update.bin/transferred"};{"_count": 1, "_sum": 123};1;True
ftp/update.bin/transferred;234;ftp_transferred_bytes;{"file": "update.bin","topic": "ftp/update.bin/transferred"};{"_count": 2, "_sum": 357};1;True
ftp/update.bin/transferred;34;ftp_transferred_bytes;{"file": "update.bin","topic": "ftp/update.bin/transferred"};{"_count": 3, "_sum": 391};1;True
ftp/update.bin/transferred;45;ftp_transferred_bytes;{"file": "update.bin","topic": "ftp/update.bin/transferred"};{"_count": 4, "_sum": 436};1;True
ftp/update.bin/transferred;89;ftp_transferred_bytes;{"file": "update.bin","topic": "ftp/update.bin/transferred"};{"_count": 5, "_sum": 525};1;True
ftp/update.bin/transferred;11111;ftp_transferred_bytes;{"file": "update.bin","topic": "ftp/update.bin/transferred"};{"_count": 6, "_sum": 11636};1;True
Can't render this file because it contains an unexpected character in line 2 and column 68.

View File

@@ -0,0 +1,99 @@
# Config file for MQTT prometheus exporter
# Metric definitions
metrics:
# - name: 'mqtt_broker'
# help: 'System events from broker'
# type: 'gauge'
# topic: '$SYS/broker/#'
# label_configs:
# - source_labels: ['__msg_topic__']
# target_label: '__topic__'
# - source_labels: ['__value__']
# regex: '^(\d+([,.]\d*)?)$|^([,.]\d+)$'
# action: 'keep'
- name: 'mqtt_broker_version'
help: 'Mosquitto version (static)'
type: 'gauge'
topic: '$SYS/broker/version'
label_configs:
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ['__value__']
regex: '^\D+((?:\d+[\.]?)+)$'
target_label: 'version'
replacement: '\1'
action: 'replace'
- source_labels: ['__value__']
replacement: '1'
target_label: '__value__'
action: 'replace'
- name: 'mqtt_broker_changeset'
help: 'Mosquitto build changeset (static)'
type: 'gauge'
topic: '$SYS/broker/changeset'
label_configs:
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ['__value__']
target_label: 'changeset'
action: 'replace'
- source_labels: ['__value__']
replacement: '1'
target_label: '__value__'
action: 'replace'
- name: 'mqtt_broker_timestamp'
help: 'Mosquitto build timestamp (static)'
type: 'gauge'
topic: '$SYS/broker/timestamp'
label_configs:
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ['__value__']
target_label: 'timestamp'
action: 'replace'
- source_labels: ['__value__']
replacement: '1'
target_label: '__value__'
action: 'replace'
- name: "fhem_temperature_celsius"
help: "443 Mhz Sensors, Temperature in C"
type: "gauge"
topic: "fhem/+/+/temperature"
label_configs:
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ["__msg_topic__"]
regex: "fhem/([^/]+).*"
target_label: "location"
replacement: '\1'
action: "replace"
- name: "fhem_humidity_percent"
help: "443 Mhz Sensors, Humidity in %"
type: "gauge"
topic: "fhem/+/+/humidity"
label_configs:
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ["__msg_topic__"]
regex: "fhem/([^/]+).*"
target_label: "location"
replacement: '\1'
action: "replace"
- name: "fhem_rain_mm"
help: "443 Mhz Sensors, rain in mm/m2"
type: "counter"
topic: "fhem/+/+/rain_total"
label_configs:
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ["__msg_topic__"]
regex: "fhem/([^/]+).*"
target_label: "location"
replacement: '\1'
action: "replace"

View File

@@ -0,0 +1,15 @@
in_topic;in_payload;out_name;out_labels;out_value;delay;assert
$SYS/broker/version;' 2.0.11'; mqtt_broker_version; {"topic": "$SYS/broker/version", "version": "2.0.11"}; 1.0; 0; True
fhem/Terrasse/TerrasseWeiss/humidity; 20.0; fhem_humidity_percent; {"location": "Terrasse", "topic": "fhem/Terrasse/TerrasseWeiss/humidity"}; 20.0; 0; True
fhem/Terrasse/TermPearl02/temperature; 17.5; fhem_temperature_celsius; {"location": "Terrasse", "topic": "fhem/Terrasse/TermPearl02/temperature"}; 17.5; 0; True
fhem/Terrasse/TermPearl02/humidity; 20.0; fhem_humidity_percent; {"location": "Terrasse", "topic": "fhem/Terrasse/TermPearl02/humidity"}; 20.0; 0; True
fhem/Terrasse/TermPearl02/humidity; 21.0; fhem_humidity_percent; {"location": "Terrasse", "topic": "fhem/Terrasse/TermPearl02/humidity"}; 21.0; 0; True
fhem/Terrasse/TermPearl02/temperature; 17.6; fhem_temperature_celsius; {"location": "Terrasse", "topic": "fhem/Terrasse/TermPearl02/temperature"}; 17.6; 0; True
fhem/Garten/TermFetanten01/humidity; 66.0; fhem_humidity_percent; {"location": "Garten", "topic": "fhem/Garten/TermFetanten01/humidity"}; 66.0; 0; True
fhem/Terrasse/TermPearl02/temperature; 17.5; fhem_temperature_celsius; {"location": "Terrasse", "topic": "fhem/Terrasse/TermPearl02/temperature"}; 17.5; 0; True
$SYS/broker/version;' 2.0.11'; mqtt_broker_version; {"topic": "$SYS/broker/version", "version": "2.0.11"}; 1.0; 0; True
fhem/Garten/rainmeter01/rain_total; 106.426; fhem_rain_mm; {"location": "Garten", "topic": "fhem/Garten/rainmeter01/rain_total"}; {"_total": 106.426, "_created": 1628459492.695393}; 0; True
fhem/Garten/TermFetanten01/humidity; 65.0; fhem_humidity_percent; {"location": "Garten", "topic": "fhem/Garten/TermFetanten01/humidity"}; 65.0; 0; True
fhem/paz/TermPearl01/temperature; 24.3; fhem_temperature_celsius; {"location": "paz", "topic": "fhem/paz/TermPearl01/temperature"}; 24.3; 0; True
fhem/paz/TermPearl01/humidity; 17.0; fhem_humidity_percent; {"location": "paz", "topic": "fhem/paz/TermPearl01/humidity"}; 17.0; 0; True
fhem/Terrasse/TerrasseWeiss/humidity; 20.0; fhem_humidity_percent; {"location": "paz", "topic": "fhem/paz/TermPearl01/humidity"}; 17.0; 0; True
Can't render this file because it contains an unexpected character in line 2 and column 54.

View File

@@ -0,0 +1,22 @@
# Logging
logging:
# logfile: 'conf/mqttexperter.log' # Optional default '' (stdout)
level: 'debug' # Optional default 'info'
timescale: 0
# Metric definitions
metrics:
- name: "fhem_rain_mm"
help: "443 Mhz Sensors, rain in mm/m2"
type: "counter_absolute"
topic: "fhem/+/+/rain_total"
label_configs:
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ["__msg_topic__"]
regex: "fhem/([^/]+).*"
target_label: "location"
replacement: '\1'
action: "replace"

View File

@@ -0,0 +1,13 @@
in_topic;in_payload;out_name;out_labels;out_value;delay;assert
fhem/Garten/rainmeter01/rain_total;4.8;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};4.8;4;True
fhem/Garten/rainmeter01/rain_total;11.1;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};11.1;3;True
fhem/Garten/rainmeter01/rain_total;110;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};110;5;True
fhem/Garten/rainmeter01/rain_total;134.8;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};134.8;4;True
fhem/Garten/rainmeter01/rain_total;211.1;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};211.1;3;True
fhem/Garten/rainmeter01/rain_total;155.9;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};155.9;5;True
fhem/Garten/rainmeter01/rain_total;2134.8;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};2134.8;4;True
fhem/Garten/rainmeter01/rain_total;11.1;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};11.1;3;True
fhem/Garten/rainmeter01/rain_total;155.9;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};155.9;5;True
fhem/Garten/rainmeter01/rain_total;23134.8;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};23134.8;4;True
fhem/Garten/rainmeter01/rain_total;1233123.123123123123;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};1233123.123123123123;3;True
fhem/Garten/rainmeter01/rain_total;1233123.123123123124;fhem_rain_mm;{"location":"Garten","topic": "fhem/Garten/rainmeter01/rain_total"};1233123.123123123124;5;True
Can't render this file because it contains an unexpected character in line 2 and column 54.

View File

@@ -0,0 +1,58 @@
# Logging
logging:
# logfile: 'conf/mqttexperter.log' # Optional default '' (stdout)
level: 'debug' # Optional default 'info'
timescale: 0
# Metric definitions
metrics:
- name: "fhem_light_state"
help: "Light state on/off"
type: "enum"
topic: "fhem/+/+/light"
parameters:
states:
- 'on'
- 'off'
label_configs:
- source_labels: ['__value__']
regex: "(ON|0)"
target_label: '__value__'
replacement: 'on'
action: "replace"
- source_labels: ['__value__']
regex: "(OFF|1)"
target_label: '__value__'
replacement: 'off'
action: "replace"
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ["__msg_topic__"]
regex: "fhem/([^/]+).*"
target_label: "location"
replacement: '\1'
action: "replace"
- name: 'network_ping_ms'
help: 'ping response in ms'
type: 'histogram'
topic: 'network/+/+/ping'
parameters:
buckets:
- 0.5
- 5
- 10
label_configs:
- source_labels: ['__msg_topic__']
target_label: '__topic__'
- source_labels: ["__msg_topic__"]
regex: "network/([^/]+).*"
target_label: "network"
replacement: '\1'
action: "replace"
- source_labels: ["__msg_topic__"]
regex: "network/[^/]+/([^/]+).*"
target_label: "server"
replacement: '\1'
action: "replace"

View File

@@ -0,0 +1,23 @@
in_topic;in_payload;out_name;out_labels;out_value;delay;assert
fhem/room01/desk/light01;on;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};0;4;True
fhem/room01/desk/light01;on;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};0;3;True
fhem/room01/desk/light01;off;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};1;5;True
fhem/room01/desk/light01;on;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};0;4;True
fhem/room01/desk/light01;off;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};1;3;True
fhem/room01/desk/light01;ON;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};0;5;True
fhem/room01/desk/light01;OFF;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};1;4;True
fhem/room01/desk/light01;off;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};1;3;True
fhem/room01/desk/light01;1;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};1;5;True
fhem/room01/desk/light01;0;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};0;4;True
fhem/room01/desk/light01;on;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};0;3;True
fhem/room01/desk/light01;off;fhem_light_state;{"location":"Garten","topic": "fhem/room01/desk/light01"};1;5;True
network/vlan11/srv01.local/ping;2;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "5.0"};{"_count": 1, "_sum": 2, "_bucket": 1};2;True
network/vlan11/srv01.local/ping;4;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "5.0"};{"_count": 2, "_sum": 6, "_bucket": 2};6;True
network/vlan11/srv01.local/ping;7;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "10.0"};{"_count": 3, "_sum": 13, "_bucket": 3};1;True
network/vlan11/srv01.local/ping;0.4;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "0.5"};{"_count": 4, "_sum": 13.4, "_bucket": 1};4;True
network/vlan11/srv01.local/ping;20;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "+Inf"};{"_count": 5, "_sum": 33.4, "_bucket": 5};5;True
network/vlan11/srv01.local/ping;11.1;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "+Inf"};{"_count": 6, "_sum": 44.5, "_bucket": 6};2;True
network/vlan11/srv01.local/ping;5;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "5.0"};{"_count": 7, "_sum": 49.5, "_bucket": 4};4;True
network/vlan11/srv01.local/ping;6;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "10.0"};{"_count": 8, "_sum": 55.5, "_bucket": 6};1;True
network/vlan11/srv01.local/ping;0.05;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "0.5"};{"_count": 9, "_sum": 55.55, "_bucket": 2};4;True
network/vlan11/srv01.local/ping;30;network_ping_ms;{"network": "vlan11","topic": "network/vlan11/srv01.local/ping", "server": "srv01.local", "le": "+Inf"};{"_count": 10, "_sum": 85.55, "_bucket": 10};5;True
Can't render this file because it contains an unexpected character in line 2 and column 47.

178
tests/test_mqtt_exporter.py Normal file
View File

@@ -0,0 +1,178 @@
"""
py test testing for mqtt_exporter
"""
import csv
import distutils.util
import json
from json.decoder import JSONDecodeError
import os
import time
import logging
import prometheus_client as prometheus
import prometheus_client.registry
import mqtt_exporter
import pytest
logging.basicConfig(level=logging.DEBUG)
TMP_DIR=os.path.join(
os.path.dirname(__file__),
'tmp_data'
)
DATA_DIR=os.path.join(
os.path.dirname(__file__),
'test_data'
)
def setup_module(module): #pylint: disable=unused-argument
""" setup any state specific to the execution of the given module."""
delete_temp_test_files()
def delete_temp_test_files():
# delete TEMP files
for file in os.listdir(TMP_DIR):
if file == '.gitkeep':
continue
os.remove(os.path.join(TMP_DIR, file))
class MqttCVS:
in_topic = "in_topic"
in_payload = "in_payload"
out_name = "out_name"
out_labels = "out_labels"
out_value = "out_value"
delay = "delay"
expected_assert = "assert"
def _get_mqtt_data(file_name):
"""
Reads mqtt fake data and expected results from file
"""
mqtt_data = []
with open(file_name, newline='') as mqtt_data_csv:
csv_reader = csv.DictReader(mqtt_data_csv, quotechar="'", delimiter=';')
for row in csv_reader:
row[MqttCVS.in_topic] = row[MqttCVS.in_topic].strip()
row[MqttCVS.out_name] = row[MqttCVS.out_name].strip()
# covert payloud to bytes, as in a MQTT Message
row[MqttCVS.in_payload] = row[MqttCVS.in_payload].encode('UTF-8')
# parse labels, to a python object.
try:
row[MqttCVS.out_labels] = json.loads(row.get(MqttCVS.out_labels, '{}'))
except json.decoder.JSONDecodeError as jde:
logging.error(f"json.decoder.JSONDecodeError while decoding {row.get(MqttCVS.out_labels, '{}')}")
raise jde
# Value could be a JSON, a float or anthing else.
try:
row[MqttCVS.out_value] = float(row.get(MqttCVS.out_value))
except ValueError:
try:
row[MqttCVS.out_value] = json.loads(row.get(MqttCVS.out_value))
except (JSONDecodeError, TypeError):
pass # leave as it is
# set delay to 0 if not a number
try:
row[MqttCVS.delay] = float(row.get(MqttCVS.delay, 0))
except ValueError:
row[MqttCVS.delay] = 0
# convert string to bool for expected assertion.
row[MqttCVS.expected_assert] = bool(
distutils.util.strtobool(row.get(MqttCVS.expected_assert, "True").strip()))
mqtt_data.append(row)
return mqtt_data
def _get_test_data():
"""
Reads test data from DATA_DIR sub directories.
Each subdirectory is expected to contain a `conf.yaml` file with a metrics config (like in the config file)
and a CSV file `mqtt_msg.csv` with fake mqtt data ";" delimited:
`in_topic;in_payload;out_name;out_labels;out_value;delay;assert`
where
lables_out: json string with all expected lables
delay: delay until the next line is processed
assert: expected assert result, True if out_value matches prometheus metric
"""
test_data_sets = []
test_data_dirs = [f.path for f in os.scandir(DATA_DIR) if f.is_dir()]
test_names = [ os.path.basename(os.path.normpath(name)) for name in test_data_dirs]
for test_data_dir in test_data_dirs:
conf_file = os.path.join(test_data_dir, 'conf.yaml')
mqtt_data_file = os.path.join(test_data_dir, 'mqtt_msg.csv')
if not os.path.isfile(conf_file) or not os.path.isfile(mqtt_data_file):
logging.error(f"Test data dir {test_data_dir} doesn't contain required files, skipping")
continue
config_yaml = mqtt_exporter._read_config(conf_file)
config_yaml = mqtt_exporter._parse_config_and_add_defaults(config_yaml)
test_data_sets.append((
config_yaml['metrics'],
_get_mqtt_data(mqtt_data_file),
config_yaml.get('timescale', 0),
))
return test_names, test_data_sets
def _get_suffixes_by_metric_name(metrics, metric_name):
metric_type = None
for _, outer_metric in metrics.items():
for metric in outer_metric:
if metric['name'] == metric_name:
metric_type = metric['type']
break
for suffix in mqtt_exporter.SUFFIXES_PER_TYPE[metric_type]:
if len(suffix) == 0:
yield suffix
else:
yield f"_{suffix}"
class FakeMSG():
""""Simulate MQTT Msg"""
def __init__(self, topic, payload) -> None:
self.topic = topic
self.payload = payload
param_test_data_dirs, param_test_data_sets = _get_test_data()
@pytest.mark.parametrize("metrics,mqtt_data_set,timescale", param_test_data_sets, ids=param_test_data_dirs)
def test_update_metrics(caplog, request, metrics, mqtt_data_set, timescale):
"""
reads a label_config and some mqtt data and asserts if they are in the metrics
"""
logging.info(f"Start test_update_metrics with ID {request.node.callspec.id}")
# reset prometheus registry between tests
collectors = list(prometheus.REGISTRY._collector_to_names.keys())
for collector in collectors:
prometheus.REGISTRY.unregister(collector)
i = 1
for mqtt_data in mqtt_data_set:
msg = FakeMSG(mqtt_data[MqttCVS.in_topic], mqtt_data[MqttCVS.in_payload])
mqtt_exporter._on_message(None, metrics, msg)
prometheus.REGISTRY.collect()
prometheus.write_to_textfile(os.path.join(TMP_DIR, f"metric_{request.node.callspec.id}_{i:02}.txt"), prometheus.REGISTRY)
# depending on metric type one or more metrics with different suffixes are added.
for suffix in _get_suffixes_by_metric_name(metrics, mqtt_data[MqttCVS.out_name]):
# historgram with buckets need special handling, remove bucket labe label 'le'
labels = mqtt_data[MqttCVS.out_labels].copy()
if not suffix == "_bucket" and labels.get('le'):
labels.pop('le')
expected_result = mqtt_data[MqttCVS.out_value]
expected_result = expected_result if not isinstance(expected_result, dict) else expected_result[suffix]
logging.info(f"Assert {mqtt_data[MqttCVS.out_name]}{suffix} from testdata record {i}")
assert ( prometheus.REGISTRY.get_sample_value(
f"{mqtt_data[MqttCVS.out_name]}{suffix}",
labels
) == expected_result ) == mqtt_data[MqttCVS.expected_assert]
time.sleep(mqtt_data[MqttCVS.delay] * timescale)
i += 1
for record in caplog.records:
assert record.levelno < logging.ERROR

View File

@@ -0,0 +1,72 @@
"""
Pytest for prometheus client enhancements
"""
import os
import logging
import time
import pytest
from utils.prometheus_additions import CounterAbsolute
import prometheus_client as prometheus
logging.basicConfig(level=logging.DEBUG)
TMP_DIR=os.path.join(
os.path.dirname(__file__),
'tmp_data'
)
DATA_DIR=os.path.join(
os.path.dirname(__file__),
'test_data'
)
@pytest.fixture(scope="class")
def get_registry():
yield prometheus.REGISTRY
# reset prometheus registry between tests
collectors = list(prometheus.REGISTRY._collector_to_names.keys())
for collector in collectors:
prometheus.REGISTRY.unregister(collector)
old_creation_time = 0.0
class TestCounterWithReset:
a_counter_absolute = CounterAbsolute('Absolute_Counter', 'Test metric' )
old_creation_time = 0.0
param_test_data_sets = [
(10, False),
(10, True),
(11, True),
(110, True),
(110, True),
(210, True),
(310.7, True),
(110, False),
(210, True),
(310.7, True),
]
@pytest.mark.parametrize("value, same_creation_time", param_test_data_sets)
def test_counter_absolute(self, request, get_registry, value, same_creation_time):
global old_creation_time
self.a_counter_absolute.set(value)
creation_time = self.a_counter_absolute._created
logging.info(f"Creation time: {creation_time:e}")
registry = get_registry
registry.collect()
prometheus.write_to_textfile(os.path.join(TMP_DIR, f"absolute_counter_{request.node.callspec.id}_{value:05}.txt"), prometheus.REGISTRY)
assert self.a_counter_absolute._value.get() == value
assert (creation_time == old_creation_time ) == same_creation_time
old_creation_time = creation_time
time.sleep(0.005)
class TestCounterRestForbidden:
a_counter_absolute = CounterAbsolute('Strict_Absolute_Counter', "This Counters doesn't allow reset")
def test_counter_reset(self):
val_first = 0.3324234
val_second = 0.3324233
self.a_counter_absolute.set(val_first, fail_on_decrease=True)
with pytest.raises(ValueError, match=rf"Counter must increase {val_second} lower {val_first}"):
self.a_counter_absolute.set(val_second, fail_on_decrease=True)

0
tests/tmp_data/.gitkeep Normal file
View File