@adalo/metrics 0.1.173 → 0.1.175
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 +7 -0
- package/__tests__/httpMetricsRedisCollector.test.js +203 -0
- package/__tests__/httpMetricsRedisRecorder.test.js +60 -0
- package/__tests__/httpMetricsRedisStore.test.js +431 -0
- package/docs/http-metrics-redis.md +19 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +44 -0
- package/lib/index.js.map +1 -1
- package/lib/metrics/baseMetricsClient.d.ts +2 -0
- package/lib/metrics/baseMetricsClient.d.ts.map +1 -1
- package/lib/metrics/baseMetricsClient.js +6 -3
- package/lib/metrics/baseMetricsClient.js.map +1 -1
- package/lib/metrics/httpMetricsRedisCollector.d.ts +50 -0
- package/lib/metrics/httpMetricsRedisCollector.d.ts.map +1 -0
- package/lib/metrics/httpMetricsRedisCollector.js +115 -0
- package/lib/metrics/httpMetricsRedisCollector.js.map +1 -0
- package/lib/metrics/httpMetricsRedisRecorder.d.ts +48 -0
- package/lib/metrics/httpMetricsRedisRecorder.d.ts.map +1 -0
- package/lib/metrics/httpMetricsRedisRecorder.js +86 -0
- package/lib/metrics/httpMetricsRedisRecorder.js.map +1 -0
- package/lib/metrics/httpMetricsRedisStore.d.ts +88 -0
- package/lib/metrics/httpMetricsRedisStore.d.ts.map +1 -0
- package/lib/metrics/httpMetricsRedisStore.js +223 -0
- package/lib/metrics/httpMetricsRedisStore.js.map +1 -0
- package/lib/metrics/metricsClient.d.ts +34 -27
- package/lib/metrics/metricsClient.d.ts.map +1 -1
- package/lib/metrics/metricsClient.js +35 -37
- package/lib/metrics/metricsClient.js.map +1 -1
- package/lib/metrics/metricsDatabaseClient.d.ts.map +1 -1
- package/lib/metrics/metricsDatabaseClient.js +6 -1
- package/lib/metrics/metricsDatabaseClient.js.map +1 -1
- package/lib/metrics/metricsProcessTypeUtils.d.ts +58 -0
- package/lib/metrics/metricsProcessTypeUtils.d.ts.map +1 -0
- package/lib/metrics/metricsProcessTypeUtils.js +86 -0
- package/lib/metrics/metricsProcessTypeUtils.js.map +1 -0
- package/lib/metrics/metricsQueueRedisClient.d.ts.map +1 -1
- package/lib/metrics/metricsQueueRedisClient.js +5 -0
- package/lib/metrics/metricsQueueRedisClient.js.map +1 -1
- package/lib/metrics/metricsRedisClient.d.ts.map +1 -1
- package/lib/metrics/metricsRedisClient.js +7 -1
- package/lib/metrics/metricsRedisClient.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +4 -0
- package/src/metrics/baseMetricsClient.js +4 -1
- package/src/metrics/httpMetricsRedisCollector.js +121 -0
- package/src/metrics/httpMetricsRedisRecorder.js +74 -0
- package/src/metrics/httpMetricsRedisStore.js +208 -0
- package/src/metrics/metricsClient.js +34 -53
- package/src/metrics/metricsDatabaseClient.js +7 -1
- package/src/metrics/metricsProcessTypeUtils.js +98 -0
- package/src/metrics/metricsQueueRedisClient.js +6 -0
- package/src/metrics/metricsRedisClient.js +12 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record separator for hash fields (avoids collisions when route contains "|").
|
|
3
|
+
* @type {string}
|
|
4
|
+
*/
|
|
5
|
+
const FIELD_SEP = '\x1e'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default Redis key TTL in seconds (sliding: refreshed on each `record` write).
|
|
9
|
+
* @type {number}
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_HTTP_METRICS_REDIS_TTL_SEC = 120
|
|
12
|
+
|
|
13
|
+
const DRAIN_LUA = `
|
|
14
|
+
local function drain(key)
|
|
15
|
+
local v = redis.call('HGETALL', key)
|
|
16
|
+
redis.call('DEL', key)
|
|
17
|
+
return v
|
|
18
|
+
end
|
|
19
|
+
return {drain(KEYS[1]), drain(KEYS[2])}
|
|
20
|
+
`
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} method
|
|
24
|
+
* @param {string} route
|
|
25
|
+
* @param {number} statusCode
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function buildFieldKey(method, route, statusCode) {
|
|
29
|
+
return [method, route, String(statusCode)].join(FIELD_SEP)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hgetallPairsToObject(pairs) {
|
|
33
|
+
const o = {}
|
|
34
|
+
if (!pairs || !pairs.length) {
|
|
35
|
+
return o
|
|
36
|
+
}
|
|
37
|
+
for (let i = 0; i < pairs.length; i += 2) {
|
|
38
|
+
o[pairs[i]] = pairs[i + 1]
|
|
39
|
+
}
|
|
40
|
+
return o
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {unknown} raw redis eval result [countPairs, durPairs]
|
|
45
|
+
* @returns {{ labels: { method: string, route: string, status_code: string }, count: number, dur: number }[]}
|
|
46
|
+
*/
|
|
47
|
+
function rowsFromDrainRaw(raw) {
|
|
48
|
+
const rows = []
|
|
49
|
+
if (!raw || !Array.isArray(raw) || raw.length < 2) {
|
|
50
|
+
return rows
|
|
51
|
+
}
|
|
52
|
+
const counts = hgetallPairsToObject(raw[0])
|
|
53
|
+
const durs = hgetallPairsToObject(raw[1])
|
|
54
|
+
const fieldKeys = Object.keys(counts)
|
|
55
|
+
for (const field of fieldKeys) {
|
|
56
|
+
const count = parseInt(counts[field], 10)
|
|
57
|
+
if (!count || count < 1) {
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
const dur = parseInt(durs[field] || '0', 10) || 0
|
|
61
|
+
const parts = field.split(FIELD_SEP)
|
|
62
|
+
let labels = null
|
|
63
|
+
if (parts.length === 3) {
|
|
64
|
+
const [m, route, statusStr] = parts
|
|
65
|
+
labels = { method: m, route, status_code: statusStr }
|
|
66
|
+
} else if (parts.length === 5 || parts.length === 6) {
|
|
67
|
+
const [m, route, statusStr] = parts
|
|
68
|
+
labels = { method: m, route, status_code: statusStr }
|
|
69
|
+
}
|
|
70
|
+
if (!labels) {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
rows.push({ labels, count, dur })
|
|
74
|
+
}
|
|
75
|
+
return rows
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).
|
|
80
|
+
* Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`.
|
|
81
|
+
*
|
|
82
|
+
* **Structure:** `countKey` / `durKey` are each **one Redis hash**. Each **field name** encodes
|
|
83
|
+
* `(method, route, status_code)`. Values: `:count` is HINCRBY 1 per request; `:dur` is summed ms.
|
|
84
|
+
*/
|
|
85
|
+
class HttpMetricsRedisStore {
|
|
86
|
+
/**
|
|
87
|
+
* @param {Object} opts
|
|
88
|
+
* @param {import('redis').RedisClient} opts.redisClient
|
|
89
|
+
* @param {string} opts.appName BUILD_APP_NAME (key segment)
|
|
90
|
+
* @param {string} opts.processType logical process for key (e.g. web)
|
|
91
|
+
* @param {number} [opts.ttlSec] Expire keys after this many seconds (default 120)
|
|
92
|
+
*/
|
|
93
|
+
constructor({ redisClient, appName, processType, ttlSec }) {
|
|
94
|
+
if (redisClient == null) {
|
|
95
|
+
throw new Error('HttpMetricsRedisStore: redisClient is required')
|
|
96
|
+
}
|
|
97
|
+
this._client = redisClient
|
|
98
|
+
this.ttlSec =
|
|
99
|
+
typeof ttlSec === 'number' && ttlSec > 0
|
|
100
|
+
? ttlSec
|
|
101
|
+
: DEFAULT_HTTP_METRICS_REDIS_TTL_SEC
|
|
102
|
+
const keySeg = `${encodeURIComponent(appName)}:${encodeURIComponent(
|
|
103
|
+
processType
|
|
104
|
+
)}`
|
|
105
|
+
this.countKey = `metrics:http:v2:${keySeg}:count`
|
|
106
|
+
this.durKey = `metrics:http:v2:${keySeg}:dur`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @returns {import('redis').RedisClient}
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
_ensureClient() {
|
|
114
|
+
return this._client
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {string} method
|
|
119
|
+
* @param {string} route
|
|
120
|
+
* @param {number} statusCode
|
|
121
|
+
* @param {number} durationMs
|
|
122
|
+
*/
|
|
123
|
+
record(method, route, statusCode, durationMs) {
|
|
124
|
+
try {
|
|
125
|
+
const client = this._ensureClient()
|
|
126
|
+
const field = buildFieldKey(method, route, statusCode)
|
|
127
|
+
const dur = Math.max(0, Math.round(Number(durationMs) || 0))
|
|
128
|
+
client
|
|
129
|
+
.multi()
|
|
130
|
+
.hincrby(this.countKey, field, 1)
|
|
131
|
+
.hincrby(this.durKey, field, dur)
|
|
132
|
+
.expire(this.countKey, this.ttlSec)
|
|
133
|
+
.expire(this.durKey, this.ttlSec)
|
|
134
|
+
.exec(err => {
|
|
135
|
+
if (err) {
|
|
136
|
+
console.error('[HttpMetricsRedisStore] record failed:', err.message)
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.error('[HttpMetricsRedisStore] record:', e.message)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Atomically drain Redis hashes (same Lua as `flushToCounters`) and return parsed rows.
|
|
146
|
+
*
|
|
147
|
+
* @returns {Promise<{ ok: boolean, rows: { labels: Object, count: number, dur: number }[] }>}
|
|
148
|
+
*/
|
|
149
|
+
drainRows() {
|
|
150
|
+
let client
|
|
151
|
+
try {
|
|
152
|
+
client = this._ensureClient()
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.error('[HttpMetricsRedisStore] drainRows:', e.message)
|
|
155
|
+
return Promise.resolve({ ok: false, rows: [] })
|
|
156
|
+
}
|
|
157
|
+
return new Promise(resolve => {
|
|
158
|
+
client.eval(DRAIN_LUA, 2, this.countKey, this.durKey, (evalErr, raw) => {
|
|
159
|
+
if (evalErr) {
|
|
160
|
+
console.error(
|
|
161
|
+
'[HttpMetricsRedisStore] drain failed:',
|
|
162
|
+
evalErr.message
|
|
163
|
+
)
|
|
164
|
+
resolve({ ok: false, rows: [] })
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const rows = rowsFromDrainRaw(raw)
|
|
169
|
+
resolve({ ok: true, rows })
|
|
170
|
+
} catch (e) {
|
|
171
|
+
console.error(
|
|
172
|
+
'[HttpMetricsRedisStore] drainRows parse failed:',
|
|
173
|
+
e.message
|
|
174
|
+
)
|
|
175
|
+
resolve({ ok: false, rows: [] })
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @param {(labels: Object, value: number) => void} applyCount
|
|
183
|
+
* @param {(labels: Object, value: number) => void} applyDuration
|
|
184
|
+
* @returns {Promise<boolean>}
|
|
185
|
+
*/
|
|
186
|
+
flushToCounters(applyCount, applyDuration) {
|
|
187
|
+
return this.drainRows().then(({ ok, rows }) => {
|
|
188
|
+
if (!ok) {
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
for (const row of rows) {
|
|
192
|
+
applyCount(row.labels, row.count)
|
|
193
|
+
if (row.dur > 0) {
|
|
194
|
+
applyDuration(row.labels, row.dur)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return true
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
HttpMetricsRedisStore,
|
|
204
|
+
buildFieldKey,
|
|
205
|
+
FIELD_SEP,
|
|
206
|
+
DEFAULT_HTTP_METRICS_REDIS_TTL_SEC,
|
|
207
|
+
rowsFromDrainRaw,
|
|
208
|
+
}
|
|
@@ -4,32 +4,38 @@ const { BaseMetricsClient } = require('./baseMetricsClient')
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* MetricsClient handles Prometheus metrics collection and push.
|
|
7
|
-
* Supports gauges,
|
|
8
|
-
*
|
|
7
|
+
* Supports gauges, default process metrics, optional HTTP counters, and custom metrics.
|
|
8
|
+
*
|
|
9
|
+
* **HTTP metrics:** In-process counters only (`app_requests_*`), gated by `httpMetricsEnabled` /
|
|
10
|
+
* `METRICS_HTTP_ENABLED`. For Redis-backed HTTP aggregation (multi-web / cluster), use
|
|
11
|
+
* {@link HttpMetricsRedisRecorder} and {@link HttpMetricsRedisCollector} — not this class.
|
|
12
|
+
*
|
|
13
|
+
* @extends BaseMetricsClient
|
|
9
14
|
*/
|
|
10
15
|
class MetricsClient extends BaseMetricsClient {
|
|
11
16
|
/**
|
|
12
|
-
* @param {Object} config
|
|
17
|
+
* @param {Object} [config]
|
|
13
18
|
* @param {string} [config.appName] Name of the application
|
|
14
19
|
* @param {string} [config.dynoId] Dyno/instance ID
|
|
15
20
|
* @param {string} [config.processType] Process type (web, worker, etc.)
|
|
16
21
|
* @param {boolean} [config.enabled] Enable metrics collection
|
|
17
|
-
* @param {boolean} [config.httpMetricsEnabled
|
|
22
|
+
* @param {boolean} [config.httpMetricsEnabled] Enable HTTP request metrics (`app_requests_total`, `app_requests_total_duration`); defaults from `METRICS_HTTP_ENABLED === 'true'`
|
|
18
23
|
* @param {boolean} [config.logValues] Log metrics values to console
|
|
19
|
-
* @param {string} [config.pushgatewayUrl]
|
|
20
|
-
* @param {string} [config.pushgatewaySecret]
|
|
24
|
+
* @param {string} [config.pushgatewayUrl] Push URL (VM-agent import endpoint, e.g. .../api/v1/import/prometheus). /metrics is for GET (scrape), not POST (push).
|
|
25
|
+
* @param {string} [config.pushgatewaySecret] Basic auth secret (Base64 of `user:password`)
|
|
21
26
|
* @param {number} [config.intervalSec] Interval in seconds for pushing metrics
|
|
22
27
|
* @param {boolean} [config.removeOldMetrics] Enable to clear metrics by service name
|
|
23
|
-
* @param {function} [config.startupValidation] Add to validate on start push
|
|
24
|
-
* @param {boolean} [config.disablePushgateway] Disable pushing to
|
|
28
|
+
* @param {function} [config.startupValidation] Add to validate on start push
|
|
29
|
+
* @param {boolean} [config.disablePushgateway] Disable pushing to VM-agent (use HTTP scraping instead)
|
|
30
|
+
* @param {boolean} [config.blockNodeDefaultMetrics] When true, skip prom-client default process metrics (rare; see {@link BaseMetricsClient})
|
|
25
31
|
*/
|
|
26
32
|
constructor(config = {}) {
|
|
27
33
|
super(config)
|
|
28
34
|
|
|
29
35
|
this.httpMetricsEnabled =
|
|
30
|
-
config.httpMetricsEnabled
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
config.httpMetricsEnabled !== undefined
|
|
37
|
+
? config.httpMetricsEnabled
|
|
38
|
+
: process.env.METRICS_HTTP_ENABLED === 'true'
|
|
33
39
|
|
|
34
40
|
this._lastUsageMicros = 0
|
|
35
41
|
this._lastCheckTime = Date.now()
|
|
@@ -85,8 +91,6 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
85
91
|
labelNames: this.withDefaultLabelsWithoutDynoId([
|
|
86
92
|
'method',
|
|
87
93
|
'route',
|
|
88
|
-
'appId',
|
|
89
|
-
'databaseId',
|
|
90
94
|
'status_code',
|
|
91
95
|
]),
|
|
92
96
|
useLabelsWithoutDynoId: true,
|
|
@@ -98,8 +102,6 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
98
102
|
labelNames: this.withDefaultLabelsWithoutDynoId([
|
|
99
103
|
'method',
|
|
100
104
|
'route',
|
|
101
|
-
'appId',
|
|
102
|
-
'databaseId',
|
|
103
105
|
'status_code',
|
|
104
106
|
]),
|
|
105
107
|
useLabelsWithoutDynoId: true,
|
|
@@ -139,7 +141,7 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
/**
|
|
142
|
-
*
|
|
144
|
+
* Available CPU cores (cgroup quota or `os.cpus().length`).
|
|
143
145
|
* @returns {number}
|
|
144
146
|
*/
|
|
145
147
|
getAvailableCPUs() {
|
|
@@ -160,7 +162,7 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
/**
|
|
163
|
-
*
|
|
165
|
+
* Container memory usage in bytes (`memory.current` or RSS fallback).
|
|
164
166
|
* @returns {number}
|
|
165
167
|
*/
|
|
166
168
|
getContainerMemoryUsage() {
|
|
@@ -175,7 +177,7 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
/**
|
|
178
|
-
*
|
|
180
|
+
* Container memory limit in bytes (`memory.max` or host total).
|
|
179
181
|
* @returns {number}
|
|
180
182
|
*/
|
|
181
183
|
getContainerMemoryLimit() {
|
|
@@ -195,7 +197,7 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
/**
|
|
198
|
-
*
|
|
200
|
+
* Event loop lag sample in milliseconds.
|
|
199
201
|
* @returns {Promise<number>}
|
|
200
202
|
*/
|
|
201
203
|
measureLag() {
|
|
@@ -206,48 +208,39 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
206
208
|
}
|
|
207
209
|
|
|
208
210
|
/**
|
|
209
|
-
* Increment
|
|
211
|
+
* Increment HTTP request counters (in-process). No-op if `httpMetricsEnabled` is false.
|
|
210
212
|
*
|
|
211
|
-
* @param {Object} params
|
|
212
|
-
* @param {string} params.method
|
|
213
|
-
* @param {string} params.route
|
|
214
|
-
* @param {number} params.status_code
|
|
215
|
-
* @param {
|
|
216
|
-
* @param {string} [params.databaseId=''] - Optional database identifier.
|
|
217
|
-
* @param {number} params.duration - Request duration in milliseconds.
|
|
213
|
+
* @param {Object} params
|
|
214
|
+
* @param {string} params.method HTTP method
|
|
215
|
+
* @param {string} params.route Route or path pattern
|
|
216
|
+
* @param {number} params.status_code HTTP status code
|
|
217
|
+
* @param {number} params.duration Duration in milliseconds
|
|
218
218
|
*/
|
|
219
|
-
trackHttpRequest({
|
|
220
|
-
method,
|
|
221
|
-
route,
|
|
222
|
-
status_code,
|
|
223
|
-
appId = '',
|
|
224
|
-
databaseId = '',
|
|
225
|
-
duration,
|
|
226
|
-
}) {
|
|
219
|
+
trackHttpRequest({ method, route, status_code, duration }) {
|
|
227
220
|
if (!this.httpMetricsEnabled) return
|
|
228
221
|
|
|
229
222
|
this.countersFunctions?.app_requests_total({
|
|
230
223
|
method,
|
|
231
224
|
route,
|
|
232
225
|
status_code,
|
|
233
|
-
appId,
|
|
234
|
-
databaseId,
|
|
235
226
|
})
|
|
236
227
|
this.countersFunctions?.app_requests_total_duration(
|
|
237
228
|
{
|
|
238
229
|
method,
|
|
239
230
|
route,
|
|
240
231
|
status_code,
|
|
241
|
-
appId,
|
|
242
|
-
databaseId,
|
|
243
232
|
},
|
|
244
233
|
duration
|
|
245
234
|
)
|
|
246
235
|
}
|
|
247
236
|
|
|
248
237
|
/**
|
|
249
|
-
* Express middleware
|
|
250
|
-
*
|
|
238
|
+
* Express middleware: records `app_requests_*` on response finish.
|
|
239
|
+
* Skips when disabled, HTTP metrics off, or `OPTIONS`.
|
|
240
|
+
*
|
|
241
|
+
* @param {import('http').IncomingMessage} req
|
|
242
|
+
* @param {import('http').ServerResponse} res
|
|
243
|
+
* @param {function} next
|
|
251
244
|
*/
|
|
252
245
|
trackHttpRequestMiddleware = (req, res, next) => {
|
|
253
246
|
if (!this.enabled || !this.httpMetricsEnabled || req.method === 'OPTIONS') {
|
|
@@ -258,23 +251,11 @@ class MetricsClient extends BaseMetricsClient {
|
|
|
258
251
|
const start = Date.now()
|
|
259
252
|
res.on('finish', () => {
|
|
260
253
|
const route = req.route?.path || req.path || 'unknown'
|
|
261
|
-
const appId =
|
|
262
|
-
req.params?.appId || req.body?.appId || req.query?.appId || ''
|
|
263
|
-
const databaseId =
|
|
264
|
-
req.params?.databaseId ||
|
|
265
|
-
req.body?.databaseId ||
|
|
266
|
-
req.query?.databaseId ||
|
|
267
|
-
req.params?.datasourceId ||
|
|
268
|
-
req.body?.datasourceId ||
|
|
269
|
-
req.query?.datasourceId ||
|
|
270
|
-
''
|
|
271
254
|
|
|
272
255
|
this.trackHttpRequest({
|
|
273
256
|
method: req.method,
|
|
274
257
|
route,
|
|
275
258
|
status_code: res.statusCode,
|
|
276
|
-
appId,
|
|
277
|
-
databaseId,
|
|
278
259
|
duration: Date.now() - start,
|
|
279
260
|
})
|
|
280
261
|
})
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const { Pool } = require('pg')
|
|
2
2
|
const { BaseMetricsClient } = require('./baseMetricsClient')
|
|
3
|
+
const {
|
|
4
|
+
exitUnlessProcessTypeIs,
|
|
5
|
+
METRICS_PROCESS_TYPE_DATABASE,
|
|
6
|
+
} = require('./metricsProcessTypeUtils')
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* DatabaseMetricsClient collects Postgres connection metrics
|
|
@@ -31,6 +35,8 @@ class DatabaseMetricsClient extends BaseMetricsClient {
|
|
|
31
35
|
additional_database_urls = {},
|
|
32
36
|
...metricsConfig
|
|
33
37
|
} = {}) {
|
|
38
|
+
exitUnlessProcessTypeIs(metricsConfig, METRICS_PROCESS_TYPE_DATABASE)
|
|
39
|
+
|
|
34
40
|
const intervalSec =
|
|
35
41
|
metricsConfig.intervalSec ||
|
|
36
42
|
parseInt(process.env.METRICS_DATABASE_INTERVAL_SEC || '', 10) ||
|
|
@@ -78,7 +84,7 @@ class DatabaseMetricsClient extends BaseMetricsClient {
|
|
|
78
84
|
|
|
79
85
|
super({
|
|
80
86
|
...metricsConfig,
|
|
81
|
-
processType: metricsConfig.processType ||
|
|
87
|
+
processType: metricsConfig.processType || METRICS_PROCESS_TYPE_DATABASE,
|
|
82
88
|
intervalSec,
|
|
83
89
|
startupValidation,
|
|
84
90
|
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for resolving `processType` and silently exiting when a specialized metrics client
|
|
3
|
+
* is constructed on the wrong dyno / process (no log output).
|
|
4
|
+
*
|
|
5
|
+
* **Canonical names** align with typical Procfile processes (e.g. backend: `web`, `worker`,
|
|
6
|
+
* `queue-metrics`, `database-metrics`, `http-metrics`). The compile **worker** dyno serves queues/builds
|
|
7
|
+
* — it does not run HTTP request metrics. HTTP Redis **key segment** for API traffic is fixed **`web`**
|
|
8
|
+
* (`HttpMetricsRedisCollector` / `HttpMetricsRedisRecorder`), not `BUILD_DYNO_PROCESS_TYPE`.
|
|
9
|
+
*
|
|
10
|
+
* @module metrics/metricsProcessTypeUtils
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** DB-only metrics dyno (`database-metrics` in Procfile). */
|
|
14
|
+
const METRICS_PROCESS_TYPE_DATABASE = 'database-metrics'
|
|
15
|
+
|
|
16
|
+
/** Queue + Redis metrics dyno (`queue-metrics` in Procfile). */
|
|
17
|
+
const METRICS_PROCESS_TYPE_QUEUE = 'queue-metrics'
|
|
18
|
+
|
|
19
|
+
/** Redis-only metrics dyno (no Bee Queue; optional separate process). */
|
|
20
|
+
const METRICS_PROCESS_TYPE_REDIS = 'redis-metrics'
|
|
21
|
+
|
|
22
|
+
/** Web servers — HTTP traffic, HTTP Redis **writers** typically use this in Redis key segment. */
|
|
23
|
+
const METRICS_PROCESS_TYPE_WEB = 'web'
|
|
24
|
+
|
|
25
|
+
/** Build/compile workers and similar (e.g. backend `worker:`) — no HTTP server metrics here. */
|
|
26
|
+
const METRICS_PROCESS_TYPE_WORKER = 'worker'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parent {@link RedisMetricsClient} allows either redis-only or queue stack (`QueueRedisMetricsClient`).
|
|
30
|
+
* @type {readonly string[]}
|
|
31
|
+
*/
|
|
32
|
+
const REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES = Object.freeze([
|
|
33
|
+
METRICS_PROCESS_TYPE_REDIS,
|
|
34
|
+
METRICS_PROCESS_TYPE_QUEUE,
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve logical process type the same way specialized metrics clients do.
|
|
39
|
+
*
|
|
40
|
+
* @param {{ processType?: string }} metricsConfig - Remainder of constructor options (e.g. after destructuring `redisClient`, `databaseUrl`, …)
|
|
41
|
+
* @param {string} defaultProcessType - Fallback when `metricsConfig.processType` and `BUILD_DYNO_PROCESS_TYPE` are unset
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function resolveMetricsProcessType(metricsConfig, defaultProcessType) {
|
|
45
|
+
return (
|
|
46
|
+
metricsConfig.processType ||
|
|
47
|
+
process.env.BUILD_DYNO_PROCESS_TYPE ||
|
|
48
|
+
defaultProcessType
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Exit with no logs if the resolved process type is not exactly `expectedProcessType`.
|
|
54
|
+
*
|
|
55
|
+
* @param {{ processType?: string }} metricsConfig
|
|
56
|
+
* @param {string} expectedProcessType - Single allowed value (use a module constant, e.g. {@link METRICS_PROCESS_TYPE_DATABASE})
|
|
57
|
+
* @returns {void}
|
|
58
|
+
*/
|
|
59
|
+
function exitUnlessProcessTypeIs(metricsConfig, expectedProcessType) {
|
|
60
|
+
const resolved = resolveMetricsProcessType(metricsConfig, expectedProcessType)
|
|
61
|
+
if (resolved !== expectedProcessType) {
|
|
62
|
+
process.exit(0)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Exit with no logs if the resolved process type is not in `allowedProcessTypes`.
|
|
68
|
+
*
|
|
69
|
+
* @param {{ processType?: string }} metricsConfig
|
|
70
|
+
* @param {readonly string[]} allowedProcessTypes
|
|
71
|
+
* @param {string} defaultWhenUnspecified - Used only to resolve when config/env omit `processType` (e.g. {@link METRICS_PROCESS_TYPE_QUEUE} for {@link RedisMetricsClient})
|
|
72
|
+
* @returns {void}
|
|
73
|
+
*/
|
|
74
|
+
function exitUnlessProcessTypeIn(
|
|
75
|
+
metricsConfig,
|
|
76
|
+
allowedProcessTypes,
|
|
77
|
+
defaultWhenUnspecified
|
|
78
|
+
) {
|
|
79
|
+
const resolved = resolveMetricsProcessType(
|
|
80
|
+
metricsConfig,
|
|
81
|
+
defaultWhenUnspecified
|
|
82
|
+
)
|
|
83
|
+
if (!allowedProcessTypes.includes(resolved)) {
|
|
84
|
+
process.exit(0)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
resolveMetricsProcessType,
|
|
90
|
+
exitUnlessProcessTypeIs,
|
|
91
|
+
exitUnlessProcessTypeIn,
|
|
92
|
+
METRICS_PROCESS_TYPE_DATABASE,
|
|
93
|
+
METRICS_PROCESS_TYPE_QUEUE,
|
|
94
|
+
METRICS_PROCESS_TYPE_REDIS,
|
|
95
|
+
METRICS_PROCESS_TYPE_WEB,
|
|
96
|
+
METRICS_PROCESS_TYPE_WORKER,
|
|
97
|
+
REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES,
|
|
98
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const { RedisMetricsClient } = require('./metricsRedisClient')
|
|
2
2
|
const { IOREDIS, REDIS_V3, REDIS_V4 } = require('../redisUtils')
|
|
3
|
+
const {
|
|
4
|
+
exitUnlessProcessTypeIs,
|
|
5
|
+
METRICS_PROCESS_TYPE_QUEUE,
|
|
6
|
+
} = require('./metricsProcessTypeUtils')
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* QueueRedisMetricsClient extends RedisMetricsClient to collect
|
|
@@ -24,6 +28,8 @@ class QueueRedisMetricsClient extends RedisMetricsClient {
|
|
|
24
28
|
* @param {boolean} [options.disablePushgateway] - Disable pushing to Pushgateway (use HTTP scraping instead)
|
|
25
29
|
*/
|
|
26
30
|
constructor({ redisClient, ...metricsConfig } = {}) {
|
|
31
|
+
exitUnlessProcessTypeIs(metricsConfig, METRICS_PROCESS_TYPE_QUEUE)
|
|
32
|
+
|
|
27
33
|
const getConfiguredQueueNames = () => {
|
|
28
34
|
if (!process.env.METRICS_APP_REDIS_BQ) {
|
|
29
35
|
throw new Error(
|
|
@@ -5,6 +5,11 @@ const {
|
|
|
5
5
|
IOREDIS,
|
|
6
6
|
REDIS_V3,
|
|
7
7
|
} = require('../redisUtils')
|
|
8
|
+
const {
|
|
9
|
+
exitUnlessProcessTypeIn,
|
|
10
|
+
METRICS_PROCESS_TYPE_QUEUE,
|
|
11
|
+
REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES,
|
|
12
|
+
} = require('./metricsProcessTypeUtils')
|
|
8
13
|
|
|
9
14
|
const redisConnectionStableFields = ['name', 'flags', 'cmd']
|
|
10
15
|
const redisConnectionFields = ['name', 'flags', 'tot-mem', 'cmd']
|
|
@@ -36,6 +41,12 @@ class RedisMetricsClient extends BaseMetricsClient {
|
|
|
36
41
|
throw new Error('RedisMetricsClient requires redisClient')
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
exitUnlessProcessTypeIn(
|
|
45
|
+
metricsConfig,
|
|
46
|
+
REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES,
|
|
47
|
+
METRICS_PROCESS_TYPE_QUEUE
|
|
48
|
+
)
|
|
49
|
+
|
|
39
50
|
const intervalSec =
|
|
40
51
|
metricsConfig.intervalSec ||
|
|
41
52
|
parseInt(process.env.METRICS_QUEUE_INTERVAL_SEC || '', 10) ||
|
|
@@ -43,7 +54,7 @@ class RedisMetricsClient extends BaseMetricsClient {
|
|
|
43
54
|
|
|
44
55
|
super({
|
|
45
56
|
...metricsConfig,
|
|
46
|
-
processType: metricsConfig.processType ||
|
|
57
|
+
processType: metricsConfig.processType || METRICS_PROCESS_TYPE_QUEUE,
|
|
47
58
|
intervalSec,
|
|
48
59
|
})
|
|
49
60
|
|