@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.
- package/README.md +69 -2
- package/bin/pmx-io-node.js +143 -0
- package/index.d.ts +381 -0
- package/index.js +27 -0
- package/index.mjs +47 -0
- package/lib/actions.js +120 -0
- package/lib/activate.js +402 -0
- package/lib/application.js +106 -0
- package/lib/bun.js +155 -0
- package/lib/config.js +67 -0
- package/lib/constants.js +17 -0
- package/lib/debug.js +48 -0
- package/lib/esm-hooks.mjs +111 -0
- package/lib/exit.js +79 -0
- package/lib/features/metrics.js +409 -0
- package/lib/features/notify.js +358 -0
- package/lib/features/profiling.js +208 -0
- package/lib/hooks.js +151 -0
- package/lib/inbound.js +174 -0
- package/lib/integrations.js +372 -0
- package/lib/itg/amqplib.js +169 -0
- package/lib/itg/bullmq.js +141 -0
- package/lib/itg/common.js +657 -0
- package/lib/itg/dbcomment.js +114 -0
- package/lib/itg/index.js +103 -0
- package/lib/itg/ioredis.js +212 -0
- package/lib/itg/kafkajs.js +205 -0
- package/lib/itg/mongodb.js +322 -0
- package/lib/itg/mysql2.js +205 -0
- package/lib/itg/nats.js +531 -0
- package/lib/itg/noderedis.js +236 -0
- package/lib/itg/pg.js +253 -0
- package/lib/itg/pgqueue.js +305 -0
- package/lib/itg/postgres.js +252 -0
- package/lib/itg/redis-common.js +265 -0
- package/lib/itg/sql.js +302 -0
- package/lib/metrics/service.js +286 -0
- package/lib/metrics/types.js +342 -0
- package/lib/modules.js +260 -0
- package/lib/node-options.js +62 -0
- package/lib/otel/bridge.js +797 -0
- package/lib/otel/env.js +114 -0
- package/lib/outbound.js +362 -0
- package/lib/pmx.js +757 -0
- package/lib/relay/hub.js +279 -0
- package/lib/relay/transport.js +253 -0
- package/lib/standalone/activate.js +307 -0
- package/lib/standalone/capture.js +55 -0
- package/lib/standalone/container.js +909 -0
- package/lib/standalone/transport.js +1023 -0
- package/lib/tracing/context.js +90 -0
- package/lib/tracing/control.js +124 -0
- package/lib/tracing/exporter.js +115 -0
- package/lib/tracing/index.js +242 -0
- package/lib/tracing/logs.js +176 -0
- package/lib/tracing/options.js +105 -0
- package/lib/tracing/paths.js +34 -0
- package/lib/tracing/tracer.js +850 -0
- package/lib/tracing/w3c.js +415 -0
- package/lib/transport/ipc.js +97 -0
- package/lib/transport/ws.js +511 -0
- package/lib/util.js +109 -0
- package/package.json +48 -4
- package/preload.cjs +26 -0
- package/preload.mjs +25 -0
- package/standalone.cjs +16 -0
- 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
|
+
}
|