#!/usr/bin/env node // Verifie que CHAQUE requete des tableaux de bord Grafana s'execute vraiment. // // Grafana n'annonce une requete cassee qu'a l'ecran, panneau par panneau. Ce // controle rejoue les requetes SQL contre PostgreSQL et les requetes Flux // contre InfluxDB, avec les macros substituees comme Grafana le fait. // // node tools/verifier-dashboards.mjs // // Sortie non nulle si une seule requete echoue. import fs from 'node:fs'; import path from 'node:path'; import pg from 'pg'; const env = Object.fromEntries( fs.readFileSync('.env', 'utf8').split('\n').filter(l => l.includes('=')) .map(l => [l.slice(0, l.indexOf('=')).trim(), l.slice(l.indexOf('=') + 1).trim()])); const DEPUIS = new Date(Date.now() - 24 * 3600e3).toISOString(); const JUSQUA = new Date().toISOString(); // Les macros de Grafana, telles qu'il les developpe cote serveur. const macros = (q) => q .replace(/\$__timeFilter\(([^)]+)\)/g, `$1 BETWEEN '${DEPUIS}' AND '${JUSQUA}'`) .replace(/\$__timeFrom\(\)/g, `'${DEPUIS}'`) .replace(/\$__timeTo\(\)/g, `'${JUSQUA}'`); const fluxVars = (q) => q .replace(/v\.timeRangeStart/g, `${DEPUIS}`) .replace(/v\.timeRangeStop/g, `${JUSQUA}`) .replace(/v\.windowPeriod/g, '1m'); const client = new pg.Client({ host: '10.10.10.22', port: 5433, database: 'evcs', user: 'evcs_lecture', password: env.PG_LECTURE_PASSWORD }); await client.connect(); async function flux(q) { const r = await fetch('http://10.10.10.22:8086/api/v2/query?org=airtria', { method: 'POST', headers: { Authorization: `Token ${env.INFLUXDB_TOKEN}`, 'Content-Type': 'application/vnd.flux', Accept: 'application/csv' }, body: q }); const t = await r.text(); if (!r.ok) throw new Error(t.slice(0, 300)); return t.split('\n').filter(l => l.includes(',_result,')).length; } const dossier = 'deploy/historisation/grafana/dashboards'; let echecs = 0, total = 0; for (const f of fs.readdirSync(dossier).filter(f => f.endsWith('.json')).sort()) { const d = JSON.parse(fs.readFileSync(path.join(dossier, f), 'utf8')); console.log(`\n=== ${d.title} (${f}) ===`); // Les annotations sont des requetes comme les autres, et elles s'executent // sur TOUS les panneaux : une annotation cassee casse tout le tableau. const cibles = []; for (const a of d.annotations.list) { if (a.target?.rawSql) cibles.push({ ou: 'annotation ' + a.name, sql: a.target.rawSql }); } for (const p of d.panels) { for (const t of p.targets ?? []) { if (t.rawSql) cibles.push({ ou: p.title, sql: t.rawSql }); else if (t.query) cibles.push({ ou: p.title, flux: t.query }); } } for (const c of cibles) { total++; try { const n = c.sql ? (await client.query(macros(c.sql))).rows.length : await flux(fluxVars(c.flux)); const vide = n === 0 ? ' (aucune ligne sur 24 h — normal si la mesure vient de demarrer)' : ''; console.log(` OK ${String(n).padStart(5)} lignes ${c.ou}${vide}`); } catch (e) { echecs++; console.log(` ECHEC ${c.ou}\n ${String(e.message).split('\n')[0]}`); } } } await client.end(); console.log(`\n${total - echecs}/${total} requetes valides`); process.exit(echecs ? 1 : 0);