implemented 09-system-metrics.md: added system metrics: introduced system monitoring with a metrics collector, REST API endpoint, Thymeleaf UI rendering, and lifecycle management
This commit is contained in:
@@ -86,6 +86,12 @@ tbody.is-stale { opacity: 0.55; }
|
||||
.duration-cell { white-space: nowrap; }
|
||||
.empty { padding: 28px 14px; color: var(--muted); text-align: center; }
|
||||
|
||||
/* system metrics */
|
||||
#system-table { min-width: 640px; }
|
||||
.num { text-align: right; font-variant-numeric: tabular-nums; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; }
|
||||
th.num { font-family: inherit; font-size: 12px; }
|
||||
.meta { margin: 14px 0 0; color: var(--muted); font-size: 13px; display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 6px; }
|
||||
|
||||
/* status badges */
|
||||
.status { display: inline-flex; align-items: center; min-width: 72px; justify-content: center; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; text-transform: uppercase; }
|
||||
.status-success { background: var(--success-bg); color: var(--success-text); }
|
||||
@@ -144,4 +150,6 @@ tbody.is-stale { opacity: 0.55; }
|
||||
td + td { border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent); }
|
||||
td[data-label]::before { content: attr(data-label); width: 90px; flex-shrink: 0; font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--muted); }
|
||||
.actions-cell { justify-content: center; background: color-mix(in srgb, var(--border) 18%, transparent); width: auto; }
|
||||
#system-table { min-width: 0; }
|
||||
.num { text-align: left; }
|
||||
}
|
||||
|
||||
@@ -34,6 +34,20 @@ function abbrevCommit(commit) {
|
||||
return (commit || "").slice(0, 12);
|
||||
}
|
||||
|
||||
/** Two-decimal metric value like `UiFormats.metric`; "n/a" when the source is unavailable. */
|
||||
function formatMetric(value) {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : "n/a";
|
||||
}
|
||||
|
||||
function formatTimeOfDay(iso) {
|
||||
const date = new Date(iso);
|
||||
if (!iso || Number.isNaN(date.getTime())) {
|
||||
return "n/a";
|
||||
}
|
||||
const two = (n) => String(n).padStart(2, "0");
|
||||
return `${two(date.getHours())}:${two(date.getMinutes())}:${two(date.getSeconds())}`;
|
||||
}
|
||||
|
||||
const KNOWN_STATUSES = new Set(
|
||||
["success", "failed", "running", "pending", "interrupted", "cancelled", "unknown", "error", "finished"],
|
||||
);
|
||||
@@ -47,6 +61,7 @@ function statusCssClass(status) {
|
||||
const FETCH_TIMEOUT_MS = 8000;
|
||||
const TABLE_POLL_MS = 10000;
|
||||
const CURRENT_POLL_MS = 3000;
|
||||
const SYSTEM_POLL_MS = 60000;
|
||||
|
||||
function metaContent(name) {
|
||||
const element = document.querySelector(`meta[name="${name}"]`);
|
||||
@@ -376,6 +391,39 @@ function initCurrentBuilds() {
|
||||
startPolling(refresh, CURRENT_POLL_MS);
|
||||
}
|
||||
|
||||
// ---- system metrics ----------------------------------------------------------
|
||||
|
||||
/** The metric rows are fixed, so only the cell texts are updated — never rebuilt. */
|
||||
function initSystemTable() {
|
||||
const table = document.getElementById("system-table");
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
function setText(id, text) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const metrics = await fetchJson(table.dataset.api);
|
||||
table.querySelectorAll("tbody tr[data-metric]").forEach((row) => {
|
||||
const aggregate = metrics[row.dataset.metric];
|
||||
row.querySelectorAll("[data-field]").forEach((cell) => {
|
||||
cell.textContent = formatMetric(aggregate ? aggregate[cell.dataset.field] : null);
|
||||
});
|
||||
});
|
||||
setText("info-cpu-count", metrics.cpuCount != null ? metrics.cpuCount + " cores" : "n/a");
|
||||
setText("info-ram-total", metrics.ramTotalGib != null ? formatMetric(metrics.ramTotalGib) + " GiB" : "n/a");
|
||||
setText("info-disk-total", metrics.diskTotalGib != null ? formatMetric(metrics.diskTotalGib) + " GiB" : "n/a");
|
||||
setText("info-updated", formatTimeOfDay(metrics.timestamp));
|
||||
}
|
||||
|
||||
startPolling(refresh, SYSTEM_POLL_MS);
|
||||
}
|
||||
|
||||
// ---- running-duration ticking ------------------------------------------------
|
||||
|
||||
function tickRunningDurations() {
|
||||
@@ -438,5 +486,6 @@ document.addEventListener("click", async (event) => {
|
||||
|
||||
initBuildsTable();
|
||||
initCurrentBuilds();
|
||||
initSystemTable();
|
||||
setInterval(tickRunningDurations, 1000);
|
||||
tickRunningDurations();
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
<a th:unless="${view == 'history'}" href="/history">History</a>
|
||||
<span th:if="${view == 'current'}">Current</span>
|
||||
<a th:unless="${view == 'current'}" href="/current">Current</a>
|
||||
<span th:if="${view == 'system'}">System</span>
|
||||
<a th:unless="${view == 'system'}" href="/system">System</a>
|
||||
</nav>
|
||||
<span class="view-row-actions">
|
||||
<span id="live-indicator" class="status status-unknown" title="live-update state">static</span>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<!doctype html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="en">
|
||||
<head th:replace="~{fragments :: head(${pageTitle})}"></head>
|
||||
<body>
|
||||
<main>
|
||||
<h1 th:replace="~{fragments :: header(${pageTitle})}"></h1>
|
||||
<div th:replace="~{fragments :: nav(${view})}"></div>
|
||||
<div class="table-wrap">
|
||||
<table id="system-table" data-api="/api/system">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Metric</th>
|
||||
<th class="num">Current</th>
|
||||
<th class="num">Min</th>
|
||||
<th class="num">Max</th>
|
||||
<th class="num">Avg</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="system-rows">
|
||||
<tr th:each="row : ${metrics.rows}" th:attr="data-metric=${row.key}">
|
||||
<td data-label="Metric" th:text="${row.label}">CPU used (cores)</td>
|
||||
<td class="num" data-label="Current" data-field="current" th:text="${row.current}">0.42</td>
|
||||
<td class="num" data-label="Min" data-field="min" th:text="${row.min}">0.10</td>
|
||||
<td class="num" data-label="Max" data-field="max" th:text="${row.max}">3.20</td>
|
||||
<td class="num" data-label="Avg" data-field="avg" th:text="${row.avg}">0.80</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="meta">
|
||||
<span>
|
||||
CPU total: <strong id="info-cpu-count" th:text="${metrics.cpuCount}">8 cores</strong>
|
||||
· RAM total: <strong id="info-ram-total" th:text="${metrics.ramTotal}">31.29 GiB</strong>
|
||||
· Disk total: <strong id="info-disk-total" th:text="${metrics.diskTotal}">465.12 GiB</strong>
|
||||
· Updated: <span id="info-updated" th:text="${metrics.updated}">12:00:00</span>
|
||||
(updated every 60s)
|
||||
</span>
|
||||
<span>(*: min/max/avg since the first server start)</span>
|
||||
</p>
|
||||
</main>
|
||||
<footer th:replace="~{fragments :: footer}"></footer>
|
||||
<script src="/gittally.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user