System Log
CI / test (push) Successful in 1m4s
Release rolling / release (push) Successful in 6m57s

This commit is contained in:
Raven Scott
2026-07-18 23:05:44 -04:00
parent 7815037737
commit 6ef3afe821
33 changed files with 1360 additions and 32 deletions
+201
View File
@@ -0,0 +1,201 @@
import test from 'brittle'
import fs from 'fs'
import os from 'os'
import path from 'path'
import { Roles } from '../shared/protocol.js'
import { validateMethodArgs } from '../shared/schema.js'
import {
assertLogSourceAllowed,
buildJournalArgv,
clampLimit,
isJournalEnabled,
matchesFilters,
normalizeAnomaly,
normalizeAudit,
normalizeJournal,
parseTimeBound,
queryLogs,
readAuditFileLines,
} from '../server/services/logs.js'
test('clampLimit bounds', (t) => {
t.is(clampLimit(50), 50)
t.is(clampLimit(99999), 2000)
t.is(clampLimit(0), 200)
t.is(clampLimit('x'), 200)
})
test('parseTimeBound absolute and relative', (t) => {
const sec = 1_700_000_000
t.is(parseTimeBound(sec), sec * 1000)
t.ok(Math.abs((parseTimeBound('-1h') || 0) - (Date.now() - 3_600_000)) < 2000)
t.ok(parseTimeBound('2020-01-01T00:00:00.000Z') === Date.parse('2020-01-01T00:00:00.000Z'))
})
test('assertLogSourceAllowed roles', (t) => {
t.ok(assertLogSourceAllowed(Roles.viewer, 'anomaly').ok)
t.absent(assertLogSourceAllowed(Roles.viewer, 'audit').ok)
t.absent(assertLogSourceAllowed(Roles.operator, 'journal').ok)
t.ok(assertLogSourceAllowed(Roles.admin, 'audit').ok)
t.ok(assertLogSourceAllowed(Roles.admin, 'journal').ok)
t.is(assertLogSourceAllowed(Roles.viewer, 'audit').code, 'PERMISSION_DENIED')
})
test('buildJournalArgv fixed flags', (t) => {
const argv = buildJournalArgv({
limit: 50,
sinceMs: Date.parse('2024-01-01T00:00:00.000Z'),
untilMs: Date.parse('2024-01-01T01:00:00.000Z'),
priority: '3',
unit: 'peardata.service',
q: 'error',
})
t.ok(argv.includes('--output=json'))
t.ok(argv.includes('--no-pager'))
t.ok(argv.includes('-n'))
t.ok(argv.includes('50'))
t.ok(argv.includes('--since'))
t.ok(argv.includes('--until'))
t.ok(argv.includes('-p'))
t.ok(argv.includes('3'))
t.ok(argv.includes('-u'))
t.ok(argv.includes('peardata.service'))
t.ok(argv.includes('--grep'))
t.ok(argv.includes('error'))
t.absent(argv.some((a) => String(a).includes(';')))
})
test('normalize helpers', (t) => {
const a = normalizeAnomaly({
ts: 1_700_000_000_000,
chart: 'system.cpu',
severity: 'critical',
message: 'cpu high',
})
t.is(a.source, 'anomaly')
t.is(a.unit, 'system.cpu')
t.ok(a.message.includes('cpu'))
const audit = normalizeAudit({
ts: '2024-06-01T12:00:00.000Z',
method: 'mintInvite',
peerId: 'abcd',
role: 'admin',
ok: true,
})
t.is(audit.source, 'audit')
t.ok(audit.message.includes('mintInvite'))
const j = normalizeJournal({
__REALTIME_TIMESTAMP: String(1_700_000_000_000_000),
PRIORITY: '3',
_SYSTEMD_UNIT: 'sshd.service',
MESSAGE: 'Failed password',
})
t.is(j.source, 'journal')
t.is(j.severity, 'err')
t.is(j.unit, 'sshd.service')
})
test('matchesFilters q and window', (t) => {
const row = { ts: 1000, message: 'Hello World', unit: 'cpu', severity: 'warning' }
t.ok(matchesFilters({ q: 'hello' }, row))
t.absent(matchesFilters({ q: 'nope' }, row))
t.absent(matchesFilters({ since: 2000 }, row))
t.ok(matchesFilters({ since: 500, until: 1500 }, row))
})
test('queryLogs anomaly with mock engine', async (t) => {
const anomalies = {
listRecent() {
return [
{ ts: Date.now() - 1000, chart: 'system.cpu', severity: 'warning', message: 'cpu warn' },
{ ts: Date.now() - 500, chart: 'system.ram', severity: 'critical', message: 'ram crit' },
]
},
}
const res = await queryLogs(
{ source: 'anomaly', q: 'ram', role: Roles.viewer, limit: 50 },
{ anomalies }
)
t.ok(res.ok)
t.is(res.entries.length, 1)
t.is(res.entries[0].unit, 'system.ram')
})
test('queryLogs audit from temp file', async (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'peardata-audit-'))
const file = path.join(dir, 'audit.log')
const lines = [
JSON.stringify({
ts: new Date(Date.now() - 60_000).toISOString(),
method: 'mintInvite',
peerId: 'aabb',
role: 'admin',
ok: true,
error: null,
}),
JSON.stringify({
ts: new Date().toISOString(),
method: 'runJob',
peerId: 'ccdd',
role: 'operator',
ok: false,
error: 'boom',
}),
]
fs.writeFileSync(file, lines.join('\n') + '\n')
t.is(readAuditFileLines(file).length, 2)
const denied = await queryLogs({ source: 'audit', role: Roles.viewer, auditPath: file })
t.absent(denied.ok)
t.is(denied.code, 'PERMISSION_DENIED')
const res = await queryLogs(
{ source: 'audit', role: Roles.admin, q: 'runJob', auditPath: file, limit: 20 },
{}
)
t.ok(res.ok)
t.is(res.entries.length, 1)
t.ok(res.entries[0].message.includes('runJob'))
})
test('queryLogs journal disabled / unsupported', async (t) => {
const prev = process.env.PEARDATA_JOURNAL
delete process.env.PEARDATA_JOURNAL
t.absent(isJournalEnabled())
const off = await queryLogs({ source: 'journal', role: Roles.admin })
t.absent(off.ok)
t.is(off.error, 'journal_disabled')
process.env.PEARDATA_JOURNAL = '1'
t.ok(isJournalEnabled())
if (os.platform() !== 'linux') {
const uns = await queryLogs({ source: 'journal', role: Roles.admin })
t.absent(uns.ok)
t.is(uns.error, 'unsupported')
} else {
// On Linux with journal enabled, inject a fake spawn that fails cleanly
const res = await queryLogs(
{ source: 'journal', role: Roles.admin, limit: 5 },
{
spawnJournal: async () => ({
stdout: '',
stderr: 'No journal files were found',
code: 1,
}),
}
)
t.absent(res.ok)
t.is(res.error, 'journalctl_failed')
}
if (prev == null) delete process.env.PEARDATA_JOURNAL
else process.env.PEARDATA_JOURNAL = prev
})
test('validateMethodArgs queryLogs', (t) => {
t.ok(validateMethodArgs('queryLogs', {}).ok)
t.ok(validateMethodArgs('queryLogs', { source: 'anomaly', limit: 10 }).ok)
t.absent(validateMethodArgs('queryLogs', { source: 'nope' }).ok)
t.absent(validateMethodArgs('queryLogs', { limit: 0 }).ok)
})
+1
View File
@@ -40,6 +40,7 @@ test('method roles cover monitoring surface', (t) => {
'getFleetHealth',
'listChildPeers',
'getWeights',
'queryLogs',
]) {
t.ok(MethodRoles[m], m)
t.is(Methods[m], m)