mirror of
https://github.com/wekan/wekan.git
synced 2026-11-12 14:45:00 -05:00
WeKan and FerretDB on one machine can drive CPU high enough to starve other software. Add a monitor, a governor, and a report. Design: docs/Features/CPU/CPU.md. Monitor (server/lib/cpuMonitor.js): samples system-wide CPU% from os.cpus() idle/total deltas on an interval (so it includes WeKan, FerretDB and every other process). A pure, hysteresis state machine (models/lib/cpuHighTracker.js) turns the samples into ONLY the START and END of each sustained high-CPU period — two rows per episode, never a flood. Enters high after 3 consecutive samples >= 85%, leaves after 3 consecutive < 70%. Each row carries duration, peak, system load, and what WeKan was doing (setActivity label). Recorded to a new 'cpu' event stream (server/lib/cpuLog.js). Governor: cpuMonitor exports isHighCpu() and pauseIfBusy(), so long batch operations await a short pause and yield the CPU only while the system is busy (no cost when idle). Wired into the existing-file extension corrector. Tunable via WEKAN_CPU_* env vars. Report: 'cpu' added to EVENT_STREAMS; reuses the generic eventStreamReport (stream="cpu") with search + pagination; a "CPU usage" item is added to the Admin Panel / Problems side menu (cpuReportTitle i18n). Tests: cpuHighTracker unit (start/end, duration/peak, no-flap hysteresis; positive + negative) and cpuMonitorWiring source guards. CHANGELOG + design doc.
148 lines
6.2 KiB
JavaScript
148 lines
6.2 KiB
JavaScript
import { Mongo } from 'meteor/mongo';
|
|
import { Meteor } from 'meteor/meteor';
|
|
import { check, Match } from 'meteor/check';
|
|
const { SimpleSchema } = require('/imports/simpleSchema');
|
|
|
|
// ============================================================================
|
|
// EventLog — the single collection backing the Admin Panel → Reports →
|
|
// Security / Speed / Tests screens (design: docs/Security/Remediation/WeKan.md).
|
|
//
|
|
// Stored in the EXISTING WeKan database (MongoDB or FerretDB) via normal Meteor
|
|
// JavaScript queries — no new files or databases. FerretDB does NOT write here
|
|
// itself; when it detects/remediates a problem it reports it to WeKan, and WeKan
|
|
// records it with the same JS query, so the feature works identically on FerretDB,
|
|
// MongoDB, etc.
|
|
//
|
|
// One document per event; the `stream` field ('security' | 'speed' | 'tests')
|
|
// discriminates the three Reports.
|
|
// ============================================================================
|
|
|
|
const EventLog = new Mongo.Collection('eventlog');
|
|
|
|
EventLog.attachSchema(
|
|
new SimpleSchema({
|
|
stream: { type: String }, // 'security' | 'speed' | 'tests'
|
|
at: { type: Date }, // server time of the event
|
|
severity: { type: String, optional: true }, // info|low|medium|high|critical
|
|
category: { type: String, optional: true }, // general class
|
|
bleed: { type: String, optional: true }, // hall-of-fame *Bleed name (or generic)
|
|
action: { type: String, optional: true }, // blocked|remediated|sanitized|rate-limited|detected|failed
|
|
source: { type: String, optional: true }, // guard/module/test name (wekan… or sqlite…/ferretdb…)
|
|
cwe: { type: String, optional: true },
|
|
userId: { type: String, optional: true },
|
|
detail: { type: String, optional: true },
|
|
}),
|
|
);
|
|
|
|
// Per-stream acknowledgment: when an admin clicks "Acknowledge" for a problem
|
|
// area, we upsert { stream, at:now } here. The new-problem count for a stream is
|
|
// the number of eventlog docs newer than its ack `at` — so acknowledging zeroes
|
|
// the count and removes the info from the top of the Admin Panel.
|
|
export const EventLogAcks = new Mongo.Collection('eventlogAcks');
|
|
|
|
EventLogAcks.attachSchema(
|
|
new SimpleSchema({
|
|
stream: { type: String },
|
|
at: { type: Date },
|
|
}),
|
|
);
|
|
|
|
export const EVENT_STREAMS = ['security', 'speed', 'tests', 'cpu'];
|
|
|
|
if (Meteor.isServer) {
|
|
// The Security/Speed/Tests report pages filter by `stream` and sort by `at`
|
|
// descending (streamSelector + eventLogPage). This compound index makes that a
|
|
// bounded index scan instead of a full-collection scan + in-memory sort as the
|
|
// event log grows, keeping the paginated tables fast.
|
|
const { ensureIndex } = require('/server/lib/mongoStartup');
|
|
Meteor.startup(async () => {
|
|
await ensureIndex(EventLog, { stream: 1, at: -1 });
|
|
await ensureIndex(EventLogAcks, { stream: 1 });
|
|
});
|
|
|
|
async function requireAdmin(context) {
|
|
const uid = context.userId;
|
|
const user = uid && (await Meteor.users.findOneAsync(uid));
|
|
if (!user || !user.isAdmin) {
|
|
throw new Meteor.Error('not-authorized', 'Admin only');
|
|
}
|
|
return user;
|
|
}
|
|
|
|
// Build a read-only find selector for one stream, with an optional
|
|
// case-insensitive search across the text columns.
|
|
function streamSelector(stream, search) {
|
|
const selector = { stream };
|
|
if (search) {
|
|
const rx = { $regex: String(search).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' };
|
|
selector.$or = [{ category: rx }, { bleed: rx }, { source: rx }, { detail: rx }];
|
|
}
|
|
return selector;
|
|
}
|
|
|
|
Meteor.methods({
|
|
// Admin-only: for each stream, the count of events NEWER than its
|
|
// acknowledgment. Returns only streams with count > 0, so the Admin Panel
|
|
// "Problems" button (red) and the Summary page show exactly what needs
|
|
// attention.
|
|
async eventLogProblemAreas() {
|
|
await requireAdmin(this);
|
|
const areas = [];
|
|
for (const stream of EVENT_STREAMS) {
|
|
const ack = await EventLogAcks.findOneAsync({ stream });
|
|
const selector = { stream };
|
|
if (ack && ack.at) selector.at = { $gt: ack.at };
|
|
const count = await EventLog.find(selector).countAsync();
|
|
if (count > 0) areas.push({ stream, count });
|
|
}
|
|
return areas;
|
|
},
|
|
|
|
// Admin-only: mark the newest problems in the given stream(s) as seen
|
|
// (resets each one's count). Accepts a single stream or an array, so the
|
|
// Admin Panel banner can acknowledge all checked areas with one button.
|
|
async acknowledgeEventLog(streams) {
|
|
check(streams, Match.OneOf(String, [String]));
|
|
await requireAdmin(this);
|
|
const list = Array.isArray(streams) ? streams : [streams];
|
|
const now = new Date();
|
|
for (const stream of list) {
|
|
if (!EVENT_STREAMS.includes(stream)) {
|
|
throw new Meteor.Error('invalid-stream', 'Unknown event stream');
|
|
}
|
|
await EventLogAcks.upsertAsync({ stream }, { $set: { stream, at: now } });
|
|
}
|
|
return true;
|
|
},
|
|
|
|
// Admin-only, READ-ONLY: total count of events in a stream (optional search),
|
|
// for the Security/Speed/Tests detail pages' pagination.
|
|
async eventLogCount(stream, search) {
|
|
// check() every argument BEFORE any await/throw: Meteor's
|
|
// audit-argument-checks otherwise reports "Did not check() all arguments"
|
|
// (masking the real error) when requireAdmin throws for a non-admin call.
|
|
check(stream, String);
|
|
check(search, Match.Optional(String));
|
|
await requireAdmin(this);
|
|
return EventLog.find(streamSelector(stream, search)).countAsync();
|
|
},
|
|
|
|
// Admin-only, READ-ONLY: one page of a stream's events, newest first.
|
|
async eventLogPage(stream, limit, skip, search) {
|
|
// check() every argument BEFORE requireAdmin (see eventLogCount).
|
|
check(stream, String);
|
|
check(limit, Number);
|
|
check(skip, Number);
|
|
check(search, Match.Optional(String));
|
|
await requireAdmin(this);
|
|
return EventLog.find(streamSelector(stream, search), {
|
|
sort: { at: -1 },
|
|
limit: Math.max(1, Math.min(200, limit)),
|
|
skip: Math.max(0, skip),
|
|
}).fetchAsync();
|
|
},
|
|
});
|
|
}
|
|
|
|
export default EventLog;
|