220 lines
6.7 KiB
JavaScript
220 lines
6.7 KiB
JavaScript
/**
|
|
* Human-friendly container / cgroup labels for Docker metrics.
|
|
* Keeps chart ids stable (shortId) while titles/families show real names.
|
|
*/
|
|
|
|
/**
|
|
* @param {string|null|undefined} s
|
|
* @param {number} [minLen]
|
|
*/
|
|
export function isHexId(s, minLen = 12) {
|
|
if (typeof s !== 'string') return false
|
|
const t = s.trim()
|
|
if (t.length < minLen || t.length > 64) return false
|
|
return /^[0-9a-f]+$/i.test(t)
|
|
}
|
|
|
|
/**
|
|
* Extract a container id from a cgroup directory / scope name.
|
|
* @param {string} name
|
|
* @returns {string|null} lowercase id
|
|
*/
|
|
export function extractContainerIdFromCgroupName(name) {
|
|
const s = String(name || '')
|
|
const m =
|
|
s.match(/(?:^|\/)(?:docker|libpod|cri-containerd)-([0-9a-f]{12,64})(?:\.scope)?$/i) ||
|
|
s.match(/^([0-9a-f]{64})$/i) ||
|
|
s.match(/^docker-([0-9a-f]{12,64})$/i) ||
|
|
s.match(/^libpod-([0-9a-f]{12,64})$/i)
|
|
return m ? m[1].toLowerCase() : null
|
|
}
|
|
|
|
/**
|
|
* Prefer Compose service labels / short Names over project_service_1 / hashes.
|
|
* @param {{ Id?: string, Names?: string[], Labels?: Record<string, string>, Image?: string }} container
|
|
*/
|
|
export function pickContainerDisplayName(container) {
|
|
const labels = container?.Labels || {}
|
|
const service =
|
|
labels['com.docker.compose.service'] ||
|
|
labels['com.docker.swarm.service.name'] ||
|
|
labels['io.kubernetes.container.name']
|
|
const num = labels['com.docker.compose.container-number']
|
|
if (service && !isHexId(service, 12)) {
|
|
if (num && String(num) !== '1') return `${service} · ${num}`
|
|
return String(service).slice(0, 64)
|
|
}
|
|
|
|
const names = (container?.Names || [])
|
|
.map((n) => String(n || '').replace(/^\//, '').trim())
|
|
.filter(Boolean)
|
|
|
|
const human = names
|
|
.filter((n) => !isHexId(n, 12))
|
|
.sort((a, b) => a.length - b.length)
|
|
if (human[0]) return beautifyComposeStyleName(human[0], labels).slice(0, 64)
|
|
|
|
if (names[0] && !isHexId(names[0], 12)) {
|
|
return beautifyComposeStyleName(names[0], labels).slice(0, 64)
|
|
}
|
|
|
|
const img = String(container?.Image || '')
|
|
.split('@')[0]
|
|
.split('/')
|
|
.pop()
|
|
?.split(':')[0]
|
|
if (img && img !== 'sha256' && !isHexId(img, 12)) return img.slice(0, 64)
|
|
|
|
const id = String(container?.Id || '').toLowerCase()
|
|
if (id) return `container ${id.slice(0, 12)}`
|
|
return 'container'
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {Record<string, string>} labels
|
|
*/
|
|
function beautifyComposeStyleName(name, labels = {}) {
|
|
const project = labels['com.docker.compose.project']
|
|
if (project && name.startsWith(`${project}_`)) {
|
|
const rest = name.slice(project.length + 1)
|
|
return stripTrailingReplica(rest)
|
|
}
|
|
return stripTrailingReplica(name)
|
|
}
|
|
|
|
/** myapp_web_1 → web · 1 (or web when replica 1) */
|
|
function stripTrailingReplica(name) {
|
|
const m = String(name).match(/^(.+)_(\d+)$/)
|
|
if (!m || isHexId(m[1], 8)) return name
|
|
return m[2] === '1' ? m[1] : `${m[1]} · ${m[2]}`
|
|
}
|
|
|
|
/**
|
|
* Index a container id under full + short prefixes for lookup.
|
|
* @param {Map<string, string>} map
|
|
* @param {string} id
|
|
* @param {string} name
|
|
*/
|
|
export function indexContainerName(map, id, name) {
|
|
const full = String(id || '').toLowerCase()
|
|
const label = String(name || '').trim()
|
|
if (!full || !label || !map) return
|
|
map.set(full, label)
|
|
const max = Math.min(64, full.length)
|
|
for (let n = 12; n <= max; n++) map.set(full.slice(0, n), label)
|
|
}
|
|
|
|
/**
|
|
* Look up a display name from id/shortId/cgroup title via a name map.
|
|
* @param {string} idOrTitle
|
|
* @param {Map<string, string>|null|undefined} nameMap
|
|
* @returns {string}
|
|
*/
|
|
export function resolveContainerLabel(idOrTitle, nameMap) {
|
|
const raw = String(idOrTitle || '').trim()
|
|
if (!raw) return 'container'
|
|
|
|
const tryKeys = [raw, raw.toLowerCase()]
|
|
const extracted = extractContainerIdFromCgroupName(raw)
|
|
if (extracted) {
|
|
tryKeys.push(extracted, extracted.slice(0, 12))
|
|
}
|
|
if (isHexId(raw, 12)) {
|
|
tryKeys.push(raw.toLowerCase(), raw.toLowerCase().slice(0, 12))
|
|
}
|
|
|
|
if (nameMap?.size) {
|
|
for (const k of tryKeys) {
|
|
const hit = nameMap.get(k)
|
|
if (hit && !looksLikeHashLabel(hit)) return hit
|
|
if (hit) return hit
|
|
}
|
|
// Prefix match: cgroup id vs API id length differences
|
|
const needle = (extracted || (isHexId(raw, 12) ? raw.toLowerCase() : '')).slice(0, 12)
|
|
if (needle.length >= 12) {
|
|
for (const [k, v] of nameMap) {
|
|
if (k.length < 12 || looksLikeHashLabel(v)) continue
|
|
if (k.startsWith(needle) || needle.startsWith(k.slice(0, 12))) return v
|
|
}
|
|
}
|
|
}
|
|
|
|
if (extracted) return `container ${extracted.slice(0, 12)}`
|
|
if (isHexId(raw, 12)) return `container ${raw.toLowerCase().slice(0, 12)}`
|
|
|
|
// docker-<hash> without map
|
|
const stripped = raw.replace(/\.(service|scope)$/i, '')
|
|
if (extractContainerIdFromCgroupName(stripped)) {
|
|
const id = extractContainerIdFromCgroupName(stripped)
|
|
return `container ${id.slice(0, 12)}`
|
|
}
|
|
return stripped.slice(0, 64)
|
|
}
|
|
|
|
function looksLikeHashLabel(name) {
|
|
const s = String(name || '')
|
|
if (s.startsWith('container ')) return true
|
|
return isHexId(s.replace(/^container\s+/i, ''), 12)
|
|
}
|
|
|
|
/**
|
|
* Card / catalog title: "nginx · CPU"
|
|
* @param {string} metric e.g. CPU, memory, I/O
|
|
* @param {string} displayName
|
|
*/
|
|
export function containerMetricTitle(metric, displayName) {
|
|
const name = String(displayName || 'container').trim() || 'container'
|
|
return `${name} · ${metric}`
|
|
}
|
|
|
|
/**
|
|
* Whether a chart id is a per-container instance chart.
|
|
* @param {string} id
|
|
*/
|
|
export function isContainerInstanceChart(id) {
|
|
const s = String(id || '')
|
|
return (
|
|
/^docker\.(cpu|mem)\./.test(s) ||
|
|
/^cgroup\./.test(s) ||
|
|
/^peardock\.(cpu|mem)\./.test(s)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Metric card subtitle — units + friendly label; never bare docker.cpu.<hash>.
|
|
* @param {string} id
|
|
* @param {{ title?: string, family?: string, units?: string, context?: string }} meta
|
|
*/
|
|
export function metricCardSubtitle(id, meta = {}) {
|
|
const units = meta.units ? String(meta.units) : ''
|
|
if (isContainerInstanceChart(id)) {
|
|
// Title already has "name · metric"; subtitle stays light
|
|
return units || String(meta.context || '').replace(/^docker\./, '') || 'container'
|
|
}
|
|
if (id === 'docker.containers' || id === 'peardock.containers') {
|
|
return units || 'containers'
|
|
}
|
|
const bits = []
|
|
if (units) bits.push(units)
|
|
bits.push(id)
|
|
return bits.join(' · ')
|
|
}
|
|
|
|
/**
|
|
* Explore / select option label.
|
|
* @param {string} id
|
|
* @param {{ title?: string, family?: string }} [meta]
|
|
*/
|
|
export function chartOptionLabel(id, meta = {}) {
|
|
if (meta.title && meta.title !== id) return meta.title
|
|
if (meta.family && !isHexId(String(meta.family).replace(/^container\s+/, ''), 12)) {
|
|
return String(meta.family)
|
|
}
|
|
if (isContainerInstanceChart(id)) {
|
|
const short = id.split('.').pop() || id
|
|
return isHexId(short, 12) ? `container ${short.slice(0, 12)}` : short
|
|
}
|
|
return id
|
|
}
|