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:
2026-08-20 21:31:06 +02:00
parent b1b8f39ff4
commit 23fe7d2ed2
12 changed files with 302 additions and 14 deletions
+6
View File
@@ -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
+16 -2
View File
@@ -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.
+55
View File
@@ -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
}
+4 -1
View File
@@ -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>
+19 -5
View File
@@ -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}
+3 -1
View File
@@ -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}
+18 -2
View File
@@ -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');
+9 -2
View File
@@ -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 -1
View File
@@ -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: []}
} }
+67
View File
@@ -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
}
+4
View File
@@ -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'
} }
+97
View File
@@ -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
}