666 lines
20 KiB
JavaScript
666 lines
20 KiB
JavaScript
/**
|
|
* Tool-friendly composite reads for QVAC / local AI copilots.
|
|
* Keep these lean — no LLM, no model load on the agent.
|
|
*/
|
|
import os from 'os'
|
|
import { APP_VERSION } from '../../shared/protocol.js'
|
|
import { CHART_BY_ID, chartSummary } from '../../shared/metrics.js'
|
|
import { getStore } from './store.js'
|
|
import { getCollector } from './collector.js'
|
|
import { getAnomalyEngine } from './anomaly.js'
|
|
import { listAlerts } from './alerts.js'
|
|
import { getStorageInfo, getRetentionConfig } from './retention.js'
|
|
import { getServerPublicKeyHex } from '../core/auth-keys.js'
|
|
|
|
const KPI_CHARTS = [
|
|
{ id: 'system.cpu', dim: 'user', label: 'cpu_user', alt: ['used'] },
|
|
{ id: 'system.ram', dim: 'used', label: 'ram_used' },
|
|
{ id: 'system.load', dim: 'load1', label: 'load1' },
|
|
{ id: 'system.net', dim: 'received', label: 'net_rx' },
|
|
{ id: 'system.io', dim: 'reads', label: 'io_reads', alt: ['in'] },
|
|
]
|
|
|
|
/**
|
|
* Latest value for a chart dimension from hot memory (no HyperDB).
|
|
* @param {string} chartId
|
|
* @param {string} dim
|
|
* @param {string[]} [alts]
|
|
*/
|
|
function latestDim(chartId, dim, alts = []) {
|
|
const store = getStore()
|
|
const entry = store.series?.get?.(chartId)
|
|
const pts = entry?.points
|
|
if (!pts?.length) return null
|
|
const last = pts[pts.length - 1]
|
|
const vals = last?.values || {}
|
|
if (vals[dim] != null && Number.isFinite(Number(vals[dim]))) {
|
|
return { value: Number(vals[dim]), ts: last.ts, dim }
|
|
}
|
|
for (const a of alts) {
|
|
if (vals[a] != null && Number.isFinite(Number(vals[a]))) {
|
|
return { value: Number(vals[a]), ts: last.ts, dim: a }
|
|
}
|
|
}
|
|
// first finite numeric
|
|
for (const [k, v] of Object.entries(vals)) {
|
|
if (v != null && Number.isFinite(Number(v))) {
|
|
return { value: Number(v), ts: last.ts, dim: k }
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Compact host snapshot for AI tools.
|
|
* @returns {Promise<object>}
|
|
*/
|
|
export async function getHostSnapshot() {
|
|
const store = getStore()
|
|
const collector = getCollector()
|
|
const anomalies = getAnomalyEngine()
|
|
const health = anomalies.getHealth()
|
|
const recent = anomalies.listRecent?.(20) || []
|
|
const openAlerts = (listAlerts() || []).filter(
|
|
(a) => a && !a.acked && !a.silenced && a.severity !== 'cleared'
|
|
)
|
|
|
|
/** @type {Record<string, { value: number, ts: number, dim: string, chart: string }|null>} */
|
|
const kpis = {}
|
|
for (const k of KPI_CHARTS) {
|
|
const hit = latestDim(k.id, k.dim, k.alt || [])
|
|
kpis[k.label] = hit
|
|
? { value: hit.value, ts: hit.ts, dim: hit.dim, chart: k.id }
|
|
: null
|
|
}
|
|
|
|
// Derived CPU used ≈ 100 - idle when available
|
|
const idle = latestDim('system.cpu', 'idle')
|
|
if (idle && kpis.cpu_user == null) {
|
|
kpis.cpu_used = {
|
|
value: Math.max(0, Math.min(100, 100 - idle.value)),
|
|
ts: idle.ts,
|
|
dim: 'used_est',
|
|
chart: 'system.cpu',
|
|
}
|
|
} else if (idle) {
|
|
kpis.cpu_used = {
|
|
value: Math.max(0, Math.min(100, 100 - idle.value)),
|
|
ts: idle.ts,
|
|
dim: 'used_est',
|
|
chart: 'system.cpu',
|
|
}
|
|
}
|
|
|
|
let storage = null
|
|
let retention = null
|
|
try {
|
|
storage = await getStorageInfo()
|
|
} catch {
|
|
storage = null
|
|
}
|
|
try {
|
|
retention = getRetentionConfig()
|
|
} catch {
|
|
retention = null
|
|
}
|
|
|
|
const mem = store.memoryStats?.() || {}
|
|
const charts = store.listChartSummaries?.() || {}
|
|
|
|
return {
|
|
ts: Date.now(),
|
|
hostname: os.hostname(),
|
|
platform: os.platform(),
|
|
release: os.release(),
|
|
cores: os.cpus()?.length || 0,
|
|
totalRamBytes: os.totalmem(),
|
|
freeRamBytes: os.freemem(),
|
|
agentVersion: APP_VERSION,
|
|
publicKeyHex: getServerPublicKeyHex(),
|
|
sampleCount: collector?.sampleCount ?? 0,
|
|
health,
|
|
kpis,
|
|
anomalies: recent.slice(0, 12).map(compactAnomaly),
|
|
alerts: openAlerts.slice(0, 12).map(compactAlert),
|
|
catalog: {
|
|
chartCount: Object.keys(charts).length,
|
|
memory: {
|
|
charts: mem.charts ?? 0,
|
|
tier0Points: mem.tier0Points ?? 0,
|
|
tier1Points: mem.tier1Points ?? 0,
|
|
},
|
|
},
|
|
storage: storage
|
|
? {
|
|
dataDir: storage.dataDir,
|
|
usage: storage.usage,
|
|
memory: storage.memory,
|
|
}
|
|
: null,
|
|
retention: retention || null,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search chart catalog by free text.
|
|
* @param {{ q?: string, limit?: number }} args
|
|
*/
|
|
export function searchCharts(args = {}) {
|
|
const q = String(args.q || '')
|
|
.trim()
|
|
.toLowerCase()
|
|
const limit = Math.min(100, Math.max(1, Number(args.limit) || 30))
|
|
const store = getStore()
|
|
const charts = store.listChartSummaries?.() || {}
|
|
/** @type {Array<object>} */
|
|
const rows = []
|
|
for (const [id, meta] of Object.entries(charts)) {
|
|
const hay = [
|
|
id,
|
|
meta.title,
|
|
meta.context,
|
|
meta.family,
|
|
meta.plugin,
|
|
meta.units,
|
|
...(Array.isArray(meta.dimensions)
|
|
? meta.dimensions.map((d) => (typeof d === 'string' ? d : d?.id || d?.name || ''))
|
|
: []),
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
.toLowerCase()
|
|
if (!q || hay.includes(q)) {
|
|
rows.push({
|
|
id,
|
|
title: meta.title || id,
|
|
context: meta.context || '',
|
|
family: meta.family || '',
|
|
units: meta.units || '',
|
|
plugin: meta.plugin || '',
|
|
chartType: meta.chartType || meta.chart_type || 'line',
|
|
})
|
|
}
|
|
}
|
|
// Prefer exact id / title prefix matches
|
|
rows.sort((a, b) => {
|
|
if (!q) return a.id.localeCompare(b.id)
|
|
const as = scoreMatch(a, q)
|
|
const bs = scoreMatch(b, q)
|
|
if (as !== bs) return bs - as
|
|
return a.id.localeCompare(b.id)
|
|
})
|
|
return {
|
|
q,
|
|
count: rows.length,
|
|
results: rows.slice(0, limit),
|
|
}
|
|
}
|
|
|
|
function scoreMatch(row, q) {
|
|
let s = 0
|
|
if (row.id === q) s += 100
|
|
if (row.id.startsWith(q)) s += 40
|
|
if (row.id.includes(q)) s += 20
|
|
if ((row.title || '').toLowerCase().includes(q)) s += 15
|
|
if ((row.context || '').toLowerCase().includes(q)) s += 8
|
|
if ((row.family || '').toLowerCase().includes(q)) s += 5
|
|
return s
|
|
}
|
|
|
|
/**
|
|
* Windowed summary for one chart.
|
|
* @param {{ chart?: string, id?: string, after?: number, points?: number, group?: string }} args
|
|
*/
|
|
export async function summarizeChart(args = {}) {
|
|
const chart = String(args.chart || args.id || '')
|
|
if (!chart) return { error: 'chart required' }
|
|
const store = getStore()
|
|
const def = CHART_BY_ID.get(chart)
|
|
const meta = store.getMeta?.(chart) || (def ? chartSummary(def) : null)
|
|
if (!meta && !def) return { error: 'unknown chart', chart }
|
|
|
|
const points = Math.min(600, Math.max(10, Number(args.points) || 90))
|
|
const after = args.after != null ? Number(args.after) : -Math.min(points, 300)
|
|
const q = await store.query({
|
|
chart,
|
|
after,
|
|
points,
|
|
group: args.group || 'average',
|
|
})
|
|
if (q.error) return q
|
|
|
|
const labels = Array.isArray(q.labels) ? q.labels.filter((l) => l && l !== 'time') : []
|
|
const data = Array.isArray(q.data) ? q.data : []
|
|
/** @type {Record<string, { min: number, max: number, avg: number, last: number, n: number }>} */
|
|
const dims = {}
|
|
for (let di = 0; di < labels.length; di++) {
|
|
const name = labels[di]
|
|
let min = Infinity
|
|
let max = -Infinity
|
|
let sum = 0
|
|
let n = 0
|
|
let last = 0
|
|
for (const row of data) {
|
|
const v = Number(row[di + 1])
|
|
if (!Number.isFinite(v)) continue
|
|
min = Math.min(min, v)
|
|
max = Math.max(max, v)
|
|
sum += v
|
|
n++
|
|
last = v
|
|
}
|
|
if (n) {
|
|
dims[name] = { min, max, avg: sum / n, last, n }
|
|
}
|
|
}
|
|
|
|
const anomalies = getAnomalyEngine()
|
|
const thr = anomalies.getThreshold?.(chart) || anomalies.thresholds?.get?.(chart) || null
|
|
const recentForChart = (anomalies.listRecent?.(50) || []).filter((a) => a.chart === chart)
|
|
|
|
return {
|
|
chart,
|
|
meta: meta || chartSummary(def),
|
|
source: q.source || 'memory',
|
|
points: data.length,
|
|
after,
|
|
dims,
|
|
latestTs: data.length ? data[data.length - 1][0] : null,
|
|
threshold: thr,
|
|
recentAnomalies: recentForChart.slice(0, 5).map(compactAnomaly),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deep investigation pack — one call for “what's wrong / diagnose this host”.
|
|
* Combines snapshot + hot charts + top processes + open alerts.
|
|
* @param {{ processLimit?: number, hotLimit?: number }} [args]
|
|
*/
|
|
export async function investigateHost(args = {}) {
|
|
const processLimit = Math.min(25, Math.max(5, Number(args.processLimit) || 12))
|
|
const hotLimit = Math.min(30, Math.max(5, Number(args.hotLimit) || 12))
|
|
|
|
const snapshot = await getHostSnapshot()
|
|
const hot = await hotMetrics({ limit: hotLimit, window: 120 })
|
|
let processes = null
|
|
try {
|
|
const { listProcesses } = await import('./processes.js')
|
|
processes = listProcesses({ sort: 'cpu', limit: processLimit, filter: 'all' })
|
|
} catch (err) {
|
|
processes = { error: err?.message || String(err), supported: false }
|
|
}
|
|
|
|
const findings = []
|
|
const health = snapshot.health
|
|
if (health?.status && health.status !== 'ok' && health.status !== 'healthy') {
|
|
findings.push({
|
|
severity: health.critical ? 'critical' : 'warning',
|
|
area: 'health',
|
|
message: `Agent health status=${health.status} warnings=${health.warnings ?? 0} critical=${health.critical ?? 0}`,
|
|
})
|
|
}
|
|
const ramFree = snapshot.freeRamBytes
|
|
const ramTotal = snapshot.totalRamBytes
|
|
if (ramFree != null && ramTotal > 0 && ramFree / ramTotal < 0.08) {
|
|
findings.push({
|
|
severity: 'warning',
|
|
area: 'memory',
|
|
message: `Low free RAM: ${Math.round(ramFree / 1e6)} MiB free of ${Math.round(ramTotal / 1e6)} MiB`,
|
|
})
|
|
}
|
|
const load = snapshot.kpis?.load1?.value
|
|
const cores = snapshot.cores || 1
|
|
if (load != null && load > cores * 1.5) {
|
|
findings.push({
|
|
severity: load > cores * 3 ? 'critical' : 'warning',
|
|
area: 'load',
|
|
message: `Load1=${Number(load).toFixed(2)} vs ${cores} cores`,
|
|
})
|
|
}
|
|
for (const a of snapshot.anomalies || []) {
|
|
if (a.cleared) continue
|
|
findings.push({
|
|
severity: a.severity || 'warning',
|
|
area: 'anomaly',
|
|
chart: a.chart,
|
|
message: a.message || `${a.chart} anomaly`,
|
|
})
|
|
}
|
|
for (const a of snapshot.alerts || []) {
|
|
findings.push({
|
|
severity: a.severity || 'warning',
|
|
area: 'alert',
|
|
chart: a.chart,
|
|
message: a.message || a.id,
|
|
})
|
|
}
|
|
for (const h of (hot.results || []).slice(0, 6)) {
|
|
if (h.score >= 0.4) {
|
|
findings.push({
|
|
severity: h.score >= 0.75 ? 'warning' : 'info',
|
|
area: 'hot_metric',
|
|
chart: h.chart,
|
|
message: `${h.chart}: ${h.reason || 'elevated activity'} (score=${h.score.toFixed(2)})`,
|
|
})
|
|
}
|
|
}
|
|
|
|
const severityRank = { critical: 3, warning: 2, info: 1 }
|
|
findings.sort(
|
|
(a, b) => (severityRank[b.severity] || 0) - (severityRank[a.severity] || 0)
|
|
)
|
|
|
|
return {
|
|
ts: Date.now(),
|
|
hostname: snapshot.hostname,
|
|
summary: {
|
|
findingCount: findings.length,
|
|
topSeverity: findings[0]?.severity || 'ok',
|
|
health: health?.status || 'unknown',
|
|
anomalyCount: (snapshot.anomalies || []).length,
|
|
alertCount: (snapshot.alerts || []).length,
|
|
hotCharts: (hot.results || []).length,
|
|
},
|
|
findings: findings.slice(0, 24),
|
|
kpis: snapshot.kpis,
|
|
health,
|
|
hot: (hot.results || []).slice(0, hotLimit),
|
|
processes: processes?.processes
|
|
? {
|
|
supported: processes.supported,
|
|
summary: processes.summary,
|
|
top: processes.processes.slice(0, processLimit).map(compactProcess),
|
|
}
|
|
: processes,
|
|
storage: snapshot.storage,
|
|
catalog: snapshot.catalog,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charts with the most recent change / anomaly signal (investigation starting points).
|
|
* @param {{ limit?: number, window?: number, family?: string }} [args]
|
|
*/
|
|
export async function hotMetrics(args = {}) {
|
|
const limit = Math.min(50, Math.max(1, Number(args.limit) || 15))
|
|
const window = Math.min(600, Math.max(20, Number(args.window) || 120))
|
|
const familyFilter = args.family ? String(args.family).toLowerCase() : ''
|
|
const store = getStore()
|
|
const anomalies = getAnomalyEngine()
|
|
const charts = store.listChartSummaries?.() || {}
|
|
const recentAnoms = anomalies.listRecent?.(80) || []
|
|
/** @type {Map<string, number>} */
|
|
const anomScore = new Map()
|
|
for (const a of recentAnoms) {
|
|
if (!a?.chart || a.cleared) continue
|
|
const prev = anomScore.get(a.chart) || 0
|
|
const s = a.severity === 'critical' ? 1 : a.severity === 'warning' ? 0.7 : 0.4
|
|
anomScore.set(a.chart, Math.max(prev, s + (Number(a.score) || 0) * 0.05))
|
|
}
|
|
|
|
/** @type {Array<object>} */
|
|
const scored = []
|
|
for (const [id, meta] of Object.entries(charts)) {
|
|
if (familyFilter) {
|
|
const fam = String(meta.family || meta.context || '').toLowerCase()
|
|
if (!fam.includes(familyFilter) && !id.toLowerCase().includes(familyFilter)) continue
|
|
}
|
|
let deltaScore = 0
|
|
let last = null
|
|
let avg = null
|
|
let reason = ''
|
|
try {
|
|
const q = await store.query({
|
|
chart: id,
|
|
after: -window,
|
|
points: Math.min(window, 90),
|
|
group: 'average',
|
|
})
|
|
const labels = Array.isArray(q.labels) ? q.labels.filter((l) => l && l !== 'time') : []
|
|
const data = Array.isArray(q.data) ? q.data : []
|
|
if (data.length >= 4 && labels.length) {
|
|
// Use first dim with finite values
|
|
let di = 0
|
|
for (let i = 0; i < labels.length; i++) {
|
|
if (data.some((row) => Number.isFinite(Number(row[i + 1])))) {
|
|
di = i
|
|
break
|
|
}
|
|
}
|
|
const vals = data
|
|
.map((row) => Number(row[di + 1]))
|
|
.filter((v) => Number.isFinite(v))
|
|
if (vals.length >= 4) {
|
|
const half = Math.floor(vals.length / 2)
|
|
const early = vals.slice(0, half)
|
|
const late = vals.slice(half)
|
|
const mean = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length
|
|
const mEarly = mean(early)
|
|
const mLate = mean(late)
|
|
last = vals[vals.length - 1]
|
|
avg = mean(vals)
|
|
const denom = Math.max(Math.abs(mEarly), Math.abs(avg), 1e-9)
|
|
const rel = Math.abs(mLate - mEarly) / denom
|
|
deltaScore = Math.min(1, rel)
|
|
if (rel >= 0.15) {
|
|
reason = `${labels[di]} ${mLate >= mEarly ? '↑' : '↓'} ${(rel * 100).toFixed(0)}% vs earlier window`
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// skip chart
|
|
}
|
|
|
|
const aScore = anomScore.get(id) || 0
|
|
const score = Math.min(1.5, deltaScore * 0.85 + aScore)
|
|
if (score < 0.12 && !aScore) continue
|
|
if (!reason && aScore) reason = 'recent anomaly'
|
|
if (!reason) reason = 'activity'
|
|
|
|
scored.push({
|
|
chart: id,
|
|
title: meta.title || id,
|
|
family: meta.family || '',
|
|
context: meta.context || '',
|
|
score: Math.round(score * 1000) / 1000,
|
|
last,
|
|
avg,
|
|
reason,
|
|
anomaly: Boolean(aScore),
|
|
})
|
|
}
|
|
|
|
scored.sort((a, b) => b.score - a.score || a.chart.localeCompare(b.chart))
|
|
return {
|
|
window,
|
|
count: scored.length,
|
|
results: scored.slice(0, limit),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Related charts for investigation (catalog + optional weights).
|
|
* @param {{ chart?: string, id?: string, limit?: number }} args
|
|
*/
|
|
export async function relatedCharts(args = {}) {
|
|
const chart = String(args.chart || args.id || '').trim()
|
|
if (!chart) return { error: 'chart required' }
|
|
const limit = Math.min(40, Math.max(1, Number(args.limit) || 12))
|
|
const store = getStore()
|
|
const catalog = store.listChartSummaries?.() || {}
|
|
if (!catalog[chart] && !CHART_BY_ID.has(chart)) {
|
|
return { error: 'unknown chart', chart }
|
|
}
|
|
|
|
let weights = null
|
|
try {
|
|
const { computeWeights } = await import('./weights.js')
|
|
const w = await computeWeights({ chart, limit: 40, method: 'alerts' })
|
|
weights = w?.results || w?.weights || null
|
|
} catch {
|
|
weights = null
|
|
}
|
|
|
|
const { rankRelatedCharts } = await import('../../shared/related-metrics.js')
|
|
const ranked = rankRelatedCharts(chart, catalog, null, { limit, weights })
|
|
return {
|
|
chart,
|
|
seed: catalog[chart] || chartSummary(CHART_BY_ID.get(chart)),
|
|
count: ranked.length,
|
|
results: ranked.map((r) => ({
|
|
id: r.id,
|
|
score: Math.round(r.score * 100) / 100,
|
|
reason: r.reason,
|
|
title: catalog[r.id]?.title || r.id,
|
|
family: catalog[r.id]?.family || '',
|
|
})),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compare two time windows on one chart (baseline vs highlight).
|
|
* @param {{
|
|
* chart?: string,
|
|
* baselineAfter?: number,
|
|
* baselineBefore?: number,
|
|
* after?: number,
|
|
* before?: number,
|
|
* points?: number,
|
|
* }} args
|
|
*/
|
|
export async function compareChartWindows(args = {}) {
|
|
const chart = String(args.chart || args.id || '').trim()
|
|
if (!chart) return { error: 'chart required' }
|
|
const points = Math.min(300, Math.max(10, Number(args.points) || 60))
|
|
// Default: highlight last 5m, baseline prior 20m
|
|
const after = args.after != null ? Number(args.after) : -300
|
|
const before = args.before != null ? Number(args.before) : 0
|
|
const baselineAfter =
|
|
args.baselineAfter != null ? Number(args.baselineAfter) : -1500
|
|
const baselineBefore =
|
|
args.baselineBefore != null ? Number(args.baselineBefore) : -300
|
|
|
|
const [highlight, baseline] = await Promise.all([
|
|
summarizeChart({ chart, after, points, group: args.group || 'average' }),
|
|
summarizeChart({
|
|
chart,
|
|
after: baselineAfter,
|
|
points,
|
|
group: args.group || 'average',
|
|
}),
|
|
])
|
|
if (highlight.error) return highlight
|
|
if (baseline.error) return { ...baseline, phase: 'baseline' }
|
|
|
|
/** @type {Record<string, object>} */
|
|
const delta = {}
|
|
const dims = new Set([
|
|
...Object.keys(highlight.dims || {}),
|
|
...Object.keys(baseline.dims || {}),
|
|
])
|
|
for (const d of dims) {
|
|
const h = highlight.dims?.[d]
|
|
const b = baseline.dims?.[d]
|
|
if (!h && !b) continue
|
|
const hLast = h?.last
|
|
const bAvg = b?.avg
|
|
let rel = null
|
|
if (hLast != null && bAvg != null && Number.isFinite(hLast) && Number.isFinite(bAvg)) {
|
|
const denom = Math.max(Math.abs(bAvg), 1e-9)
|
|
rel = (hLast - bAvg) / denom
|
|
}
|
|
delta[d] = {
|
|
highlightLast: h?.last ?? null,
|
|
highlightAvg: h?.avg ?? null,
|
|
baselineAvg: b?.avg ?? null,
|
|
baselineMin: b?.min ?? null,
|
|
baselineMax: b?.max ?? null,
|
|
relChange: rel != null ? Math.round(rel * 1000) / 1000 : null,
|
|
}
|
|
}
|
|
|
|
return {
|
|
chart,
|
|
highlight: { after, before, points: highlight.points, dims: highlight.dims },
|
|
baseline: {
|
|
after: baselineAfter,
|
|
before: baselineBefore,
|
|
points: baseline.points,
|
|
dims: baseline.dims,
|
|
},
|
|
delta,
|
|
meta: highlight.meta,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Batch summarize several charts (compact for tool payloads).
|
|
* @param {{ charts?: string[], after?: number, points?: number }} args
|
|
*/
|
|
export async function summarizeCharts(args = {}) {
|
|
let charts = args.charts
|
|
if (typeof charts === 'string') {
|
|
charts = charts.split(/[\s,]+/).filter(Boolean)
|
|
}
|
|
if (!Array.isArray(charts) || !charts.length) {
|
|
return { error: 'charts array required' }
|
|
}
|
|
charts = charts.map(String).slice(0, 12)
|
|
const after = args.after != null ? Number(args.after) : -120
|
|
const points = Math.min(120, Math.max(10, Number(args.points) || 60))
|
|
const results = []
|
|
for (const chart of charts) {
|
|
const s = await summarizeChart({ chart, after, points })
|
|
if (s.error) {
|
|
results.push({ chart, error: s.error })
|
|
} else {
|
|
results.push({
|
|
chart,
|
|
points: s.points,
|
|
dims: s.dims,
|
|
latestTs: s.latestTs,
|
|
recentAnomalies: s.recentAnomalies,
|
|
})
|
|
}
|
|
}
|
|
return { after, points, count: results.length, results }
|
|
}
|
|
|
|
function compactProcess(p) {
|
|
if (!p) return p
|
|
return {
|
|
pid: p.pid,
|
|
name: p.name || p.comm,
|
|
user: p.user || p.username,
|
|
cpu: p.cpu,
|
|
mem: p.mem,
|
|
rss: p.rss,
|
|
state: p.state,
|
|
cmd: p.cmd ? String(p.cmd).slice(0, 120) : undefined,
|
|
}
|
|
}
|
|
|
|
function compactAnomaly(a) {
|
|
if (!a) return a
|
|
return {
|
|
chart: a.chart,
|
|
severity: a.severity,
|
|
message: a.message,
|
|
score: a.score,
|
|
ts: a.ts,
|
|
cleared: Boolean(a.cleared),
|
|
}
|
|
}
|
|
|
|
function compactAlert(a) {
|
|
if (!a) return a
|
|
return {
|
|
id: a.id,
|
|
chart: a.chart,
|
|
severity: a.severity,
|
|
message: a.message || a.name,
|
|
ts: a.ts,
|
|
silenced: Boolean(a.silenced),
|
|
}
|
|
}
|