Files
peardata/shared/container-names.js
T
Raven Scott 926a89878b
CI / test (push) Successful in 1m31s
Release rolling / release (push) Successful in 7m10s
Docker Socket Updates attempts (Bare Safe)
2026-07-19 13:38:48 -04:00

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
}