Files
peardata/shared/taxonomy.js
T
Raven Scott 41f5476a60
CI / test (push) Successful in 58s
Release rolling / release (push) Successful in 6m56s
Updates
2026-07-18 20:17:34 -04:00

243 lines
6.0 KiB
JavaScript

/**
* Metrics wall taxonomy — maps chart catalog entries into sections/groups.
* Versioned independently so new collectors can land without UI rewrites.
*/
export const TAXONOMY_VERSION = 1
/** @typedef {{ id: string, title: string, order: number, match: (id: string, meta: object) => boolean }} SectionDef */
/** @type {SectionDef[]} */
export const SECTIONS = [
{
id: 'system',
title: 'System',
order: 10,
match: (id, meta) =>
starts(id, [
'system.',
'cpu.',
'mem.',
'disk_',
'disk.',
'net.',
'ip.',
'ipv4.',
'ipv6.',
'processes.',
]) || ctxStarts(meta, ['system.', 'cpu.', 'mem.', 'disk.', 'net.', 'ip.']),
},
{
id: 'containers',
title: 'Containers',
order: 20,
match: (id, meta) =>
starts(id, ['docker.', 'cgroup.', 'peardock.']) ||
ctxStarts(meta, ['docker.', 'cgroup.', 'peardock.']),
},
{
id: 'storage',
title: 'Storage',
order: 30,
match: (id, meta) =>
starts(id, ['zfs.', 'bcache.', 'dmcache.', 'md.', 'mdstat.', 'smart.', 'fs.', 'ioping.']) ||
ctxStarts(meta, ['zfs.', 'bcache.', 'dmcache.', 'md.', 'smart.', 'fs.']),
},
{
id: 'hardware',
title: 'Hardware',
order: 40,
match: (id, meta) =>
starts(id, ['sensors.', 'gpu.', 'nvidia.', 'ipmi.', 'system.power', 'power.']) ||
ctxStarts(meta, ['sensors.', 'gpu.', 'ipmi.', 'power.']),
},
{
id: 'applications',
title: 'Applications',
order: 50,
match: (id, meta) =>
starts(id, [
'nginx.',
'apache.',
'redis.',
'mysql.',
'postgres.',
'memcached.',
'mongodb.',
'rabbitmq.',
'kafka.',
'nats.',
'unbound.',
]) ||
ctxStarts(meta, [
'nginx.',
'apache.',
'redis.',
'mysql.',
'postgres.',
'memcached.',
'mongodb.',
'rabbitmq.',
'kafka.',
'nats.',
'unbound.',
]),
},
{
id: 'network-svc',
title: 'Network services',
order: 60,
match: (id, meta) =>
starts(id, ['sockets.', 'net.conntrack', 'system.softnet', 'ip.tcp', 'ip.udp']) ||
ctxStarts(meta, ['sockets.', 'net.conntrack']),
},
{
id: 'observability',
title: 'Observability',
order: 70,
match: (id, meta) =>
starts(id, ['ebpf.', 'statsd.', 'prometheus.', 'peardata.', 'self.']) ||
ctxStarts(meta, ['ebpf.', 'statsd.', 'prometheus.', 'peardata.']),
},
{
id: 'fleet',
title: 'Fleet',
order: 80,
match: (id, meta) => starts(id, ['fleet.']) || ctxStarts(meta, ['fleet.']),
},
{
id: 'other',
title: 'Other',
order: 900,
match: () => true,
},
]
/**
* @param {string} id
* @param {string[]} prefixes
*/
function starts(id, prefixes) {
const s = String(id || '')
return prefixes.some((p) => s === p || s.startsWith(p))
}
/**
* @param {object} meta
* @param {string[]} prefixes
*/
function ctxStarts(meta, prefixes) {
const c = String(meta?.context || meta?.name || '')
return prefixes.some((p) => c === p || c.startsWith(p))
}
/**
* @param {string} id
* @param {object} meta
*/
export function sectionForChart(id, meta = {}) {
for (const sec of SECTIONS) {
if (sec.id === 'other') continue
if (sec.match(id, meta)) return sec
}
return SECTIONS[SECTIONS.length - 1]
}
/**
* @param {Record<string, object>} catalog
* @param {string} [filter]
* @returns {{
* sections: Array<{
* id: string,
* title: string,
* order: number,
* count: number,
* groups: Array<{ family: string, charts: Array<{ id: string, meta: object }> }>
* }>
* }}
*/
export function groupCatalog(catalog, filter = '') {
const q = String(filter || '')
.trim()
.toLowerCase()
/** @type {Map<string, { id: string, title: string, order: number, families: Map<string, Array<{ id: string, meta: object }>> }>} */
const bySection = new Map()
for (const sec of SECTIONS) {
bySection.set(sec.id, {
id: sec.id,
title: sec.title,
order: sec.order,
families: new Map(),
})
}
const ids = Object.keys(catalog || {})
for (const id of ids) {
const meta = catalog[id] || {}
if (q && !matchesFilter(id, meta, q)) continue
const sec = sectionForChart(id, meta)
const bucket = bySection.get(sec.id)
if (!bucket) continue
const family = String(meta.family || meta.context || '_default')
if (!bucket.families.has(family)) bucket.families.set(family, [])
bucket.families.get(family).push({ id, meta })
}
const sections = [...bySection.values()]
.map((sec) => {
const groups = [...sec.families.entries()]
.map(([family, charts]) => ({
family,
charts: charts.sort(compareCharts),
}))
.sort((a, b) => a.family.localeCompare(b.family))
const count = groups.reduce((n, g) => n + g.charts.length, 0)
return { id: sec.id, title: sec.title, order: sec.order, count, groups }
})
.filter((s) => s.count > 0)
.sort((a, b) => a.order - b.order)
return { sections }
}
/**
* @param {string} id
* @param {object} meta
* @param {string} q lowercase
*/
function matchesFilter(id, meta, q) {
const hay = [
id,
meta.title,
meta.name,
meta.context,
meta.family,
meta.plugin,
meta.units,
]
.filter(Boolean)
.join(' ')
.toLowerCase()
return hay.includes(q)
}
/**
* @param {{ id: string, meta: object }} a
* @param {{ id: string, meta: object }} b
*/
function compareCharts(a, b) {
const pa = Number(a.meta?.priority) || 99999
const pb = Number(b.meta?.priority) || 99999
if (pa !== pb) return pa - pb
return a.id.localeCompare(b.id)
}
/** Time presets for the global metrics bar (seconds of history). */
export const TIME_PRESETS = [
{ id: '1m', label: '1m', seconds: 60 },
{ id: '5m', label: '5m', seconds: 300 },
{ id: '15m', label: '15m', seconds: 900 },
{ id: '1h', label: '1h', seconds: 3600 },
{ id: '6h', label: '6h', seconds: 21600 },
]