feat: add prometheus metrics and dashboard
Integration Tests / integration-test (pull_request) Successful in 2m33s
Integration Tests / integration-test (pull_request) Successful in 2m33s
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>pgsql-broker metrics</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #0f1115;
|
||||
--panel: #161a22;
|
||||
--border: #262b36;
|
||||
--text: #e6e9ef;
|
||||
--muted: #8b93a7;
|
||||
--accent: #5aa8ff;
|
||||
--good: #4caf7d;
|
||||
--bad: #e5636b;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.25rem;
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
#status {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
.card h2 {
|
||||
font-size: 0.9rem;
|
||||
margin: 0 0 0.75rem;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.card .help {
|
||||
color: var(--muted);
|
||||
font-size: 0.75rem;
|
||||
margin: -0.5rem 0 0.75rem;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
td, th {
|
||||
text-align: left;
|
||||
padding: 0.2rem 0.4rem 0.2rem 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
td.value {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
td.labels {
|
||||
color: var(--muted);
|
||||
}
|
||||
.completed .value { color: var(--good); }
|
||||
.failed .value { color: var(--bad); }
|
||||
.empty {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>pgsql-broker metrics</h1>
|
||||
<div id="status">loading…</div>
|
||||
<div class="grid" id="grid"></div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Metric name -> { help, className } for the cards we render, in order.
|
||||
var METRICS = [
|
||||
{ name: "broker_jobs_completed_total", title: "Jobs completed", cls: "completed" },
|
||||
{ name: "broker_jobs_failed_total", title: "Jobs failed (dead-lettered)", cls: "failed" },
|
||||
{ name: "broker_jobs_requeued_total", title: "Jobs requeued (retries)", cls: "" },
|
||||
{ name: "broker_jobs_queued", title: "Jobs currently queued", cls: "" },
|
||||
{ name: "broker_job_duration_seconds_sum", title: "Job duration, total seconds by group/name", cls: "" },
|
||||
{ name: "broker_job_duration_seconds_count", title: "Job duration, sample count by group/name", cls: "" },
|
||||
{ name: "broker_databases", title: "Databases managed", cls: "" },
|
||||
{ name: "broker_queues", title: "Queues per database", cls: "" }
|
||||
];
|
||||
|
||||
// Parses Prometheus text exposition format into { name: [{labels, value}] }.
|
||||
function parseMetrics(text) {
|
||||
var byName = {};
|
||||
var lines = text.split("\n");
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i];
|
||||
if (!line || line[0] === "#") continue;
|
||||
|
||||
var name, labels = {}, rest;
|
||||
var braceIdx = line.indexOf("{");
|
||||
var spaceIdx;
|
||||
if (braceIdx !== -1) {
|
||||
name = line.slice(0, braceIdx);
|
||||
var closeIdx = line.indexOf("}", braceIdx);
|
||||
if (closeIdx === -1) continue;
|
||||
var labelStr = line.slice(braceIdx + 1, closeIdx);
|
||||
var labelRe = /(\w+)="((?:[^"\\]|\\.)*)"/g;
|
||||
var m;
|
||||
while ((m = labelRe.exec(labelStr)) !== null) {
|
||||
labels[m[1]] = m[2].replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
||||
}
|
||||
rest = line.slice(closeIdx + 1).trim();
|
||||
} else {
|
||||
spaceIdx = line.indexOf(" ");
|
||||
if (spaceIdx === -1) continue;
|
||||
name = line.slice(0, spaceIdx);
|
||||
rest = line.slice(spaceIdx + 1).trim();
|
||||
}
|
||||
|
||||
var value = parseFloat(rest.split(" ")[0]);
|
||||
if (isNaN(value)) continue;
|
||||
|
||||
if (!byName[name]) byName[name] = [];
|
||||
byName[name].push({ labels: labels, value: value });
|
||||
}
|
||||
return byName;
|
||||
}
|
||||
|
||||
function formatLabels(labels) {
|
||||
var keys = Object.keys(labels).sort();
|
||||
return keys.map(function (k) { return k + "=" + labels[k]; }).join(", ");
|
||||
}
|
||||
|
||||
function formatValue(v) {
|
||||
if (Number.isInteger(v)) return String(v);
|
||||
return v.toFixed(3);
|
||||
}
|
||||
|
||||
function render(byName) {
|
||||
var grid = document.getElementById("grid");
|
||||
grid.innerHTML = "";
|
||||
|
||||
METRICS.forEach(function (spec) {
|
||||
var series = byName[spec.name] || [];
|
||||
var card = document.createElement("div");
|
||||
card.className = "card " + spec.cls;
|
||||
|
||||
var h2 = document.createElement("h2");
|
||||
h2.textContent = spec.title;
|
||||
card.appendChild(h2);
|
||||
|
||||
if (series.length === 0) {
|
||||
var empty = document.createElement("div");
|
||||
empty.className = "empty";
|
||||
empty.textContent = "no data yet";
|
||||
card.appendChild(empty);
|
||||
} else {
|
||||
var table = document.createElement("table");
|
||||
series.sort(function (a, b) { return b.value - a.value; });
|
||||
series.forEach(function (s) {
|
||||
var tr = document.createElement("tr");
|
||||
var tdLabels = document.createElement("td");
|
||||
tdLabels.className = "labels";
|
||||
tdLabels.textContent = formatLabels(s.labels) || "(none)";
|
||||
var tdValue = document.createElement("td");
|
||||
tdValue.className = "value";
|
||||
tdValue.textContent = formatValue(s.value);
|
||||
tr.appendChild(tdLabels);
|
||||
tr.appendChild(tdValue);
|
||||
table.appendChild(tr);
|
||||
});
|
||||
card.appendChild(table);
|
||||
}
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetch("metrics", { cache: "no-store" })
|
||||
.then(function (resp) {
|
||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
||||
return resp.text();
|
||||
})
|
||||
.then(function (text) {
|
||||
render(parseMetrics(text));
|
||||
document.getElementById("status").textContent =
|
||||
"last updated " + new Date().toLocaleTimeString();
|
||||
})
|
||||
.catch(function (err) {
|
||||
document.getElementById("status").textContent =
|
||||
"failed to load metrics: " + err.message;
|
||||
});
|
||||
}
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 5000);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user