@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,286 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Metric registry and the 990 ms `axm:monitor` push (80 A.3.6).
|
|
5
|
+
|
|
6
|
+
const { Gauge, Counter, Meter, Histogram } = require('./types')
|
|
7
|
+
const { METRIC_PUSH_INTERVAL } = require('../constants')
|
|
8
|
+
|
|
9
|
+
const RELAY_INTERVAL = 1000
|
|
10
|
+
const { round2 } = require('../util')
|
|
11
|
+
const debug = require('../debug')('axm:services:metrics')
|
|
12
|
+
|
|
13
|
+
function normalizeOpts (opts) {
|
|
14
|
+
if (typeof opts === 'string') return { name: opts }
|
|
15
|
+
if (opts === null || typeof opts !== 'object') return { name: opts }
|
|
16
|
+
return opts
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isReportable (v) {
|
|
20
|
+
if (typeof v === 'string' || typeof v === 'boolean') return true
|
|
21
|
+
return typeof v === 'number' && Number.isFinite(v)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class MetricService {
|
|
25
|
+
constructor (transport) {
|
|
26
|
+
this._transport = transport
|
|
27
|
+
this._metrics = new Map()
|
|
28
|
+
this._instruments = new Map() // relay key → { key, kind, impl, opts } (80 Part G.5)
|
|
29
|
+
this._relay = null
|
|
30
|
+
this._timer = null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
start () {
|
|
34
|
+
if (this._timer) return
|
|
35
|
+
this._timer = setInterval(() => (this._relay ? this.flushContributions() : this.push()), this._relay ? RELAY_INTERVAL : METRIC_PUSH_INTERVAL)
|
|
36
|
+
if (this._timer.unref) this._timer.unref()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Relay mode (80 NIO-263, NIO-291, NIO-293): instead of `axm:monitor`, contributions of the
|
|
41
|
+
* relay-capable instruments are sent every 1000 ms with `send(list)`.
|
|
42
|
+
*/
|
|
43
|
+
setRelay (send) {
|
|
44
|
+
this._relay = send
|
|
45
|
+
for (const i of this._instruments.values()) if (i.impl) i.impl._track = true
|
|
46
|
+
if (this._timer) {
|
|
47
|
+
clearInterval(this._timer)
|
|
48
|
+
this._timer = null
|
|
49
|
+
this.start()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Declare a relay-capable instrument (built-in key, or a custom metric's name). */
|
|
54
|
+
instrument (key, kind, impl, opts) {
|
|
55
|
+
if (typeof key !== 'string') return
|
|
56
|
+
if (this._relay && impl) impl._track = true
|
|
57
|
+
this._instruments.set(key, { key, kind, impl, opts: opts || {} })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
contributions () {
|
|
61
|
+
const out = []
|
|
62
|
+
for (const i of this._instruments.values()) {
|
|
63
|
+
let c
|
|
64
|
+
try {
|
|
65
|
+
c = i.kind === 'gauge' && typeof i.impl === 'function' ? i.impl() : i.impl._drain()
|
|
66
|
+
} catch (_) {
|
|
67
|
+
c = undefined
|
|
68
|
+
}
|
|
69
|
+
if (c === undefined || c === null || (typeof c === 'number' && !Number.isFinite(c))) continue
|
|
70
|
+
out.push({ k: i.key, t: i.kind, o: i.opts, c })
|
|
71
|
+
}
|
|
72
|
+
return out
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
flushContributions () {
|
|
76
|
+
if (!this._relay) return
|
|
77
|
+
const c = this.contributions()
|
|
78
|
+
if (c.length) {
|
|
79
|
+
try {
|
|
80
|
+
this._relay(c)
|
|
81
|
+
} catch (_) {}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Root side: apply one relay contribution `{k, t, o, c}`. */
|
|
86
|
+
applyContribution (item) {
|
|
87
|
+
if (!item || typeof item.k !== 'string') return
|
|
88
|
+
let i = this._instruments.get(item.k)
|
|
89
|
+
if (!i) {
|
|
90
|
+
if (item.k.startsWith('internal/')) return
|
|
91
|
+
const o = Object.assign({}, item.o && typeof item.o === 'object' ? item.o : {}, { name: item.k })
|
|
92
|
+
switch (item.t) {
|
|
93
|
+
case 'counter': this.counter(o); break
|
|
94
|
+
case 'meter': this.meter(o); break
|
|
95
|
+
case 'histogram': this.histogram(o); break
|
|
96
|
+
case 'gauge': this.gauge(o); break
|
|
97
|
+
default: return
|
|
98
|
+
}
|
|
99
|
+
i = this._instruments.get(item.k)
|
|
100
|
+
if (!i) return
|
|
101
|
+
}
|
|
102
|
+
if (!i.impl || typeof i.impl._apply !== 'function') return
|
|
103
|
+
try {
|
|
104
|
+
i.impl._apply(item.c)
|
|
105
|
+
} catch (_) {}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Register (or replace) a metric.
|
|
110
|
+
* @param {{name: string, id?: string, unit?: string, historic?: boolean, handler: Function, implementation?: any}} def
|
|
111
|
+
*/
|
|
112
|
+
register (def) {
|
|
113
|
+
if (typeof def.name !== 'string') {
|
|
114
|
+
console.error('Invalid metric name declared: ' + def.name)
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
if (def.id !== undefined && typeof def.id !== 'string') {
|
|
118
|
+
console.error('Invalid metric type declared: ' + def.id)
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
if (typeof def.handler !== 'function') {
|
|
122
|
+
console.error('Invalid metric handler declared for ' + def.name)
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
const old = this._metrics.get(def.name)
|
|
126
|
+
if (old && old.implementation && typeof old.implementation._stop === 'function' && old.implementation !== def.implementation) {
|
|
127
|
+
old.implementation._stop()
|
|
128
|
+
}
|
|
129
|
+
this._metrics.set(def.name, {
|
|
130
|
+
name: def.name,
|
|
131
|
+
id: def.id,
|
|
132
|
+
unit: def.unit,
|
|
133
|
+
historic: def.historic === undefined ? true : Boolean(def.historic),
|
|
134
|
+
handler: def.handler,
|
|
135
|
+
implementation: def.implementation
|
|
136
|
+
})
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
unregister (name) {
|
|
141
|
+
const m = this._metrics.get(name)
|
|
142
|
+
if (!m) return
|
|
143
|
+
if (m.implementation && typeof m.implementation._stop === 'function') m.implementation._stop()
|
|
144
|
+
this._metrics.delete(name)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
has (name) {
|
|
148
|
+
return this._metrics.has(name)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Evaluate every metric into the `axm:monitor` payload shape. */
|
|
152
|
+
collect () {
|
|
153
|
+
const data = {}
|
|
154
|
+
for (const m of this._metrics.values()) {
|
|
155
|
+
let value
|
|
156
|
+
try {
|
|
157
|
+
value = m.handler()
|
|
158
|
+
} catch (err) {
|
|
159
|
+
debug('metric %s handler threw: %s', m.name, err && err.message)
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if (!isReportable(value)) continue
|
|
163
|
+
const entry = { value }
|
|
164
|
+
if (m.unit !== undefined && m.unit !== null) entry.unit = m.unit
|
|
165
|
+
if (m.id !== undefined && m.id !== null) entry.type = m.id
|
|
166
|
+
entry.historic = m.historic
|
|
167
|
+
data[m.name] = entry
|
|
168
|
+
}
|
|
169
|
+
return data
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
push () {
|
|
173
|
+
this._transport.send('axm:monitor', this.collect())
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---- creators (80 A.3.1 – A.3.5) ----
|
|
177
|
+
|
|
178
|
+
_common (opts) {
|
|
179
|
+
return { name: opts.name, id: opts.id, unit: opts.unit, historic: opts.historic }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
_relayOpts (opts) {
|
|
183
|
+
const o = {}
|
|
184
|
+
for (const k of ['name', 'unit', 'id', 'historic', 'measurement', 'samples', 'timeframe']) if (opts[k] !== undefined) o[k] = opts[k]
|
|
185
|
+
return o
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
gauge (opts) {
|
|
189
|
+
opts = normalizeOpts(opts)
|
|
190
|
+
if (typeof opts.value === 'function') {
|
|
191
|
+
const fn = opts.value
|
|
192
|
+
if (this.register(Object.assign(this._common(opts), { handler: fn }))) this.instrument(opts.name, 'gauge', fn, this._relayOpts(opts))
|
|
193
|
+
return undefined
|
|
194
|
+
}
|
|
195
|
+
const g = new Gauge()
|
|
196
|
+
const ok = this.register(Object.assign(this._common(opts), {
|
|
197
|
+
implementation: g,
|
|
198
|
+
handler: () => (g.isUsed() ? g.val() : undefined)
|
|
199
|
+
}))
|
|
200
|
+
if (ok) this.instrument(opts.name, 'gauge', g, this._relayOpts(opts))
|
|
201
|
+
return ok ? g : undefined
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
counter (opts) {
|
|
205
|
+
opts = normalizeOpts(opts)
|
|
206
|
+
const c = new Counter(opts)
|
|
207
|
+
const ok = this.register(Object.assign(this._common(opts), {
|
|
208
|
+
implementation: c,
|
|
209
|
+
handler: () => (c.isUsed() ? c.val() : undefined)
|
|
210
|
+
}))
|
|
211
|
+
if (ok) this.instrument(opts.name, 'counter', c, this._relayOpts(opts))
|
|
212
|
+
return ok ? c : undefined
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
meter (opts) {
|
|
216
|
+
opts = normalizeOpts(opts)
|
|
217
|
+
if (typeof opts.name !== 'string') {
|
|
218
|
+
console.error('Invalid metric name declared: ' + opts.name)
|
|
219
|
+
return undefined
|
|
220
|
+
}
|
|
221
|
+
const m = new Meter(opts)
|
|
222
|
+
this.register(Object.assign(this._common(opts), {
|
|
223
|
+
implementation: m,
|
|
224
|
+
handler: () => (m.isUsed() ? m.val() : undefined)
|
|
225
|
+
}))
|
|
226
|
+
this.instrument(opts.name, 'meter', m, this._relayOpts(opts))
|
|
227
|
+
return m
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
histogram (opts) {
|
|
231
|
+
opts = normalizeOpts(opts)
|
|
232
|
+
const h = new Histogram(opts)
|
|
233
|
+
const ok = this.register(Object.assign(this._common(opts), {
|
|
234
|
+
implementation: h,
|
|
235
|
+
handler: () => {
|
|
236
|
+
if (!h.isUsed()) return undefined
|
|
237
|
+
const v = h.val()
|
|
238
|
+
return typeof v === 'number' ? round2(v) : v
|
|
239
|
+
}
|
|
240
|
+
}))
|
|
241
|
+
if (ok) this.instrument(opts.name, 'histogram', h, this._relayOpts(opts))
|
|
242
|
+
return ok ? h : undefined
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
metrics (list) {
|
|
246
|
+
if (list === null || list === undefined) {
|
|
247
|
+
console.error('Received empty metric to create')
|
|
248
|
+
console.trace()
|
|
249
|
+
return []
|
|
250
|
+
}
|
|
251
|
+
const arr = Array.isArray(list) ? list : [list]
|
|
252
|
+
return arr.map((def) => {
|
|
253
|
+
if (!def || typeof def.name !== 'string') {
|
|
254
|
+
console.error('Trying to create a metrics without a name')
|
|
255
|
+
console.trace()
|
|
256
|
+
return {}
|
|
257
|
+
}
|
|
258
|
+
switch (def.type) {
|
|
259
|
+
case 'counter': return this.counter(def)
|
|
260
|
+
case 'histogram': return this.histogram(def)
|
|
261
|
+
case 'meter': return this.meter(def)
|
|
262
|
+
case 'gauge':
|
|
263
|
+
case 'metric':
|
|
264
|
+
case undefined:
|
|
265
|
+
case null:
|
|
266
|
+
return this.gauge(def)
|
|
267
|
+
default:
|
|
268
|
+
console.error('Invalid metric type ' + def.type + ' for metric ' + def.name)
|
|
269
|
+
console.trace()
|
|
270
|
+
return {}
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
destroy () {
|
|
276
|
+
if (this._timer) clearInterval(this._timer)
|
|
277
|
+
this._timer = null
|
|
278
|
+
for (const m of this._metrics.values()) {
|
|
279
|
+
if (m.implementation && typeof m.implementation._stop === 'function') m.implementation._stop()
|
|
280
|
+
}
|
|
281
|
+
this._metrics.clear()
|
|
282
|
+
this._instruments.clear()
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = { MetricService, normalizeOpts }
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Metric implementations (80 A.3): Gauge, Counter, Meter (EWMA), Histogram with an
|
|
5
|
+
// exponentially decaying reservoir (forward-decay priority sampling).
|
|
6
|
+
|
|
7
|
+
const { round2 } = require('../util')
|
|
8
|
+
|
|
9
|
+
const RESERVOIR_SIZE = 1028
|
|
10
|
+
const RESERVOIR_ALPHA = 0.015 // per second
|
|
11
|
+
const RESCALE_INTERVAL_MS = 60 * 60 * 1000
|
|
12
|
+
|
|
13
|
+
class Gauge {
|
|
14
|
+
constructor (value) {
|
|
15
|
+
this._value = value === undefined ? 0 : value
|
|
16
|
+
this._used = false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
set (value) {
|
|
20
|
+
this._value = value
|
|
21
|
+
this._used = true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
val () {
|
|
25
|
+
return this._value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
isUsed () {
|
|
29
|
+
return this._used
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Relay contribution (80 Part G.5): the last value. */
|
|
33
|
+
_drain () {
|
|
34
|
+
return this._used ? this._value : undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_apply (c) {
|
|
38
|
+
this.set(c)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class Counter {
|
|
43
|
+
constructor (opts) {
|
|
44
|
+
opts = opts || {}
|
|
45
|
+
this._count = typeof opts.count === 'number' ? opts.count : 0
|
|
46
|
+
this._used = false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
inc (n) {
|
|
50
|
+
this._count += n || 1
|
|
51
|
+
this._used = true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
dec (n) {
|
|
55
|
+
this._count -= n || 1
|
|
56
|
+
this._used = true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
reset (count) {
|
|
60
|
+
this._count = count || 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Relay contribution: the net change since the previous drain. */
|
|
64
|
+
_drain () {
|
|
65
|
+
const base = this._relayBase || 0
|
|
66
|
+
this._relayBase = this._count
|
|
67
|
+
const d = this._count - base
|
|
68
|
+
return d !== 0 ? d : undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_apply (c) {
|
|
72
|
+
if (typeof c === 'number' && Number.isFinite(c)) {
|
|
73
|
+
this._count += c
|
|
74
|
+
this._used = true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
val () {
|
|
79
|
+
return this._count
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
isUsed () {
|
|
83
|
+
return this._used
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Exponentially weighted moving average rate (80 A.3.3).
|
|
89
|
+
* rate is in events per millisecond; val() = events per `samples` seconds.
|
|
90
|
+
*/
|
|
91
|
+
class Meter {
|
|
92
|
+
constructor (opts) {
|
|
93
|
+
opts = opts || {}
|
|
94
|
+
this._samples = Number(opts.samples || opts.seconds || 1)
|
|
95
|
+
this._timeframe = Number(opts.timeframe || 60)
|
|
96
|
+
this._tickInterval = Number(opts.tickInterval || 5000)
|
|
97
|
+
this._alpha = 1 - Math.exp(-this._tickInterval / (this._timeframe * 1000))
|
|
98
|
+
this._pending = 0
|
|
99
|
+
this._rate = 0
|
|
100
|
+
this._used = false
|
|
101
|
+
this._timer = setInterval(() => this._tick(), this._tickInterval)
|
|
102
|
+
if (this._timer.unref) this._timer.unref()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_tick () {
|
|
106
|
+
const instant = this._pending / this._tickInterval
|
|
107
|
+
this._pending = 0
|
|
108
|
+
this._rate += this._alpha * (instant - this._rate)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
mark (n) {
|
|
112
|
+
this._pending += n || 1
|
|
113
|
+
this._used = true
|
|
114
|
+
if (this._track) this._relayMarks = (this._relayMarks || 0) + (n || 1)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Relay contribution: the number of marks since the previous drain. */
|
|
118
|
+
_drain () {
|
|
119
|
+
const m = this._relayMarks || 0
|
|
120
|
+
this._relayMarks = 0
|
|
121
|
+
return m > 0 ? m : undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_apply (c) {
|
|
125
|
+
if (typeof c === 'number' && c > 0) this.mark(c)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Unrounded rate in events per `samples` seconds. */
|
|
129
|
+
rawVal () {
|
|
130
|
+
return this._rate * this._samples * 1000
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
val () {
|
|
134
|
+
return round2(this.rawVal())
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
isUsed () {
|
|
138
|
+
return this._used
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_stop () {
|
|
142
|
+
clearInterval(this._timer)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
class ExponentiallyDecayingReservoir {
|
|
147
|
+
constructor (size, alpha) {
|
|
148
|
+
this._size = size || RESERVOIR_SIZE
|
|
149
|
+
this._alpha = alpha || RESERVOIR_ALPHA
|
|
150
|
+
this.clear()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
clear () {
|
|
154
|
+
this._samples = [] // { priority, value }
|
|
155
|
+
this._landmark = Date.now()
|
|
156
|
+
this._nextRescale = this._landmark + RESCALE_INTERVAL_MS
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_rescale (now) {
|
|
160
|
+
const factor = Math.exp(-this._alpha * (now - this._landmark) / 1000)
|
|
161
|
+
for (const s of this._samples) s.priority *= factor
|
|
162
|
+
this._landmark = now
|
|
163
|
+
this._nextRescale = now + RESCALE_INTERVAL_MS
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
update (value, now) {
|
|
167
|
+
now = now === undefined ? Date.now() : now
|
|
168
|
+
if (now >= this._nextRescale) this._rescale(now)
|
|
169
|
+
const weight = Math.exp(this._alpha * (now - this._landmark) / 1000)
|
|
170
|
+
const u = 1 - Math.random() // (0, 1]
|
|
171
|
+
const priority = weight / u
|
|
172
|
+
if (this._samples.length < this._size) {
|
|
173
|
+
this._samples.push({ priority, value })
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
let minIdx = 0
|
|
177
|
+
for (let i = 1; i < this._samples.length; i++) {
|
|
178
|
+
if (this._samples[i].priority < this._samples[minIdx].priority) minIdx = i
|
|
179
|
+
}
|
|
180
|
+
if (priority > this._samples[minIdx].priority) {
|
|
181
|
+
this._samples[minIdx] = { priority, value }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
size () {
|
|
186
|
+
return this._samples.length
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
sortedValues () {
|
|
190
|
+
return this._samples.map((s) => s.value).sort((a, b) => a - b)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Percentile rule of 80 A.3.4 over ascending values. */
|
|
195
|
+
function percentile (sorted, p) {
|
|
196
|
+
const n = sorted.length
|
|
197
|
+
if (n === 0) return null
|
|
198
|
+
const pos = p * (n + 1)
|
|
199
|
+
if (pos < 1) return sorted[0]
|
|
200
|
+
if (pos >= n) return sorted[n - 1]
|
|
201
|
+
const lower = sorted[Math.floor(pos) - 1]
|
|
202
|
+
const upper = sorted[Math.ceil(pos) - 1]
|
|
203
|
+
return lower + (pos - Math.floor(pos)) * (upper - lower)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const MEASUREMENTS = ['min', 'max', 'sum', 'count', 'variance', 'mean', 'stddev', 'median', 'p75', 'p95', 'p99', 'p999', 'ema']
|
|
207
|
+
|
|
208
|
+
class Histogram {
|
|
209
|
+
constructor (opts) {
|
|
210
|
+
opts = opts || {}
|
|
211
|
+
this._measurement = MEASUREMENTS.includes(opts.measurement) ? opts.measurement : 'mean'
|
|
212
|
+
this._reservoir = new ExponentiallyDecayingReservoir()
|
|
213
|
+
this._min = null
|
|
214
|
+
this._max = null
|
|
215
|
+
this._sum = 0
|
|
216
|
+
this._count = 0
|
|
217
|
+
this._mean = 0 // Welford running mean
|
|
218
|
+
this._m2 = 0
|
|
219
|
+
this._ema = 0
|
|
220
|
+
this._used = false
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
update (value) {
|
|
224
|
+
value = Number(value)
|
|
225
|
+
if (Number.isNaN(value)) return
|
|
226
|
+
this._used = true
|
|
227
|
+
this._count++
|
|
228
|
+
this._sum += value
|
|
229
|
+
if (this._min === null || value < this._min) this._min = value
|
|
230
|
+
if (this._max === null || value > this._max) this._max = value
|
|
231
|
+
const delta = value - this._mean
|
|
232
|
+
this._mean += delta / this._count
|
|
233
|
+
this._m2 += delta * (value - this._mean)
|
|
234
|
+
const alpha = 2 / (1 + this._count)
|
|
235
|
+
this._ema = this._count === 1 ? value : this._ema + alpha * (value - this._ema)
|
|
236
|
+
this._reservoir.update(value)
|
|
237
|
+
if (this._track) {
|
|
238
|
+
if (!this._relayValues) this._relayValues = []
|
|
239
|
+
if (this._relayValues.length < 2000) this._relayValues.push(value)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Relay contribution: the values recorded since the previous drain (at most 2 000). */
|
|
244
|
+
_drain () {
|
|
245
|
+
const v = this._relayValues
|
|
246
|
+
this._relayValues = null
|
|
247
|
+
return v && v.length ? v : undefined
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_apply (c) {
|
|
251
|
+
if (!Array.isArray(c)) return
|
|
252
|
+
for (const v of c.slice(0, 2000)) this.update(v)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
getMin () { return this._min }
|
|
256
|
+
getMax () { return this._max }
|
|
257
|
+
getSum () { return this._sum }
|
|
258
|
+
getCount () { return this._count }
|
|
259
|
+
getEma () { return this._ema }
|
|
260
|
+
getMean () { return this._count === 0 ? 0 : this._sum / this._count }
|
|
261
|
+
getVariance () { return this._count <= 1 ? null : this._m2 / (this._count - 1) }
|
|
262
|
+
|
|
263
|
+
percentiles (fractions) {
|
|
264
|
+
const sorted = this._reservoir.sortedValues()
|
|
265
|
+
const out = {}
|
|
266
|
+
for (const p of fractions || []) out[p] = percentile(sorted, p)
|
|
267
|
+
return out
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
fullResults () {
|
|
271
|
+
const pc = this.percentiles([0.5, 0.75, 0.95, 0.99, 0.999])
|
|
272
|
+
return {
|
|
273
|
+
min: this._min,
|
|
274
|
+
max: this._max,
|
|
275
|
+
sum: this._sum,
|
|
276
|
+
variance: this.getVariance(),
|
|
277
|
+
mean: this.getMean(),
|
|
278
|
+
count: this._count,
|
|
279
|
+
median: pc[0.5],
|
|
280
|
+
p75: pc[0.75],
|
|
281
|
+
p95: pc[0.95],
|
|
282
|
+
p99: pc[0.99],
|
|
283
|
+
p999: pc[0.999],
|
|
284
|
+
ema: this._ema
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_compute (measurement) {
|
|
289
|
+
switch (measurement) {
|
|
290
|
+
case 'min': return this._min
|
|
291
|
+
case 'max': return this._max
|
|
292
|
+
case 'sum': return this._sum
|
|
293
|
+
case 'count': return this._count
|
|
294
|
+
case 'variance': return this.getVariance()
|
|
295
|
+
case 'mean': return this.getMean()
|
|
296
|
+
case 'stddev': {
|
|
297
|
+
const v = this.getVariance()
|
|
298
|
+
return v === null ? null : Math.sqrt(v)
|
|
299
|
+
}
|
|
300
|
+
case 'ema': return this._ema
|
|
301
|
+
case 'median': return this.percentiles([0.5])[0.5]
|
|
302
|
+
case 'p75': return this.percentiles([0.75])[0.75]
|
|
303
|
+
case 'p95': return this.percentiles([0.95])[0.95]
|
|
304
|
+
case 'p99': return this.percentiles([0.99])[0.99]
|
|
305
|
+
case 'p999': return this.percentiles([0.999])[0.999]
|
|
306
|
+
default: return this.getMean()
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
val () {
|
|
311
|
+
return this._compute(this._measurement)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
isUsed () {
|
|
315
|
+
return this._used
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const MetricType = Object.freeze({
|
|
320
|
+
meter: 'meter',
|
|
321
|
+
histogram: 'histogram',
|
|
322
|
+
counter: 'counter',
|
|
323
|
+
gauge: 'gauge',
|
|
324
|
+
metric: 'metric'
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
const MetricMeasurements = Object.freeze(MEASUREMENTS.reduce((acc, m) => {
|
|
328
|
+
acc[m] = m
|
|
329
|
+
return acc
|
|
330
|
+
}, {}))
|
|
331
|
+
|
|
332
|
+
module.exports = {
|
|
333
|
+
Gauge,
|
|
334
|
+
Counter,
|
|
335
|
+
Meter,
|
|
336
|
+
Histogram,
|
|
337
|
+
ExponentiallyDecayingReservoir,
|
|
338
|
+
percentile,
|
|
339
|
+
MetricType,
|
|
340
|
+
MetricMeasurements,
|
|
341
|
+
MEASUREMENTS
|
|
342
|
+
}
|