init
commit
38fdd94792
|
@ -0,0 +1 @@
|
|||
data
|
|
@ -0,0 +1,86 @@
|
|||
# mqtt2prom
|
||||
|
||||
This service is a mqtt-to-prometheus gateway.
|
||||
|
||||
it is configured with the following ENV-Variables, the ones suffiexed with a `*` are mandatory, default values are after `|`:
|
||||
|
||||
```
|
||||
MQTT_HOST*
|
||||
MQTT_PORT|1883
|
||||
MQTT_USER
|
||||
MQTT_PASS
|
||||
MQTT_CLIENT_ID|mqtt2prometheus
|
||||
MQTT_TOPIC|prometheus
|
||||
MQTT_QOS|2
|
||||
|
||||
```
|
||||
|
||||
it will listen on the configured mqtt server at `$MQTT_TOPIC` for metrics in json_format.
|
||||
|
||||
The payload needs to be an object containing only the following keys:
|
||||
|
||||
- `name` (string): the name of the metric
|
||||
- `value` (number): the value of the metric
|
||||
- `labels` (object, optional): an object containing the labels for the metric (key-value pairs of strings)
|
||||
|
||||
According to prometheus conventions, the metric name and the label names need to be a lower or upper case letter or a underscore followed by all case leters, numbers or underscores.
|
||||
|
||||
These are all valid payloads:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "temperature",
|
||||
"value": 30.7,
|
||||
"labels": {
|
||||
"unit": "°C",
|
||||
"location": "Werkstatt"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "temperature",
|
||||
"value": 30.7,
|
||||
"labels": {}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "temperature",
|
||||
"value": 30.7
|
||||
}
|
||||
```
|
||||
|
||||
The service will expose the metrics on the `/metrics` endpoint.
|
||||
All metrics will be presented for about 5 minutes, and exported with their eventtime as a timestamp.
|
||||
|
||||
Metric types are not currently implemented, all metrics are exported without a type.
|
||||
This might change when prometheus supports it.
|
||||
|
||||
The service will report errors in received messages to STDERR and to $MQTT_TOPIC/error.
|
||||
Possible errors are:
|
||||
|
||||
- invalid json
|
||||
- missing `name` or `value` key
|
||||
- invalid `name` (prometheus conventions)
|
||||
- invalid `value` (not a number)
|
||||
- invalid `labels` (not an object)
|
||||
- invalid label names (not a sting or not a valid prometheus label name)
|
||||
- invalid label values (not a string)
|
||||
- additional information in the payload (not one of `name`, `value`, `labels`)
|
||||
|
||||
The error message describes the error type and repeats the payload that caused the error.
|
||||
|
||||
There is a comment block on top if the metric that shows some metadata for human consumption.
|
||||
|
||||
mem usage and mem usage peak is according to [memory_get_usage()](https://www.php.net/manual/en/function.memory-get-usage.php) and [memory_get_peak_usage()](https://www.php.net/manual/en/function.memory-get-peak-usage.php) respectively.
|
||||
in both cases it gives 2 values, the first is the acutally used value, the second is tha allocated.
|
||||
|
||||
A new export is written either
|
||||
|
||||
- once oper second if new events are incoming.
|
||||
- every 15 seconds if no new events are incoming, to update stats and remove stale entries.
|
||||
|
||||
There is a 60 second period on startup where metrics are received but not exported, in case the service crashes and there are previous exported metrics that are not polled by prometheus yet.
|
|
@ -0,0 +1,19 @@
|
|||
version: "3"
|
||||
services:
|
||||
worker:
|
||||
build: mqtt2prom
|
||||
restart: always
|
||||
environment:
|
||||
MQTT_HOST: 10.0.10.2
|
||||
MQTT_PORT: 1883
|
||||
# MQTT_USER: mqtt
|
||||
# MQTT_PASS: pass
|
||||
volumes:
|
||||
- ./data/metrics:/www/metrics
|
||||
webserv:
|
||||
build: webserv
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./data/metrics:/www/metrics
|
|
@ -0,0 +1,14 @@
|
|||
FROM debian:12
|
||||
RUN apt-get update && apt-get upgrade -y
|
||||
RUN apt-get install -y php-dev php-cli php-pear php-mbstring php-curl git build-essential libmosquitto-dev libmosquitto-dev php-sqlite3
|
||||
WORKDIR /tmp
|
||||
RUN git clone https://github.com/nismoryco/Mosquitto-PHP.git
|
||||
WORKDIR /tmp/Mosquitto-PHP
|
||||
RUN phpize
|
||||
RUN ./configure
|
||||
RUN make
|
||||
RUN make install
|
||||
RUN echo "extension=mosquitto.so" > /etc/php/8.2/cli/php.ini
|
||||
ADD run /usr/local/bin/entrypoint
|
||||
RUN chmod +x /usr/local/bin/entrypoint
|
||||
CMD ["/usr/local/bin/entrypoint"]
|
|
@ -0,0 +1,295 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
use Mosquitto\Message;
|
||||
|
||||
define("STARTED", microtime(true));
|
||||
$mqttHost = getenv("MQTT_HOST");
|
||||
if (!$mqttHost) {
|
||||
echo "Please set MQTT_HOST environment variable\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
pcntl_signal(SIGINT, "endit");
|
||||
pcntl_signal(SIGTERM, "endit");
|
||||
pcntl_signal(SIGHUP, "endit");
|
||||
|
||||
|
||||
$mqttPort = getEnvWithDefaultInt("MQTT_PORT", 1883);
|
||||
|
||||
$mqttUser = getEnvWithDefaultStr("MQTT_USER", "");
|
||||
$mqttPass = getEnvWithDefaultStr("MQTT_PASS", "");
|
||||
$mqttClientId = getEnvWithDefaultStr("MQTT_CLIENT_ID", "mqtt2prometheus");
|
||||
$mqttTopic = getEnvWithDefaultStr("MQTT_TOPIC", "prometheus");
|
||||
$qos = getEnvWithDefaultInt("MQTT_QOS", 2);
|
||||
|
||||
$serviceReloadTime = getEnvWithDefaultStr("SERVICE_RELOAD_TIME", "03:30");
|
||||
|
||||
$mqtt = new Mosquitto\Client($mqttClientId);
|
||||
|
||||
$data = [];
|
||||
$mqtt->setCredentials($mqttUser, $mqttPass);
|
||||
|
||||
$usedConfig = [
|
||||
"MQTT_HOST" => $mqttHost,
|
||||
"MQTT_PORT" => $mqttPort,
|
||||
"MQTT_USER" => $mqttUser,
|
||||
"MQTT_PASS" => $mqttPass,
|
||||
"MQTT_CLIENT_ID" => $mqttClientId,
|
||||
"MQTT_TOPIC" => $mqttTopic,
|
||||
"MQTT_QOS" => $qos,
|
||||
"SERVICE_RELOAD_TIME" => $serviceReloadTime,
|
||||
];
|
||||
|
||||
$padLength = max(array_map("strlen", array_keys($usedConfig)));
|
||||
|
||||
echo "Using the following config:\n";
|
||||
foreach ($usedConfig as $key => $value) {
|
||||
echo str_pad($key, $padLength) . ": $value\n";
|
||||
}
|
||||
|
||||
echo "Connecting to $mqttHost:$mqttPort";
|
||||
$mqtt->connect($mqttHost, $mqttPort);
|
||||
echo "Connected\n";
|
||||
|
||||
$data = [];
|
||||
$stats = [
|
||||
"messages" => [],
|
||||
"errors" => [],
|
||||
"metrics" => [],
|
||||
];
|
||||
$news = true;
|
||||
$mqtt->onMessage(function ($message) use (&$data, &$stats, &$news) {
|
||||
$stats["messages"][] = time();
|
||||
if (!precheck($message)) {
|
||||
$stats["errors"][] = time();
|
||||
return;
|
||||
}
|
||||
$stats["metrics"][] = time();
|
||||
$payload = (array) json_decode($message->payload, true);
|
||||
|
||||
$payload["timestamp"] = microtime(true);
|
||||
$data[] = $payload;
|
||||
$news = true;
|
||||
});
|
||||
|
||||
echo "Subscribing to $mqttTopic with QoS $qos ";
|
||||
$mqtt->subscribe($mqttTopic, $qos);
|
||||
echo "Subscribed\n";
|
||||
|
||||
$timer = 0;
|
||||
echo "Started listening for incoming messages\n will wait 60 seconds before starting to export data\n";
|
||||
$realStart = time() + 60;
|
||||
echo "waiting till " .
|
||||
date("Y-m-d H:i:s", $realStart) .
|
||||
" that's in " .
|
||||
($realStart - time()) .
|
||||
" seconds\n";
|
||||
while (time() < $realStart) {
|
||||
$wait = time() + 5;
|
||||
$mqtt->loop(5000);
|
||||
sleep($wait - time());
|
||||
echo "Still waiting...\n";
|
||||
}
|
||||
echo "\n Starting to export data\n";
|
||||
|
||||
while (date("H:i") != $serviceReloadTime) {
|
||||
$mqtt->loop(1000);
|
||||
if ($news || time() - $timer > 15) {
|
||||
output();
|
||||
$news = false;
|
||||
$timer = time();
|
||||
}
|
||||
}
|
||||
|
||||
function error(string $msg): void
|
||||
{
|
||||
global $mqtt, $mqttTopic;
|
||||
$mqtt->publish($mqttTopic . "/error", $msg, 2);
|
||||
fwrite(STDERR, "ERROR: " . $msg . "\n");
|
||||
}
|
||||
function precheck(Message $message): bool
|
||||
{
|
||||
$payloadRaw = $message->payload;
|
||||
$payload = json_decode($payloadRaw, true);
|
||||
if (json_last_error() != JSON_ERROR_NONE) {
|
||||
error(
|
||||
"Invalid json: $message->payload (" . json_last_error_msg() . ")"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!is_array($payload)) {
|
||||
error("Payload is not an object: $payloadRaw");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($payload["name"]) || !isset($payload["value"])) {
|
||||
error(
|
||||
"Invalid payload, at least name and value is needed: $payloadRaw"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $payload["name"])) {
|
||||
error("Invalid name: " . $payload["name"]);
|
||||
return false;
|
||||
}
|
||||
if (!is_numeric($payload["value"])) {
|
||||
error("Value is not a number: $payloadRaw");
|
||||
return false;
|
||||
}
|
||||
$payload["labels"] = $payload["labels"] ?? [];
|
||||
if (!is_array($payload["labels"])) {
|
||||
error("Labels must be an object: $payloadRaw");
|
||||
return false;
|
||||
}
|
||||
foreach ($payload["labels"] as $labelName => $labelValue) {
|
||||
if (!is_string($labelName) || !is_scalar($labelValue)) {
|
||||
error("Label names and values must be strings: $payloadRaw");
|
||||
return false;
|
||||
}
|
||||
if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $labelName)) {
|
||||
error("Invalid label name: $labelName");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
foreach (array_keys($payload) as $key) {
|
||||
if (!in_array($key, ["name", "value", "labels"])) {
|
||||
error("Unknown key: $key");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function output(): void
|
||||
{
|
||||
filter();
|
||||
global $data, $stats;
|
||||
$t = microtime(true);
|
||||
|
||||
$prom = "";
|
||||
$prom .=
|
||||
"# service started at : " .
|
||||
STARTED .
|
||||
" / " .
|
||||
date("Y-m-d H:i:s", intval(STARTED)) .
|
||||
"\n";
|
||||
$prom .=
|
||||
"# exported at : $t / " .
|
||||
date("Y-m-d H:i:s", intval($t)) .
|
||||
"\n";
|
||||
$prom .= "# \n";
|
||||
$prom .=
|
||||
"# mem usage : " .
|
||||
round(memory_get_usage() / 1024 / 1024, 1) .
|
||||
" / " .
|
||||
round(memory_get_usage(true) / 1024 / 1024, 1) .
|
||||
" MB\n";
|
||||
$prom .=
|
||||
"# mem usage peak : " .
|
||||
round(memory_get_peak_usage() / 1024 / 1024, 1) .
|
||||
" / " .
|
||||
round(memory_get_peak_usage(true) / 1024 / 1024, 1) .
|
||||
" MB\n";
|
||||
$timeframe = [
|
||||
60 => "Minute",
|
||||
300 => "5 Minutes",
|
||||
900 => "15 Minutes",
|
||||
1800 => "30 Minutes",
|
||||
3600 => "Hour",
|
||||
21600 => "6 Hours",
|
||||
43200 => "12 Hours",
|
||||
86400 => "Day",
|
||||
];
|
||||
foreach ($stats as $key => $values) {
|
||||
$prom .= "# \n";
|
||||
foreach ($timeframe as $time => $name) {
|
||||
$count = count(
|
||||
array_filter($values, function ($v) use ($time) {
|
||||
return $v > time() - $time;
|
||||
})
|
||||
);
|
||||
$fkey = str_pad($key, 8);
|
||||
$fname = str_pad($name, 10);
|
||||
$prom .= "# $fkey in the last $fname: $count\n";
|
||||
}
|
||||
}
|
||||
foreach ($data as $entry) {
|
||||
$labels = [];
|
||||
if (isset($entry["labels"])) {
|
||||
foreach ($entry["labels"] as $labelName => $labelValue) {
|
||||
$labels[] =
|
||||
"$labelName=" .
|
||||
json_encode(
|
||||
strval($labelValue),
|
||||
JSON_UNESCAPED_UNICODE + JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
}
|
||||
}
|
||||
$labels = implode(", ", $labels);
|
||||
if ($labels != "") {
|
||||
$labels = "{" . $labels . "}";
|
||||
}
|
||||
|
||||
$prom .=
|
||||
$entry["name"] .
|
||||
$labels .
|
||||
" " .
|
||||
$entry["value"] .
|
||||
" " .
|
||||
$entry["timestamp"] .
|
||||
"\n";
|
||||
}
|
||||
file_put_contents("/www/metrics/new.prom", $prom);
|
||||
rename("/www/metrics/new.prom", "/www/metrics/index.prom");
|
||||
// return $prom;
|
||||
}
|
||||
|
||||
function filter(): void
|
||||
{
|
||||
global $data, $stats;
|
||||
$data = array_filter($data, function ($entry) {
|
||||
return $entry["timestamp"] > time() - 5 * 60; // 5 min is the hardcoded value in prometheus
|
||||
});
|
||||
// remove stats older than 24h
|
||||
foreach ($stats as $key => $values) {
|
||||
$stats[$key] = array_filter($values, function ($v) {
|
||||
return $v > time() - 24 * 60 * 60;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getEnvWithDefaultStr(string $key, string $default): string
|
||||
{
|
||||
return strval(getEnvWithDefault($key, $default));
|
||||
}
|
||||
|
||||
function getEnvWithDefaultInt(string $key, int $default): int
|
||||
{
|
||||
return intval(getEnvWithDefault($key, $default));
|
||||
}
|
||||
function getEnvWithDefault(string $key, bool|string|int $default): bool|string|int
|
||||
{
|
||||
$value = getenv($key);
|
||||
if(!is_scalar($value)){
|
||||
return $default;
|
||||
}
|
||||
if ($value === false) {
|
||||
return $default;
|
||||
}
|
||||
if(is_numeric($default)){
|
||||
return intval($value);
|
||||
}
|
||||
return strval($value);
|
||||
}
|
||||
|
||||
function endit(): void
|
||||
{
|
||||
global $mqtt;
|
||||
error("Exiting");
|
||||
$mqtt->loop(100);
|
||||
$mqtt->disconnect();
|
||||
exit(0);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
FROM debian:12
|
||||
RUN apt-get update && apt-get install -y nginx-light
|
||||
ADD prom.conf /etc/nginx/sites-available
|
||||
RUN rm /etc/nginx/sites-enabled/*
|
||||
RUN ln -s /etc/nginx/sites-available/prom.conf /etc/nginx/sites-enabled/
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
|
@ -0,0 +1,8 @@
|
|||
server {
|
||||
listen 80 default_server;
|
||||
root /www/metrics;
|
||||
index index.prom;
|
||||
types {
|
||||
text/plain prom;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue