VictoriaMetrics als zusätzliche Datenbank-Option für Messwerte ergänzt (STORE/DBASE=victoria)
Dritte, zu mongo/influx exklusive Auswahl für die laufenden Messwerte. Schreibpfad nutzt das bestehende Influx-Line-Protocol unverändert (common/victoria_post.js); Lesepfad (sensorapi/databases/victoria.js + victoria2json.js) holt Rohdaten per VictoriaMetrics' /api/v1/export und bucketet/aggregiert stundenweise clientseitig, nach Mongo-Konvention (Stunden-Start als Label, kein Zeit-Shift nötig wie bei Influx). Scope bewusst auf die schon heute per DBASE umschaltbaren Funktionen begrenzt (getActData/getNoiseAVGData) - getAvgData/getLongAvg/getGeigerData bleiben wie bisher. Docker-Compose um victoriametrics-Service ergänzt (Retention explizit auf 100y gesetzt, da VictoriaMetrics sonst nach 1 Monat Daten löscht). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,3 +9,9 @@ log/
|
|||||||
|
|
||||||
# Produktionsstand vom Server, nur Referenz
|
# Produktionsstand vom Server, nur Referenz
|
||||||
noisesensors/vonRemote/
|
noisesensors/vonRemote/
|
||||||
|
|
||||||
|
# Laufzeitdaten der lokal getesteten Container (DB-Dateien, aktuelle Messwerte)
|
||||||
|
noisesensors/data/mongo/data/
|
||||||
|
noisesensors/data/influx/
|
||||||
|
noisesensors/data/victoria/
|
||||||
|
noisesensors/data/aktdata.json
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ Die Datei **noise.tgz** enthält die folgenden Dateien und Verzeichnisse:
|
|||||||
| | | +- <f>create.js
|
| | | +- <f>create.js
|
||||||
| + <d>influx
|
| + <d>influx
|
||||||
| | + <d>data
|
| | + <d>data
|
||||||
|
| + <d>victoria
|
||||||
|
| | + <d>data
|
||||||
|
|
||||||
<f>: file, <d>: directory
|
<f>: file, <d>: directory
|
||||||
~~~
|
~~~
|
||||||
@@ -42,6 +44,10 @@ hier liegt die Datei **create.js**, mit deren Hilfe bei ersten Start der Datenba
|
|||||||
Verzeichnis für die Influx-Datenbank
|
Verzeichnis für die Influx-Datenbank
|
||||||
* **data/influx/data**
|
* **data/influx/data**
|
||||||
Hier dann die eingelesenen Daten
|
Hier dann die eingelesenen Daten
|
||||||
|
* **data/victoria**
|
||||||
|
Verzeichnis für die VictoriaMetrics-Datenbank (optionale Alternative zu Influx, siehe unten)
|
||||||
|
* **data/victoria/data**
|
||||||
|
Hier dann die eingelesenen Daten
|
||||||
|
|
||||||
|
|
||||||
### Aufrufe
|
### Aufrufe
|
||||||
@@ -60,6 +66,8 @@ STORE=influx
|
|||||||
~~~
|
~~~
|
||||||
\<username>, \<passwort>, \<username1>, \<passwort1> und \<token> sind anzupassen.
|
\<username>, \<passwort>, \<username1>, \<passwort1> und \<token> sind anzupassen.
|
||||||
|
|
||||||
|
**STORE** legt fest, in welche Datenbank(en) *readin* die laufenden Messwerte schreibt: `influx`, `mongo`, `victoria` oder `both` (= mongo **und** influx gleichzeitig; Default). `victoria` ist dabei ein zu `influx` alternativer, exklusiver Wert (nicht über `both` kombinierbar). *sensorapi* liest die Messwerte unabhängig davon über **DBASE** (dieselben Werte `influx`/`mongo`/`victoria`, siehe Container-Beschreibung unten).
|
||||||
|
|
||||||
Danach einloggen in das Docker-Registry auf *citysensor.de*:
|
Danach einloggen in das Docker-Registry auf *citysensor.de*:
|
||||||
|
|
||||||
~~~
|
~~~
|
||||||
@@ -91,7 +99,7 @@ Im **Portainer** als eigenes Stack anlegen (Inhalt von `readarchive-stack.yml`)
|
|||||||
`-s` | `START` | gestern
|
`-s` | `START` | gestern
|
||||||
`-e` | `END` | Startdatum + 1 Tag (**exklusiv**)
|
`-e` | `END` | Startdatum + 1 Tag (**exklusiv**)
|
||||||
`-t` | `TYP` | `noise`
|
`-t` | `TYP` | `noise`
|
||||||
`-d` | `DBASE` | `both`, in der Stack-Datei auf `influx` vorbelegt
|
`-d` | `DBASE` | `mongo`, `influx`, `victoria` oder `both` (= mongo+influx); in der Stack-Datei auf `influx` vorbelegt
|
||||||
|
|
||||||
*Deploy the stack* startet den Lauf, der Container endet danach und bleibt als „Exited" stehen. Für den nächsten Zeitraum nur die Variablen ändern und *Update the stack*. Der Haupt-Stack wird dabei nicht angefasst.
|
*Deploy the stack* startet den Lauf, der Container endet danach und bleibt als „Exited" stehen. Für den nächsten Zeitraum nur die Variablen ändern und *Update the stack*. Der Haupt-Stack wird dabei nicht angefasst.
|
||||||
|
|
||||||
@@ -143,13 +151,19 @@ Die Influx-Datenbank. Hier werden in dem Bucket *sensor_data* die reinen Messwe
|
|||||||
http://\<server>:8086
|
http://\<server>:8086
|
||||||
Logindaten: Entsprechen der im Portainer hinterlegten.
|
Logindaten: Entsprechen der im Portainer hinterlegten.
|
||||||
|
|
||||||
|
* **victoriametrics**
|
||||||
|
Alternative zur Influx-Datenbank für die reinen Messwerte, wählbar über `STORE`/`DBASE=victoria`. Läuft als Single-Node-Instanz auf **Port 8428**, ohne Login/Auth.
|
||||||
|
**Wichtig:** VictoriaMetrics löscht standardmäßig Daten, die älter als 1 Monat sind (`-retentionPeriod=1`). Im `docker-compose.yml` ist das Flag deshalb explizit auf `--retentionPeriod=100y` gesetzt — das **nicht** entfernen, sonst verschwinden alte Messwerte nach 30 Tagen kommentarlos.
|
||||||
|
**Zugriff:**
|
||||||
|
http://\<server>:8428 (z.B. `/api/v1/export?match[]={sid="..."}` zum Rohdaten-Export)
|
||||||
|
|
||||||
|
|
||||||
## Sourcen für die Container
|
## Sourcen für die Container
|
||||||
|
|
||||||
Das gesamte Projekt ist im GitHub unter **laermsensor-stack** abgelegt. Für jeden der 4 Container existiert darunter ein Verzeichnis:
|
Das gesamte Projekt ist im GitHub unter **laermsensor-stack** abgelegt. Für jeden der 4 Container existiert darunter ein Verzeichnis:
|
||||||
**readin**, **readarchive**, **sensorapi** und **noise**. In diesen Verzeichnissen sind alle benötigten Sourcen enthalten.
|
**readin**, **readarchive**, **sensorapi** und **noise**. In diesen Verzeichnissen sind alle benötigten Sourcen enthalten.
|
||||||
|
|
||||||
Daneben gibt es **common** mit den Modulen, die sich *readin* und *readarchive* teilen: `mongo.js`, `influx_post.js`, `logit.js` und `statistics.js`. Sie lagen früher in beiden Komponenten doppelt und sind auseinandergelaufen. Wer dort etwas ändert, ändert es für beide — nach einer Änderung also **beide** Container neu bauen.
|
Daneben gibt es **common** mit den Modulen, die sich *readin* und *readarchive* teilen: `mongo.js`, `influx_post.js`, `victoria_post.js`, `logit.js` und `statistics.js`. Sie lagen früher in beiden Komponenten doppelt und sind auseinandergelaufen. Wer dort etwas ändert, ändert es für beide — nach einer Änderung also **beide** Container neu bauen.
|
||||||
|
|
||||||
Sollte was geändert werden, so muss mit **deploy.sh** der Container neu erzeugt und auf die Registry (siehe oben) gepushed werden. Der Build-Kontext von *readin* und *readarchive* ist wegen `common/` das Wurzelverzeichnis des Repositories; `deploy.sh` wechselt selbst dorthin und kann aus dem Komponentenverzeichnis aufgerufen werden.
|
Sollte was geändert werden, so muss mit **deploy.sh** der Container neu erzeugt und auf die Registry (siehe oben) gepushed werden. Der Build-Kontext von *readin* und *readarchive* ist wegen `common/` das Wurzelverzeichnis des Repositories; `deploy.sh` wechselt selbst dorthin und kann aus dem Komponentenverzeichnis aufgerufen werden.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/* Zugriff auf VictoriaMetrics per HTTP (InfluxDB-Line-Protocol-kompatibler Write-Endpunkt)
|
||||||
|
|
||||||
|
Gemeinsam genutzt von readin (Live-API) und readarchive (CSV-Archiv).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import { logit, logerror } from './logit.js'
|
||||||
|
import { statistics } from './statistics.js'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
|
||||||
|
let VICTORIAHOST = process.env.VICTORIAHOST || "localhost"
|
||||||
|
let VICTORIAPORT = process.env.VICTORIAPORT || 8428
|
||||||
|
|
||||||
|
const VICTORIAURL_WRITE = `http://${VICTORIAHOST}:${VICTORIAPORT}/write?precision=ms`
|
||||||
|
|
||||||
|
// `${e}` liefert bei einem AggregateError nur "AggregateError" - die einzelnen
|
||||||
|
// Verbindungsfehler (je einer pro aufgeloester IP) stecken in e.errors bzw. e.cause.errors
|
||||||
|
function describeError(e) {
|
||||||
|
const sub = e.errors || e.cause?.errors
|
||||||
|
if (sub?.length) {
|
||||||
|
return `${e.message || e.name}: ` + sub.map(s => `${s.code} ${s.address}:${s.port}`).join(', ')
|
||||||
|
}
|
||||||
|
if (e.response) {
|
||||||
|
return `HTTP ${e.response.status} ${JSON.stringify(e.response.data)}`
|
||||||
|
}
|
||||||
|
return `${e.code ? e.code + ' ' : ''}${e.message || e}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// liefert true, wenn VictoriaMetrics die Daten uebernommen hat, sonst false
|
||||||
|
export const victoriaWrite = async (data) => {
|
||||||
|
let start = DateTime.now()
|
||||||
|
let ok = false
|
||||||
|
try {
|
||||||
|
const ret = await axios({
|
||||||
|
method: 'post',
|
||||||
|
url: VICTORIAURL_WRITE,
|
||||||
|
data: data,
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'text/plain; charset=utf-8'
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
})
|
||||||
|
if (ret.status != 204) {
|
||||||
|
logerror(`doWrite2API Status: ${ret.status}`)
|
||||||
|
} else {
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logerror(`doWrite2API ${VICTORIAURL_WRITE} ${describeError(e)}`)
|
||||||
|
}
|
||||||
|
statistics['writeVictoriaData[sensor_data]Time'] = DateTime.now().diff(start, ['seconds']).toObject().seconds
|
||||||
|
logit(`Victoria-Write-Time: ${start.diffNow('seconds').toObject().seconds * -1} sec`)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
@@ -9,9 +9,12 @@ MONGO_ROOT_PASSWORD=<Mongo_Passwort>
|
|||||||
|
|
||||||
LOCALDIR=<Aktuelle Direcrory ab />
|
LOCALDIR=<Aktuelle Direcrory ab />
|
||||||
|
|
||||||
# Datenbank, in welche die laufenden Messwerte gespeichert werden (kannn 'influx' oder 'mongo' sein)
|
# Datenbank, in welche die laufenden Messwerte gespeichert werden (kann 'influx', 'mongo' oder 'victoria' sein)
|
||||||
STORE=influx
|
STORE=influx
|
||||||
|
|
||||||
|
# Datenbank, aus welcher sensorapi die Messwerte liest (kann 'influx', 'mongo' oder 'victoria' sein)
|
||||||
|
DBASE=influx
|
||||||
|
|
||||||
# Secret zum Signieren der Session-Cookies von esp2sensor (langer Zufallsstring)
|
# Secret zum Signieren der Session-Cookies von esp2sensor (langer Zufallsstring)
|
||||||
ESP2SENSOR_SESSION_SECRET=<zufaelliger_string>
|
ESP2SENSOR_SESSION_SECRET=<zufaelliger_string>
|
||||||
|
|
||||||
|
|||||||
@@ -26,20 +26,33 @@ services:
|
|||||||
DOCKER_INFLUXDB_INIT_ORG: citysensor
|
DOCKER_INFLUXDB_INIT_ORG: citysensor
|
||||||
DOCKER_INFLUXDB_INIT_BUCKET: sensor_data
|
DOCKER_INFLUXDB_INIT_BUCKET: sensor_data
|
||||||
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${DOCKER_INFLUXTOKEN}
|
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${DOCKER_INFLUXTOKEN}
|
||||||
restart:
|
restart:
|
||||||
unless-stopped
|
unless-stopped
|
||||||
container_name: influxdb
|
container_name: influxdb
|
||||||
|
|
||||||
|
victoriametrics:
|
||||||
|
image: victoriametrics/victoria-metrics:v1.102.0
|
||||||
|
ports:
|
||||||
|
- "8428:8428"
|
||||||
|
volumes:
|
||||||
|
- ${LOCALDIR}/noisesensors/data/victoria/data:/victoria-metrics-data
|
||||||
|
command:
|
||||||
|
- "--storageDataPath=/victoria-metrics-data"
|
||||||
|
- "--retentionPeriod=100y"
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: victoriametrics
|
||||||
|
|
||||||
readin:
|
readin:
|
||||||
image: docker.citysensor.de/readin
|
image: docker.citysensor.de/readin
|
||||||
environment:
|
environment:
|
||||||
MONGOHOST: mongodb
|
MONGOHOST: mongodb
|
||||||
INFLUXHOST: influxdb
|
INFLUXHOST: influxdb
|
||||||
|
VICTORIAHOST: victoriametrics
|
||||||
TYP: "[\"noise\"]"
|
TYP: "[\"noise\"]"
|
||||||
STORE: ${STORE:-mongo}
|
STORE: ${STORE:-mongo}
|
||||||
MONGOAUTH: true
|
MONGOAUTH: true
|
||||||
MONGOUSRP: ${MONGO_ROOT_USERNAME}:${MONGO_ROOT_PASSWORD}
|
MONGOUSRP: ${MONGO_ROOT_USERNAME}:${MONGO_ROOT_PASSWORD}
|
||||||
INFLUXTOKEN: ${DOCKER_INFLUXTOKEN}
|
INFLUXTOKEN: ${DOCKER_INFLUXTOKEN}
|
||||||
volumes:
|
volumes:
|
||||||
- ${LOCALDIR}/noisesensors/log:/var/log
|
- ${LOCALDIR}/noisesensors/log:/var/log
|
||||||
# Pfad im Image ist jetzt /opt/app/readin, weil readin und readarchive
|
# Pfad im Image ist jetzt /opt/app/readin, weil readin und readarchive
|
||||||
@@ -67,7 +80,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
INFLUXHOST: influxdb
|
INFLUXHOST: influxdb
|
||||||
INFLUXTOKEN: ${DOCKER_INFLUXTOKEN}
|
INFLUXTOKEN: ${DOCKER_INFLUXTOKEN}
|
||||||
DBASE: influx
|
VICTORIAHOST: victoriametrics
|
||||||
|
DBASE: ${DBASE:-influx}
|
||||||
MONGOHOST: mongodb
|
MONGOHOST: mongodb
|
||||||
MONGOAUTH: true
|
MONGOAUTH: true
|
||||||
MONGOUSRP: ${MONGO_ROOT_USERNAME}:${MONGO_ROOT_PASSWORD}
|
MONGOUSRP: ${MONGO_ROOT_USERNAME}:${MONGO_ROOT_PASSWORD}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
#
|
#
|
||||||
# readFromcsv.js liest jeden Parameter auch aus einer Env-Variablen:
|
# readFromcsv.js liest jeden Parameter auch aus einer Env-Variablen:
|
||||||
# -s = START (Default: gestern), -e = END (Default: Startdatum + 1 Tag,
|
# -s = START (Default: gestern), -e = END (Default: Startdatum + 1 Tag,
|
||||||
# exklusiv), -t = TYP (Default: noise), -d = DBASE (Default: both),
|
# exklusiv), -t = TYP (Default: noise), -d = DBASE ('mongo', 'influx',
|
||||||
|
# 'victoria' oder 'both' = mongo+influx; Default: both),
|
||||||
# -p = PARALLEL (Default: 4) - so viele Sensoren werden gleichzeitig geholt.
|
# -p = PARALLEL (Default: 4) - so viele Sensoren werden gleichzeitig geholt.
|
||||||
#
|
#
|
||||||
# Voraussetzungen:
|
# Voraussetzungen:
|
||||||
@@ -39,6 +40,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
MONGOHOST: mongodb
|
MONGOHOST: mongodb
|
||||||
INFLUXHOST: influxdb
|
INFLUXHOST: influxdb
|
||||||
|
VICTORIAHOST: victoriametrics
|
||||||
MONGOAUTH: "true"
|
MONGOAUTH: "true"
|
||||||
MONGOUSRP: ${MONGO_ROOT_USERNAME}:${MONGO_ROOT_PASSWORD}
|
MONGOUSRP: ${MONGO_ROOT_USERNAME}:${MONGO_ROOT_PASSWORD}
|
||||||
INFLUXTOKEN: ${DOCKER_INFLUXTOKEN}
|
INFLUXTOKEN: ${DOCKER_INFLUXTOKEN}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const DBASE = process.env.DBASE || 'both'
|
|||||||
// Mehr als 4 bringt nichts: ab da liefert das Archiv nicht mehr schneller.
|
// Mehr als 4 bringt nichts: ab da liefert das Archiv nicht mehr schneller.
|
||||||
const PARALLEL = parseInt(process.env.PARALLEL) || 4
|
const PARALLEL = parseInt(process.env.PARALLEL) || 4
|
||||||
|
|
||||||
const DATABASE = ['mongo', 'influx', 'both']
|
const DATABASE = ['mongo', 'influx', 'victoria', 'both']
|
||||||
|
|
||||||
const DEVELOP = process.env.DEVELOP || false;
|
const DEVELOP = process.env.DEVELOP || false;
|
||||||
const LIVE = (process.env.LIVE == "true") || true
|
const LIVE = (process.env.LIVE == "true") || true
|
||||||
@@ -47,6 +47,7 @@ import axios from 'axios'
|
|||||||
import https from 'https'
|
import https from 'https'
|
||||||
import * as mongo from '../common/mongo.js'
|
import * as mongo from '../common/mongo.js'
|
||||||
import * as influx from '../common/influx_post.js'
|
import * as influx from '../common/influx_post.js'
|
||||||
|
import * as victoria from '../common/victoria_post.js'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import csv from 'csvtojson'
|
import csv from 'csvtojson'
|
||||||
import pkg from './package.json' with { type: "json" }
|
import pkg from './package.json' with { type: "json" }
|
||||||
@@ -67,6 +68,8 @@ const newProps = []
|
|||||||
|
|
||||||
// Sensor/Tag-Paare, deren Influx-Write fehlgeschlagen ist - Auswertung am Ende von main()
|
// Sensor/Tag-Paare, deren Influx-Write fehlgeschlagen ist - Auswertung am Ende von main()
|
||||||
const failedInfluxWrites = []
|
const failedInfluxWrites = []
|
||||||
|
// Sensor/Tag-Paare, deren VictoriaMetrics-Write fehlgeschlagen ist - Auswertung am Ende von main()
|
||||||
|
const failedVictoriaWrites = []
|
||||||
|
|
||||||
const saveList = (list) => {
|
const saveList = (list) => {
|
||||||
fs.writeFileSync('data/list.json', JSON.stringify(list))
|
fs.writeFileSync('data/list.json', JSON.stringify(list))
|
||||||
@@ -397,6 +400,12 @@ async function enterOneSensorinDB(client, sid, erg, dataline, day, styp, args) {
|
|||||||
failedInfluxWrites.push({sensorid: sid, day: day})
|
failedInfluxWrites.push({sensorid: sid, day: day})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ((args.database === 'victoria') && (dataline !== '')) {
|
||||||
|
const ok = await victoria.victoriaWrite(dataline)
|
||||||
|
if (!ok) {
|
||||||
|
failedVictoriaWrites.push({sensorid: sid, day: day})
|
||||||
|
}
|
||||||
|
}
|
||||||
if((args.database === 'both') || (args.database === 'mongo')) {
|
if((args.database === 'both') || (args.database === 'mongo')) {
|
||||||
if(erg.length !== 0) {
|
if(erg.length !== 0) {
|
||||||
await mongo.writeDataArray(client, mongo.dataCollName(styp), erg)
|
await mongo.writeDataArray(client, mongo.dataCollName(styp), erg)
|
||||||
@@ -507,7 +516,7 @@ function parse_cmdline(argv) {
|
|||||||
console.log(" will be used and endDate = startDate + 1 day")
|
console.log(" will be used and endDate = startDate + 1 day")
|
||||||
console.log(" -t sensorType: if given, only those sensors will be used; default: 'noise'");
|
console.log(" -t sensorType: if given, only those sensors will be used; default: 'noise'");
|
||||||
console.log(" allowed types: 'noise', 'radiactivity', 'pm' and 'thp'")
|
console.log(" allowed types: 'noise', 'radiactivity', 'pm' and 'thp'")
|
||||||
console.log(" -d database: 'mongo', 'influx' or 'both'; default: 'both'")
|
console.log(" -d database: 'mongo', 'influx', 'victoria' or 'both' ('both' = mongo + influx); default: 'both'")
|
||||||
console.log(` -p parallel: number of sensors fetched at the same time; default: ${PARALLEL}`)
|
console.log(` -p parallel: number of sensors fetched at the same time; default: ${PARALLEL}`)
|
||||||
console.log(" -v version: show version");
|
console.log(" -v version: show version");
|
||||||
console.log(" -h this help text");
|
console.log(" -h this help text");
|
||||||
@@ -555,6 +564,13 @@ async function main() {
|
|||||||
}
|
}
|
||||||
process.exitCode = 1 // damit cron/docker den Fehlschlag sieht
|
process.exitCode = 1 // damit cron/docker den Fehlschlag sieht
|
||||||
}
|
}
|
||||||
|
if (failedVictoriaWrites.length > 0) {
|
||||||
|
logerror(`${failedVictoriaWrites.length} Victoria-Writes fehlgeschlagen - diese Daten fehlen in VictoriaMetrics:`)
|
||||||
|
for (const f of failedVictoriaWrites) {
|
||||||
|
logerror(` Sensor ${f.sensorid}, Tag ${f.day}`)
|
||||||
|
}
|
||||||
|
process.exitCode = 1 // damit cron/docker den Fehlschlag sieht
|
||||||
|
}
|
||||||
let dauer = starttime.diffNow('seconds').toObject().seconds * -1
|
let dauer = starttime.diffNow('seconds').toObject().seconds * -1
|
||||||
const duration1 = Duration.fromObject({ seconds: dauer });
|
const duration1 = Duration.fromObject({ seconds: dauer });
|
||||||
const output1 = duration1.toFormat('hh:mm:ss');
|
const output1 = duration1.toFormat('hh:mm:ss');
|
||||||
|
|||||||
@@ -14,13 +14,14 @@
|
|||||||
// TYP: Ist ein Array von Strings, wenn nur einzelne Typen gespeichert werden sollen, also z.B.:
|
// TYP: Ist ein Array von Strings, wenn nur einzelne Typen gespeichert werden sollen, also z.B.:
|
||||||
// ['pm', 'noise']
|
// ['pm', 'noise']
|
||||||
const TYP = process.env.TYP || ''
|
const TYP = process.env.TYP || ''
|
||||||
const STORE = (process.env.STORE || 'both').toLowerCase() // 'mongo' | 'influx' | 'both'
|
const STORE = (process.env.STORE || 'both').toLowerCase() // 'mongo' | 'influx' | 'victoria' | 'both' ('both' = mongo + influx)
|
||||||
|
|
||||||
import { doReadfromAPI as readin } from './readdata.js'
|
import { doReadfromAPI as readin } from './readdata.js'
|
||||||
import { constructDBaseEntries as parse} from './parse.js'
|
import { constructDBaseEntries as parse} from './parse.js'
|
||||||
|
|
||||||
import * as mongo from '../common/mongo.js'
|
import * as mongo from '../common/mongo.js'
|
||||||
import * as influx from '../common/influx_post.js'
|
import * as influx from '../common/influx_post.js'
|
||||||
|
import * as victoria from '../common/victoria_post.js'
|
||||||
import { logit, logerror } from '../common/logit.js'
|
import { logit, logerror } from '../common/logit.js'
|
||||||
import { statistics } from '../common/statistics.js'
|
import { statistics } from '../common/statistics.js'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
@@ -62,6 +63,11 @@ const fetchNewData = async (args) => {
|
|||||||
await influx.influxWrite(idata)
|
await influx.influxWrite(idata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// write sensor data to VictoriaMetrics
|
||||||
|
if(args.victoria) {
|
||||||
|
await victoria.victoriaWrite(idata)
|
||||||
|
}
|
||||||
|
|
||||||
// write properties to mongoDB
|
// write properties to mongoDB
|
||||||
await mongo.bulkWrite(client, mongo.property_coll, props)
|
await mongo.bulkWrite(client, mongo.property_coll, props)
|
||||||
}
|
}
|
||||||
@@ -78,7 +84,7 @@ const fetchNewData = async (args) => {
|
|||||||
function parse_cmdline(argv) {
|
function parse_cmdline(argv) {
|
||||||
let parser = new mod_getopt.BasicParser('i(influx)m(mongo)t:(typ)h(help)v(version)',argv);
|
let parser = new mod_getopt.BasicParser('i(influx)m(mongo)t:(typ)h(help)v(version)',argv);
|
||||||
let option;
|
let option;
|
||||||
let ret = {influx: STORE === 'both' || STORE === 'influx', mongo: STORE === 'both' || STORE === 'mongo', typ: TYP}
|
let ret = {influx: STORE === 'both' || STORE === 'influx', mongo: STORE === 'both' || STORE === 'mongo', victoria: STORE === 'victoria', typ: TYP}
|
||||||
while((option = parser.getopt()) !== undefined) {
|
while((option = parser.getopt()) !== undefined) {
|
||||||
switch(option.option) {
|
switch(option.option) {
|
||||||
case 'i':
|
case 'i':
|
||||||
@@ -111,6 +117,7 @@ function parse_cmdline(argv) {
|
|||||||
console.log("Params:");
|
console.log("Params:");
|
||||||
console.log(" -i use only InfluxDB to store the data (default use both, InfluxDB and MongoDB))");
|
console.log(" -i use only InfluxDB to store the data (default use both, InfluxDB and MongoDB))");
|
||||||
console.log(" -m use only MongoDB to store the data (default use both, InfluxDB and MongoDB)");
|
console.log(" -m use only MongoDB to store the data (default use both, InfluxDB and MongoDB)");
|
||||||
|
console.log(" (VictoriaMetrics storage is selected exclusively via STORE=victoria, no CLI flag)");
|
||||||
console.log(" -t [ sensorType, ..]: if given, only those sensors will be used (ex: laerm) default: all");
|
console.log(" -t [ sensorType, ..]: if given, only those sensors will be used (ex: laerm) default: all");
|
||||||
console.log(" MUST BE AN ARRAY!; allowed types: 'pm', 'noise', 'radiactivity', 'thp', 'gps'.")
|
console.log(" MUST BE AN ARRAY!; allowed types: 'pm', 'noise', 'radiactivity', 'thp', 'gps'.")
|
||||||
console.log(" -v version: show version");
|
console.log(" -v version: show version");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const DBASE = process.env.DBASE || 'mongo'
|
|||||||
import {DateTime} from "luxon"
|
import {DateTime} from "luxon"
|
||||||
import * as influx from "../databases/influx.js"
|
import * as influx from "../databases/influx.js"
|
||||||
import * as mongo from "../databases/mongo.js"
|
import * as mongo from "../databases/mongo.js"
|
||||||
|
import * as victoria from "../databases/victoria.js"
|
||||||
import {returnOnError} from "../utilities/reporterror.js"
|
import {returnOnError} from "../utilities/reporterror.js"
|
||||||
import {csv2Json} from "../utilities/csv2json.js"
|
import {csv2Json} from "../utilities/csv2json.js"
|
||||||
import checkParams from "../utilities/checkparams.js"
|
import checkParams from "../utilities/checkparams.js"
|
||||||
@@ -155,8 +156,10 @@ export async function getActData(opts) {
|
|||||||
return await mongo.fetchActData(opts)
|
return await mongo.fetchActData(opts)
|
||||||
} else if (DBASE === 'influx') {
|
} else if (DBASE === 'influx') {
|
||||||
return await influx.fetchActData(opts)
|
return await influx.fetchActData(opts)
|
||||||
|
} else if (DBASE === 'victoria') {
|
||||||
|
return await victoria.fetchActData(opts)
|
||||||
}
|
}
|
||||||
return {err: 'DBASEUNKNOWN', values: []}
|
return {err: 'DBASEUNKNOWN', values: []}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Access to VictoriaMetrics via HTTP (Prometheus-compatible /api/v1/export,
|
||||||
|
// no Flux support - see common/victoria_post.js for the write side, which
|
||||||
|
// reuses the InfluxDB line-protocol format on VictoriaMetrics' /write endpoint).
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import { logit, logerror } from '../utilities/logit.js'
|
||||||
|
import { returnOnError } from "../utilities/reporterror.js"
|
||||||
|
import { exportToRows, bucketNoiseAVG } from "../utilities/victoria2json.js"
|
||||||
|
|
||||||
|
let VICTORIAHOST = process.env.VICTORIAHOST || "localhost"
|
||||||
|
let VICTORIAPORT = process.env.VICTORIAPORT || 8428
|
||||||
|
|
||||||
|
const VICTORIAURL_EXPORT = `http://${VICTORIAHOST}:${VICTORIAPORT}/api/v1/export`
|
||||||
|
|
||||||
|
// opts.start/opts.stop arrive as Flux range() fragments ("start: <iso>" /
|
||||||
|
// "stop: <iso>") built by calcRange() in getsensorData.js - strip the Flux
|
||||||
|
// keyword the same way sensorapi/databases/mongo.js already does.
|
||||||
|
const isoStart = (opts) => opts.start.slice(7)
|
||||||
|
const isoStop = (opts) => opts.stop.slice(6)
|
||||||
|
|
||||||
|
const victoriaExport = async (matchSelector, start, end) => {
|
||||||
|
let erg = { values: '', err: null }
|
||||||
|
try {
|
||||||
|
let ret = await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: VICTORIAURL_EXPORT,
|
||||||
|
params: { 'match[]': matchSelector, start, end },
|
||||||
|
timeout: 10000,
|
||||||
|
transformResponse: [(data) => data], // response body is ndjson, not a single JSON document - keep it raw
|
||||||
|
})
|
||||||
|
if (ret.status !== 200) {
|
||||||
|
return returnOnError(erg, 'RESPSTATUS', victoriaExport.name, ret.status)
|
||||||
|
}
|
||||||
|
erg.values = ret.data
|
||||||
|
} catch (e) {
|
||||||
|
return returnOnError(erg, e, victoriaExport.name)
|
||||||
|
}
|
||||||
|
return erg
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchActData = async (opts) => {
|
||||||
|
let ret = { err: null, values: [] }
|
||||||
|
const match = `{__name__=~"noise_(LAeq|LA_min|LA_max|E10tel_eq)", sid="${opts.sensorid}"}`
|
||||||
|
let { values, err } = await victoriaExport(match, isoStart(opts), isoStop(opts))
|
||||||
|
if (err) {
|
||||||
|
return returnOnError(ret, err, fetchActData.name)
|
||||||
|
}
|
||||||
|
ret.values = exportToRows(values, opts.sort)
|
||||||
|
if (ret.values.length === 0) {
|
||||||
|
return returnOnError(ret, 'NODATA', fetchActData.name)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchNoiseAVGData = async (opts) => {
|
||||||
|
let ret = { err: null, values: [] }
|
||||||
|
const match = `{__name__=~"noise_(E10tel_eq|LA_max)", sid="${opts.sensorid}"}`
|
||||||
|
let { values, err } = await victoriaExport(match, isoStart(opts), isoStop(opts))
|
||||||
|
if (err) {
|
||||||
|
return returnOnError(ret, err, fetchNoiseAVGData.name)
|
||||||
|
}
|
||||||
|
ret.values = bucketNoiseAVG(values, opts.peak, opts.long)
|
||||||
|
if (ret.values.length === 0) {
|
||||||
|
return returnOnError(ret, 'NODATA', fetchNoiseAVGData.name)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import {DateTime} from 'luxon'
|
|||||||
import { translate as trans } from '../routes/api.js'
|
import { translate as trans } from '../routes/api.js'
|
||||||
import * as influx from "../databases/influx.js"
|
import * as influx from "../databases/influx.js"
|
||||||
import * as mongo from "../databases/mongo.js"
|
import * as mongo from "../databases/mongo.js"
|
||||||
|
import * as victoria from "../databases/victoria.js"
|
||||||
import { setoptionfromtable } from "../utilities/chartoptions.js"
|
import { setoptionfromtable } from "../utilities/chartoptions.js"
|
||||||
|
|
||||||
export const getNoiseData = async (params, possibles, props) => {
|
export const getNoiseData = async (params, possibles, props) => {
|
||||||
@@ -451,6 +452,9 @@ const getNoiseAVGData = async (opts) => {
|
|||||||
for (let x=0; x < ret.values.length; x++) {
|
for (let x=0; x < ret.values.length; x++) {
|
||||||
ret.values[x].datetime = DateTime.fromISO(ret.values[x].datetime).toUTC().minus({hours:1}).toFormat("yyyy-LL-dd'T'HH:mm:ss'Z'")
|
ret.values[x].datetime = DateTime.fromISO(ret.values[x].datetime).toUTC().minus({hours:1}).toFormat("yyyy-LL-dd'T'HH:mm:ss'Z'")
|
||||||
}
|
}
|
||||||
|
} else if (DBASE === 'victoria') {
|
||||||
|
// victoria.js buckets by hour-start itself (like mongo), so no shift is needed here
|
||||||
|
ret = await victoria.fetchNoiseAVGData(opts)
|
||||||
} else {
|
} else {
|
||||||
ret.err = 'DBASEUNKNOWN'
|
ret.err = 'DBASEUNKNOWN'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Parse VictoriaMetrics' /api/v1/export ndjson responses into the same row
|
||||||
|
// shapes influx.js builds via csv2Json() / pivot(), for the noise-specific
|
||||||
|
// fields (measurement "noise", separator "_", i.e. metric names noise_LAeq,
|
||||||
|
// noise_LA_min, noise_LA_max, noise_E10tel_eq).
|
||||||
|
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
|
||||||
|
const parseExportLines = (ndjsonBody) => {
|
||||||
|
return String(ndjsonBody)
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line.trim() !== '')
|
||||||
|
.map((line) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(line)
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((series) => series !== null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldName = (metricName) => metricName.replace(/^noise_/, '')
|
||||||
|
|
||||||
|
// Merges the per-field series (one ndjson line per field) back into rows of
|
||||||
|
// {datetime, LAeq, LA_min, LA_max, E10tel_eq}, matched by raw timestamp -
|
||||||
|
// mirrors influx's pivot(rowKey:["_time"], columnKey:["_field"]).
|
||||||
|
export const exportToRows = (ndjsonBody, sort) => {
|
||||||
|
const rowsByTime = new Map()
|
||||||
|
for (const series of parseExportLines(ndjsonBody)) {
|
||||||
|
const field = fieldName(series.metric.__name__)
|
||||||
|
const { values, timestamps } = series
|
||||||
|
for (let i = 0; i < timestamps.length; i++) {
|
||||||
|
const ts = timestamps[i]
|
||||||
|
let row = rowsByTime.get(ts)
|
||||||
|
if (!row) {
|
||||||
|
row = { datetime: DateTime.fromMillis(ts).toUTC().toISO() }
|
||||||
|
rowsByTime.set(ts, row)
|
||||||
|
}
|
||||||
|
row[field] = values[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rows = Array.from(rowsByTime.values())
|
||||||
|
rows.sort((a, b) => (a.datetime < b.datetime ? -1 : a.datetime > b.datetime ? 1 : 0))
|
||||||
|
if (sort === -1) {
|
||||||
|
rows.reverse()
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buckets raw E10tel_eq/LA_max samples by hour (truncated to the start of the
|
||||||
|
// hour of each sample, same convention as sensorapi/databases/mongo.js's
|
||||||
|
// $dateToString hour-grouping) and computes n_AVG/n_sum/count/peakcount -
|
||||||
|
// equivalent to influx.js's aggregateWindow()/reduce() Flux pipeline.
|
||||||
|
// Bucketing by hour-start (not hour-end, as Influx does) means no extra
|
||||||
|
// 1-hour shift is needed downstream, unlike the DBASE === 'influx' branch.
|
||||||
|
export const bucketNoiseAVG = (ndjsonBody, peak, long) => {
|
||||||
|
const buckets = new Map()
|
||||||
|
for (const series of parseExportLines(ndjsonBody)) {
|
||||||
|
const field = fieldName(series.metric.__name__)
|
||||||
|
if (field !== 'E10tel_eq' && field !== 'LA_max') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const { values, timestamps } = series
|
||||||
|
for (let i = 0; i < timestamps.length; i++) {
|
||||||
|
const key = DateTime.fromMillis(timestamps[i]).toUTC().startOf('hour').toISO()
|
||||||
|
let b = buckets.get(key)
|
||||||
|
if (!b) {
|
||||||
|
b = { sum: 0, count: 0, peakcount: 0 }
|
||||||
|
buckets.set(key, b)
|
||||||
|
}
|
||||||
|
if (field === 'E10tel_eq') {
|
||||||
|
b.sum += values[i]
|
||||||
|
b.count += 1
|
||||||
|
} else {
|
||||||
|
if (values[i] >= peak) {
|
||||||
|
b.peakcount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rows = Array.from(buckets.entries())
|
||||||
|
.filter(([, b]) => b.count > 0) // matches Influx's inner join: hours without E10tel_eq samples are dropped
|
||||||
|
.map(([datetime, b]) => {
|
||||||
|
let row = {
|
||||||
|
datetime,
|
||||||
|
n_AVG: 10 * Math.log10(b.sum / b.count),
|
||||||
|
peakcount: b.peakcount,
|
||||||
|
}
|
||||||
|
if (long) {
|
||||||
|
row.count = b.count
|
||||||
|
row.n_sum = b.sum
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
})
|
||||||
|
rows.sort((a, b) => (a.datetime < b.datetime ? -1 : a.datetime > b.datetime ? 1 : 0))
|
||||||
|
return rows
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user