Files
wekan/models/lib/diskHighTracker.js
Lauri Ojansivu b6a3d98736 Admin Panel / Problems: Memory Usage and Disk usage reports.
The RAM and Disk usage pages were designs that no code carried out. Both
monitors now run beside the CPU one and, like it, record only the start and
the end of a sustained high-usage period:

- Memory: RAM from /proc/meminfo's MemAvailable - os.freemem() is MemFree,
  which counts the page cache as used and shows a healthy server as full -
  and swap, which alone also starts a period. Without /proc/meminfo it uses
  what Node reports. Settings WEKAN_RAM_* and WEKAN_SWAP_HIGH_PERCENT.
- Disk: each filesystem WeKan writes to - the files root, its temp directory
  for uploads and the snap's data directory - measured once per device, with
  an episode of its own. WEKAN_DISK_WATCH_PATHS replaces the paths.
- Each report has a live line above its rows and a URL
  (/admin/problems/ram, /admin/problems/disk). The menu and titles use the
  existing memory-usage and sandstorm-disk-usage keys, which every language
  translates, and the Summary names the cpu, ram and disk streams.

Observe and report only, as designed. The three monitors share one event
recorder (server/lib/resourceLog.js) and one intEnv (models/lib/intEnv.js),
which three files had each defined. The pure halves are tested without
Meteor, and resourceMonitorsWiring checks the wiring and that no copy of
either shared piece returns.

Thanks to xet7 !
2026-10-17 01:14:16 +03:00

70 lines
2.8 KiB
JavaScript

'use strict';
// Disk-usage monitor, the pure half (docs/Features/Admin-Panel/Problems/Disk-usage.md):
// turn fs.statfs() numbers into a used percentage, decide which filesystems to
// watch, and follow each one's high-usage episodes. server/lib/diskMonitor.js
// feeds it. The episode hysteresis is the CPU monitor's, one tracker per
// filesystem, so a full disk on one mount never hides another.
const { HighCpuTracker } = require('./cpuHighTracker');
// statfs: bsize x blocks is the size, bsize x bavail what a non-root process
// may still write. null when the numbers are missing or the size is zero.
function statfsUsage(stat) {
if (!stat) return null;
const bsize = Number(stat.bsize);
const blocks = Number(stat.blocks);
const bavail = Number(stat.bavail);
if (!(bsize > 0) || !(blocks > 0) || !Number.isFinite(bavail) || bavail < 0) return null;
const total = bsize * blocks;
const available = Math.min(total, bsize * bavail);
return { total, available, usedPct: Math.round(((total - available) / total) * 100) };
}
// The paths to watch, each once, with what it holds. `filesRoot` is the
// directory WeKan stores files under (WRITABLE_PATH or its files/). `override` is
// WEKAN_DISK_WATCH_PATHS (':'-separated) and replaces the defaults.
function watchedPaths({ override, dataPath, filesRoot }) {
const entries = [];
if (override && String(override).trim()) {
String(override).split(':').map(p => p.trim()).filter(Boolean)
.forEach(p => entries.push({ path: p, role: 'watched' }));
} else {
if (dataPath) entries.push({ path: dataPath, role: 'database' });
// Uploads are sanitized in <files root>/temp, beside attachments/ and
// avatars/ (server/lib/fileStatusAudit.js, models/lib/fileTypeCorrection.js).
if (filesRoot) {
entries.push({ path: filesRoot, role: 'attachments' });
entries.push({ path: `${filesRoot.replace(/\/+$/, '')}/temp`, role: 'uploads' });
}
}
const seen = new Set();
return entries.filter(e => (seen.has(e.path) ? false : seen.add(e.path)));
}
// One HighCpuTracker per filesystem. `fsKey` identifies the filesystem (the
// device id when it is known, so two paths on one disk share an episode).
class DiskEpisodes {
constructor(opts = {}) {
this.opts = {
highPct: opts.highPct != null ? opts.highPct : 90,
lowPct: opts.lowPct != null ? opts.lowPct : 85,
enterSamples: opts.enterSamples != null ? opts.enterSamples : 2,
exitSamples: opts.exitSamples != null ? opts.exitSamples : 2,
};
this.trackers = new Map();
}
update(fsKey, usedPct, now) {
if (!this.trackers.has(fsKey)) this.trackers.set(fsKey, new HighCpuTracker(this.opts));
return this.trackers.get(fsKey).update(usedPct, now);
}
isHigh(fsKey) {
const t = this.trackers.get(fsKey);
return !!(t && t.isHigh());
}
}
module.exports = { statfsUsage, watchedPaths, DiskEpisodes };