@adalo/metrics 0.0.0-staging.20 → 0.0.0-staging.22
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 +1 -1
- package/__tests__/httpMetricsRedisCollector.test.js +74 -0
- package/__tests__/httpMetricsRedisRecorder.test.js +56 -0
- package/__tests__/httpMetricsRedisStore.test.js +182 -0
- package/lib/metrics/httpMetricsRedisCollector.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisCollector.js +1 -15
- package/lib/metrics/httpMetricsRedisCollector.js.map +1 -1
- package/lib/metrics/httpMetricsRedisStore.d.ts +3 -0
- package/lib/metrics/httpMetricsRedisStore.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisStore.js +3 -30
- package/lib/metrics/httpMetricsRedisStore.js.map +1 -1
- package/lib/metrics/metricsProcessTypeUtils.d.ts +3 -2
- package/lib/metrics/metricsProcessTypeUtils.d.ts.map +1 -1
- package/lib/metrics/metricsProcessTypeUtils.js +3 -2
- package/lib/metrics/metricsProcessTypeUtils.js.map +1 -1
- package/package.json +1 -1
- package/src/metrics/httpMetricsRedisCollector.js +1 -19
- package/src/metrics/httpMetricsRedisStore.js +3 -63
- package/src/metrics/metricsProcessTypeUtils.js +3 -2
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ new MetricsClient({
|
|
|
31
31
|
|
|
32
32
|
### HTTP metrics: `MetricsClient` vs Redis
|
|
33
33
|
|
|
34
|
-
**`MetricsClient`** only registers **in-process** `app_requests_*` counters when `METRICS_HTTP_ENABLED` / `httpMetricsEnabled` is on. For **Redis-backed** HTTP aggregation (multi-web / cluster), use **`HttpMetricsRedisRecorder`** (writers) and **`HttpMetricsRedisCollector`** (drain + push) with an injected **`redisClient`** — same idea as `RedisMetricsClient`, not mixed into `MetricsClient`.
|
|
34
|
+
**`MetricsClient`** only registers **in-process** `app_requests_*` counters when `METRICS_HTTP_ENABLED` / `httpMetricsEnabled` is on. For **Redis-backed** HTTP aggregation (multi-web / cluster), use **`HttpMetricsRedisRecorder`** (writers) and **`HttpMetricsRedisCollector`** (drain + push) with an injected **`redisClient`** — same idea as `RedisMetricsClient`, not mixed into `MetricsClient`. See **`docs/http-metrics-redis.md`** for key segments and what is stored in Redis.
|
|
35
35
|
|
|
36
36
|
`DatabaseMetricsClient` / `QueueRedisMetricsClient` / `RedisMetricsClient` call **`process.exit(0)` with no logs** if `BUILD_DYNO_PROCESS_TYPE` (or `processType`) does not match `database-metrics` / `queue-metrics` / (`redis-metrics` or `queue-metrics`) respectively.
|
|
37
37
|
## Example Usage
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const { HttpMetricsRedisCollector } = require('../src/metrics/httpMetricsRedisCollector')
|
|
2
|
+
const { buildFieldKey } = require('../src/metrics/httpMetricsRedisStore')
|
|
3
|
+
|
|
4
|
+
function createRedisV3Mock() {
|
|
5
|
+
const multiChain = {
|
|
6
|
+
hincrby: jest.fn().mockReturnThis(),
|
|
7
|
+
expire: jest.fn().mockReturnThis(),
|
|
8
|
+
exec: jest.fn(cb => {
|
|
9
|
+
if (cb) {
|
|
10
|
+
setImmediate(() => cb(null))
|
|
11
|
+
}
|
|
12
|
+
}),
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
multi: jest.fn(() => multiChain),
|
|
16
|
+
set: jest.fn(),
|
|
17
|
+
eval: jest.fn(),
|
|
18
|
+
del: jest.fn(),
|
|
19
|
+
_multiChain: multiChain,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('HttpMetricsRedisCollector', () => {
|
|
24
|
+
const originalEnv = process.env
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
process.env = { ...originalEnv, METRICS_DISABLE_PUSHGATEWAY: 'true' }
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
process.env = originalEnv
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('throws without redisClient', () => {
|
|
35
|
+
expect(() => new HttpMetricsRedisCollector({ appName: 'a' })).toThrow('redisClient is required')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('pushMetrics drains Redis then completes without network push', async () => {
|
|
39
|
+
const redis = createRedisV3Mock()
|
|
40
|
+
redis.set.mockImplementation((key, val, mode, ttl, nx, cb) => {
|
|
41
|
+
cb(null, 'OK')
|
|
42
|
+
})
|
|
43
|
+
const field = buildFieldKey('GET', '/health', 200, '', '')
|
|
44
|
+
redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
|
|
45
|
+
cb(null, [
|
|
46
|
+
[field, '1'],
|
|
47
|
+
[field, '10'],
|
|
48
|
+
])
|
|
49
|
+
})
|
|
50
|
+
redis.del.mockImplementation((key, cb) => {
|
|
51
|
+
if (cb) {
|
|
52
|
+
cb()
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const collector = new HttpMetricsRedisCollector({
|
|
57
|
+
redisClient: redis,
|
|
58
|
+
appName: 'test-app',
|
|
59
|
+
dynoId: 'dyno-1',
|
|
60
|
+
processType: 'http-metrics',
|
|
61
|
+
redisProcessTypeForKeys: 'web',
|
|
62
|
+
disablePushgateway: true,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
await collector.pushMetrics()
|
|
66
|
+
|
|
67
|
+
expect(redis.eval).toHaveBeenCalled()
|
|
68
|
+
const metrics = await collector._registry.getMetricsAsJSON()
|
|
69
|
+
const total = metrics.find(m => m.name === 'app_requests_total')
|
|
70
|
+
expect(total).toBeDefined()
|
|
71
|
+
const totalDur = metrics.find(m => m.name === 'app_requests_total_duration')
|
|
72
|
+
expect(totalDur).toBeDefined()
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const { HttpMetricsRedisRecorder } = require('../src/metrics/httpMetricsRedisRecorder')
|
|
2
|
+
const { buildFieldKey } = require('../src/metrics/httpMetricsRedisStore')
|
|
3
|
+
|
|
4
|
+
const flushMicrotasks = () => new Promise(resolve => setImmediate(resolve))
|
|
5
|
+
|
|
6
|
+
function createRedisV3Mock() {
|
|
7
|
+
const multiChain = {
|
|
8
|
+
hincrby: jest.fn().mockReturnThis(),
|
|
9
|
+
expire: jest.fn().mockReturnThis(),
|
|
10
|
+
exec: jest.fn(cb => {
|
|
11
|
+
if (cb) {
|
|
12
|
+
setImmediate(() => cb(null))
|
|
13
|
+
}
|
|
14
|
+
}),
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
multi: jest.fn(() => multiChain),
|
|
18
|
+
set: jest.fn(),
|
|
19
|
+
eval: jest.fn(),
|
|
20
|
+
del: jest.fn(),
|
|
21
|
+
_multiChain: multiChain,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('HttpMetricsRedisRecorder', () => {
|
|
26
|
+
it('throws without redisClient', () => {
|
|
27
|
+
expect(() => new HttpMetricsRedisRecorder({ appName: 'a', processType: 'web' })).toThrow(
|
|
28
|
+
'redisClient is required'
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('trackHttpRequest forwards to Redis multi/hincrby', async () => {
|
|
33
|
+
const redis = createRedisV3Mock()
|
|
34
|
+
const rec = new HttpMetricsRedisRecorder({
|
|
35
|
+
redisClient: redis,
|
|
36
|
+
appName: 'app',
|
|
37
|
+
processType: 'web',
|
|
38
|
+
})
|
|
39
|
+
const countKey = rec._store.countKey
|
|
40
|
+
const durKey = rec._store.durKey
|
|
41
|
+
const field = buildFieldKey('GET', '/p', 404, 'x', 'y')
|
|
42
|
+
|
|
43
|
+
rec.trackHttpRequest({
|
|
44
|
+
method: 'GET',
|
|
45
|
+
route: '/p',
|
|
46
|
+
status_code: 404,
|
|
47
|
+
appId: 'x',
|
|
48
|
+
databaseId: 'y',
|
|
49
|
+
duration: 5,
|
|
50
|
+
})
|
|
51
|
+
await flushMicrotasks()
|
|
52
|
+
|
|
53
|
+
expect(redis._multiChain.hincrby).toHaveBeenCalledWith(countKey, field, 1)
|
|
54
|
+
expect(redis._multiChain.hincrby).toHaveBeenCalledWith(durKey, field, 5)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const {
|
|
2
|
+
HttpMetricsRedisStore,
|
|
3
|
+
buildFieldKey,
|
|
4
|
+
FIELD_SEP,
|
|
5
|
+
} = require('../src/metrics/httpMetricsRedisStore')
|
|
6
|
+
|
|
7
|
+
const flushMicrotasks = () => new Promise(resolve => setImmediate(resolve))
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* redis@3-style mock: multi().hincrby().expire().exec(cb)
|
|
11
|
+
*/
|
|
12
|
+
function createRedisV3Mock() {
|
|
13
|
+
const multiChain = {
|
|
14
|
+
hincrby: jest.fn().mockReturnThis(),
|
|
15
|
+
expire: jest.fn().mockReturnThis(),
|
|
16
|
+
exec: jest.fn(cb => {
|
|
17
|
+
if (cb) {
|
|
18
|
+
setImmediate(() => cb(null))
|
|
19
|
+
}
|
|
20
|
+
}),
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
multi: jest.fn(() => multiChain),
|
|
24
|
+
set: jest.fn(),
|
|
25
|
+
eval: jest.fn(),
|
|
26
|
+
del: jest.fn(),
|
|
27
|
+
_multiChain: multiChain,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('HttpMetricsRedisStore', () => {
|
|
32
|
+
describe('buildFieldKey', () => {
|
|
33
|
+
it('joins parts with FIELD_SEP', () => {
|
|
34
|
+
expect(buildFieldKey('GET', '/api', 200, 'app1', 'db1')).toBe(
|
|
35
|
+
['GET', '/api', '200', 'app1', 'db1'].join(FIELD_SEP)
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('constructor', () => {
|
|
41
|
+
it('throws without redisClient', () => {
|
|
42
|
+
expect(() => new HttpMetricsRedisStore({ appName: 'a', processType: 'web' })).toThrow(
|
|
43
|
+
'redisClient is required'
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('builds encoded v2 keys', () => {
|
|
48
|
+
const redis = createRedisV3Mock()
|
|
49
|
+
const store = new HttpMetricsRedisStore({
|
|
50
|
+
redisClient: redis,
|
|
51
|
+
appName: 'my app',
|
|
52
|
+
processType: 'web',
|
|
53
|
+
})
|
|
54
|
+
const seg = `${encodeURIComponent('my app')}:${encodeURIComponent('web')}`
|
|
55
|
+
expect(store.countKey).toBe(`metrics:http:v2:${seg}:count`)
|
|
56
|
+
expect(store.durKey).toBe(`metrics:http:v2:${seg}:dur`)
|
|
57
|
+
expect(store.lockKey).toBe(`metrics:http:v2:${seg}:drain_lock`)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('record', () => {
|
|
62
|
+
it('increments count and duration hashes with TTL', async () => {
|
|
63
|
+
const redis = createRedisV3Mock()
|
|
64
|
+
const store = new HttpMetricsRedisStore({
|
|
65
|
+
redisClient: redis,
|
|
66
|
+
appName: 'app',
|
|
67
|
+
processType: 'web',
|
|
68
|
+
ttlSec: 90,
|
|
69
|
+
})
|
|
70
|
+
const field = buildFieldKey('GET', '/x', 200, '', '')
|
|
71
|
+
|
|
72
|
+
store.record('GET', '/x', 200, '', '', 12)
|
|
73
|
+
await flushMicrotasks()
|
|
74
|
+
|
|
75
|
+
expect(redis.multi).toHaveBeenCalled()
|
|
76
|
+
expect(redis._multiChain.hincrby).toHaveBeenCalledWith(store.countKey, field, 1)
|
|
77
|
+
expect(redis._multiChain.hincrby).toHaveBeenCalledWith(store.durKey, field, 12)
|
|
78
|
+
expect(redis._multiChain.expire).toHaveBeenCalledWith(store.countKey, 90)
|
|
79
|
+
expect(redis._multiChain.expire).toHaveBeenCalledWith(store.durKey, 90)
|
|
80
|
+
expect(redis._multiChain.exec).toHaveBeenCalled()
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('flushToCounters', () => {
|
|
85
|
+
it('returns false when lock is not acquired', async () => {
|
|
86
|
+
const redis = createRedisV3Mock()
|
|
87
|
+
redis.set.mockImplementation((key, val, mode, ttl, nx, cb) => {
|
|
88
|
+
cb(null, null)
|
|
89
|
+
})
|
|
90
|
+
const store = new HttpMetricsRedisStore({
|
|
91
|
+
redisClient: redis,
|
|
92
|
+
appName: 'app',
|
|
93
|
+
processType: 'web',
|
|
94
|
+
})
|
|
95
|
+
const ok = await store.flushToCounters(jest.fn(), jest.fn())
|
|
96
|
+
expect(ok).toBe(false)
|
|
97
|
+
expect(redis.eval).not.toHaveBeenCalled()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('drains hashes and applies count and duration', async () => {
|
|
101
|
+
const redis = createRedisV3Mock()
|
|
102
|
+
redis.set.mockImplementation((key, val, mode, ttl, nx, cb) => {
|
|
103
|
+
cb(null, 'OK')
|
|
104
|
+
})
|
|
105
|
+
const field = buildFieldKey('POST', '/r', 201, 'a1', 'd1')
|
|
106
|
+
redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
|
|
107
|
+
cb(null, [
|
|
108
|
+
[field, '2'],
|
|
109
|
+
[field, '50'],
|
|
110
|
+
])
|
|
111
|
+
})
|
|
112
|
+
redis.del.mockImplementation((key, cb) => {
|
|
113
|
+
if (cb) {
|
|
114
|
+
cb()
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const store = new HttpMetricsRedisStore({
|
|
119
|
+
redisClient: redis,
|
|
120
|
+
appName: 'app',
|
|
121
|
+
processType: 'web',
|
|
122
|
+
})
|
|
123
|
+
const applyCount = jest.fn()
|
|
124
|
+
const applyDuration = jest.fn()
|
|
125
|
+
|
|
126
|
+
const ok = await store.flushToCounters(applyCount, applyDuration)
|
|
127
|
+
|
|
128
|
+
expect(ok).toBe(true)
|
|
129
|
+
expect(redis.eval).toHaveBeenCalledWith(
|
|
130
|
+
expect.stringContaining('HGETALL'),
|
|
131
|
+
2,
|
|
132
|
+
store.countKey,
|
|
133
|
+
store.durKey,
|
|
134
|
+
expect.any(Function)
|
|
135
|
+
)
|
|
136
|
+
expect(applyCount).toHaveBeenCalledWith(
|
|
137
|
+
{
|
|
138
|
+
method: 'POST',
|
|
139
|
+
route: '/r',
|
|
140
|
+
status_code: '201',
|
|
141
|
+
appId: 'a1',
|
|
142
|
+
databaseId: 'd1',
|
|
143
|
+
},
|
|
144
|
+
2
|
|
145
|
+
)
|
|
146
|
+
expect(applyDuration).toHaveBeenCalledWith(
|
|
147
|
+
{
|
|
148
|
+
method: 'POST',
|
|
149
|
+
route: '/r',
|
|
150
|
+
status_code: '201',
|
|
151
|
+
appId: 'a1',
|
|
152
|
+
databaseId: 'd1',
|
|
153
|
+
},
|
|
154
|
+
50
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('resolves true with no applies when eval returns short array', async () => {
|
|
159
|
+
const redis = createRedisV3Mock()
|
|
160
|
+
redis.set.mockImplementation((key, val, mode, ttl, nx, cb) => {
|
|
161
|
+
cb(null, 'OK')
|
|
162
|
+
})
|
|
163
|
+
redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
|
|
164
|
+
cb(null, [])
|
|
165
|
+
})
|
|
166
|
+
redis.del.mockImplementation((key, cb) => {
|
|
167
|
+
if (cb) {
|
|
168
|
+
cb()
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
const store = new HttpMetricsRedisStore({
|
|
172
|
+
redisClient: redis,
|
|
173
|
+
appName: 'app',
|
|
174
|
+
processType: 'web',
|
|
175
|
+
})
|
|
176
|
+
const applyCount = jest.fn()
|
|
177
|
+
const ok = await store.flushToCounters(applyCount, jest.fn())
|
|
178
|
+
expect(ok).toBe(true)
|
|
179
|
+
expect(applyCount).not.toHaveBeenCalled()
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisCollector.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisCollector.js"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH;IACE;;;;;;;;;;;;;;;;OAgBG;IACH;qBAfW,OAAO,OAAO,EAAE,WAAW;;;;;;;;;;;;;;mBAoErC;IAhCC,8BAKE;
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisCollector.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisCollector.js"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH;IACE;;;;;;;;;;;;;;;;OAgBG;IACH;qBAfW,OAAO,OAAO,EAAE,WAAW;;;;;;;;;;;;;;mBAoErC;IAhCC,8BAKE;CAgDL"}
|
|
@@ -75,21 +75,7 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
75
75
|
*/
|
|
76
76
|
pushMetrics = async () => {
|
|
77
77
|
if (this._store && this.countersFunctions?.app_requests_total && this.countersFunctions?.app_requests_total_duration) {
|
|
78
|
-
|
|
79
|
-
pid
|
|
80
|
-
} = process;
|
|
81
|
-
const t0 = Date.now();
|
|
82
|
-
// --- TEMP_HTTP_METRICS_DIAG: remove when done debugging ---
|
|
83
|
-
console.warn(`[TEMP_HTTP_METRICS_DIAG] http_collector_flush_begin pid=${pid} dyno_id=${this.dynoId} process_type=${this.processType} app=${this.appName}`);
|
|
84
|
-
// --- end TEMP_HTTP_METRICS_DIAG ---
|
|
85
|
-
const drainedOk = await this._store.flushToCounters((labels, value) => this.countersFunctions.app_requests_total(labels, value), (labels, value) => this.countersFunctions.app_requests_total_duration(labels, value));
|
|
86
|
-
// --- TEMP_HTTP_METRICS_DIAG: remove when done debugging ---
|
|
87
|
-
console.warn(`[TEMP_HTTP_METRICS_DIAG] http_collector_flush_end pid=${pid} lock_acquired_and_ran_drain=${drainedOk} elapsed_ms=${Date.now() - t0}`);
|
|
88
|
-
// --- end TEMP_HTTP_METRICS_DIAG ---
|
|
89
|
-
} else {
|
|
90
|
-
// --- TEMP_HTTP_METRICS_DIAG: remove when done debugging ---
|
|
91
|
-
console.warn(`[TEMP_HTTP_METRICS_DIAG] http_collector_flush_skipped pid=${process.pid} reason=missing_store_or_http_counters`);
|
|
92
|
-
// --- end TEMP_HTTP_METRICS_DIAG ---
|
|
78
|
+
await this._store.flushToCounters((labels, value) => this.countersFunctions.app_requests_total(labels, value), (labels, value) => this.countersFunctions.app_requests_total_duration(labels, value));
|
|
93
79
|
}
|
|
94
80
|
return this._pushMetrics();
|
|
95
81
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisCollector.js","names":["BaseMetricsClient","require","HttpMetricsRedisStore","HttpMetricsRedisCollector","constructor","config","redisClient","Error","blockNodeDefaultMetrics","keyProcessType","redisProcessTypeForKeys","process","env","METRICS_HTTP_REDIS_KEY_PROCESS_TYPE","defaultLabelsWithoutDynoId","app","appName","process_type","_store","processType","ttlSec","createCounter","name","help","labelNames","withDefaultLabelsWithoutDynoId","useLabelsWithoutDynoId","pushMetrics","countersFunctions","app_requests_total","app_requests_total_duration","
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisCollector.js","names":["BaseMetricsClient","require","HttpMetricsRedisStore","HttpMetricsRedisCollector","constructor","config","redisClient","Error","blockNodeDefaultMetrics","keyProcessType","redisProcessTypeForKeys","process","env","METRICS_HTTP_REDIS_KEY_PROCESS_TYPE","defaultLabelsWithoutDynoId","app","appName","process_type","_store","processType","ttlSec","createCounter","name","help","labelNames","withDefaultLabelsWithoutDynoId","useLabelsWithoutDynoId","pushMetrics","countersFunctions","app_requests_total","app_requests_total_duration","flushToCounters","labels","value","_pushMetrics","module","exports"],"sources":["../../src/metrics/httpMetricsRedisCollector.js"],"sourcesContent":["const { BaseMetricsClient } = require('./baseMetricsClient')\nconst { HttpMetricsRedisStore } = require('./httpMetricsRedisStore')\n\n/**\n * Drain worker: reads HTTP aggregates from Redis (written by {@link HttpMetricsRedisRecorder}),\n * applies them to `app_requests_*` counters, then pushes the registry to the VM-agent (same as {@link BaseMetricsClient}).\n * Always passes `blockNodeDefaultMetrics: true` to the base client (HTTP-only registry; no default Node heap/event-loop metrics).\n * Redis key segment must match writers (`appName` + `processType` / {@link HttpMetricsRedisStore}).\n *\n * @extends BaseMetricsClient\n */\nclass HttpMetricsRedisCollector extends BaseMetricsClient {\n /**\n * @param {Object} [config]\n * @param {import('redis').RedisClient} config.redisClient **Required.** Injected client (same pattern as {@link RedisMetricsClient}).\n * @param {string} [config.appName] Application name (defaults per {@link BaseMetricsClient})\n * @param {string} [config.dynoId] Dyno/instance ID\n * @param {string} [config.processType] Label `process_type` on push (default from env / base)\n * @param {boolean} [config.enabled] Enable collection and push\n * @param {boolean} [config.logValues] Log metric JSON to console\n * @param {string} [config.pushgatewayUrl] VM-agent import URL\n * @param {string} [config.pushgatewaySecret] Basic auth secret (Base64)\n * @param {number} [config.intervalSec] Push interval (seconds)\n * @param {boolean} [config.removeOldMetrics] Clear old series on shutdown where supported\n * @param {function} [config.startupValidation] Run before first push\n * @param {boolean} [config.disablePushgateway] Skip POST to VM-agent\n * @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default `web`; env `METRICS_HTTP_REDIS_KEY_PROCESS_TYPE` overrides)\n * @param {number} [config.ttlSec] Passed to {@link HttpMetricsRedisStore} (should match writers)\n */\n constructor(config = {}) {\n const { redisClient } = config\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisCollector: redisClient is required')\n }\n\n super({\n ...config,\n blockNodeDefaultMetrics: true,\n })\n\n const keyProcessType =\n config.redisProcessTypeForKeys ||\n process.env.METRICS_HTTP_REDIS_KEY_PROCESS_TYPE ||\n 'web'\n\n this.defaultLabelsWithoutDynoId = {\n app: this.appName,\n process_type: keyProcessType,\n }\n\n this._store = new HttpMetricsRedisStore({\n redisClient,\n appName: this.appName,\n processType: keyProcessType,\n ttlSec: config.ttlSec,\n })\n\n this.createCounter({\n name: 'app_requests_total',\n help: 'Total number of HTTP requests',\n labelNames: this.withDefaultLabelsWithoutDynoId([\n 'method',\n 'route',\n 'appId',\n 'databaseId',\n 'status_code',\n ]),\n useLabelsWithoutDynoId: true,\n })\n\n this.createCounter({\n name: 'app_requests_total_duration',\n help: 'Total duration of HTTP requests in milliseconds',\n labelNames: this.withDefaultLabelsWithoutDynoId([\n 'method',\n 'route',\n 'appId',\n 'databaseId',\n 'status_code',\n ]),\n useLabelsWithoutDynoId: true,\n })\n }\n\n /**\n * Drains Redis into counters, then runs gauge updates and VM-agent push ({@link BaseMetricsClient#_pushMetrics}).\n * @returns {Promise<void>}\n */\n pushMetrics = async () => {\n if (\n this._store &&\n this.countersFunctions?.app_requests_total &&\n this.countersFunctions?.app_requests_total_duration\n ) {\n await this._store.flushToCounters(\n (labels, value) =>\n this.countersFunctions.app_requests_total(labels, value),\n (labels, value) =>\n this.countersFunctions.app_requests_total_duration(labels, value)\n )\n }\n return this._pushMetrics()\n }\n}\n\nmodule.exports = { HttpMetricsRedisCollector }\n"],"mappings":";;AAAA,MAAM;EAAEA;AAAkB,CAAC,GAAGC,OAAO,CAAC,qBAAqB,CAAC;AAC5D,MAAM;EAAEC;AAAsB,CAAC,GAAGD,OAAO,CAAC,yBAAyB,CAAC;;AAEpE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAME,yBAAyB,SAASH,iBAAiB,CAAC;EACxD;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACEI,WAAWA,CAACC,MAAM,GAAG,CAAC,CAAC,EAAE;IACvB,MAAM;MAAEC;IAAY,CAAC,GAAGD,MAAM;IAC9B,IAAIC,WAAW,IAAI,IAAI,EAAE;MACvB,MAAM,IAAIC,KAAK,CAAC,oDAAoD,CAAC;IACvE;IAEA,KAAK,CAAC;MACJ,GAAGF,MAAM;MACTG,uBAAuB,EAAE;IAC3B,CAAC,CAAC;IAEF,MAAMC,cAAc,GAClBJ,MAAM,CAACK,uBAAuB,IAC9BC,OAAO,CAACC,GAAG,CAACC,mCAAmC,IAC/C,KAAK;IAEP,IAAI,CAACC,0BAA0B,GAAG;MAChCC,GAAG,EAAE,IAAI,CAACC,OAAO;MACjBC,YAAY,EAAER;IAChB,CAAC;IAED,IAAI,CAACS,MAAM,GAAG,IAAIhB,qBAAqB,CAAC;MACtCI,WAAW;MACXU,OAAO,EAAE,IAAI,CAACA,OAAO;MACrBG,WAAW,EAAEV,cAAc;MAC3BW,MAAM,EAAEf,MAAM,CAACe;IACjB,CAAC,CAAC;IAEF,IAAI,CAACC,aAAa,CAAC;MACjBC,IAAI,EAAE,oBAAoB;MAC1BC,IAAI,EAAE,+BAA+B;MACrCC,UAAU,EAAE,IAAI,CAACC,8BAA8B,CAAC,CAC9C,QAAQ,EACR,OAAO,EACP,OAAO,EACP,YAAY,EACZ,aAAa,CACd,CAAC;MACFC,sBAAsB,EAAE;IAC1B,CAAC,CAAC;IAEF,IAAI,CAACL,aAAa,CAAC;MACjBC,IAAI,EAAE,6BAA6B;MACnCC,IAAI,EAAE,iDAAiD;MACvDC,UAAU,EAAE,IAAI,CAACC,8BAA8B,CAAC,CAC9C,QAAQ,EACR,OAAO,EACP,OAAO,EACP,YAAY,EACZ,aAAa,CACd,CAAC;MACFC,sBAAsB,EAAE;IAC1B,CAAC,CAAC;EACJ;;EAEA;AACF;AACA;AACA;EACEC,WAAW,GAAG,MAAAA,CAAA,KAAY;IACxB,IACE,IAAI,CAACT,MAAM,IACX,IAAI,CAACU,iBAAiB,EAAEC,kBAAkB,IAC1C,IAAI,CAACD,iBAAiB,EAAEE,2BAA2B,EACnD;MACA,MAAM,IAAI,CAACZ,MAAM,CAACa,eAAe,CAC/B,CAACC,MAAM,EAAEC,KAAK,KACZ,IAAI,CAACL,iBAAiB,CAACC,kBAAkB,CAACG,MAAM,EAAEC,KAAK,CAAC,EAC1D,CAACD,MAAM,EAAEC,KAAK,KACZ,IAAI,CAACL,iBAAiB,CAACE,2BAA2B,CAACE,MAAM,EAAEC,KAAK,CACpE,CAAC;IACH;IACA,OAAO,IAAI,CAACC,YAAY,CAAC,CAAC;EAC5B,CAAC;AACH;AAEAC,MAAM,CAACC,OAAO,GAAG;EAAEjC;AAA0B,CAAC","ignoreList":[]}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).
|
|
3
3
|
* Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`, `set`, `del`.
|
|
4
|
+
*
|
|
5
|
+
* **Stored metrics:** two hashes per app/segment — `:count` (HINCRBY per route group) and `:dur`
|
|
6
|
+
* (sum of duration ms per same field). Hash field = `method\\x1eroute\\x1estatus\\x1eappId\\x1edatabaseId`.
|
|
4
7
|
*/
|
|
5
8
|
export class HttpMetricsRedisStore {
|
|
6
9
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisStore.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisStore.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisStore.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisStore.js"],"names":[],"mappings":"AAwDA;;;;;;GAMG;AACH;IACE;;;;;;OAMG;IACH;QAL6C,WAAW,EAA7C,OAAO,OAAO,EAAE,WAAW;QACd,OAAO,EAApB,MAAM;QACO,WAAW,EAAxB,MAAM;QACQ,MAAM;OAiB9B;IAXC,qCAA0B;IAC1B,eAGwC;IAIxC,iBAAiD;IACjD,eAA6C;IAC7C,gBAAqD;IAGvD;;;OAGG;IACH,sBAEC;IAED;;;;;;;OAOG;IACH,eAPW,MAAM,SACN,MAAM,cACN,MAAM,SACN,MAAM,cACN,MAAM,cACN,MAAM,QAqBhB;IAED;;;;OAIG;IACH,qCAJoB,MAAM,SAAS,MAAM,KAAK,IAAI,0BAC9B,MAAM,SAAS,MAAM,KAAK,IAAI,GACrC,QAAQ,OAAO,CAAC,CA6E5B;CACF;AA7KD;;;;;;;GAOG;AACH,sCAPW,MAAM,SACN,MAAM,cACN,MAAM,SACN,MAAM,cACN,MAAM,GACJ,MAAM,CAIlB;AA3CD;;;GAGG;AACH,wBAFU,MAAM,CAEQ;AAiBxB;;GAEG;AACH,wCAFa,OAAO,CASnB;AAzBD;;;GAGG;AACH,iDAFU,MAAM,CAE8B"}
|
|
@@ -54,18 +54,12 @@ function hgetallPairsToObject(pairs) {
|
|
|
54
54
|
return o;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/** --- TEMP_HTTP_METRICS_DIAG: delete this helper and all calls when done debugging --- */
|
|
58
|
-
function tempHttpDiagLog(message) {
|
|
59
|
-
console.warn(`[TEMP_HTTP_METRICS_DIAG] ${message}`);
|
|
60
|
-
}
|
|
61
|
-
function truncateForDiag(s, maxLen = 220) {
|
|
62
|
-
const t = String(s);
|
|
63
|
-
return t.length > maxLen ? `${t.slice(0, maxLen - 3)}...` : t;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
57
|
/**
|
|
67
58
|
* Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).
|
|
68
59
|
* Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`, `set`, `del`.
|
|
60
|
+
*
|
|
61
|
+
* **Stored metrics:** two hashes per app/segment — `:count` (HINCRBY per route group) and `:dur`
|
|
62
|
+
* (sum of duration ms per same field). Hash field = `method\\x1eroute\\x1estatus\\x1eappId\\x1edatabaseId`.
|
|
69
63
|
*/
|
|
70
64
|
class HttpMetricsRedisStore {
|
|
71
65
|
/**
|
|
@@ -113,7 +107,6 @@ class HttpMetricsRedisStore {
|
|
|
113
107
|
const client = this._ensureClient();
|
|
114
108
|
const field = buildFieldKey(method, route, statusCode, appId, databaseId);
|
|
115
109
|
const dur = Math.max(0, Math.round(Number(durationMs) || 0));
|
|
116
|
-
tempHttpDiagLog(`http_redis_write_prepare pid=${process.pid} countKey=${this.countKey} durKey=${this.durKey} hincr_field=${truncateForDiag(field)} durationMs=${dur}`);
|
|
117
110
|
client.multi().hincrby(this.countKey, field, 1).hincrby(this.durKey, field, dur).expire(this.countKey, this.ttlSec).expire(this.durKey, this.ttlSec).exec(err => {
|
|
118
111
|
if (err) {
|
|
119
112
|
console.error('[HttpMetricsRedisStore] record failed:', err.message);
|
|
@@ -140,7 +133,6 @@ class HttpMetricsRedisStore {
|
|
|
140
133
|
return new Promise(resolve => {
|
|
141
134
|
client.set(this.lockKey, '1', 'EX', 25, 'NX', (setErr, ok) => {
|
|
142
135
|
if (setErr || ok !== 'OK') {
|
|
143
|
-
tempHttpDiagLog(`http_redis_drain_skip pid=${process.pid} countKey=${this.countKey} reason=${setErr ? setErr.message : 'lock_not_held'}`);
|
|
144
136
|
resolve(false);
|
|
145
137
|
return;
|
|
146
138
|
}
|
|
@@ -150,22 +142,17 @@ class HttpMetricsRedisStore {
|
|
|
150
142
|
};
|
|
151
143
|
if (evalErr) {
|
|
152
144
|
console.error('[HttpMetricsRedisStore] drain failed:', evalErr.message);
|
|
153
|
-
tempHttpDiagLog(`http_redis_drain_got_error pid=${process.pid} message=${evalErr.message}`);
|
|
154
145
|
finish();
|
|
155
146
|
return;
|
|
156
147
|
}
|
|
157
148
|
try {
|
|
158
149
|
if (!raw || !Array.isArray(raw) || raw.length < 2) {
|
|
159
|
-
tempHttpDiagLog(`http_redis_drain_collected pid=${process.pid} countKey=${this.countKey} hash_fields=0 sum_request_counts=0 (empty — keys deleted or no data)`);
|
|
160
150
|
finish();
|
|
161
151
|
return;
|
|
162
152
|
}
|
|
163
153
|
const counts = hgetallPairsToObject(raw[0]);
|
|
164
154
|
const durs = hgetallPairsToObject(raw[1]);
|
|
165
155
|
const fieldKeys = Object.keys(counts);
|
|
166
|
-
let totalUnits = 0;
|
|
167
|
-
let appliedGroups = 0;
|
|
168
|
-
const samples = [];
|
|
169
156
|
for (const field of fieldKeys) {
|
|
170
157
|
const count = parseInt(counts[field], 10);
|
|
171
158
|
if (!count || count < 1) {
|
|
@@ -176,12 +163,7 @@ class HttpMetricsRedisStore {
|
|
|
176
163
|
if (parts.length !== 5) {
|
|
177
164
|
continue;
|
|
178
165
|
}
|
|
179
|
-
totalUnits += count;
|
|
180
|
-
appliedGroups += 1;
|
|
181
166
|
const [m, route, statusStr, aid, did] = parts;
|
|
182
|
-
if (samples.length < 4) {
|
|
183
|
-
samples.push(`${m} ${route} ${statusStr} x${count}`);
|
|
184
|
-
}
|
|
185
167
|
const labels = {
|
|
186
168
|
method: m,
|
|
187
169
|
route,
|
|
@@ -194,17 +176,8 @@ class HttpMetricsRedisStore {
|
|
|
194
176
|
applyDuration(labels, dur);
|
|
195
177
|
}
|
|
196
178
|
}
|
|
197
|
-
let emptyHint = 'ok';
|
|
198
|
-
if (fieldKeys.length === 0) {
|
|
199
|
-
emptyHint = 'no_hash_entries';
|
|
200
|
-
} else if (totalUnits === 0) {
|
|
201
|
-
emptyHint = 'entries_present_but_zero_counts';
|
|
202
|
-
}
|
|
203
|
-
const noTrafficHint = fieldKeys.length === 0 ? ' hint=web_must_use_HttpMetricsRedisRecorder_same_redis_APP_web_segment' : '';
|
|
204
|
-
tempHttpDiagLog(truncateForDiag(`http_redis_drain_collected pid=${process.pid} countKey=${this.countKey} durKey=${this.durKey} hash_fields_read=${fieldKeys.length} label_groups_applied=${appliedGroups} sum_request_counts=${totalUnits} state=${emptyHint} sample=${samples.join(' | ') || '—'}${noTrafficHint}`, 900));
|
|
205
179
|
} catch (e) {
|
|
206
180
|
console.error('[HttpMetricsRedisStore] flush apply failed:', e.message);
|
|
207
|
-
tempHttpDiagLog(`http_redis_drain_apply_failed pid=${process.pid} ${e.message}`);
|
|
208
181
|
}
|
|
209
182
|
finish();
|
|
210
183
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisStore.js","names":["FIELD_SEP","DEFAULT_HTTP_METRICS_REDIS_TTL_SEC","DRAIN_LUA","isRedisPeerInstalled","require","resolve","buildFieldKey","method","route","statusCode","appId","databaseId","String","join","hgetallPairsToObject","pairs","o","length","i","tempHttpDiagLog","message","console","warn","truncateForDiag","s","maxLen","t","slice","HttpMetricsRedisStore","constructor","redisClient","appName","processType","ttlSec","Error","_client","keySeg","encodeURIComponent","countKey","durKey","lockKey","_ensureClient","record","durationMs","client","field","dur","Math","max","round","Number","process","pid","multi","hincrby","expire","exec","err","error","e","flushToCounters","applyCount","applyDuration","Promise","set","setErr","ok","eval","evalErr","raw","finish","del","Array","isArray","counts","durs","fieldKeys","Object","keys","totalUnits","appliedGroups","samples","count","parseInt","parts","split","m","statusStr","aid","did","push","labels","status_code","emptyHint","noTrafficHint","module","exports"],"sources":["../../src/metrics/httpMetricsRedisStore.js"],"sourcesContent":["/**\n * Record separator for hash fields (avoids collisions when route contains \"|\").\n * @type {string}\n */\nconst FIELD_SEP = '\\x1e'\n\n/**\n * Default Redis key TTL in seconds (sliding: refreshed on each `record` write).\n * @type {number}\n */\nconst DEFAULT_HTTP_METRICS_REDIS_TTL_SEC = 120\n\nconst DRAIN_LUA = `\nlocal function drain(key)\n local v = redis.call('HGETALL', key)\n redis.call('DEL', key)\n return v\nend\nreturn {drain(KEYS[1]), drain(KEYS[2])}\n`\n\n/**\n * @returns {boolean} Whether the `redis` npm package is resolvable (optional peer).\n */\nfunction isRedisPeerInstalled() {\n try {\n require.resolve('redis')\n return true\n } catch {\n return false\n }\n}\n\n/**\n * @param {string} method\n * @param {string} route\n * @param {number} statusCode\n * @param {string} appId\n * @param {string} databaseId\n * @returns {string}\n */\nfunction buildFieldKey(method, route, statusCode, appId, databaseId) {\n return [method, route, String(statusCode), appId, databaseId].join(FIELD_SEP)\n}\n\nfunction hgetallPairsToObject(pairs) {\n const o = {}\n if (!pairs || !pairs.length) {\n return o\n }\n for (let i = 0; i < pairs.length; i += 2) {\n o[pairs[i]] = pairs[i + 1]\n }\n return o\n}\n\n/** --- TEMP_HTTP_METRICS_DIAG: delete this helper and all calls when done debugging --- */\nfunction tempHttpDiagLog(message) {\n console.warn(`[TEMP_HTTP_METRICS_DIAG] ${message}`)\n}\n\nfunction truncateForDiag(s, maxLen = 220) {\n const t = String(s)\n return t.length > maxLen ? `${t.slice(0, maxLen - 3)}...` : t\n}\n\n/**\n * Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).\n * Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`, `set`, `del`.\n */\nclass HttpMetricsRedisStore {\n /**\n * @param {Object} opts\n * @param {import('redis').RedisClient} opts.redisClient\n * @param {string} opts.appName BUILD_APP_NAME (key segment)\n * @param {string} opts.processType logical process for key (e.g. web)\n * @param {number} [opts.ttlSec] Expire keys after this many seconds (default 120)\n */\n constructor({ redisClient, appName, processType, ttlSec }) {\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisStore: redisClient is required')\n }\n this._client = redisClient\n this.ttlSec =\n typeof ttlSec === 'number' && ttlSec > 0\n ? ttlSec\n : DEFAULT_HTTP_METRICS_REDIS_TTL_SEC\n const keySeg = `${encodeURIComponent(appName)}:${encodeURIComponent(\n processType\n )}`\n this.countKey = `metrics:http:v2:${keySeg}:count`\n this.durKey = `metrics:http:v2:${keySeg}:dur`\n this.lockKey = `metrics:http:v2:${keySeg}:drain_lock`\n }\n\n /**\n * @returns {import('redis').RedisClient}\n * @private\n */\n _ensureClient() {\n return this._client\n }\n\n /**\n * @param {string} method\n * @param {string} route\n * @param {number} statusCode\n * @param {string} appId\n * @param {string} databaseId\n * @param {number} durationMs\n */\n record(method, route, statusCode, appId, databaseId, durationMs) {\n try {\n const client = this._ensureClient()\n const field = buildFieldKey(method, route, statusCode, appId, databaseId)\n const dur = Math.max(0, Math.round(Number(durationMs) || 0))\n tempHttpDiagLog(\n `http_redis_write_prepare pid=${process.pid} countKey=${\n this.countKey\n } durKey=${this.durKey} hincr_field=${truncateForDiag(\n field\n )} durationMs=${dur}`\n )\n client\n .multi()\n .hincrby(this.countKey, field, 1)\n .hincrby(this.durKey, field, dur)\n .expire(this.countKey, this.ttlSec)\n .expire(this.durKey, this.ttlSec)\n .exec(err => {\n if (err) {\n console.error('[HttpMetricsRedisStore] record failed:', err.message)\n }\n })\n } catch (e) {\n console.error('[HttpMetricsRedisStore] record:', e.message)\n }\n }\n\n /**\n * @param {(labels: Object, value: number) => void} applyCount\n * @param {(labels: Object, value: number) => void} applyDuration\n * @returns {Promise<boolean>}\n */\n flushToCounters(applyCount, applyDuration) {\n let client\n try {\n client = this._ensureClient()\n } catch (e) {\n console.error('[HttpMetricsRedisStore] flush:', e.message)\n return Promise.resolve(false)\n }\n return new Promise(resolve => {\n client.set(this.lockKey, '1', 'EX', 25, 'NX', (setErr, ok) => {\n if (setErr || ok !== 'OK') {\n tempHttpDiagLog(\n `http_redis_drain_skip pid=${process.pid} countKey=${\n this.countKey\n } reason=${setErr ? setErr.message : 'lock_not_held'}`\n )\n resolve(false)\n return\n }\n client.eval(\n DRAIN_LUA,\n 2,\n this.countKey,\n this.durKey,\n (evalErr, raw) => {\n const finish = () => {\n client.del(this.lockKey, () => resolve(true))\n }\n\n if (evalErr) {\n console.error(\n '[HttpMetricsRedisStore] drain failed:',\n evalErr.message\n )\n tempHttpDiagLog(\n `http_redis_drain_got_error pid=${process.pid} message=${evalErr.message}`\n )\n finish()\n return\n }\n\n try {\n if (!raw || !Array.isArray(raw) || raw.length < 2) {\n tempHttpDiagLog(\n `http_redis_drain_collected pid=${process.pid} countKey=${\n this.countKey\n } hash_fields=0 sum_request_counts=0 (empty — keys deleted or no data)`\n )\n finish()\n return\n }\n const counts = hgetallPairsToObject(raw[0])\n const durs = hgetallPairsToObject(raw[1])\n const fieldKeys = Object.keys(counts)\n let totalUnits = 0\n let appliedGroups = 0\n const samples = []\n for (const field of fieldKeys) {\n const count = parseInt(counts[field], 10)\n if (!count || count < 1) {\n continue\n }\n const dur = parseInt(durs[field] || '0', 10) || 0\n const parts = field.split(FIELD_SEP)\n if (parts.length !== 5) {\n continue\n }\n totalUnits += count\n appliedGroups += 1\n const [m, route, statusStr, aid, did] = parts\n if (samples.length < 4) {\n samples.push(`${m} ${route} ${statusStr} x${count}`)\n }\n const labels = {\n method: m,\n route,\n status_code: statusStr,\n appId: aid,\n databaseId: did,\n }\n applyCount(labels, count)\n if (dur > 0) {\n applyDuration(labels, dur)\n }\n }\n let emptyHint = 'ok'\n if (fieldKeys.length === 0) {\n emptyHint = 'no_hash_entries'\n } else if (totalUnits === 0) {\n emptyHint = 'entries_present_but_zero_counts'\n }\n const noTrafficHint =\n fieldKeys.length === 0\n ? ' hint=web_must_use_HttpMetricsRedisRecorder_same_redis_APP_web_segment'\n : ''\n tempHttpDiagLog(\n truncateForDiag(\n `http_redis_drain_collected pid=${process.pid} countKey=${\n this.countKey\n } durKey=${this.durKey} hash_fields_read=${\n fieldKeys.length\n } label_groups_applied=${appliedGroups} sum_request_counts=${totalUnits} state=${emptyHint} sample=${\n samples.join(' | ') || '—'\n }${noTrafficHint}`,\n 900\n )\n )\n } catch (e) {\n console.error(\n '[HttpMetricsRedisStore] flush apply failed:',\n e.message\n )\n tempHttpDiagLog(\n `http_redis_drain_apply_failed pid=${process.pid} ${e.message}`\n )\n }\n finish()\n }\n )\n })\n })\n }\n}\n\nmodule.exports = {\n HttpMetricsRedisStore,\n buildFieldKey,\n FIELD_SEP,\n isRedisPeerInstalled,\n DEFAULT_HTTP_METRICS_REDIS_TTL_SEC,\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisStore.js","names":["FIELD_SEP","DEFAULT_HTTP_METRICS_REDIS_TTL_SEC","DRAIN_LUA","isRedisPeerInstalled","require","resolve","buildFieldKey","method","route","statusCode","appId","databaseId","String","join","hgetallPairsToObject","pairs","o","length","i","HttpMetricsRedisStore","constructor","redisClient","appName","processType","ttlSec","Error","_client","keySeg","encodeURIComponent","countKey","durKey","lockKey","_ensureClient","record","durationMs","client","field","dur","Math","max","round","Number","multi","hincrby","expire","exec","err","console","error","message","e","flushToCounters","applyCount","applyDuration","Promise","set","setErr","ok","eval","evalErr","raw","finish","del","Array","isArray","counts","durs","fieldKeys","Object","keys","count","parseInt","parts","split","m","statusStr","aid","did","labels","status_code","module","exports"],"sources":["../../src/metrics/httpMetricsRedisStore.js"],"sourcesContent":["/**\n * Record separator for hash fields (avoids collisions when route contains \"|\").\n * @type {string}\n */\nconst FIELD_SEP = '\\x1e'\n\n/**\n * Default Redis key TTL in seconds (sliding: refreshed on each `record` write).\n * @type {number}\n */\nconst DEFAULT_HTTP_METRICS_REDIS_TTL_SEC = 120\n\nconst DRAIN_LUA = `\nlocal function drain(key)\n local v = redis.call('HGETALL', key)\n redis.call('DEL', key)\n return v\nend\nreturn {drain(KEYS[1]), drain(KEYS[2])}\n`\n\n/**\n * @returns {boolean} Whether the `redis` npm package is resolvable (optional peer).\n */\nfunction isRedisPeerInstalled() {\n try {\n require.resolve('redis')\n return true\n } catch {\n return false\n }\n}\n\n/**\n * @param {string} method\n * @param {string} route\n * @param {number} statusCode\n * @param {string} appId\n * @param {string} databaseId\n * @returns {string}\n */\nfunction buildFieldKey(method, route, statusCode, appId, databaseId) {\n return [method, route, String(statusCode), appId, databaseId].join(FIELD_SEP)\n}\n\nfunction hgetallPairsToObject(pairs) {\n const o = {}\n if (!pairs || !pairs.length) {\n return o\n }\n for (let i = 0; i < pairs.length; i += 2) {\n o[pairs[i]] = pairs[i + 1]\n }\n return o\n}\n\n/**\n * Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).\n * Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`, `set`, `del`.\n *\n * **Stored metrics:** two hashes per app/segment — `:count` (HINCRBY per route group) and `:dur`\n * (sum of duration ms per same field). Hash field = `method\\\\x1eroute\\\\x1estatus\\\\x1eappId\\\\x1edatabaseId`.\n */\nclass HttpMetricsRedisStore {\n /**\n * @param {Object} opts\n * @param {import('redis').RedisClient} opts.redisClient\n * @param {string} opts.appName BUILD_APP_NAME (key segment)\n * @param {string} opts.processType logical process for key (e.g. web)\n * @param {number} [opts.ttlSec] Expire keys after this many seconds (default 120)\n */\n constructor({ redisClient, appName, processType, ttlSec }) {\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisStore: redisClient is required')\n }\n this._client = redisClient\n this.ttlSec =\n typeof ttlSec === 'number' && ttlSec > 0\n ? ttlSec\n : DEFAULT_HTTP_METRICS_REDIS_TTL_SEC\n const keySeg = `${encodeURIComponent(appName)}:${encodeURIComponent(\n processType\n )}`\n this.countKey = `metrics:http:v2:${keySeg}:count`\n this.durKey = `metrics:http:v2:${keySeg}:dur`\n this.lockKey = `metrics:http:v2:${keySeg}:drain_lock`\n }\n\n /**\n * @returns {import('redis').RedisClient}\n * @private\n */\n _ensureClient() {\n return this._client\n }\n\n /**\n * @param {string} method\n * @param {string} route\n * @param {number} statusCode\n * @param {string} appId\n * @param {string} databaseId\n * @param {number} durationMs\n */\n record(method, route, statusCode, appId, databaseId, durationMs) {\n try {\n const client = this._ensureClient()\n const field = buildFieldKey(method, route, statusCode, appId, databaseId)\n const dur = Math.max(0, Math.round(Number(durationMs) || 0))\n client\n .multi()\n .hincrby(this.countKey, field, 1)\n .hincrby(this.durKey, field, dur)\n .expire(this.countKey, this.ttlSec)\n .expire(this.durKey, this.ttlSec)\n .exec(err => {\n if (err) {\n console.error('[HttpMetricsRedisStore] record failed:', err.message)\n }\n })\n } catch (e) {\n console.error('[HttpMetricsRedisStore] record:', e.message)\n }\n }\n\n /**\n * @param {(labels: Object, value: number) => void} applyCount\n * @param {(labels: Object, value: number) => void} applyDuration\n * @returns {Promise<boolean>}\n */\n flushToCounters(applyCount, applyDuration) {\n let client\n try {\n client = this._ensureClient()\n } catch (e) {\n console.error('[HttpMetricsRedisStore] flush:', e.message)\n return Promise.resolve(false)\n }\n return new Promise(resolve => {\n client.set(this.lockKey, '1', 'EX', 25, 'NX', (setErr, ok) => {\n if (setErr || ok !== 'OK') {\n resolve(false)\n return\n }\n client.eval(\n DRAIN_LUA,\n 2,\n this.countKey,\n this.durKey,\n (evalErr, raw) => {\n const finish = () => {\n client.del(this.lockKey, () => resolve(true))\n }\n\n if (evalErr) {\n console.error(\n '[HttpMetricsRedisStore] drain failed:',\n evalErr.message\n )\n finish()\n return\n }\n\n try {\n if (!raw || !Array.isArray(raw) || raw.length < 2) {\n finish()\n return\n }\n const counts = hgetallPairsToObject(raw[0])\n const durs = hgetallPairsToObject(raw[1])\n const fieldKeys = Object.keys(counts)\n for (const field of fieldKeys) {\n const count = parseInt(counts[field], 10)\n if (!count || count < 1) {\n continue\n }\n const dur = parseInt(durs[field] || '0', 10) || 0\n const parts = field.split(FIELD_SEP)\n if (parts.length !== 5) {\n continue\n }\n const [m, route, statusStr, aid, did] = parts\n const labels = {\n method: m,\n route,\n status_code: statusStr,\n appId: aid,\n databaseId: did,\n }\n applyCount(labels, count)\n if (dur > 0) {\n applyDuration(labels, dur)\n }\n }\n } catch (e) {\n console.error(\n '[HttpMetricsRedisStore] flush apply failed:',\n e.message\n )\n }\n finish()\n }\n )\n })\n })\n }\n}\n\nmodule.exports = {\n HttpMetricsRedisStore,\n buildFieldKey,\n FIELD_SEP,\n isRedisPeerInstalled,\n DEFAULT_HTTP_METRICS_REDIS_TTL_SEC,\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -35,8 +35,9 @@ export function exitUnlessProcessTypeIn(metricsConfig: {
|
|
|
35
35
|
*
|
|
36
36
|
* **Canonical names** align with typical Procfile processes (e.g. backend: `web`, `worker`,
|
|
37
37
|
* `queue-metrics`, `database-metrics`, `http-metrics`). The compile **worker** dyno serves queues/builds
|
|
38
|
-
* — it does not run HTTP request metrics
|
|
39
|
-
*
|
|
38
|
+
* — it does not run HTTP request metrics. HTTP Redis **key segment** is **`METRICS_HTTP_REDIS_KEY_PROCESS_TYPE`**
|
|
39
|
+
* (default **`web`**), not `BUILD_DYNO_PROCESS_TYPE` (workers/collector differ). The HTTP **collector** dyno
|
|
40
|
+
* identity for logs is often {@link METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR}.
|
|
40
41
|
*
|
|
41
42
|
* @module metrics/metricsProcessTypeUtils
|
|
42
43
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metricsProcessTypeUtils.d.ts","sourceRoot":"","sources":["../../src/metrics/metricsProcessTypeUtils.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"metricsProcessTypeUtils.d.ts","sourceRoot":"","sources":["../../src/metrics/metricsProcessTypeUtils.js"],"names":[],"mappings":"AAwCA;;;;;;GAMG;AACH,yDAJW;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,sBACxB,MAAM,GACJ,MAAM,CAQlB;AAED;;;;;;GAMG;AACH,uDAJW;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,uBACxB,MAAM,GACJ,IAAI,CAUhB;AAED;;;;;;;GAOG;AACH,uDALW;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,uBACxB,SAAS,MAAM,EAAE,0BACjB,MAAM,GACJ,IAAI,CAchB;AA5FD;;;;;;;;;;;GAWG;AAEH,6DAA6D;AAC7D,+DAAwD;AAExD,gEAAgE;AAChE,yDAAkD;AAElD,yEAAyE;AACzE,yDAAkD;AAElD,kGAAkG;AAClG,6CAAsC;AAEtC,gGAAgG;AAChG,mDAA4C;AAE5C,wGAAwG;AACxG,yEAAkE;AAElE;;;GAGG;AACH,yDAFU,SAAS,MAAM,EAAE,CAKzB"}
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* **Canonical names** align with typical Procfile processes (e.g. backend: `web`, `worker`,
|
|
8
8
|
* `queue-metrics`, `database-metrics`, `http-metrics`). The compile **worker** dyno serves queues/builds
|
|
9
|
-
* — it does not run HTTP request metrics
|
|
10
|
-
*
|
|
9
|
+
* — it does not run HTTP request metrics. HTTP Redis **key segment** is **`METRICS_HTTP_REDIS_KEY_PROCESS_TYPE`**
|
|
10
|
+
* (default **`web`**), not `BUILD_DYNO_PROCESS_TYPE` (workers/collector differ). The HTTP **collector** dyno
|
|
11
|
+
* identity for logs is often {@link METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR}.
|
|
11
12
|
*
|
|
12
13
|
* @module metrics/metricsProcessTypeUtils
|
|
13
14
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metricsProcessTypeUtils.js","names":["METRICS_PROCESS_TYPE_DATABASE","METRICS_PROCESS_TYPE_QUEUE","METRICS_PROCESS_TYPE_REDIS","METRICS_PROCESS_TYPE_WEB","METRICS_PROCESS_TYPE_WORKER","METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR","REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES","Object","freeze","resolveMetricsProcessType","metricsConfig","defaultProcessType","processType","process","env","BUILD_DYNO_PROCESS_TYPE","exitUnlessProcessTypeIs","expectedProcessType","resolved","exit","exitUnlessProcessTypeIn","allowedProcessTypes","defaultWhenUnspecified","includes","module","exports"],"sources":["../../src/metrics/metricsProcessTypeUtils.js"],"sourcesContent":["/**\n * Helpers for resolving `processType` and silently exiting when a specialized metrics client\n * is constructed on the wrong dyno / process (no log output).\n *\n * **Canonical names** align with typical Procfile processes (e.g. backend: `web`, `worker`,\n * `queue-metrics`, `database-metrics`, `http-metrics`). The compile **worker** dyno serves queues/builds\n * — it does not run HTTP request metrics
|
|
1
|
+
{"version":3,"file":"metricsProcessTypeUtils.js","names":["METRICS_PROCESS_TYPE_DATABASE","METRICS_PROCESS_TYPE_QUEUE","METRICS_PROCESS_TYPE_REDIS","METRICS_PROCESS_TYPE_WEB","METRICS_PROCESS_TYPE_WORKER","METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR","REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES","Object","freeze","resolveMetricsProcessType","metricsConfig","defaultProcessType","processType","process","env","BUILD_DYNO_PROCESS_TYPE","exitUnlessProcessTypeIs","expectedProcessType","resolved","exit","exitUnlessProcessTypeIn","allowedProcessTypes","defaultWhenUnspecified","includes","module","exports"],"sources":["../../src/metrics/metricsProcessTypeUtils.js"],"sourcesContent":["/**\n * Helpers for resolving `processType` and silently exiting when a specialized metrics client\n * is constructed on the wrong dyno / process (no log output).\n *\n * **Canonical names** align with typical Procfile processes (e.g. backend: `web`, `worker`,\n * `queue-metrics`, `database-metrics`, `http-metrics`). The compile **worker** dyno serves queues/builds\n * — it does not run HTTP request metrics. HTTP Redis **key segment** is **`METRICS_HTTP_REDIS_KEY_PROCESS_TYPE`**\n * (default **`web`**), not `BUILD_DYNO_PROCESS_TYPE` (workers/collector differ). The HTTP **collector** dyno\n * identity for logs is often {@link METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR}.\n *\n * @module metrics/metricsProcessTypeUtils\n */\n\n/** DB-only metrics dyno (`database-metrics` in Procfile). */\nconst METRICS_PROCESS_TYPE_DATABASE = 'database-metrics'\n\n/** Queue + Redis metrics dyno (`queue-metrics` in Procfile). */\nconst METRICS_PROCESS_TYPE_QUEUE = 'queue-metrics'\n\n/** Redis-only metrics dyno (no Bee Queue; optional separate process). */\nconst METRICS_PROCESS_TYPE_REDIS = 'redis-metrics'\n\n/** Web servers — HTTP traffic, HTTP Redis **writers** typically use this in Redis key segment. */\nconst METRICS_PROCESS_TYPE_WEB = 'web'\n\n/** Build/compile workers and similar (e.g. backend `worker:`) — no HTTP server metrics here. */\nconst METRICS_PROCESS_TYPE_WORKER = 'worker'\n\n/** HTTP Redis **drain + push** process (e.g. backend `http-metrics:` / `http-metrics-collector.ts`). */\nconst METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR = 'http-metrics'\n\n/**\n * Parent {@link RedisMetricsClient} allows either redis-only or queue stack (`QueueRedisMetricsClient`).\n * @type {readonly string[]}\n */\nconst REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES = Object.freeze([\n METRICS_PROCESS_TYPE_REDIS,\n METRICS_PROCESS_TYPE_QUEUE,\n])\n\n/**\n * Resolve logical process type the same way specialized metrics clients do.\n *\n * @param {{ processType?: string }} metricsConfig - Remainder of constructor options (e.g. after destructuring `redisClient`, `databaseUrl`, …)\n * @param {string} defaultProcessType - Fallback when `metricsConfig.processType` and `BUILD_DYNO_PROCESS_TYPE` are unset\n * @returns {string}\n */\nfunction resolveMetricsProcessType(metricsConfig, defaultProcessType) {\n return (\n metricsConfig.processType ||\n process.env.BUILD_DYNO_PROCESS_TYPE ||\n defaultProcessType\n )\n}\n\n/**\n * Exit with no logs if the resolved process type is not exactly `expectedProcessType`.\n *\n * @param {{ processType?: string }} metricsConfig\n * @param {string} expectedProcessType - Single allowed value (use a module constant, e.g. {@link METRICS_PROCESS_TYPE_DATABASE})\n * @returns {void}\n */\nfunction exitUnlessProcessTypeIs(metricsConfig, expectedProcessType) {\n const resolved = resolveMetricsProcessType(\n metricsConfig,\n expectedProcessType\n )\n if (resolved !== expectedProcessType) {\n process.exit(0)\n }\n}\n\n/**\n * Exit with no logs if the resolved process type is not in `allowedProcessTypes`.\n *\n * @param {{ processType?: string }} metricsConfig\n * @param {readonly string[]} allowedProcessTypes\n * @param {string} defaultWhenUnspecified - Used only to resolve when config/env omit `processType` (e.g. {@link METRICS_PROCESS_TYPE_QUEUE} for {@link RedisMetricsClient})\n * @returns {void}\n */\nfunction exitUnlessProcessTypeIn(\n metricsConfig,\n allowedProcessTypes,\n defaultWhenUnspecified\n) {\n const resolved = resolveMetricsProcessType(\n metricsConfig,\n defaultWhenUnspecified\n )\n if (!allowedProcessTypes.includes(resolved)) {\n process.exit(0)\n }\n}\n\nmodule.exports = {\n resolveMetricsProcessType,\n exitUnlessProcessTypeIs,\n exitUnlessProcessTypeIn,\n METRICS_PROCESS_TYPE_DATABASE,\n METRICS_PROCESS_TYPE_QUEUE,\n METRICS_PROCESS_TYPE_REDIS,\n METRICS_PROCESS_TYPE_WEB,\n METRICS_PROCESS_TYPE_WORKER,\n METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR,\n REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES,\n}\n"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,MAAMA,6BAA6B,GAAG,kBAAkB;;AAExD;AACA,MAAMC,0BAA0B,GAAG,eAAe;;AAElD;AACA,MAAMC,0BAA0B,GAAG,eAAe;;AAElD;AACA,MAAMC,wBAAwB,GAAG,KAAK;;AAEtC;AACA,MAAMC,2BAA2B,GAAG,QAAQ;;AAE5C;AACA,MAAMC,2CAA2C,GAAG,cAAc;;AAElE;AACA;AACA;AACA;AACA,MAAMC,0CAA0C,GAAGC,MAAM,CAACC,MAAM,CAAC,CAC/DN,0BAA0B,EAC1BD,0BAA0B,CAC3B,CAAC;;AAEF;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASQ,yBAAyBA,CAACC,aAAa,EAAEC,kBAAkB,EAAE;EACpE,OACED,aAAa,CAACE,WAAW,IACzBC,OAAO,CAACC,GAAG,CAACC,uBAAuB,IACnCJ,kBAAkB;AAEtB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASK,uBAAuBA,CAACN,aAAa,EAAEO,mBAAmB,EAAE;EACnE,MAAMC,QAAQ,GAAGT,yBAAyB,CACxCC,aAAa,EACbO,mBACF,CAAC;EACD,IAAIC,QAAQ,KAAKD,mBAAmB,EAAE;IACpCJ,OAAO,CAACM,IAAI,CAAC,CAAC,CAAC;EACjB;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,uBAAuBA,CAC9BV,aAAa,EACbW,mBAAmB,EACnBC,sBAAsB,EACtB;EACA,MAAMJ,QAAQ,GAAGT,yBAAyB,CACxCC,aAAa,EACbY,sBACF,CAAC;EACD,IAAI,CAACD,mBAAmB,CAACE,QAAQ,CAACL,QAAQ,CAAC,EAAE;IAC3CL,OAAO,CAACM,IAAI,CAAC,CAAC,CAAC;EACjB;AACF;AAEAK,MAAM,CAACC,OAAO,GAAG;EACfhB,yBAAyB;EACzBO,uBAAuB;EACvBI,uBAAuB;EACvBpB,6BAA6B;EAC7BC,0BAA0B;EAC1BC,0BAA0B;EAC1BC,wBAAwB;EACxBC,2BAA2B;EAC3BC,2CAA2C;EAC3CC;AACF,CAAC","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -92,30 +92,12 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
92
92
|
this.countersFunctions?.app_requests_total &&
|
|
93
93
|
this.countersFunctions?.app_requests_total_duration
|
|
94
94
|
) {
|
|
95
|
-
|
|
96
|
-
const t0 = Date.now()
|
|
97
|
-
// --- TEMP_HTTP_METRICS_DIAG: remove when done debugging ---
|
|
98
|
-
console.warn(
|
|
99
|
-
`[TEMP_HTTP_METRICS_DIAG] http_collector_flush_begin pid=${pid} dyno_id=${this.dynoId} process_type=${this.processType} app=${this.appName}`
|
|
100
|
-
)
|
|
101
|
-
// --- end TEMP_HTTP_METRICS_DIAG ---
|
|
102
|
-
const drainedOk = await this._store.flushToCounters(
|
|
95
|
+
await this._store.flushToCounters(
|
|
103
96
|
(labels, value) =>
|
|
104
97
|
this.countersFunctions.app_requests_total(labels, value),
|
|
105
98
|
(labels, value) =>
|
|
106
99
|
this.countersFunctions.app_requests_total_duration(labels, value)
|
|
107
100
|
)
|
|
108
|
-
// --- TEMP_HTTP_METRICS_DIAG: remove when done debugging ---
|
|
109
|
-
console.warn(
|
|
110
|
-
`[TEMP_HTTP_METRICS_DIAG] http_collector_flush_end pid=${pid} lock_acquired_and_ran_drain=${drainedOk} elapsed_ms=${Date.now() - t0}`
|
|
111
|
-
)
|
|
112
|
-
// --- end TEMP_HTTP_METRICS_DIAG ---
|
|
113
|
-
} else {
|
|
114
|
-
// --- TEMP_HTTP_METRICS_DIAG: remove when done debugging ---
|
|
115
|
-
console.warn(
|
|
116
|
-
`[TEMP_HTTP_METRICS_DIAG] http_collector_flush_skipped pid=${process.pid} reason=missing_store_or_http_counters`
|
|
117
|
-
)
|
|
118
|
-
// --- end TEMP_HTTP_METRICS_DIAG ---
|
|
119
101
|
}
|
|
120
102
|
return this._pushMetrics()
|
|
121
103
|
}
|
|
@@ -54,19 +54,12 @@ function hgetallPairsToObject(pairs) {
|
|
|
54
54
|
return o
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/** --- TEMP_HTTP_METRICS_DIAG: delete this helper and all calls when done debugging --- */
|
|
58
|
-
function tempHttpDiagLog(message) {
|
|
59
|
-
console.warn(`[TEMP_HTTP_METRICS_DIAG] ${message}`)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function truncateForDiag(s, maxLen = 220) {
|
|
63
|
-
const t = String(s)
|
|
64
|
-
return t.length > maxLen ? `${t.slice(0, maxLen - 3)}...` : t
|
|
65
|
-
}
|
|
66
|
-
|
|
67
57
|
/**
|
|
68
58
|
* Redis HTTP aggregate store. Uses an **injected** client (same pattern as {@link RedisMetricsClient}).
|
|
69
59
|
* Expects `redis` v3-style API: `multi().hincrby().expire().exec(cb)`, `eval`, `set`, `del`.
|
|
60
|
+
*
|
|
61
|
+
* **Stored metrics:** two hashes per app/segment — `:count` (HINCRBY per route group) and `:dur`
|
|
62
|
+
* (sum of duration ms per same field). Hash field = `method\\x1eroute\\x1estatus\\x1eappId\\x1edatabaseId`.
|
|
70
63
|
*/
|
|
71
64
|
class HttpMetricsRedisStore {
|
|
72
65
|
/**
|
|
@@ -114,13 +107,6 @@ class HttpMetricsRedisStore {
|
|
|
114
107
|
const client = this._ensureClient()
|
|
115
108
|
const field = buildFieldKey(method, route, statusCode, appId, databaseId)
|
|
116
109
|
const dur = Math.max(0, Math.round(Number(durationMs) || 0))
|
|
117
|
-
tempHttpDiagLog(
|
|
118
|
-
`http_redis_write_prepare pid=${process.pid} countKey=${
|
|
119
|
-
this.countKey
|
|
120
|
-
} durKey=${this.durKey} hincr_field=${truncateForDiag(
|
|
121
|
-
field
|
|
122
|
-
)} durationMs=${dur}`
|
|
123
|
-
)
|
|
124
110
|
client
|
|
125
111
|
.multi()
|
|
126
112
|
.hincrby(this.countKey, field, 1)
|
|
@@ -153,11 +139,6 @@ class HttpMetricsRedisStore {
|
|
|
153
139
|
return new Promise(resolve => {
|
|
154
140
|
client.set(this.lockKey, '1', 'EX', 25, 'NX', (setErr, ok) => {
|
|
155
141
|
if (setErr || ok !== 'OK') {
|
|
156
|
-
tempHttpDiagLog(
|
|
157
|
-
`http_redis_drain_skip pid=${process.pid} countKey=${
|
|
158
|
-
this.countKey
|
|
159
|
-
} reason=${setErr ? setErr.message : 'lock_not_held'}`
|
|
160
|
-
)
|
|
161
142
|
resolve(false)
|
|
162
143
|
return
|
|
163
144
|
}
|
|
@@ -176,29 +157,18 @@ class HttpMetricsRedisStore {
|
|
|
176
157
|
'[HttpMetricsRedisStore] drain failed:',
|
|
177
158
|
evalErr.message
|
|
178
159
|
)
|
|
179
|
-
tempHttpDiagLog(
|
|
180
|
-
`http_redis_drain_got_error pid=${process.pid} message=${evalErr.message}`
|
|
181
|
-
)
|
|
182
160
|
finish()
|
|
183
161
|
return
|
|
184
162
|
}
|
|
185
163
|
|
|
186
164
|
try {
|
|
187
165
|
if (!raw || !Array.isArray(raw) || raw.length < 2) {
|
|
188
|
-
tempHttpDiagLog(
|
|
189
|
-
`http_redis_drain_collected pid=${process.pid} countKey=${
|
|
190
|
-
this.countKey
|
|
191
|
-
} hash_fields=0 sum_request_counts=0 (empty — keys deleted or no data)`
|
|
192
|
-
)
|
|
193
166
|
finish()
|
|
194
167
|
return
|
|
195
168
|
}
|
|
196
169
|
const counts = hgetallPairsToObject(raw[0])
|
|
197
170
|
const durs = hgetallPairsToObject(raw[1])
|
|
198
171
|
const fieldKeys = Object.keys(counts)
|
|
199
|
-
let totalUnits = 0
|
|
200
|
-
let appliedGroups = 0
|
|
201
|
-
const samples = []
|
|
202
172
|
for (const field of fieldKeys) {
|
|
203
173
|
const count = parseInt(counts[field], 10)
|
|
204
174
|
if (!count || count < 1) {
|
|
@@ -209,12 +179,7 @@ class HttpMetricsRedisStore {
|
|
|
209
179
|
if (parts.length !== 5) {
|
|
210
180
|
continue
|
|
211
181
|
}
|
|
212
|
-
totalUnits += count
|
|
213
|
-
appliedGroups += 1
|
|
214
182
|
const [m, route, statusStr, aid, did] = parts
|
|
215
|
-
if (samples.length < 4) {
|
|
216
|
-
samples.push(`${m} ${route} ${statusStr} x${count}`)
|
|
217
|
-
}
|
|
218
183
|
const labels = {
|
|
219
184
|
method: m,
|
|
220
185
|
route,
|
|
@@ -227,36 +192,11 @@ class HttpMetricsRedisStore {
|
|
|
227
192
|
applyDuration(labels, dur)
|
|
228
193
|
}
|
|
229
194
|
}
|
|
230
|
-
let emptyHint = 'ok'
|
|
231
|
-
if (fieldKeys.length === 0) {
|
|
232
|
-
emptyHint = 'no_hash_entries'
|
|
233
|
-
} else if (totalUnits === 0) {
|
|
234
|
-
emptyHint = 'entries_present_but_zero_counts'
|
|
235
|
-
}
|
|
236
|
-
const noTrafficHint =
|
|
237
|
-
fieldKeys.length === 0
|
|
238
|
-
? ' hint=web_must_use_HttpMetricsRedisRecorder_same_redis_APP_web_segment'
|
|
239
|
-
: ''
|
|
240
|
-
tempHttpDiagLog(
|
|
241
|
-
truncateForDiag(
|
|
242
|
-
`http_redis_drain_collected pid=${process.pid} countKey=${
|
|
243
|
-
this.countKey
|
|
244
|
-
} durKey=${this.durKey} hash_fields_read=${
|
|
245
|
-
fieldKeys.length
|
|
246
|
-
} label_groups_applied=${appliedGroups} sum_request_counts=${totalUnits} state=${emptyHint} sample=${
|
|
247
|
-
samples.join(' | ') || '—'
|
|
248
|
-
}${noTrafficHint}`,
|
|
249
|
-
900
|
|
250
|
-
)
|
|
251
|
-
)
|
|
252
195
|
} catch (e) {
|
|
253
196
|
console.error(
|
|
254
197
|
'[HttpMetricsRedisStore] flush apply failed:',
|
|
255
198
|
e.message
|
|
256
199
|
)
|
|
257
|
-
tempHttpDiagLog(
|
|
258
|
-
`http_redis_drain_apply_failed pid=${process.pid} ${e.message}`
|
|
259
|
-
)
|
|
260
200
|
}
|
|
261
201
|
finish()
|
|
262
202
|
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* **Canonical names** align with typical Procfile processes (e.g. backend: `web`, `worker`,
|
|
6
6
|
* `queue-metrics`, `database-metrics`, `http-metrics`). The compile **worker** dyno serves queues/builds
|
|
7
|
-
* — it does not run HTTP request metrics
|
|
8
|
-
*
|
|
7
|
+
* — it does not run HTTP request metrics. HTTP Redis **key segment** is **`METRICS_HTTP_REDIS_KEY_PROCESS_TYPE`**
|
|
8
|
+
* (default **`web`**), not `BUILD_DYNO_PROCESS_TYPE` (workers/collector differ). The HTTP **collector** dyno
|
|
9
|
+
* identity for logs is often {@link METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR}.
|
|
9
10
|
*
|
|
10
11
|
* @module metrics/metricsProcessTypeUtils
|
|
11
12
|
*/
|