Updates
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import test from 'brittle'
|
||||
import { scoreSeverity, AnomalyEngine } from '../server/services/anomaly.js'
|
||||
|
||||
test('scoreSeverity warning vs critical bands', (t) => {
|
||||
const warn = scoreSeverity(85, 80, 95, '>', 'warning')
|
||||
const crit = scoreSeverity(96, 80, 95, '>', 'critical')
|
||||
t.ok(warn >= 0.35 && warn < 1)
|
||||
t.ok(crit >= 0.6)
|
||||
t.ok(crit > warn)
|
||||
})
|
||||
|
||||
test('scoreSeverity low-is-worse', (t) => {
|
||||
const warn = scoreSeverity(400, 512, 256, '<', 'warning')
|
||||
const crit = scoreSeverity(200, 512, 256, '<', 'critical')
|
||||
t.ok(warn > 0)
|
||||
t.ok(crit >= 0.6)
|
||||
})
|
||||
|
||||
test('evaluate includes continuous score in message', (t) => {
|
||||
const eng = new AnomalyEngine({ cpuCount: 4 })
|
||||
eng.setConfig({
|
||||
id: 'cpu_user_high',
|
||||
chart: 'system.cpu',
|
||||
dimension: 'user',
|
||||
warn: 10,
|
||||
crit: 50,
|
||||
comparator: '>',
|
||||
enabled: true,
|
||||
info: 'CPU user',
|
||||
})
|
||||
const fired = eng.evaluate([
|
||||
{
|
||||
chart: 'system.cpu',
|
||||
context: 'system.cpu',
|
||||
ts: Date.now(),
|
||||
values: { user: 60, idle: 40 },
|
||||
},
|
||||
])
|
||||
t.ok(fired.length >= 1)
|
||||
t.ok(fired[0].score >= 0.6)
|
||||
t.ok(String(fired[0].message).includes('score'))
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import test from 'brittle'
|
||||
import {
|
||||
shouldNotify,
|
||||
meetsMinSeverity,
|
||||
isWebhookEnabled,
|
||||
} from '../server/services/notify.js'
|
||||
|
||||
test('meetsMinSeverity', (t) => {
|
||||
const prev = process.env.PEARDATA_NOTIFY_MIN_SEVERITY
|
||||
delete process.env.PEARDATA_NOTIFY_MIN_SEVERITY
|
||||
t.ok(meetsMinSeverity('warning'))
|
||||
t.ok(meetsMinSeverity('critical'))
|
||||
process.env.PEARDATA_NOTIFY_MIN_SEVERITY = 'critical'
|
||||
t.absent(meetsMinSeverity('warning'))
|
||||
t.ok(meetsMinSeverity('critical'))
|
||||
if (prev == null) delete process.env.PEARDATA_NOTIFY_MIN_SEVERITY
|
||||
else process.env.PEARDATA_NOTIFY_MIN_SEVERITY = prev
|
||||
})
|
||||
|
||||
test('shouldNotify gates on webhook + clears', (t) => {
|
||||
const prevUrl = process.env.PEARDATA_WEBHOOK_URL
|
||||
const prevClears = process.env.PEARDATA_NOTIFY_CLEARS
|
||||
delete process.env.PEARDATA_WEBHOOK_URL
|
||||
t.absent(isWebhookEnabled())
|
||||
t.absent(shouldNotify({ severity: 'critical', cleared: false }))
|
||||
|
||||
process.env.PEARDATA_WEBHOOK_URL = 'http://127.0.0.1:9/hook'
|
||||
t.ok(shouldNotify({ severity: 'warning', cleared: false }))
|
||||
t.absent(shouldNotify({ severity: 'warning', cleared: true }))
|
||||
process.env.PEARDATA_NOTIFY_CLEARS = '1'
|
||||
t.ok(shouldNotify({ severity: 'warning', cleared: true }))
|
||||
|
||||
if (prevUrl == null) delete process.env.PEARDATA_WEBHOOK_URL
|
||||
else process.env.PEARDATA_WEBHOOK_URL = prevUrl
|
||||
if (prevClears == null) delete process.env.PEARDATA_NOTIFY_CLEARS
|
||||
else process.env.PEARDATA_NOTIFY_CLEARS = prevClears
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import test from 'brittle'
|
||||
import {
|
||||
remapDockChart,
|
||||
extractDockCharts,
|
||||
isPearDockEnabled,
|
||||
parsePearDockPeers,
|
||||
} from '../server/services/collectors/peardock.js'
|
||||
|
||||
test('remapDockChart', (t) => {
|
||||
t.is(remapDockChart('docker.cpu.abc'), 'peardock.cpu.abc')
|
||||
t.is(remapDockChart('docker.containers'), 'peardock.containers')
|
||||
t.is(remapDockChart('container.mem.x'), 'peardock.mem.x')
|
||||
t.is(remapDockChart('system.cpu'), null)
|
||||
})
|
||||
|
||||
test('extractDockCharts from getAllMetrics body', (t) => {
|
||||
const rows = extractDockCharts({
|
||||
body: {
|
||||
charts: {
|
||||
'docker.cpu.abc123': { dimensions: { usage: 12.5 } },
|
||||
'system.cpu': { dimensions: { idle: 90 } },
|
||||
},
|
||||
},
|
||||
})
|
||||
t.is(rows.length, 1)
|
||||
t.is(rows[0].chart, 'peardock.cpu.abc123')
|
||||
t.is(rows[0].context, 'peardock.cpu')
|
||||
t.is(rows[0].values.usage, 12.5)
|
||||
})
|
||||
|
||||
test('isPearDockEnabled + parse peers', (t) => {
|
||||
const prev = process.env.PEARDATA_PEARDOCK
|
||||
const prevPeers = process.env.PEARDATA_PEARDOCK_PEERS
|
||||
delete process.env.PEARDATA_PEARDOCK
|
||||
t.absent(isPearDockEnabled())
|
||||
process.env.PEARDATA_PEARDOCK = '1'
|
||||
t.ok(isPearDockEnabled())
|
||||
process.env.PEARDATA_PEARDOCK_PEERS = `${'a'.repeat(64)}, nope`
|
||||
t.is(parsePearDockPeers().length, 1)
|
||||
if (prev == null) delete process.env.PEARDATA_PEARDOCK
|
||||
else process.env.PEARDATA_PEARDOCK = prev
|
||||
if (prevPeers == null) delete process.env.PEARDATA_PEARDOCK_PEERS
|
||||
else process.env.PEARDATA_PEARDOCK_PEERS = prevPeers
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import test from 'brittle'
|
||||
import {
|
||||
isRestTunnelEnabled,
|
||||
publicKeyFromTunnelSeed,
|
||||
getRestTunnelInfo,
|
||||
} from '../server/services/rest-tunnel.js'
|
||||
|
||||
test('isRestTunnelEnabled', (t) => {
|
||||
const prev = process.env.PEARDATA_REST_TUNNEL
|
||||
delete process.env.PEARDATA_REST_TUNNEL
|
||||
t.absent(isRestTunnelEnabled())
|
||||
process.env.PEARDATA_REST_TUNNEL = '1'
|
||||
t.ok(isRestTunnelEnabled())
|
||||
if (prev == null) delete process.env.PEARDATA_REST_TUNNEL
|
||||
else process.env.PEARDATA_REST_TUNNEL = prev
|
||||
})
|
||||
|
||||
test('publicKeyFromTunnelSeed is stable', (t) => {
|
||||
const seed = 'a'.repeat(64)
|
||||
const a = publicKeyFromTunnelSeed(seed)
|
||||
const b = publicKeyFromTunnelSeed(seed)
|
||||
t.is(a, b)
|
||||
t.is(a.length, 64)
|
||||
})
|
||||
|
||||
test('getRestTunnelInfo inactive by default', (t) => {
|
||||
const info = getRestTunnelInfo()
|
||||
t.ok(typeof info.enabled === 'boolean')
|
||||
t.absent(info.active)
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import test from 'brittle'
|
||||
import {
|
||||
signWebhookPayload,
|
||||
verifyWebhookSignature,
|
||||
isWebhookSigningEnabled,
|
||||
} from '../server/services/notify.js'
|
||||
|
||||
test('sign and verify webhook HMAC', (t) => {
|
||||
const secret = 'test-secret'
|
||||
const ts = '1710000000000'
|
||||
const body = '{"type":"anomaly","severity":"critical"}'
|
||||
const sig = signWebhookPayload(secret, ts, body)
|
||||
t.ok(sig.startsWith('sha256='))
|
||||
t.ok(verifyWebhookSignature(secret, ts, body, sig, { now: Number(ts) }))
|
||||
t.absent(verifyWebhookSignature(secret, ts, body + 'x', sig, { now: Number(ts) }))
|
||||
t.absent(
|
||||
verifyWebhookSignature(secret, ts, body, sig, { now: Number(ts) + 10 * 60_000 })
|
||||
)
|
||||
})
|
||||
|
||||
test('isWebhookSigningEnabled defaults on with secret', (t) => {
|
||||
const prevSecret = process.env.PEARDATA_WEBHOOK_SECRET
|
||||
const prevSign = process.env.PEARDATA_WEBHOOK_SIGN
|
||||
process.env.PEARDATA_WEBHOOK_SECRET = 'abc'
|
||||
delete process.env.PEARDATA_WEBHOOK_SIGN
|
||||
t.ok(isWebhookSigningEnabled())
|
||||
process.env.PEARDATA_WEBHOOK_SIGN = '0'
|
||||
t.absent(isWebhookSigningEnabled())
|
||||
if (prevSecret == null) delete process.env.PEARDATA_WEBHOOK_SECRET
|
||||
else process.env.PEARDATA_WEBHOOK_SECRET = prevSecret
|
||||
if (prevSign == null) delete process.env.PEARDATA_WEBHOOK_SIGN
|
||||
else process.env.PEARDATA_WEBHOOK_SIGN = prevSign
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import test from 'brittle'
|
||||
import { MetricStore } from '../server/services/store.js'
|
||||
import { AnomalyEngine } from '../server/services/anomaly.js'
|
||||
import { stats, zScore, thresholdsFromStats, seriesFromStore } from '../server/services/zscore.js'
|
||||
|
||||
test('stats and zScore', (t) => {
|
||||
const s = stats([10, 12, 11, 13, 10])
|
||||
t.ok(s.n === 5)
|
||||
t.ok(Math.abs(s.mean - 11.2) < 0.01)
|
||||
t.ok(s.stdev > 0)
|
||||
t.ok(Math.abs(zScore(11.2, s.mean, s.stdev)) < 0.01)
|
||||
t.ok(zScore(20, s.mean, s.stdev) > 2)
|
||||
})
|
||||
|
||||
test('thresholdsFromStats greater-than', (t) => {
|
||||
const thr = thresholdsFromStats({ mean: 10, stdev: 2 }, '>', { warnZ: 2, critZ: 3 })
|
||||
t.is(thr.warn, 14)
|
||||
t.is(thr.crit, 16)
|
||||
})
|
||||
|
||||
test('retrainAnomaly updates thresholds from store', (t) => {
|
||||
const store = new MetricStore()
|
||||
const now = Date.now()
|
||||
for (let i = 0; i < 40; i++) {
|
||||
store.ingest([
|
||||
{
|
||||
chart: 'system.cpu',
|
||||
context: 'system.cpu',
|
||||
ts: now + i * 1000,
|
||||
values: {
|
||||
user: 20 + (i % 3),
|
||||
system: 5,
|
||||
nice: 0,
|
||||
iowait: 0,
|
||||
irq: 0,
|
||||
softirq: 0,
|
||||
idle: 70,
|
||||
steal: 0,
|
||||
},
|
||||
},
|
||||
])
|
||||
}
|
||||
const eng = new AnomalyEngine({ cpuCount: 2 })
|
||||
const res = eng.retrain(store, { minPoints: 20, warnZ: 2, critZ: 3 })
|
||||
t.ok(res.ok)
|
||||
t.ok(res.updated.length >= 1)
|
||||
const cpu = eng.configs.get('cpu_user_high')
|
||||
t.ok(cpu.warn > 20)
|
||||
t.ok(cpu.crit >= cpu.warn)
|
||||
t.ok(seriesFromStore(store, 'system.cpu', 'user').length >= 20)
|
||||
})
|
||||
Reference in New Issue
Block a user