System Log
This commit is contained in:
@@ -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)
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user