@aidra/pmx-io 0.0.0-stage → 9.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +69 -2
  2. package/bin/pmx-io-node.js +143 -0
  3. package/index.d.ts +381 -0
  4. package/index.js +27 -0
  5. package/index.mjs +47 -0
  6. package/lib/actions.js +120 -0
  7. package/lib/activate.js +402 -0
  8. package/lib/application.js +106 -0
  9. package/lib/bun.js +155 -0
  10. package/lib/config.js +67 -0
  11. package/lib/constants.js +17 -0
  12. package/lib/debug.js +48 -0
  13. package/lib/esm-hooks.mjs +111 -0
  14. package/lib/exit.js +79 -0
  15. package/lib/features/metrics.js +409 -0
  16. package/lib/features/notify.js +358 -0
  17. package/lib/features/profiling.js +208 -0
  18. package/lib/hooks.js +151 -0
  19. package/lib/inbound.js +174 -0
  20. package/lib/integrations.js +372 -0
  21. package/lib/itg/amqplib.js +169 -0
  22. package/lib/itg/bullmq.js +141 -0
  23. package/lib/itg/common.js +657 -0
  24. package/lib/itg/dbcomment.js +114 -0
  25. package/lib/itg/index.js +103 -0
  26. package/lib/itg/ioredis.js +212 -0
  27. package/lib/itg/kafkajs.js +205 -0
  28. package/lib/itg/mongodb.js +322 -0
  29. package/lib/itg/mysql2.js +205 -0
  30. package/lib/itg/nats.js +531 -0
  31. package/lib/itg/noderedis.js +236 -0
  32. package/lib/itg/pg.js +253 -0
  33. package/lib/itg/pgqueue.js +305 -0
  34. package/lib/itg/postgres.js +252 -0
  35. package/lib/itg/redis-common.js +265 -0
  36. package/lib/itg/sql.js +302 -0
  37. package/lib/metrics/service.js +286 -0
  38. package/lib/metrics/types.js +342 -0
  39. package/lib/modules.js +260 -0
  40. package/lib/node-options.js +62 -0
  41. package/lib/otel/bridge.js +797 -0
  42. package/lib/otel/env.js +114 -0
  43. package/lib/outbound.js +362 -0
  44. package/lib/pmx.js +757 -0
  45. package/lib/relay/hub.js +279 -0
  46. package/lib/relay/transport.js +253 -0
  47. package/lib/standalone/activate.js +307 -0
  48. package/lib/standalone/capture.js +55 -0
  49. package/lib/standalone/container.js +909 -0
  50. package/lib/standalone/transport.js +1023 -0
  51. package/lib/tracing/context.js +90 -0
  52. package/lib/tracing/control.js +124 -0
  53. package/lib/tracing/exporter.js +115 -0
  54. package/lib/tracing/index.js +242 -0
  55. package/lib/tracing/logs.js +176 -0
  56. package/lib/tracing/options.js +105 -0
  57. package/lib/tracing/paths.js +34 -0
  58. package/lib/tracing/tracer.js +850 -0
  59. package/lib/tracing/w3c.js +415 -0
  60. package/lib/transport/ipc.js +97 -0
  61. package/lib/transport/ws.js +511 -0
  62. package/lib/util.js +109 -0
  63. package/package.json +48 -4
  64. package/preload.cjs +26 -0
  65. package/preload.mjs +25 -0
  66. package/standalone.cjs +16 -0
  67. package/standalone.mjs +17 -0
@@ -0,0 +1,909 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Running in containers (specs/95-containers.md) for @aidra/pmx-io standalone mode (80 B.15):
5
+ // detection (CTR-001…003), identity and metadata (CTR-010…017), the effective limits and the
6
+ // container-aware host metrics (CTR-030…033). The JavaScript twin of pmx-core's container module
7
+ // and pmx-io's _container.py: same detection order, names and metric maths. Host files are read
8
+ // through a sysroot (env PMX_TEST_SYSROOT redirects /proc, /sys, /run and /.dockerenv to a
9
+ // fixture tree, as in Rust and Python).
10
+
11
+ const fs = require('fs')
12
+ const os = require('os')
13
+ const path = require('path')
14
+ const debug = require('../debug')('axm:standalone')
15
+
16
+ const MAX_NAME_LEN = 200
17
+
18
+ class SysRoot {
19
+ constructor (root) {
20
+ this.root = root || '/'
21
+ }
22
+
23
+ static host () {
24
+ const p = process.env.PMX_TEST_SYSROOT
25
+ return new SysRoot(p || '/')
26
+ }
27
+
28
+ path (abs) {
29
+ return this.root === '/' ? abs : path.join(this.root, abs.replace(/^\/+/, ''))
30
+ }
31
+
32
+ read (abs) {
33
+ try {
34
+ return fs.readFileSync(this.path(abs), 'utf8')
35
+ } catch (_) {
36
+ return null
37
+ }
38
+ }
39
+
40
+ exists (abs) {
41
+ try {
42
+ fs.accessSync(this.path(abs))
43
+ return true
44
+ } catch (_) {
45
+ return false
46
+ }
47
+ }
48
+ }
49
+
50
+ function envFlag (v) {
51
+ if (v === undefined || v === null) return null
52
+ const s = String(v).trim().toLowerCase()
53
+ if (s === '1' || s === 'true') return true
54
+ if (s === '0' || s === 'false') return false
55
+ return null
56
+ }
57
+
58
+ function set (env, k) {
59
+ const v = env[k]
60
+ return v ? v : null
61
+ }
62
+
63
+ // -- detection (CTR-001…003) ----------------------------------------------------------------------
64
+
65
+ const MOUNT_HINTS = ['/docker/containers/', '/containers/', '/kubelet/pods/']
66
+ const CGROUP_HINTS = ['docker', 'kubepods', 'containerd', 'libpod', 'ecs']
67
+ const ORCHESTRATORS = { ecs: 'ecs', kubernetes: 'kubernetes', k8s: 'kubernetes', compose: 'compose', none: 'none' }
68
+
69
+ function mountinfoHint (text) {
70
+ for (const line of text.split('\n')) {
71
+ const f = line.split(/\s+/)
72
+ if (f.length > 4 && (f[4] === '/' || f[4] === '/etc/hostname') && MOUNT_HINTS.some((h) => line.includes(h))) return true
73
+ }
74
+ return false
75
+ }
76
+
77
+ function cgroupPaths (text) {
78
+ const out = []
79
+ for (const line of text.split('\n')) {
80
+ const parts = line.split(':')
81
+ if (parts.length >= 3) {
82
+ const p = parts.slice(2).join(':')
83
+ if (p) out.push(p)
84
+ }
85
+ }
86
+ return out
87
+ }
88
+
89
+ function cgroupHint (text) {
90
+ return cgroupPaths(text).some((p) => CGROUP_HINTS.some((h) => p.includes(h)))
91
+ }
92
+
93
+ function detect (env, root, linux) {
94
+ const dockerenv = root.exists('/.dockerenv')
95
+ const containerenv = root.exists('/run/.containerenv')
96
+ const cgroup1 = root.read('/proc/1/cgroup') || ''
97
+ const cgroupSelf = root.read('/proc/self/cgroup') || ''
98
+ const forced = envFlag(env.PMX_CONTAINER)
99
+ let inside
100
+ if (forced !== null) {
101
+ inside = forced
102
+ } else {
103
+ inside = linux && (dockerenv || containerenv || set(env, 'KUBERNETES_SERVICE_HOST') !== null ||
104
+ set(env, 'ECS_CONTAINER_METADATA_URI_V4') !== null || mountinfoHint(root.read('/proc/self/mountinfo') || '') || cgroupHint(cgroup1))
105
+ }
106
+ if (!inside) return { inContainer: false, runtime: 'unknown', orchestrator: 'none' }
107
+ const paths = cgroupPaths(cgroup1).concat(cgroupPaths(cgroupSelf))
108
+ const mentions = (w) => paths.some((p) => p.includes(w))
109
+ let runtime = 'unknown'
110
+ if (containerenv) runtime = 'podman'
111
+ else if (dockerenv || mentions('docker')) runtime = 'docker'
112
+ else if (mentions('containerd')) runtime = 'containerd'
113
+ let orch = ORCHESTRATORS[String(env.PMX_ORCHESTRATOR || '').trim().toLowerCase()]
114
+ if (!orch) {
115
+ if (set(env, 'ECS_CONTAINER_METADATA_URI_V4')) orch = 'ecs'
116
+ else if (set(env, 'KUBERNETES_SERVICE_HOST')) orch = 'kubernetes'
117
+ else if (set(env, 'COMPOSE_PROJECT_NAME')) orch = 'compose'
118
+ else orch = 'none'
119
+ }
120
+ return { inContainer: true, runtime, orchestrator: orch }
121
+ }
122
+
123
+ // -- identity (CTR-010…017) -----------------------------------------------------------------------
124
+
125
+ const MOUNT_ID = /\/containers\/([0-9a-f]{64})\/|\/docker\/([0-9a-f]{64})/
126
+ const POD_SUFFIXES = [/-[a-z0-9]{6,10}-[a-z0-9]{5}$/, /-[a-z0-9]{5}$/, /-[0-9]+$/]
127
+
128
+ function containerIdFromFiles (mountinfo, cgroup) {
129
+ for (const line of mountinfo.split('\n')) {
130
+ const m = MOUNT_ID.exec(line)
131
+ if (m) return m[1] || m[2]
132
+ }
133
+ const found = cgroup.match(/[0-9a-f]{64}/g)
134
+ return found ? found[found.length - 1] : null
135
+ }
136
+
137
+ function serviceFromPod (pod) {
138
+ for (const rx of POD_SUFFIXES) {
139
+ const m = rx.exec(pod)
140
+ if (m && m.index > 0) return pod.slice(0, m.index)
141
+ }
142
+ return pod
143
+ }
144
+
145
+ function hostname () {
146
+ try {
147
+ return os.hostname() || 'localhost'
148
+ } catch (_) {
149
+ return 'localhost'
150
+ }
151
+ }
152
+
153
+ let RANDOM = null
154
+ function processRandom () {
155
+ if (RANDOM === null) RANDOM = require('crypto').randomBytes(2).toString('hex')
156
+ return RANDOM
157
+ }
158
+
159
+ function truncateName (s) {
160
+ return s.slice(0, MAX_NAME_LEN)
161
+ }
162
+
163
+ function str (v) {
164
+ if (typeof v === 'string' && v) return v
165
+ if (typeof v === 'number' && Number.isFinite(v)) return String(v)
166
+ return null
167
+ }
168
+
169
+ function flt (v) {
170
+ if (typeof v === 'number' && Number.isFinite(v)) return v
171
+ if (typeof v === 'string' && v.trim() !== '') {
172
+ const n = Number(v.trim())
173
+ return Number.isFinite(n) ? n : null
174
+ }
175
+ return null
176
+ }
177
+
178
+ class EcsMetadata {
179
+ constructor (task, container) {
180
+ this.task = task || null
181
+ this.container = container || null
182
+ }
183
+
184
+ _t (k) { return this.task && typeof this.task === 'object' ? this.task[k] : undefined }
185
+ _c (k) { return this.container && typeof this.container === 'object' ? this.container[k] : undefined }
186
+ cluster () { return str(this._t('Cluster')) }
187
+ taskArn () { return str(this._t('TaskARN')) }
188
+ taskId () {
189
+ const arn = this.taskArn()
190
+ if (!arn) return null
191
+ const id = arn.split('/').pop()
192
+ return id || null
193
+ }
194
+
195
+ family () { return str(this._t('Family')) }
196
+ revision () { return str(this._t('Revision')) }
197
+ serviceName () { return str(this._t('ServiceName')) }
198
+ zone () { return str(this._t('AvailabilityZone')) }
199
+ taskCpus () {
200
+ const lim = this._t('Limits')
201
+ const v = lim && typeof lim === 'object' ? flt(lim.CPU) : null
202
+ return v !== null && v > 0 ? v : null
203
+ }
204
+
205
+ taskMemoryBytes () {
206
+ const lim = this._t('Limits')
207
+ const v = lim && typeof lim === 'object' ? flt(lim.Memory) : null
208
+ return v !== null && v > 0 ? Math.floor(v * 1024 * 1024) : null
209
+ }
210
+
211
+ dockerId () { return str(this._c('DockerId')) }
212
+ containerName () { return str(this._c('Name')) }
213
+ image () { return str(this._c('Image')) }
214
+ }
215
+
216
+ /** CTR-010: both ECS documents, 1 s timeout each, no retries (raw HTTP: never instrumented). */
217
+ async function fetchEcs (base) {
218
+ const { httpRequest } = require('../transport/ws')
219
+ base = base.replace(/\/+$/, '')
220
+ const errors = []
221
+ const get = async (url) => {
222
+ try {
223
+ const res = await httpRequest('GET', url, null, { Accept: 'application/json' }, { timeout: 1000 })
224
+ if (res.status < 200 || res.status >= 300) throw new Error('HTTP ' + res.status)
225
+ const doc = JSON.parse(res.body.toString('utf8'))
226
+ if (doc && typeof doc === 'object') return doc
227
+ errors.push(`ECS metadata ${url}: invalid JSON`)
228
+ } catch (err) {
229
+ errors.push(`ECS metadata ${url}: ${err && err.message}`)
230
+ }
231
+ return null
232
+ }
233
+ const task = await get(base + '/task')
234
+ const container = await get(base)
235
+ for (const e of errors) debug('%s', e)
236
+ return new EcsMetadata(task, container)
237
+ }
238
+
239
+ const ENV_KEYS = ['PMX_SERVICE_NAME', 'PMX_INSTANCE_NAME', 'PMX_IMAGE', 'PMX_EPHEMERAL', 'POD_NAME', 'HOSTNAME', 'POD_NAMESPACE', 'COMPOSE_SERVICE']
240
+
241
+ class ContainerInfo {
242
+ constructor (detection, ecs, root, env) {
243
+ this.detection = detection
244
+ this.ecs = ecs || new EcsMetadata()
245
+ this.root = root || new SysRoot()
246
+ this._env = {}
247
+ const e = env || {}
248
+ for (const k of ENV_KEYS) if (e[k] !== undefined) this._env[k] = e[k]
249
+ this._ecsUri = null
250
+ }
251
+
252
+ static detect (env, root, linux) {
253
+ const det = detect(env, root, linux)
254
+ const info = new ContainerInfo(det, null, root, env)
255
+ const uri = set(env, 'ECS_CONTAINER_METADATA_URI_V4')
256
+ if (det.inContainer && uri) info._ecsUri = uri
257
+ return info
258
+ }
259
+
260
+ ecsPending () {
261
+ return this._ecsUri !== null
262
+ }
263
+
264
+ async fetchEcs () {
265
+ const uri = this._ecsUri
266
+ this._ecsUri = null
267
+ if (!uri) return
268
+ this.ecs = await fetchEcs(uri)
269
+ }
270
+
271
+ inContainer () {
272
+ return this.detection.inContainer
273
+ }
274
+
275
+ env (k) {
276
+ const v = this._env[k]
277
+ return v || null
278
+ }
279
+
280
+ ephemeral () {
281
+ return this.inContainer() && envFlag(this.env('PMX_EPHEMERAL')) !== false
282
+ }
283
+
284
+ podName () {
285
+ if (this.detection.orchestrator !== 'kubernetes') return null
286
+ return this.env('POD_NAME') || this.env('HOSTNAME')
287
+ }
288
+
289
+ identity (firstApp, host) {
290
+ const ecs = this.ecs
291
+ const pod = this.podName()
292
+ let cid = ecs.dockerId()
293
+ if (cid === null && this.inContainer()) cid = containerIdFromFiles(this.root.read('/proc/self/mountinfo') || '', this.root.read('/proc/self/cgroup') || '')
294
+ const short = cid ? cid.slice(0, 12) : null
295
+ const service = this.env('PMX_SERVICE_NAME') || ecs.serviceName() || ecs.family() || (pod ? serviceFromPod(pod) : null) ||
296
+ this.env('COMPOSE_SERVICE') || firstApp || host
297
+ const taskId = ecs.taskId()
298
+ const instance = this.env('PMX_INSTANCE_NAME') || (taskId ? taskId.slice(0, 12) : null) || pod || short || host
299
+ let namespace = null
300
+ if (this.detection.orchestrator === 'kubernetes') {
301
+ namespace = this.env('POD_NAMESPACE')
302
+ if (namespace === null) namespace = (this.root.read('/var/run/secrets/kubernetes.io/serviceaccount/namespace') || '').trim() || null
303
+ }
304
+ return {
305
+ id: cid,
306
+ shortId: short,
307
+ name: ecs.containerName(),
308
+ image: ecs.image() || this.env('PMX_IMAGE'),
309
+ service,
310
+ instance,
311
+ task: ecs.taskArn() || pod,
312
+ taskShort: taskId || pod,
313
+ cluster: ecs.cluster(),
314
+ namespace,
315
+ revision: ecs.revision(),
316
+ zone: ecs.zone(),
317
+ hostname: host
318
+ }
319
+ }
320
+
321
+ nameContext (firstApp, host) {
322
+ if (!this.inContainer()) {
323
+ return { service: firstApp || host, instance: host, hostname: host, container: host, task: host, random: processRandom() }
324
+ }
325
+ const id = this.identity(firstApp, host)
326
+ return { service: id.service, instance: id.instance, hostname: host, container: id.shortId || host, task: id.taskShort || id.instance, random: processRandom() }
327
+ }
328
+
329
+ defaultMachineName (firstApp, host) {
330
+ if (!this.inContainer()) return null
331
+ const id = this.identity(firstApp, host)
332
+ return truncateName(`${id.service}@${id.instance}`)
333
+ }
334
+
335
+ cgroup () {
336
+ return Cgroup.locate(this.root)
337
+ }
338
+
339
+ block (firstApp, host, limits) {
340
+ const id = this.identity(firstApp, host)
341
+ return {
342
+ runtime: this.detection.runtime,
343
+ orchestrator: this.detection.orchestrator,
344
+ id: id.id,
345
+ name: id.name,
346
+ image: id.image,
347
+ service: id.service,
348
+ instance: id.instance,
349
+ task: id.task,
350
+ cluster: id.cluster,
351
+ namespace: id.namespace,
352
+ revision: id.revision,
353
+ zone: id.zone,
354
+ limits: limits.toJSON()
355
+ }
356
+ }
357
+ }
358
+
359
+ function expand (name, ctx) {
360
+ let out = name
361
+ for (const k of ['service', 'instance', 'hostname', 'container', 'task', 'random']) out = out.split('{' + k + '}').join(ctx[k] || '')
362
+ return truncateName(out)
363
+ }
364
+
365
+ let INFO = null
366
+ /** Detected once per process from process.env and the sysroot (ECS metadata: fetchEcs()). */
367
+ function globalInfo () {
368
+ if (!INFO) INFO = ContainerInfo.detect(process.env, SysRoot.host(), process.platform === 'linux')
369
+ return INFO
370
+ }
371
+
372
+ function resetForTests () {
373
+ INFO = null
374
+ RANDOM = null
375
+ }
376
+
377
+ // -- cgroup (CTR-030) -----------------------------------------------------------------------------
378
+
379
+ function unescapeMount (s) {
380
+ return s.replace(/\\([0-7]{3})/g, (_, o) => String.fromCharCode(parseInt(o, 8)))
381
+ }
382
+
383
+ function parseMountinfo (text) {
384
+ const out = []
385
+ for (const line of text.split('\n')) {
386
+ const sep = line.indexOf(' - ')
387
+ if (sep === -1) continue
388
+ const f = line.slice(0, sep).split(/\s+/)
389
+ const p = line.slice(sep + 3).split(/\s+/)
390
+ if (f.length < 5 || !p.length) continue
391
+ out.push({ root: unescapeMount(f[3]), point: unescapeMount(f[4]), fstype: p[0], source: p[1] ? unescapeMount(p[1]) : '', superOpts: p[2] || '' })
392
+ }
393
+ return out
394
+ }
395
+
396
+ function countCpuList (s) {
397
+ s = String(s || '').trim()
398
+ if (!s) return null
399
+ let n = 0
400
+ for (let part of s.split(',')) {
401
+ part = part.trim()
402
+ if (part.includes('-')) {
403
+ const [a, b] = part.split('-', 2)
404
+ const lo = Number(a)
405
+ const hi = Number(b)
406
+ if (!Number.isInteger(lo) || !Number.isInteger(hi) || a.trim() === '' || b.trim() === '' || hi < lo) return null
407
+ n += hi - lo + 1
408
+ } else {
409
+ if (!/^\d+$/.test(part)) return null
410
+ n += 1
411
+ }
412
+ }
413
+ return n
414
+ }
415
+
416
+ function kv (text, key) {
417
+ if (text === null || text === undefined) return null
418
+ for (const line of text.split('\n')) {
419
+ const parts = line.trim().split(/\s+/)
420
+ if (parts.length >= 2 && parts[0] === key) {
421
+ const n = Number(parts[1])
422
+ return Number.isFinite(n) && /^-?\d+$/.test(parts[1]) ? n : null
423
+ }
424
+ }
425
+ return null
426
+ }
427
+
428
+ function int (s) {
429
+ const n = Number(s)
430
+ return Number.isFinite(n) ? n : 0
431
+ }
432
+
433
+ function parseIoStat (text) {
434
+ let r = 0
435
+ let w = 0
436
+ for (const line of text.split('\n')) {
437
+ for (const tok of line.trim().split(/\s+/).slice(1)) {
438
+ if (tok.startsWith('rbytes=')) r += int(tok.slice(7))
439
+ else if (tok.startsWith('wbytes=')) w += int(tok.slice(7))
440
+ }
441
+ }
442
+ return [r, w]
443
+ }
444
+
445
+ function parseBlkio (text) {
446
+ let r = 0
447
+ let w = 0
448
+ for (const line of text.split('\n')) {
449
+ const f = line.trim().split(/\s+/)
450
+ if (f.length === 3) {
451
+ if (f[1] === 'Read') r += int(f[2])
452
+ else if (f[1] === 'Write') w += int(f[2])
453
+ }
454
+ }
455
+ return [r, w]
456
+ }
457
+
458
+ function readFile (p) {
459
+ try {
460
+ return fs.readFileSync(p, 'utf8')
461
+ } catch (_) {
462
+ return null
463
+ }
464
+ }
465
+
466
+ function isDir (p) {
467
+ try {
468
+ return fs.statSync(p).isDirectory()
469
+ } catch (_) {
470
+ return false
471
+ }
472
+ }
473
+
474
+ function optInt (t) {
475
+ if (t === null || t === undefined) return null
476
+ const s = String(t).trim()
477
+ if (!/^-?\d+$/.test(s)) return null
478
+ return Number(s)
479
+ }
480
+
481
+ const V1_UNLIMITED = 2 ** 60
482
+ const V1_CONTROLLERS = ['cpu', 'cpuacct', 'cpuset', 'memory', 'blkio']
483
+
484
+ class Cgroup {
485
+ constructor (version, v2, v1) {
486
+ this.version = version
487
+ this.v2 = v2 || null
488
+ this.v1 = v1 || {}
489
+ }
490
+
491
+ static locate (root) {
492
+ const text = root.read('/proc/self/cgroup')
493
+ if (text === null) return null
494
+ const mounts = parseMountinfo(root.read('/proc/self/mountinfo') || '')
495
+ const v1 = {}
496
+ let v2Path = null
497
+ for (const line of text.split('\n')) {
498
+ const parts = line.split(':')
499
+ if (parts.length < 3) continue
500
+ const ctrls = parts[1]
501
+ const p = parts.slice(2).join(':')
502
+ if (!ctrls) {
503
+ v2Path = p
504
+ continue
505
+ }
506
+ for (const c of ctrls.split(',')) {
507
+ if (!V1_CONTROLLERS.includes(c) || v1[c]) continue
508
+ const mount = mounts.find((m) => m.fstype === 'cgroup' && m.superOpts.split(',').includes(c))
509
+ let rel = p
510
+ let base
511
+ if (mount) {
512
+ if (mount.root !== '/' && p.startsWith(mount.root)) rel = p.slice(mount.root.length)
513
+ base = root.path(mount.point)
514
+ } else {
515
+ base = root.path('/sys/fs/cgroup/' + c)
516
+ }
517
+ const joined = path.join(base, rel.replace(/^\/+/, ''))
518
+ v1[c] = isDir(joined) ? joined : base
519
+ }
520
+ }
521
+ if (Object.keys(v1).length) return new Cgroup(1, null, v1)
522
+ if (v2Path === null) return null
523
+ const mount = mounts.find((m) => m.fstype === 'cgroup2')
524
+ const point = mount ? mount.point : '/sys/fs/cgroup'
525
+ const mroot = mount ? mount.root : '/'
526
+ let rel = v2Path
527
+ if (mroot !== '/' && rel.startsWith(mroot)) rel = rel.slice(mroot.length)
528
+ const base = root.path(point)
529
+ const joined = path.join(base, rel.replace(/^\/+/, ''))
530
+ return new Cgroup(2, isDir(joined) ? joined : base)
531
+ }
532
+
533
+ _v1 (controller, name) {
534
+ const d = this.v1[controller]
535
+ return d ? readFile(path.join(d, name)) : null
536
+ }
537
+
538
+ _v2 (name) {
539
+ if (this.version !== 2 || !this.v2) return null
540
+ return readFile(path.join(this.v2, name))
541
+ }
542
+
543
+ cpuQuota () {
544
+ let q
545
+ let p
546
+ if (this.version === 2) {
547
+ const t = this._v2('cpu.max')
548
+ if (t === null) return null
549
+ const parts = t.trim().split(/\s+/)
550
+ if (!parts.length || parts[0] === 'max' || parts.length < 2) return null
551
+ q = Number(parts[0])
552
+ p = Number(parts[1])
553
+ } else {
554
+ const tq = this._v1('cpu', 'cpu.cfs_quota_us')
555
+ const tp = this._v1('cpu', 'cpu.cfs_period_us')
556
+ if (tq === null || tp === null) return null
557
+ const qi = optInt(tq)
558
+ if (qi === null || qi <= 0) return null
559
+ q = qi
560
+ p = Number(tp.trim())
561
+ }
562
+ if (!Number.isFinite(q) || !Number.isFinite(p)) return null
563
+ return q > 0 && p > 0 ? q / p : null
564
+ }
565
+
566
+ cpusetCount () {
567
+ const t = this.version === 2 ? this._v2('cpuset.cpus.effective') : (this._v1('cpuset', 'cpuset.effective_cpus') || this._v1('cpuset', 'cpuset.cpus'))
568
+ return t !== null ? countCpuList(t) : null
569
+ }
570
+
571
+ memoryLimit () {
572
+ if (this.version === 2) {
573
+ const t = this._v2('memory.max')
574
+ if (t === null || t.trim() === 'max') return null
575
+ return optInt(t)
576
+ }
577
+ const v = optInt(this._v1('memory', 'memory.limit_in_bytes'))
578
+ return v !== null && v < V1_UNLIMITED ? v : null
579
+ }
580
+
581
+ sample () {
582
+ if (this.version === 2) {
583
+ const cpu = this._v2('cpu.stat') || ''
584
+ const stat = this._v2('memory.stat')
585
+ const ev = this._v2('memory.events')
586
+ const io = this._v2('io.stat')
587
+ return {
588
+ usageUsec: kv(cpu, 'usage_usec'),
589
+ nrPeriods: kv(cpu, 'nr_periods'),
590
+ nrThrottled: kv(cpu, 'nr_throttled'),
591
+ memCurrent: optInt(this._v2('memory.current')),
592
+ inactiveFile: stat !== null ? kv(stat, 'inactive_file') : null,
593
+ oomKill: ev !== null ? kv(ev, 'oom_kill') : null,
594
+ io: io !== null ? parseIoStat(io) : null
595
+ }
596
+ }
597
+ const cpu = this._v1('cpu', 'cpu.stat') || ''
598
+ const usage = optInt(this._v1('cpuacct', 'cpuacct.usage'))
599
+ const stat = this._v1('memory', 'memory.stat')
600
+ const oom = this._v1('memory', 'memory.oom_control')
601
+ const blk = this._v1('blkio', 'blkio.throttle.io_service_bytes') || this._v1('blkio', 'blkio.throttle.io_service_bytes_recursive')
602
+ return {
603
+ usageUsec: usage !== null ? Math.floor(usage / 1000) : null,
604
+ nrPeriods: kv(cpu, 'nr_periods'),
605
+ nrThrottled: kv(cpu, 'nr_throttled'),
606
+ memCurrent: optInt(this._v1('memory', 'memory.usage_in_bytes')),
607
+ inactiveFile: stat !== null ? kv(stat, 'total_inactive_file') : null,
608
+ oomKill: oom !== null ? kv(oom, 'oom_kill') : null,
609
+ io: blk !== null ? parseBlkio(blk) : null
610
+ }
611
+ }
612
+ }
613
+
614
+ function onlineCpus (root) {
615
+ const t = root.read('/sys/devices/system/cpu/online')
616
+ const n = t !== null ? countCpuList(t) : null
617
+ if (n) return n
618
+ return (os.cpus() || []).length || 1
619
+ }
620
+
621
+ function num (v) {
622
+ return Number.isInteger(v) ? v : v
623
+ }
624
+
625
+ class Limits {
626
+ constructor (cpus, hasQuota, memoryBytes) {
627
+ this.cpus = cpus
628
+ this.hasQuota = hasQuota
629
+ this.memoryBytes = memoryBytes
630
+ }
631
+
632
+ static effective (cg, root, ecsCpus, ecsMem, hostTotal) {
633
+ const online = onlineCpus(root)
634
+ const quota = cg ? cg.cpuQuota() : null
635
+ const cpuset = cg ? cg.cpusetCount() : null
636
+ const cands = [quota, ecsCpus && ecsCpus > 0 ? ecsCpus : null, cpuset, online].filter((c) => c !== null && c !== undefined)
637
+ const cpus = cands.length ? Math.min(...cands) : online
638
+ const mems = [cg ? cg.memoryLimit() : null, ecsMem && ecsMem > 0 ? ecsMem : null, hostTotal > 0 ? hostTotal : null].filter((m) => m !== null && m !== undefined)
639
+ const mem = mems.length ? Math.min(...mems) : hostTotal
640
+ return new Limits(cpus, quota !== null, mem)
641
+ }
642
+
643
+ toJSON () {
644
+ return { cpus: num(this.cpus), memoryBytes: this.memoryBytes }
645
+ }
646
+
647
+ cpusRoundedUp () {
648
+ return Math.max(1, Math.ceil(this.cpus))
649
+ }
650
+ }
651
+
652
+ // -- metrics (CTR-032/033, prodman APM-030 on hosts) ----------------------------------------------
653
+
654
+ function metric (value, unit) {
655
+ return { value, type: 'metric', unit, historic: true }
656
+ }
657
+
658
+ function round (v, d) {
659
+ const f = 10 ** d
660
+ return Math.round(v * f) / f
661
+ }
662
+
663
+ function containerValues (limits, prev, cur) {
664
+ const out = {}
665
+ const dt = prev ? prev[1] : 0
666
+ const p = prev ? prev[0] : null
667
+ const delta = (key) => {
668
+ const c = cur[key]
669
+ if (c === null || c === undefined) return [false, null]
670
+ if (!p || dt <= 0 || p[key] === null || p[key] === undefined) return [true, null]
671
+ return [true, Math.max(0, c - p[key])]
672
+ }
673
+ const [has, du] = delta('usageUsec')
674
+ if (has) {
675
+ let pct = 0
676
+ if (du !== null && dt > 0 && limits.cpus > 0) pct = Math.min(100, Math.max(0, du / (dt * 1e6 * limits.cpus) * 100))
677
+ out['CPU Usage'] = metric(round(pct, 1), '%')
678
+ }
679
+ if (limits.hasQuota) {
680
+ const [hp, dp] = delta('nrPeriods')
681
+ const [ht, dthr] = delta('nrThrottled')
682
+ if (hp && ht) {
683
+ let pct = 0
684
+ if (dp !== null && dthr !== null && dp > 0) pct = Math.min(100, dthr / dp * 100)
685
+ out['CPU Throttled'] = metric(round(pct, 1), '%')
686
+ }
687
+ }
688
+ const total = limits.memoryBytes
689
+ const current = cur.memCurrent
690
+ if (current !== null && current !== undefined) {
691
+ const active = Math.max(0, current - (cur.inactiveFile || 0))
692
+ const usage = total > 0 ? round(active / total * 100, 1) : 0
693
+ out['RAM Usage'] = metric(usage, '%')
694
+ out['RAM Total'] = metric(total, 'b')
695
+ out['RAM Available'] = metric(Math.max(0, total - active), 'b')
696
+ out['RAM Active'] = metric(active, 'b')
697
+ } else if (total > 0) {
698
+ out['RAM Total'] = metric(total, 'b')
699
+ }
700
+ const io = cur.io
701
+ if (io) {
702
+ let rr = 0
703
+ let wr = 0
704
+ if (p && dt > 0 && p.io) {
705
+ rr = Math.max(0, io[0] - p.io[0]) / dt
706
+ wr = Math.max(0, io[1] - p.io[1]) / dt
707
+ }
708
+ out['Disk Reads'] = metric(round(rr / 1e6, 3), 'mb/s')
709
+ out['Disk Writes'] = metric(round(wr / 1e6, 3), 'mb/s')
710
+ }
711
+ if (cur.oomKill !== null && cur.oomKill !== undefined) out['OOM Kills'] = metric(cur.oomKill, '')
712
+ return out
713
+ }
714
+
715
+ const PSEUDO_FS = ['proc', 'sysfs', 'tmpfs', 'devtmpfs', 'mqueue', 'devpts', 'securityfs', 'debugfs', 'tracefs', 'pstore', 'bpf', 'fusectl',
716
+ 'configfs', 'hugetlbfs', 'autofs', 'binfmt_misc', 'nsfs', 'ramfs', 'rpc_pipefs', 'efivarfs', 'selinuxfs']
717
+
718
+ function containerFsMounts (mountinfo) {
719
+ const out = []
720
+ for (const m of parseMountinfo(mountinfo)) {
721
+ const pt = m.point
722
+ let keep
723
+ if (pt === '/') {
724
+ keep = true
725
+ } else {
726
+ const fsType = m.fstype
727
+ keep = !(PSEUDO_FS.includes(fsType) || fsType.startsWith('cgroup') || fsType === 'overlay' ||
728
+ ['/proc', '/sys', '/dev'].some((x) => pt === x || pt.startsWith(x + '/')))
729
+ }
730
+ if (keep && !out.includes(pt)) out.push(pt)
731
+ }
732
+ return out
733
+ }
734
+
735
+ function readNetDev (root) {
736
+ const text = root.read('/proc/net/dev') || ''
737
+ const out = {}
738
+ for (const line of text.split('\n').slice(2)) {
739
+ const i = line.indexOf(':')
740
+ if (i === -1) continue
741
+ const name = line.slice(0, i).trim()
742
+ const f = line.slice(i + 1).trim().split(/\s+/)
743
+ if (!/^[A-Za-z0-9._-]+$/.test(name) || f.length < 12) continue
744
+ const v = [f[0], f[8], f[2], f[10], f[3], f[11]].map(Number)
745
+ if (v.some((x) => !Number.isFinite(x))) continue
746
+ out[name] = v
747
+ }
748
+ return out
749
+ }
750
+
751
+ function readMeminfo (root) {
752
+ const text = root.read('/proc/meminfo') || ''
753
+ const out = {}
754
+ for (const line of text.split('\n')) {
755
+ const parts = line.trim().split(/\s+/)
756
+ if (parts.length >= 2 && parts[0].endsWith(':')) {
757
+ const n = Number(parts[1])
758
+ if (!Number.isFinite(n)) continue
759
+ out[parts[0].slice(0, -1)] = n * (parts[2] === 'kB' ? 1024 : 1)
760
+ }
761
+ }
762
+ return out
763
+ }
764
+
765
+ function hostTotalMemory (root) {
766
+ const mi = readMeminfo(root)
767
+ if (mi.MemTotal) return mi.MemTotal
768
+ return os.totalmem()
769
+ }
770
+
771
+ function procStatCpu (root) {
772
+ const text = root.read('/proc/stat') || ''
773
+ for (const line of text.split('\n')) {
774
+ if (line.startsWith('cpu ')) {
775
+ const vals = line.trim().split(/\s+/).slice(1).map(Number)
776
+ if (vals.some((x) => !Number.isFinite(x))) return null
777
+ const idle = vals[3] + (vals.length > 4 ? vals[4] : 0)
778
+ const total = vals.slice(0, 8).reduce((a, b) => a + b, 0)
779
+ return [total - idle, total]
780
+ }
781
+ }
782
+ return null
783
+ }
784
+
785
+ class HostMetrics {
786
+ constructor (info) {
787
+ this.info = info
788
+ this.root = info.root
789
+ this.cgroup = info.inContainer() ? info.cgroup() : null
790
+ this._prevCg = null
791
+ this._prevCpu = null
792
+ this._prevNet = {}
793
+ this._last = null
794
+ this._fsAt = 0
795
+ this._fs = {}
796
+ }
797
+
798
+ limits () {
799
+ return Limits.effective(this.cgroup, this.root, this.info.ecs.taskCpus(), this.info.ecs.taskMemoryBytes(), hostTotalMemory(this.root))
800
+ }
801
+
802
+ collect () {
803
+ try {
804
+ return this.info.inContainer() ? this._container() : this._host()
805
+ } catch (err) {
806
+ debug('system metrics failed: %s', err && err.message)
807
+ return {}
808
+ }
809
+ }
810
+
811
+ _host () {
812
+ const out = {}
813
+ const cpu = procStatCpu(this.root)
814
+ if (cpu) {
815
+ const prev = this._prevCpu
816
+ this._prevCpu = cpu
817
+ if (prev && cpu[1] > prev[1]) {
818
+ const pct = (cpu[0] - prev[0]) / (cpu[1] - prev[1]) * 100
819
+ out['CPU Usage'] = metric(round(Math.min(100, Math.max(0, pct)), 1), '%')
820
+ }
821
+ }
822
+ const mi = readMeminfo(this.root)
823
+ const total = mi.MemTotal || hostTotalMemory(this.root)
824
+ if (total) {
825
+ const avail = mi.MemAvailable !== undefined ? mi.MemAvailable : (mi.MemFree !== undefined ? mi.MemFree : os.freemem())
826
+ out['RAM Total'] = metric(total, 'b')
827
+ const active = Math.max(0, total - avail)
828
+ out['RAM Available'] = metric(avail, 'b')
829
+ out['RAM Active'] = metric(active, 'b')
830
+ out['RAM Usage'] = metric(round(active / total * 100, 1), '%')
831
+ }
832
+ return out
833
+ }
834
+
835
+ _container () {
836
+ const now = Date.now() / 1000
837
+ const dt = this._last !== null ? now - this._last : 0
838
+ this._last = now
839
+ const limits = this.limits()
840
+ const cur = this.cgroup ? this.cgroup.sample() : {}
841
+ const prev = this._prevCg
842
+ this._prevCg = [now, cur]
843
+ const out = containerValues(limits, prev ? [prev[1], now - prev[0]] : null, cur)
844
+ const net = readNetDev(this.root)
845
+ for (const iface of Object.keys(net)) {
846
+ const c = net[iface]
847
+ const p = this._prevNet[iface]
848
+ const rate = (i) => (!p || dt <= 0 ? 0 : Math.max(0, c[i] - p[i]) / dt)
849
+ out['net:rx_5:' + iface] = metric(round(rate(0) / 1e6, 3), 'mb/s')
850
+ out['net:tx_5:' + iface] = metric(round(rate(1) / 1e6, 3), 'mb/s')
851
+ for (const [k, i] of [['rx_errors_60', 2], ['tx_errors_60', 3], ['rx_dropped_60', 4], ['tx_dropped_60', 5]]) {
852
+ out[`net:${k}:${iface}`] = metric(Math.round(rate(i) * 60), '/min')
853
+ }
854
+ }
855
+ this._prevNet = net
856
+ Object.assign(out, this._filesystems(now))
857
+ return out
858
+ }
859
+
860
+ _filesystems (now) {
861
+ if (now - this._fsAt < 5 && Object.keys(this._fs).length) return this._fs
862
+ this._fsAt = now
863
+ const fsOut = {}
864
+ const text = this.root.read('/proc/self/mountinfo')
865
+ if (text && typeof fs.statfsSync === 'function') {
866
+ for (const mp of containerFsMounts(text)) {
867
+ if (!isDir(mp)) continue
868
+ let st
869
+ try {
870
+ st = fs.statfsSync(mp)
871
+ } catch (_) {
872
+ continue
873
+ }
874
+ const bsize = st.bsize
875
+ const total = st.blocks * bsize
876
+ const used = total - st.bfree * bsize
877
+ const avail = st.bavail * bsize
878
+ if (used + avail <= 0) continue
879
+ fsOut['fs:use:' + mp] = metric(round(used / (used + avail) * 100, 2), '%')
880
+ fsOut['fs:size:' + mp] = metric(round(total / 1e9, 2), 'gb')
881
+ }
882
+ }
883
+ this._fs = fsOut
884
+ return fsOut
885
+ }
886
+ }
887
+
888
+ module.exports = {
889
+ SysRoot,
890
+ detect,
891
+ ContainerInfo,
892
+ EcsMetadata,
893
+ Cgroup,
894
+ Limits,
895
+ HostMetrics,
896
+ globalInfo,
897
+ resetForTests,
898
+ expand,
899
+ truncateName,
900
+ hostname,
901
+ serviceFromPod,
902
+ containerIdFromFiles,
903
+ containerValues,
904
+ containerFsMounts,
905
+ countCpuList,
906
+ parseMountinfo,
907
+ hostTotalMemory,
908
+ envFlag
909
+ }