Database statistics in Settings > Data

New GET /api/dbstats reads VictoriaLogs /metrics (stored lines, size on
disk, raw size, free space, partitions, retention) and two LogsQL queries
(period covered, distinct hosts and apps, lines of the last 24 h and hour),
cached for 30 s. The danger zone shows them, sizes in KB/MB/GB or Ko/Mo/Go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude BotandClaude Opus 5.5 committed 2026-10-03 17:08:00 +02:00
1 parent e901de442c
commit 8b5ee73ee1
8 files changed
+381 -2

No files matched your search

+87
View File
@@ -126,6 +126,16 @@ const I18N = {
fontHelp: 'Free fonts. The built-in ones (Inconsolata Condensed, the narrowest, Iosevka and Ubuntu Mono) are served by LogStream itself and work offline; they are narrow, so more text fits on each line. The others are loaded by your browser from Bunny Fonts, a privacy-friendly European font service; without internet access, the system font is used.',
previewMsgs: ['error: link down on eth1, carrier lost', 'warning: disk /dev/sda temperature 58°C', 'backup finished: ok (12.4 GB in 00:14:32)'],
dangerZone: 'Danger zone',
dbTitle: 'Database', dbRefresh: 'Refresh', dbLoading: 'Loading…',
dbUnits: ['B', 'KB', 'MB', 'GB', 'TB'],
dbRows: 'Stored lines', dbDisk: 'Size on disk', dbIndex: (x) => `including ${x} of index`,
dbRaw: 'Raw size', dbRatio: (x) => `compressed ×${x}`,
dbPeriod: 'Period', dbFromTo: (p) => `${p.from} → ${p.to}`, dbDays: (n) => `${n} day${n > 1 ? 's' : ''}`,
dbRetention: (r) => `retention ${r}`,
dbRecent: 'Last 24 h', dbLastHour: (n) => `${n} in the last hour`,
dbSources: 'Sources', dbHostsApps: (p) => `${p.h} host${p.hn > 1 ? 's' : ''} · ${p.a} app${p.an > 1 ? 's' : ''}`,
dbFree: 'Free disk space', dbEmpty: 'The database is empty.',
dbQueryErr: 'Some figures could not be computed: ',
purgeHelp: 'Permanently delete every stored log to start from scratch. Color tags and settings are kept.',
purgeBtn: 'Delete all logs…',
purgePrompt: 'This permanently deletes ALL stored logs.\n\nType PURGE to confirm:',
@@ -285,6 +295,16 @@ const I18N = {
fontHelp: 'Polices libres. Les polices intégrées (Inconsolata Condensed, la plus étroite, Iosevka et Ubuntu Mono) sont servies par LogStream lui-même et fonctionnent hors ligne ; elles sont étroites, donc chaque ligne affiche plus de texte. Les autres sont chargées par votre navigateur depuis Bunny Fonts, un service européen respectueux de la vie privée ; sans accès à internet, la police du système est utilisée.',
previewMsgs: ['error: link down on eth1, carrier lost', 'warning: disk /dev/sda temperature 58°C', 'backup finished: ok (12.4 GB in 00:14:32)'],
dangerZone: 'Zone de danger',
dbTitle: 'Base de données', dbRefresh: 'Actualiser', dbLoading: 'Chargement…',
dbUnits: ['o', 'Ko', 'Mo', 'Go', 'To'],
dbRows: 'Lignes stockées', dbDisk: 'Taille sur disque', dbIndex: (x) => `dont ${x} d'index`,
dbRaw: 'Taille brute', dbRatio: (x) => `compression ×${x}`,
dbPeriod: 'Période', dbFromTo: (p) => `${p.from} → ${p.to}`, dbDays: (n) => `${n} jour${n > 1 ? 's' : ''}`,
dbRetention: (r) => `rétention ${r}`,
dbRecent: 'Dernières 24 h', dbLastHour: (n) => `${n} sur la dernière heure`,
dbSources: 'Sources', dbHostsApps: (p) => `${p.h} hôte${p.hn > 1 ? 's' : ''} · ${p.a} application${p.an > 1 ? 's' : ''}`,
dbFree: 'Espace disque libre', dbEmpty: 'La base est vide.',
dbQueryErr: 'Certains chiffres n\'ont pas pu être calculés : ',
purgeHelp: 'Supprime définitivement tous les logs stockés pour repartir de zéro. Les tags de couleur et les réglages sont conservés.',
purgeBtn: 'Supprimer tous les logs…',
purgePrompt: 'Cette action supprime définitivement TOUS les logs stockés.\n\nTapez PURGE pour confirmer :',
@@ -509,6 +529,7 @@ function setLang(next) {
renderPresets();
renderTimeSettings();
renderPurge();
renderDbStats();
renderInterface();
renderSyslog();
renderHost();
@@ -2010,6 +2031,69 @@ setInterval(() => {
}
}, 4000);
/* ================= Database statistics ================= */
// 1536 -> "1,5 Ko" / "1.5 KB" (binary multiples, the usual reading for disk sizes).
function fmtBytes(n) {
const units = t('dbUnits');
let i = 0;
n = Number(n || 0);
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
const digits = i === 0 || n >= 100 ? 0 : 1;
return `${n.toLocaleString(t('locale'), { maximumFractionDigits: digits })} ${units[i]}`;
}
const db = { stats: null, error: null, loading: false };
function renderDbStats() {
const el = $('#dbStats');
const s = db.stats;
if (!s) {
el.innerHTML = `<div class="db-msg${db.error ? ' bad' : ''}">${esc(db.error || t('dbLoading'))}</div>`;
return;
}
const row = (k, v, sub) => `<dt>${esc(k)}</dt><dd><b>${esc(v)}</b>${sub ? ` <span class="muted">${esc(sub)}</span>` : ''}</dd>`;
const day = (x) => fmtFull(x).replace(/[.,]\d{3}(?=\D*$)/, '');
let html = row(t('dbRows'), fmtNum(s.rows));
html += row(t('dbDisk'), fmtBytes(s.diskBytes), s.indexBytes ? t('dbIndex', fmtBytes(s.indexBytes)) : '');
if (s.rawBytes) {
const ratio = s.diskBytes ? (s.rawBytes / s.diskBytes).toLocaleString(t('locale'), { maximumFractionDigits: 1 }) : '';
html += row(t('dbRaw'), fmtBytes(s.rawBytes), ratio ? t('dbRatio', ratio) : '');
}
if (s.oldest && s.newest) {
const sub = [s.days ? t('dbDays', s.days) : '', s.retention ? t('dbRetention', s.retention) : ''].filter(Boolean).join(' · ');
html += row(t('dbPeriod'), t('dbFromTo', { from: day(s.oldest), to: day(s.newest) }), sub);
} else if (s.retention) {
html += row(t('dbPeriod'), '—', t('dbRetention', s.retention));
}
if (s.rows) {
html += row(t('dbRecent'), fmtNum(s.last24h), t('dbLastHour', fmtNum(s.last1h)));
html += row(t('dbSources'), t('dbHostsApps', { h: fmtNum(s.hosts), hn: s.hosts, a: fmtNum(s.apps), an: s.apps }));
}
if (s.freeBytes) html += row(t('dbFree'), fmtBytes(s.freeBytes));
if (!s.rows) html += `<div class="db-msg">${esc(t('dbEmpty'))}</div>`;
if (s.queryError) html += `<div class="db-msg bad">${esc(t('dbQueryErr') + s.queryError)}</div>`;
el.innerHTML = html;
}
async function loadDbStats(refresh = false) {
if (db.loading) return;
db.loading = true;
$('#dbRefresh').disabled = true;
try {
db.stats = await api('/api/dbstats' + (refresh ? '?refresh=1' : ''));
db.error = null;
} catch (e) {
db.stats = null;
db.error = e.message;
} finally {
db.loading = false;
$('#dbRefresh').disabled = false;
}
renderDbStats();
}
$('#dbRefresh').addEventListener('click', () => loadDbStats(true));
/* ================= Purge ================= */
const purge = { allowed: true, running: 0, error: null, code: null, wasRunning: false, timer: null };
@@ -2039,6 +2123,7 @@ async function loadPurgeStatus() {
refresh();
loadFacets();
loadStats();
loadDbStats(true);
}
purge.running = s.running || 0;
} catch { /* shown on the next attempt */ }
@@ -2237,6 +2322,8 @@ $('#settingsBtn').addEventListener('click', () => {
loadSyslog();
loadHost();
loadDocker();
renderDbStats();
loadDbStats();
showSettingsTab(store.get('settingsTab', 'locale'));
$('#settingsDlg').showModal();
});
+5
View File
@@ -339,6 +339,11 @@
<div class="set-panel wide" role="tabpanel" data-panel="data" hidden>
<section class="set-section danger">
<h3 data-i18n="dangerZone">Danger zone</h3>
<div class="db-head">
<span class="lbl" data-i18n="dbTitle">Database</span>
<button id="dbRefresh" class="link-btn" type="button" data-i18n="dbRefresh">Refresh</button>
</div>
<dl id="dbStats" class="db-stats"></dl>
<p class="muted small" data-i18n="purgeHelp"></p>
<div class="purge-row">
<button id="purgeBtn" class="btn danger" type="button" data-i18n="purgeBtn">Delete all logs…</button>
+11
View File
@@ -559,6 +559,17 @@ select.field:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0
background: var(--panel-2); font-family: var(--mono); font-size: 12.5px;
}
.set-section.danger h3 { color: var(--sev-err); }
.db-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin: 4px 0 6px; }
.db-stats {
display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 5px 14px;
margin: 0 0 14px; padding: 10px 12px; border-radius: 8px; background: var(--panel-2); font-size: 12.5px;
}
.db-stats dt { color: var(--muted); }
.db-stats dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.db-stats dd b { font-weight: 600; font-variant-numeric: tabular-nums; }
.db-msg { grid-column: 1 / -1; color: var(--muted); }
.db-msg.bad { color: var(--sev-err); }
.link-btn:disabled { opacity: .5; cursor: default; text-decoration: none; }
.purge-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 10px; }
.btn.danger { color: var(--sev-err); border-color: color-mix(in srgb, var(--sev-err) 45%, var(--border)); }
.btn.danger:hover:not(:disabled) { background: var(--sev-err); border-color: var(--sev-err); color: #fff; }