4.4 KiB · text History 7a88816
// Runs every panel of every dashboard (or the uids given) through
// contents/ui/Grafana.js against a real instance, the same way the widget
// does, and prints what each panel would show.
//
//   node tools/check.mjs            # reads GRAFANA_URL / GRAFANA_TOKEN from .env
//   node tools/check.mjs git-gud waxx
import { readFileSync } from "node:fs"
import vm from "node:vm"

const root = new URL("..", import.meta.url).pathname
const env = Object.fromEntries(
    readFileSync(root + ".env", "utf8").split("\n").filter(l => l.includes("="))
        .map(l => [l.slice(0, l.indexOf("=")).trim(), l.slice(l.indexOf("=") + 1).trim()]))
const base = (env.GRAFANA_URL || "").replace(/\/+$/, "")
const headers = { Authorization: "Bearer " + env.GRAFANA_TOKEN, "Content-Type": "application/json", Accept: "application/json" }

const src = readFileSync(root + "contents/ui/Grafana.js", "utf8").replace(/^\.pragma library\s*$/m, "")
const sandbox = { module: { exports: {} } }
vm.runInNewContext(src, sandbox)
const G = sandbox.module.exports

async function get(path) {
    const r = await fetch(base + path, { headers })
    if (!r.ok) throw new Error(path + ": HTTP " + r.status)
    return r.json()
}

const settings = await get("/api/frontend/settings")
const uids = process.argv.slice(2).length ? process.argv.slice(2)
    : (await get("/api/search?type=dash-db&limit=500")).map(d => d.uid)

let failures = 0
for (const uid of uids) {
    const { dashboard } = await get("/api/dashboards/uid/" + uid)
    const range = G.resolveRange(dashboard.time || {}, Date.now())
    // evaluate query variables in order, each seeing the ones before it
    const fetched = {}
    let vars = G.variables(dashboard, settings.datasources, {}, fetched)
    for (const v of dashboard.templating?.list || []) {
        const req = G.varRequest(v, vars, settings.datasources, range)
        if (!req) continue
        fetched[v.name] = G.varOptions(v, req, await get(req.path))
        vars = G.variables(dashboard, settings.datasources, {}, fetched)
        console.log(`  $${v.name}: ${fetched[v.name].length} options, selected ${G.selectionText(vars[v.name])}`)
    }
    // every row open, so folded panels get checked too
    const open = Object.fromEntries((dashboard.panels || []).filter(p => p.type === "row").map(p => [p.id, false]))
    const items = G.layout(dashboard, open, vars)
    console.log(`\n== ${dashboard.title} (${uid}) — ${items.length} items, ${G.rangeLabel(dashboard.time || {})}`)
    for (const it of items) {
        const p = it.panel
        if (it.kind === "row") { console.log(`  ▸ row "${p.title}" ${it.collapsed ? "(folded, " + it.count + ")" : ""}`); continue }
        const where = `[${it.x},${it.y} ${it.w}x${it.h}]`
        if (!G.supported(p.type)) { console.log(`  ? ${where} ${p.type} "${p.title}" unsupported`); continue }
        if (p.type === "text") { console.log(`  T ${where} text "${p.title}"`); continue }
        const q = G.buildQuery(p, dashboard, vars, settings.datasources, range, 300)
        if (q.error) { console.log(`  ! ${where} ${p.type} "${p.title}": ${q.error}`); failures++; continue }
        const r = await fetch(base + "/api/ds/query", { method: "POST", headers, body: JSON.stringify(q.body) })
        const data = await r.json()
        if (!r.ok) { console.log(`  ! ${where} ${p.type} "${p.title}": HTTP ${r.status} ${data.message || ""}`); failures++; continue }
        const errs = G.resultErrors(data.results)
        const series = G.seriesFromResults(data.results, p)
        let shown
        if (p.type === "table") {
            const t = G.tableFromResults(data.results, p)
            shown = `${t.columns.length} cols × ${t.rows.length} rows: ${t.columns.map(c => c.name).join(", ")}`
        } else {
            const calc = (p.options?.reduceOptions?.calcs || ["lastNotNull"])[0]
            shown = series.slice(0, 3).map((s, i) => {
                const cfg = G.fieldConfig(p, s)
                const v = G.reduce(s.values, calc)
                return `${G.displayName(s, cfg)}=${G.format(v, cfg.unit, cfg.decimals)} ${G.seriesColor(cfg, i, v, null, "text")}`
            }).join(" | ") + (series.length > 3 ? ` (+${series.length - 3})` : "")
        }
        console.log(`  ${errs.length ? "!" : "✓"} ${where} ${p.type} "${G.panelTitle(p, vars)}" ${series.length} series ${shown}${errs.length ? " ERR " + errs.join("; ") : ""}`)
        if (errs.length) failures++
    }
}
console.log(`\n${failures} panel(s) failed`)