@adalo/metrics 0.0.0-staging.21 → 0.0.0-staging.23
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/__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 +4 -3
- package/lib/metrics/httpMetricsRedisCollector.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisCollector.js +5 -4
- package/lib/metrics/httpMetricsRedisCollector.js.map +1 -1
- package/lib/metrics/httpMetricsRedisRecorder.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisRecorder.js +6 -0
- package/lib/metrics/httpMetricsRedisRecorder.js.map +1 -1
- package/lib/metrics/httpMetricsRedisStore.d.ts.map +1 -1
- package/lib/metrics/httpMetricsRedisStore.js +12 -0
- package/lib/metrics/httpMetricsRedisStore.js.map +1 -1
- package/lib/metrics/metricsProcessTypeUtils.d.ts +2 -4
- package/lib/metrics/metricsProcessTypeUtils.d.ts.map +1 -1
- package/lib/metrics/metricsProcessTypeUtils.js +2 -6
- package/lib/metrics/metricsProcessTypeUtils.js.map +1 -1
- package/package.json +1 -1
- package/src/metrics/httpMetricsRedisCollector.js +5 -7
- package/src/metrics/httpMetricsRedisRecorder.js +12 -0
- package/src/metrics/httpMetricsRedisStore.js +25 -0
- package/src/metrics/metricsProcessTypeUtils.js +2 -6
|
@@ -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,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Drain worker: reads HTTP aggregates from Redis (written by {@link HttpMetricsRedisRecorder}),
|
|
3
3
|
* applies them to `app_requests_*` counters, then pushes the registry to the VM-agent (same as {@link BaseMetricsClient}).
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* **Minimal usage:** `{ redisClient }` only. Redis keys use segment **`web`** unless you pass **`redisProcessTypeForKeys`**.
|
|
5
|
+
* `processType` / `appName` / `dynoId` follow {@link BaseMetricsClient} defaults (e.g. `BUILD_DYNO_PROCESS_TYPE`) and do **not** select Redis hash names.
|
|
6
|
+
* Always passes `blockNodeDefaultMetrics: true` (HTTP-focused registry).
|
|
6
7
|
*
|
|
7
8
|
* @extends BaseMetricsClient
|
|
8
9
|
*/
|
|
@@ -21,7 +22,7 @@ export class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
21
22
|
* @param {boolean} [config.removeOldMetrics] Clear old series on shutdown where supported
|
|
22
23
|
* @param {function} [config.startupValidation] Run before first push
|
|
23
24
|
* @param {boolean} [config.disablePushgateway] Skip POST to VM-agent
|
|
24
|
-
* @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default
|
|
25
|
+
* @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default **`web`**). Optional; only if writers use a non-`web` segment.
|
|
25
26
|
* @param {number} [config.ttlSec] Passed to {@link HttpMetricsRedisStore} (should match writers)
|
|
26
27
|
*/
|
|
27
28
|
constructor(config?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisCollector.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisCollector.js"],"names":[],"mappings":"AAGA
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisCollector.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisCollector.js"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH;IACE;;;;;;;;;;;;;;;;OAgBG;IACH;qBAfW,OAAO,OAAO,EAAE,WAAW;;;;;;;;;;;;;;mBAiErC;IAhCC,8BAKE;CAgDL"}
|
|
@@ -10,8 +10,9 @@ const {
|
|
|
10
10
|
/**
|
|
11
11
|
* Drain worker: reads HTTP aggregates from Redis (written by {@link HttpMetricsRedisRecorder}),
|
|
12
12
|
* applies them to `app_requests_*` counters, then pushes the registry to the VM-agent (same as {@link BaseMetricsClient}).
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* **Minimal usage:** `{ redisClient }` only. Redis keys use segment **`web`** unless you pass **`redisProcessTypeForKeys`**.
|
|
14
|
+
* `processType` / `appName` / `dynoId` follow {@link BaseMetricsClient} defaults (e.g. `BUILD_DYNO_PROCESS_TYPE`) and do **not** select Redis hash names.
|
|
15
|
+
* Always passes `blockNodeDefaultMetrics: true` (HTTP-focused registry).
|
|
15
16
|
*
|
|
16
17
|
* @extends BaseMetricsClient
|
|
17
18
|
*/
|
|
@@ -30,7 +31,7 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
30
31
|
* @param {boolean} [config.removeOldMetrics] Clear old series on shutdown where supported
|
|
31
32
|
* @param {function} [config.startupValidation] Run before first push
|
|
32
33
|
* @param {boolean} [config.disablePushgateway] Skip POST to VM-agent
|
|
33
|
-
* @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default
|
|
34
|
+
* @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default **`web`**). Optional; only if writers use a non-`web` segment.
|
|
34
35
|
* @param {number} [config.ttlSec] Passed to {@link HttpMetricsRedisStore} (should match writers)
|
|
35
36
|
*/
|
|
36
37
|
constructor(config = {}) {
|
|
@@ -44,7 +45,7 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
44
45
|
...config,
|
|
45
46
|
blockNodeDefaultMetrics: true
|
|
46
47
|
});
|
|
47
|
-
const keyProcessType = config.redisProcessTypeForKeys ||
|
|
48
|
+
const keyProcessType = config.redisProcessTypeForKeys || 'web';
|
|
48
49
|
this.defaultLabelsWithoutDynoId = {
|
|
49
50
|
app: this.appName,
|
|
50
51
|
process_type: keyProcessType
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisCollector.js","names":["BaseMetricsClient","require","HttpMetricsRedisStore","HttpMetricsRedisCollector","constructor","config","redisClient","Error","blockNodeDefaultMetrics","keyProcessType","redisProcessTypeForKeys","
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisCollector.js","names":["BaseMetricsClient","require","HttpMetricsRedisStore","HttpMetricsRedisCollector","constructor","config","redisClient","Error","blockNodeDefaultMetrics","keyProcessType","redisProcessTypeForKeys","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 * **Minimal usage:** `{ redisClient }` only. Redis keys use segment **`web`** unless you pass **`redisProcessTypeForKeys`**.\n * `processType` / `appName` / `dynoId` follow {@link BaseMetricsClient} defaults (e.g. `BUILD_DYNO_PROCESS_TYPE`) and do **not** select Redis hash names.\n * Always passes `blockNodeDefaultMetrics: true` (HTTP-focused registry).\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`**). Optional; only if writers use a non-`web` segment.\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 = config.redisProcessTypeForKeys || '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;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,GAAGJ,MAAM,CAACK,uBAAuB,IAAI,KAAK;IAE9D,IAAI,CAACC,0BAA0B,GAAG;MAChCC,GAAG,EAAE,IAAI,CAACC,OAAO;MACjBC,YAAY,EAAEL;IAChB,CAAC;IAED,IAAI,CAACM,MAAM,GAAG,IAAIb,qBAAqB,CAAC;MACtCI,WAAW;MACXO,OAAO,EAAE,IAAI,CAACA,OAAO;MACrBG,WAAW,EAAEP,cAAc;MAC3BQ,MAAM,EAAEZ,MAAM,CAACY;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;EAAE9B;AAA0B,CAAC","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisRecorder.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisRecorder.d.ts","sourceRoot":"","sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"names":[],"mappings":"AASA;;;;;GAKG;AACH;IACE;;;;;;OAMG;IACH;QAL6C,WAAW,EAA7C,OAAO,OAAO,EAAE,WAAW;QACd,OAAO,EAApB,MAAM;QACO,WAAW,EAAxB,MAAM;QACQ,MAAM;OAc9B;IARC,oBAA8B;IAC9B,gBAAsB;IACtB,8BAKE;IAGJ;;;;;;;;OAQG;IACH;QAP0B,MAAM,EAArB,MAAM;QACS,KAAK,EAApB,MAAM;QACS,WAAW,EAA1B,MAAM;QACU,KAAK;QACL,UAAU;QACX,QAAQ,EAAvB,MAAM;aAgBhB;IAED;;;;;;;OAOG;IACH,kCAJW,OAAO,MAAM,EAAE,eAAe,OAC9B,OAAO,MAAM,EAAE,cAAc,0BAkCvC;CACF"}
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
const {
|
|
4
4
|
HttpMetricsRedisStore
|
|
5
5
|
} = require('./httpMetricsRedisStore');
|
|
6
|
+
const LOG = '[http-metrics-redis]';
|
|
7
|
+
function trunc(s, max = 120) {
|
|
8
|
+
const t = String(s);
|
|
9
|
+
return t.length > max ? `${t.slice(0, max - 3)}...` : t;
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).
|
|
@@ -54,6 +59,7 @@ class HttpMetricsRedisRecorder {
|
|
|
54
59
|
databaseId = '',
|
|
55
60
|
duration
|
|
56
61
|
}) {
|
|
62
|
+
console.warn(`${LOG} 1_track pid=${process.pid} app=${this.appName} segment=${this.processType} ` + `method=${method} route=${trunc(route)} status=${status_code} durationMs=${duration} ` + `appId=${trunc(appId || '—', 40)} databaseId=${trunc(databaseId || '—', 40)}`);
|
|
57
63
|
this._store.record(method, route, status_code, appId, databaseId, duration);
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpMetricsRedisRecorder.js","names":["HttpMetricsRedisStore","require","HttpMetricsRedisRecorder","constructor","redisClient","appName","processType","ttlSec","Error","_store","trackHttpRequest","method","route","status_code","appId","databaseId","duration","record","trackHttpRequestMiddleware","req","res","next","start","Date","now","on","path","params","body","query","datasourceId","statusCode","module","exports"],"sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"sourcesContent":["const { HttpMetricsRedisStore } = require('./httpMetricsRedisStore')\n\n/**\n * Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).\n * Pair with {@link HttpMetricsRedisCollector} on a drain process to flush into counters and push to the VM-agent.\n *\n * @see HttpMetricsRedisStore\n */\nclass HttpMetricsRedisRecorder {\n /**\n * @param {Object} opts\n * @param {import('redis').RedisClient} opts.redisClient **Required.** Injected client (same pattern as {@link RedisMetricsClient}).\n * @param {string} opts.appName Application name (must match collector; typically `BUILD_APP_NAME`).\n * @param {string} opts.processType Logical process segment in Redis keys (e.g. `web`; must match collector’s key segment).\n * @param {number} [opts.ttlSec] Redis hash key TTL in seconds (sliding; default 120).\n */\n constructor({ redisClient, appName, processType, ttlSec }) {\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisRecorder: redisClient is required')\n }\n this.processType = processType\n this.appName = appName\n this._store = new HttpMetricsRedisStore({\n redisClient,\n appName,\n processType,\n ttlSec,\n })\n }\n\n /**\n * @param {Object} params\n * @param {string} params.method\n * @param {string} params.route\n * @param {number} params.status_code\n * @param {string} [params.appId]\n * @param {string} [params.databaseId]\n * @param {number} params.duration\n */\n trackHttpRequest({\n method,\n route,\n status_code,\n appId = '',\n databaseId = '',\n duration,\n }) {\n this._store.record(method, route, status_code, appId, databaseId, duration)\n }\n\n /**\n * Express middleware: appends a `finish` listener and writes one aggregate row per request to Redis.\n * Does not check `METRICS_ENABLED`; the app should only mount this when HTTP Redis metrics should run.\n *\n * @param {import('http').IncomingMessage} req\n * @param {import('http').ServerResponse} res\n * @param {function} next\n */\n trackHttpRequestMiddleware = (req, res, next) => {\n if (req.method === 'OPTIONS') {\n next()\n return\n }\n\n const start = Date.now()\n res.on('finish', () => {\n const route = req.route?.path || req.path || 'unknown'\n const appId =\n req.params?.appId || req.body?.appId || req.query?.appId || ''\n const databaseId =\n req.params?.databaseId ||\n req.body?.databaseId ||\n req.query?.databaseId ||\n req.params?.datasourceId ||\n req.body?.datasourceId ||\n req.query?.datasourceId ||\n ''\n\n this.trackHttpRequest({\n method: req.method,\n route,\n status_code: res.statusCode,\n appId,\n databaseId,\n duration: Date.now() - start,\n })\n })\n\n next()\n }\n}\n\nmodule.exports = { HttpMetricsRedisRecorder }\n"],"mappings":";;AAAA,MAAM;EAAEA;AAAsB,CAAC,GAAGC,OAAO,CAAC,yBAAyB,CAAC
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisRecorder.js","names":["HttpMetricsRedisStore","require","LOG","trunc","s","max","t","String","length","slice","HttpMetricsRedisRecorder","constructor","redisClient","appName","processType","ttlSec","Error","_store","trackHttpRequest","method","route","status_code","appId","databaseId","duration","console","warn","process","pid","record","trackHttpRequestMiddleware","req","res","next","start","Date","now","on","path","params","body","query","datasourceId","statusCode","module","exports"],"sources":["../../src/metrics/httpMetricsRedisRecorder.js"],"sourcesContent":["const { HttpMetricsRedisStore } = require('./httpMetricsRedisStore')\n\nconst LOG = '[http-metrics-redis]'\n\nfunction trunc(s, max = 120) {\n const t = String(s)\n return t.length > max ? `${t.slice(0, max - 3)}...` : t\n}\n\n/**\n * Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).\n * Pair with {@link HttpMetricsRedisCollector} on a drain process to flush into counters and push to the VM-agent.\n *\n * @see HttpMetricsRedisStore\n */\nclass HttpMetricsRedisRecorder {\n /**\n * @param {Object} opts\n * @param {import('redis').RedisClient} opts.redisClient **Required.** Injected client (same pattern as {@link RedisMetricsClient}).\n * @param {string} opts.appName Application name (must match collector; typically `BUILD_APP_NAME`).\n * @param {string} opts.processType Logical process segment in Redis keys (e.g. `web`; must match collector’s key segment).\n * @param {number} [opts.ttlSec] Redis hash key TTL in seconds (sliding; default 120).\n */\n constructor({ redisClient, appName, processType, ttlSec }) {\n if (redisClient == null) {\n throw new Error('HttpMetricsRedisRecorder: redisClient is required')\n }\n this.processType = processType\n this.appName = appName\n this._store = new HttpMetricsRedisStore({\n redisClient,\n appName,\n processType,\n ttlSec,\n })\n }\n\n /**\n * @param {Object} params\n * @param {string} params.method\n * @param {string} params.route\n * @param {number} params.status_code\n * @param {string} [params.appId]\n * @param {string} [params.databaseId]\n * @param {number} params.duration\n */\n trackHttpRequest({\n method,\n route,\n status_code,\n appId = '',\n databaseId = '',\n duration,\n }) {\n console.warn(\n `${LOG} 1_track pid=${process.pid} app=${this.appName} segment=${this.processType} ` +\n `method=${method} route=${trunc(route)} status=${status_code} durationMs=${duration} ` +\n `appId=${trunc(appId || '—', 40)} databaseId=${trunc(databaseId || '—', 40)}`\n )\n this._store.record(method, route, status_code, appId, databaseId, duration)\n }\n\n /**\n * Express middleware: appends a `finish` listener and writes one aggregate row per request to Redis.\n * Does not check `METRICS_ENABLED`; the app should only mount this when HTTP Redis metrics should run.\n *\n * @param {import('http').IncomingMessage} req\n * @param {import('http').ServerResponse} res\n * @param {function} next\n */\n trackHttpRequestMiddleware = (req, res, next) => {\n if (req.method === 'OPTIONS') {\n next()\n return\n }\n\n const start = Date.now()\n res.on('finish', () => {\n const route = req.route?.path || req.path || 'unknown'\n const appId =\n req.params?.appId || req.body?.appId || req.query?.appId || ''\n const databaseId =\n req.params?.databaseId ||\n req.body?.databaseId ||\n req.query?.databaseId ||\n req.params?.datasourceId ||\n req.body?.datasourceId ||\n req.query?.datasourceId ||\n ''\n\n this.trackHttpRequest({\n method: req.method,\n route,\n status_code: res.statusCode,\n appId,\n databaseId,\n duration: Date.now() - start,\n })\n })\n\n next()\n }\n}\n\nmodule.exports = { HttpMetricsRedisRecorder }\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -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":"AA0DA;;;;;;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,QAyBhB;IAED;;;;OAIG;IACH,qCAJoB,MAAM,SAAS,MAAM,KAAK,IAAI,0BAC9B,MAAM,SAAS,MAAM,KAAK,IAAI,GACrC,QAAQ,OAAO,CAAC,CAgG5B;CACF;AApMD;;;;;;;GAOG;AACH,sCAPW,MAAM,SACN,MAAM,cACN,MAAM,SACN,MAAM,cACN,MAAM,GACJ,MAAM,CAIlB;AA7CD;;;GAGG;AACH,wBAFU,MAAM,CAEQ;AAmBxB;;GAEG;AACH,wCAFa,OAAO,CASnB;AA3BD;;;GAGG;AACH,iDAFU,MAAM,CAE8B"}
|
|
@@ -11,6 +11,7 @@ const FIELD_SEP = '\x1e';
|
|
|
11
11
|
* @type {number}
|
|
12
12
|
*/
|
|
13
13
|
const DEFAULT_HTTP_METRICS_REDIS_TTL_SEC = 120;
|
|
14
|
+
const LOG = '[http-metrics-redis]';
|
|
14
15
|
const DRAIN_LUA = `
|
|
15
16
|
local function drain(key)
|
|
16
17
|
local v = redis.call('HGETALL', key)
|
|
@@ -110,7 +111,9 @@ class HttpMetricsRedisStore {
|
|
|
110
111
|
client.multi().hincrby(this.countKey, field, 1).hincrby(this.durKey, field, dur).expire(this.countKey, this.ttlSec).expire(this.durKey, this.ttlSec).exec(err => {
|
|
111
112
|
if (err) {
|
|
112
113
|
console.error('[HttpMetricsRedisStore] record failed:', err.message);
|
|
114
|
+
return;
|
|
113
115
|
}
|
|
116
|
+
console.warn(`${LOG} 2_redis_write pid=${process.pid} countKey=${this.countKey} durKey=${this.durKey} ok`);
|
|
114
117
|
});
|
|
115
118
|
} catch (e) {
|
|
116
119
|
console.error('[HttpMetricsRedisStore] record:', e.message);
|
|
@@ -133,6 +136,7 @@ class HttpMetricsRedisStore {
|
|
|
133
136
|
return new Promise(resolve => {
|
|
134
137
|
client.set(this.lockKey, '1', 'EX', 25, 'NX', (setErr, ok) => {
|
|
135
138
|
if (setErr || ok !== 'OK') {
|
|
139
|
+
console.warn(`${LOG} 3_redis_drain_skip pid=${process.pid} countKey=${this.countKey} reason=${setErr ? `error:${setErr.message}` : 'lock_not_acquired'}`);
|
|
136
140
|
resolve(false);
|
|
137
141
|
return;
|
|
138
142
|
}
|
|
@@ -147,12 +151,15 @@ class HttpMetricsRedisStore {
|
|
|
147
151
|
}
|
|
148
152
|
try {
|
|
149
153
|
if (!raw || !Array.isArray(raw) || raw.length < 2) {
|
|
154
|
+
console.warn(`${LOG} 3_redis_drain pid=${process.pid} countKey=${this.countKey} hash_fields=0 sum_requests=0 (empty_after_lua)`);
|
|
150
155
|
finish();
|
|
151
156
|
return;
|
|
152
157
|
}
|
|
153
158
|
const counts = hgetallPairsToObject(raw[0]);
|
|
154
159
|
const durs = hgetallPairsToObject(raw[1]);
|
|
155
160
|
const fieldKeys = Object.keys(counts);
|
|
161
|
+
let sumRequests = 0;
|
|
162
|
+
const samples = [];
|
|
156
163
|
for (const field of fieldKeys) {
|
|
157
164
|
const count = parseInt(counts[field], 10);
|
|
158
165
|
if (!count || count < 1) {
|
|
@@ -164,6 +171,10 @@ class HttpMetricsRedisStore {
|
|
|
164
171
|
continue;
|
|
165
172
|
}
|
|
166
173
|
const [m, route, statusStr, aid, did] = parts;
|
|
174
|
+
sumRequests += count;
|
|
175
|
+
if (samples.length < 3) {
|
|
176
|
+
samples.push(`${m} ${route} ${statusStr} x${count}`);
|
|
177
|
+
}
|
|
167
178
|
const labels = {
|
|
168
179
|
method: m,
|
|
169
180
|
route,
|
|
@@ -176,6 +187,7 @@ class HttpMetricsRedisStore {
|
|
|
176
187
|
applyDuration(labels, dur);
|
|
177
188
|
}
|
|
178
189
|
}
|
|
190
|
+
console.warn(`${LOG} 3_redis_drain pid=${process.pid} countKey=${this.countKey} hash_fields=${fieldKeys.length} sum_requests=${sumRequests} sample=${samples.join(' | ') || '—'}`);
|
|
179
191
|
} catch (e) {
|
|
180
192
|
console.error('[HttpMetricsRedisStore] flush apply failed:', e.message);
|
|
181
193
|
}
|
|
@@ -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","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":[]}
|
|
1
|
+
{"version":3,"file":"httpMetricsRedisStore.js","names":["FIELD_SEP","DEFAULT_HTTP_METRICS_REDIS_TTL_SEC","LOG","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","warn","process","pid","e","flushToCounters","applyCount","applyDuration","Promise","set","setErr","ok","eval","evalErr","raw","finish","del","Array","isArray","counts","durs","fieldKeys","Object","keys","sumRequests","samples","count","parseInt","parts","split","m","statusStr","aid","did","push","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 LOG = '[http-metrics-redis]'\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 return\n }\n console.warn(\n `${LOG} 2_redis_write pid=${process.pid} countKey=${this.countKey} durKey=${this.durKey} ok`\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 console.warn(\n `${LOG} 3_redis_drain_skip pid=${process.pid} countKey=${this.countKey} reason=${\n setErr ? `error:${setErr.message}` : 'lock_not_acquired'\n }`\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 finish()\n return\n }\n\n try {\n if (!raw || !Array.isArray(raw) || raw.length < 2) {\n console.warn(\n `${LOG} 3_redis_drain pid=${process.pid} countKey=${this.countKey} hash_fields=0 sum_requests=0 (empty_after_lua)`\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 sumRequests = 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 const [m, route, statusStr, aid, did] = parts\n sumRequests += count\n if (samples.length < 3) {\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 console.warn(\n `${LOG} 3_redis_drain pid=${process.pid} countKey=${this.countKey} hash_fields=${\n fieldKeys.length\n } sum_requests=${sumRequests} sample=${samples.join(' | ') || '—'}`\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":";;AAAA;AACA;AACA;AACA;AACA,MAAMA,SAAS,GAAG,MAAM;;AAExB;AACA;AACA;AACA;AACA,MAAMC,kCAAkC,GAAG,GAAG;AAE9C,MAAMC,GAAG,GAAG,sBAAsB;AAElC,MAAMC,SAAS,GAAG;AAClB;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;;AAED;AACA;AACA;AACA,SAASC,oBAAoBA,CAAA,EAAG;EAC9B,IAAI;IACFC,OAAO,CAACC,OAAO,CAAC,OAAO,CAAC;IACxB,OAAO,IAAI;EACb,CAAC,CAAC,MAAM;IACN,OAAO,KAAK;EACd;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,aAAaA,CAACC,MAAM,EAAEC,KAAK,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,EAAE;EACnE,OAAO,CAACJ,MAAM,EAAEC,KAAK,EAAEI,MAAM,CAACH,UAAU,CAAC,EAAEC,KAAK,EAAEC,UAAU,CAAC,CAACE,IAAI,CAACd,SAAS,CAAC;AAC/E;AAEA,SAASe,oBAAoBA,CAACC,KAAK,EAAE;EACnC,MAAMC,CAAC,GAAG,CAAC,CAAC;EACZ,IAAI,CAACD,KAAK,IAAI,CAACA,KAAK,CAACE,MAAM,EAAE;IAC3B,OAAOD,CAAC;EACV;EACA,KAAK,IAAIE,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGH,KAAK,CAACE,MAAM,EAAEC,CAAC,IAAI,CAAC,EAAE;IACxCF,CAAC,CAACD,KAAK,CAACG,CAAC,CAAC,CAAC,GAAGH,KAAK,CAACG,CAAC,GAAG,CAAC,CAAC;EAC5B;EACA,OAAOF,CAAC;AACV;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMG,qBAAqB,CAAC;EAC1B;AACF;AACA;AACA;AACA;AACA;AACA;EACEC,WAAWA,CAAC;IAAEC,WAAW;IAAEC,OAAO;IAAEC,WAAW;IAAEC;EAAO,CAAC,EAAE;IACzD,IAAIH,WAAW,IAAI,IAAI,EAAE;MACvB,MAAM,IAAII,KAAK,CAAC,gDAAgD,CAAC;IACnE;IACA,IAAI,CAACC,OAAO,GAAGL,WAAW;IAC1B,IAAI,CAACG,MAAM,GACT,OAAOA,MAAM,KAAK,QAAQ,IAAIA,MAAM,GAAG,CAAC,GACpCA,MAAM,GACNxB,kCAAkC;IACxC,MAAM2B,MAAM,GAAG,GAAGC,kBAAkB,CAACN,OAAO,CAAC,IAAIM,kBAAkB,CACjEL,WACF,CAAC,EAAE;IACH,IAAI,CAACM,QAAQ,GAAG,mBAAmBF,MAAM,QAAQ;IACjD,IAAI,CAACG,MAAM,GAAG,mBAAmBH,MAAM,MAAM;IAC7C,IAAI,CAACI,OAAO,GAAG,mBAAmBJ,MAAM,aAAa;EACvD;;EAEA;AACF;AACA;AACA;EACEK,aAAaA,CAAA,EAAG;IACd,OAAO,IAAI,CAACN,OAAO;EACrB;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;AACA;EACEO,MAAMA,CAAC1B,MAAM,EAAEC,KAAK,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,EAAEuB,UAAU,EAAE;IAC/D,IAAI;MACF,MAAMC,MAAM,GAAG,IAAI,CAACH,aAAa,CAAC,CAAC;MACnC,MAAMI,KAAK,GAAG9B,aAAa,CAACC,MAAM,EAAEC,KAAK,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,CAAC;MACzE,MAAM0B,GAAG,GAAGC,IAAI,CAACC,GAAG,CAAC,CAAC,EAAED,IAAI,CAACE,KAAK,CAACC,MAAM,CAACP,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;MAC5DC,MAAM,CACHO,KAAK,CAAC,CAAC,CACPC,OAAO,CAAC,IAAI,CAACd,QAAQ,EAAEO,KAAK,EAAE,CAAC,CAAC,CAChCO,OAAO,CAAC,IAAI,CAACb,MAAM,EAAEM,KAAK,EAAEC,GAAG,CAAC,CAChCO,MAAM,CAAC,IAAI,CAACf,QAAQ,EAAE,IAAI,CAACL,MAAM,CAAC,CAClCoB,MAAM,CAAC,IAAI,CAACd,MAAM,EAAE,IAAI,CAACN,MAAM,CAAC,CAChCqB,IAAI,CAACC,GAAG,IAAI;QACX,IAAIA,GAAG,EAAE;UACPC,OAAO,CAACC,KAAK,CAAC,wCAAwC,EAAEF,GAAG,CAACG,OAAO,CAAC;UACpE;QACF;QACAF,OAAO,CAACG,IAAI,CACV,GAAGjD,GAAG,sBAAsBkD,OAAO,CAACC,GAAG,aAAa,IAAI,CAACvB,QAAQ,WAAW,IAAI,CAACC,MAAM,KACzF,CAAC;MACH,CAAC,CAAC;IACN,CAAC,CAAC,OAAOuB,CAAC,EAAE;MACVN,OAAO,CAACC,KAAK,CAAC,iCAAiC,EAAEK,CAAC,CAACJ,OAAO,CAAC;IAC7D;EACF;;EAEA;AACF;AACA;AACA;AACA;EACEK,eAAeA,CAACC,UAAU,EAAEC,aAAa,EAAE;IACzC,IAAIrB,MAAM;IACV,IAAI;MACFA,MAAM,GAAG,IAAI,CAACH,aAAa,CAAC,CAAC;IAC/B,CAAC,CAAC,OAAOqB,CAAC,EAAE;MACVN,OAAO,CAACC,KAAK,CAAC,gCAAgC,EAAEK,CAAC,CAACJ,OAAO,CAAC;MAC1D,OAAOQ,OAAO,CAACpD,OAAO,CAAC,KAAK,CAAC;IAC/B;IACA,OAAO,IAAIoD,OAAO,CAACpD,OAAO,IAAI;MAC5B8B,MAAM,CAACuB,GAAG,CAAC,IAAI,CAAC3B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC4B,MAAM,EAAEC,EAAE,KAAK;QAC5D,IAAID,MAAM,IAAIC,EAAE,KAAK,IAAI,EAAE;UACzBb,OAAO,CAACG,IAAI,CACV,GAAGjD,GAAG,2BAA2BkD,OAAO,CAACC,GAAG,aAAa,IAAI,CAACvB,QAAQ,WACpE8B,MAAM,GAAG,SAASA,MAAM,CAACV,OAAO,EAAE,GAAG,mBAAmB,EAE5D,CAAC;UACD5C,OAAO,CAAC,KAAK,CAAC;UACd;QACF;QACA8B,MAAM,CAAC0B,IAAI,CACT3D,SAAS,EACT,CAAC,EACD,IAAI,CAAC2B,QAAQ,EACb,IAAI,CAACC,MAAM,EACX,CAACgC,OAAO,EAAEC,GAAG,KAAK;UAChB,MAAMC,MAAM,GAAGA,CAAA,KAAM;YACnB7B,MAAM,CAAC8B,GAAG,CAAC,IAAI,CAAClC,OAAO,EAAE,MAAM1B,OAAO,CAAC,IAAI,CAAC,CAAC;UAC/C,CAAC;UAED,IAAIyD,OAAO,EAAE;YACXf,OAAO,CAACC,KAAK,CACX,uCAAuC,EACvCc,OAAO,CAACb,OACV,CAAC;YACDe,MAAM,CAAC,CAAC;YACR;UACF;UAEA,IAAI;YACF,IAAI,CAACD,GAAG,IAAI,CAACG,KAAK,CAACC,OAAO,CAACJ,GAAG,CAAC,IAAIA,GAAG,CAAC9C,MAAM,GAAG,CAAC,EAAE;cACjD8B,OAAO,CAACG,IAAI,CACV,GAAGjD,GAAG,sBAAsBkD,OAAO,CAACC,GAAG,aAAa,IAAI,CAACvB,QAAQ,iDACnE,CAAC;cACDmC,MAAM,CAAC,CAAC;cACR;YACF;YACA,MAAMI,MAAM,GAAGtD,oBAAoB,CAACiD,GAAG,CAAC,CAAC,CAAC,CAAC;YAC3C,MAAMM,IAAI,GAAGvD,oBAAoB,CAACiD,GAAG,CAAC,CAAC,CAAC,CAAC;YACzC,MAAMO,SAAS,GAAGC,MAAM,CAACC,IAAI,CAACJ,MAAM,CAAC;YACrC,IAAIK,WAAW,GAAG,CAAC;YACnB,MAAMC,OAAO,GAAG,EAAE;YAClB,KAAK,MAAMtC,KAAK,IAAIkC,SAAS,EAAE;cAC7B,MAAMK,KAAK,GAAGC,QAAQ,CAACR,MAAM,CAAChC,KAAK,CAAC,EAAE,EAAE,CAAC;cACzC,IAAI,CAACuC,KAAK,IAAIA,KAAK,GAAG,CAAC,EAAE;gBACvB;cACF;cACA,MAAMtC,GAAG,GAAGuC,QAAQ,CAACP,IAAI,CAACjC,KAAK,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC;cACjD,MAAMyC,KAAK,GAAGzC,KAAK,CAAC0C,KAAK,CAAC/E,SAAS,CAAC;cACpC,IAAI8E,KAAK,CAAC5D,MAAM,KAAK,CAAC,EAAE;gBACtB;cACF;cACA,MAAM,CAAC8D,CAAC,EAAEvE,KAAK,EAAEwE,SAAS,EAAEC,GAAG,EAAEC,GAAG,CAAC,GAAGL,KAAK;cAC7CJ,WAAW,IAAIE,KAAK;cACpB,IAAID,OAAO,CAACzD,MAAM,GAAG,CAAC,EAAE;gBACtByD,OAAO,CAACS,IAAI,CAAC,GAAGJ,CAAC,IAAIvE,KAAK,IAAIwE,SAAS,KAAKL,KAAK,EAAE,CAAC;cACtD;cACA,MAAMS,MAAM,GAAG;gBACb7E,MAAM,EAAEwE,CAAC;gBACTvE,KAAK;gBACL6E,WAAW,EAAEL,SAAS;gBACtBtE,KAAK,EAAEuE,GAAG;gBACVtE,UAAU,EAAEuE;cACd,CAAC;cACD3B,UAAU,CAAC6B,MAAM,EAAET,KAAK,CAAC;cACzB,IAAItC,GAAG,GAAG,CAAC,EAAE;gBACXmB,aAAa,CAAC4B,MAAM,EAAE/C,GAAG,CAAC;cAC5B;YACF;YACAU,OAAO,CAACG,IAAI,CACV,GAAGjD,GAAG,sBAAsBkD,OAAO,CAACC,GAAG,aAAa,IAAI,CAACvB,QAAQ,gBAC/DyC,SAAS,CAACrD,MAAM,iBACDwD,WAAW,WAAWC,OAAO,CAAC7D,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,EACnE,CAAC;UACH,CAAC,CAAC,OAAOwC,CAAC,EAAE;YACVN,OAAO,CAACC,KAAK,CACX,6CAA6C,EAC7CK,CAAC,CAACJ,OACJ,CAAC;UACH;UACAe,MAAM,CAAC,CAAC;QACV,CACF,CAAC;MACH,CAAC,CAAC;IACJ,CAAC,CAAC;EACJ;AACF;AAEAsB,MAAM,CAACC,OAAO,GAAG;EACfpE,qBAAqB;EACrBb,aAAa;EACbP,SAAS;EACTI,oBAAoB;EACpBH;AACF,CAAC","ignoreList":[]}
|
|
@@ -35,8 +35,8 @@ 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** for API traffic is fixed **`web`**
|
|
39
|
+
* (`HttpMetricsRedisCollector` / `HttpMetricsRedisRecorder`), not `BUILD_DYNO_PROCESS_TYPE`.
|
|
40
40
|
*
|
|
41
41
|
* @module metrics/metricsProcessTypeUtils
|
|
42
42
|
*/
|
|
@@ -50,8 +50,6 @@ export const METRICS_PROCESS_TYPE_REDIS: "redis-metrics";
|
|
|
50
50
|
export const METRICS_PROCESS_TYPE_WEB: "web";
|
|
51
51
|
/** Build/compile workers and similar (e.g. backend `worker:`) — no HTTP server metrics here. */
|
|
52
52
|
export const METRICS_PROCESS_TYPE_WORKER: "worker";
|
|
53
|
-
/** HTTP Redis **drain + push** process (e.g. backend `http-metrics:` / `http-metrics-collector.ts`). */
|
|
54
|
-
export const METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR: "http-metrics";
|
|
55
53
|
/**
|
|
56
54
|
* Parent {@link RedisMetricsClient} allows either redis-only or queue stack (`QueueRedisMetricsClient`).
|
|
57
55
|
* @type {readonly string[]}
|
|
@@ -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":"AAoCA;;;;;;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;AAxFD;;;;;;;;;;GAUG;AAEH,6DAA6D;AAC7D,+DAAwD;AAExD,gEAAgE;AAChE,yDAAkD;AAElD,yEAAyE;AACzE,yDAAkD;AAElD,kGAAkG;AAClG,6CAAsC;AAEtC,gGAAgG;AAChG,mDAA4C;AAE5C;;;GAGG;AACH,yDAFU,SAAS,MAAM,EAAE,CAKzB"}
|
|
@@ -6,8 +6,8 @@
|
|
|
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** for API traffic is fixed **`web`**
|
|
10
|
+
* (`HttpMetricsRedisCollector` / `HttpMetricsRedisRecorder`), not `BUILD_DYNO_PROCESS_TYPE`.
|
|
11
11
|
*
|
|
12
12
|
* @module metrics/metricsProcessTypeUtils
|
|
13
13
|
*/
|
|
@@ -27,9 +27,6 @@ const METRICS_PROCESS_TYPE_WEB = 'web';
|
|
|
27
27
|
/** Build/compile workers and similar (e.g. backend `worker:`) — no HTTP server metrics here. */
|
|
28
28
|
const METRICS_PROCESS_TYPE_WORKER = 'worker';
|
|
29
29
|
|
|
30
|
-
/** HTTP Redis **drain + push** process (e.g. backend `http-metrics:` / `http-metrics-collector.ts`). */
|
|
31
|
-
const METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR = 'http-metrics';
|
|
32
|
-
|
|
33
30
|
/**
|
|
34
31
|
* Parent {@link RedisMetricsClient} allows either redis-only or queue stack (`QueueRedisMetricsClient`).
|
|
35
32
|
* @type {readonly string[]}
|
|
@@ -84,7 +81,6 @@ module.exports = {
|
|
|
84
81
|
METRICS_PROCESS_TYPE_REDIS,
|
|
85
82
|
METRICS_PROCESS_TYPE_WEB,
|
|
86
83
|
METRICS_PROCESS_TYPE_WORKER,
|
|
87
|
-
METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR,
|
|
88
84
|
REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES
|
|
89
85
|
};
|
|
90
86
|
//# sourceMappingURL=metricsProcessTypeUtils.js.map
|
|
@@ -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","
|
|
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","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** for API traffic is fixed **`web`**\n * (`HttpMetricsRedisCollector` / `HttpMetricsRedisRecorder`), not `BUILD_DYNO_PROCESS_TYPE`.\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/**\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 REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES,\n}\n"],"mappings":";;AAAA;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;AACA;AACA;AACA,MAAMC,0CAA0C,GAAGC,MAAM,CAACC,MAAM,CAAC,CAC/DL,0BAA0B,EAC1BD,0BAA0B,CAC3B,CAAC;;AAEF;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASO,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;EACvBnB,6BAA6B;EAC7BC,0BAA0B;EAC1BC,0BAA0B;EAC1BC,wBAAwB;EACxBC,2BAA2B;EAC3BC;AACF,CAAC","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -4,8 +4,9 @@ const { HttpMetricsRedisStore } = require('./httpMetricsRedisStore')
|
|
|
4
4
|
/**
|
|
5
5
|
* Drain worker: reads HTTP aggregates from Redis (written by {@link HttpMetricsRedisRecorder}),
|
|
6
6
|
* applies them to `app_requests_*` counters, then pushes the registry to the VM-agent (same as {@link BaseMetricsClient}).
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* **Minimal usage:** `{ redisClient }` only. Redis keys use segment **`web`** unless you pass **`redisProcessTypeForKeys`**.
|
|
8
|
+
* `processType` / `appName` / `dynoId` follow {@link BaseMetricsClient} defaults (e.g. `BUILD_DYNO_PROCESS_TYPE`) and do **not** select Redis hash names.
|
|
9
|
+
* Always passes `blockNodeDefaultMetrics: true` (HTTP-focused registry).
|
|
9
10
|
*
|
|
10
11
|
* @extends BaseMetricsClient
|
|
11
12
|
*/
|
|
@@ -24,7 +25,7 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
24
25
|
* @param {boolean} [config.removeOldMetrics] Clear old series on shutdown where supported
|
|
25
26
|
* @param {function} [config.startupValidation] Run before first push
|
|
26
27
|
* @param {boolean} [config.disablePushgateway] Skip POST to VM-agent
|
|
27
|
-
* @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default
|
|
28
|
+
* @param {string} [config.redisProcessTypeForKeys] Segment in Redis keys for HTTP hashes (default **`web`**). Optional; only if writers use a non-`web` segment.
|
|
28
29
|
* @param {number} [config.ttlSec] Passed to {@link HttpMetricsRedisStore} (should match writers)
|
|
29
30
|
*/
|
|
30
31
|
constructor(config = {}) {
|
|
@@ -38,10 +39,7 @@ class HttpMetricsRedisCollector extends BaseMetricsClient {
|
|
|
38
39
|
blockNodeDefaultMetrics: true,
|
|
39
40
|
})
|
|
40
41
|
|
|
41
|
-
const keyProcessType =
|
|
42
|
-
config.redisProcessTypeForKeys ||
|
|
43
|
-
process.env.METRICS_HTTP_REDIS_KEY_PROCESS_TYPE ||
|
|
44
|
-
'web'
|
|
42
|
+
const keyProcessType = config.redisProcessTypeForKeys || 'web'
|
|
45
43
|
|
|
46
44
|
this.defaultLabelsWithoutDynoId = {
|
|
47
45
|
app: this.appName,
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
const { HttpMetricsRedisStore } = require('./httpMetricsRedisStore')
|
|
2
2
|
|
|
3
|
+
const LOG = '[http-metrics-redis]'
|
|
4
|
+
|
|
5
|
+
function trunc(s, max = 120) {
|
|
6
|
+
const t = String(s)
|
|
7
|
+
return t.length > max ? `${t.slice(0, max - 3)}...` : t
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
/**
|
|
4
11
|
* Records HTTP request aggregates only to Redis (no in-process Prometheus counters on this path).
|
|
5
12
|
* Pair with {@link HttpMetricsRedisCollector} on a drain process to flush into counters and push to the VM-agent.
|
|
@@ -45,6 +52,11 @@ class HttpMetricsRedisRecorder {
|
|
|
45
52
|
databaseId = '',
|
|
46
53
|
duration,
|
|
47
54
|
}) {
|
|
55
|
+
console.warn(
|
|
56
|
+
`${LOG} 1_track pid=${process.pid} app=${this.appName} segment=${this.processType} ` +
|
|
57
|
+
`method=${method} route=${trunc(route)} status=${status_code} durationMs=${duration} ` +
|
|
58
|
+
`appId=${trunc(appId || '—', 40)} databaseId=${trunc(databaseId || '—', 40)}`
|
|
59
|
+
)
|
|
48
60
|
this._store.record(method, route, status_code, appId, databaseId, duration)
|
|
49
61
|
}
|
|
50
62
|
|
|
@@ -10,6 +10,8 @@ const FIELD_SEP = '\x1e'
|
|
|
10
10
|
*/
|
|
11
11
|
const DEFAULT_HTTP_METRICS_REDIS_TTL_SEC = 120
|
|
12
12
|
|
|
13
|
+
const LOG = '[http-metrics-redis]'
|
|
14
|
+
|
|
13
15
|
const DRAIN_LUA = `
|
|
14
16
|
local function drain(key)
|
|
15
17
|
local v = redis.call('HGETALL', key)
|
|
@@ -116,7 +118,11 @@ class HttpMetricsRedisStore {
|
|
|
116
118
|
.exec(err => {
|
|
117
119
|
if (err) {
|
|
118
120
|
console.error('[HttpMetricsRedisStore] record failed:', err.message)
|
|
121
|
+
return
|
|
119
122
|
}
|
|
123
|
+
console.warn(
|
|
124
|
+
`${LOG} 2_redis_write pid=${process.pid} countKey=${this.countKey} durKey=${this.durKey} ok`
|
|
125
|
+
)
|
|
120
126
|
})
|
|
121
127
|
} catch (e) {
|
|
122
128
|
console.error('[HttpMetricsRedisStore] record:', e.message)
|
|
@@ -139,6 +145,11 @@ class HttpMetricsRedisStore {
|
|
|
139
145
|
return new Promise(resolve => {
|
|
140
146
|
client.set(this.lockKey, '1', 'EX', 25, 'NX', (setErr, ok) => {
|
|
141
147
|
if (setErr || ok !== 'OK') {
|
|
148
|
+
console.warn(
|
|
149
|
+
`${LOG} 3_redis_drain_skip pid=${process.pid} countKey=${this.countKey} reason=${
|
|
150
|
+
setErr ? `error:${setErr.message}` : 'lock_not_acquired'
|
|
151
|
+
}`
|
|
152
|
+
)
|
|
142
153
|
resolve(false)
|
|
143
154
|
return
|
|
144
155
|
}
|
|
@@ -163,12 +174,17 @@ class HttpMetricsRedisStore {
|
|
|
163
174
|
|
|
164
175
|
try {
|
|
165
176
|
if (!raw || !Array.isArray(raw) || raw.length < 2) {
|
|
177
|
+
console.warn(
|
|
178
|
+
`${LOG} 3_redis_drain pid=${process.pid} countKey=${this.countKey} hash_fields=0 sum_requests=0 (empty_after_lua)`
|
|
179
|
+
)
|
|
166
180
|
finish()
|
|
167
181
|
return
|
|
168
182
|
}
|
|
169
183
|
const counts = hgetallPairsToObject(raw[0])
|
|
170
184
|
const durs = hgetallPairsToObject(raw[1])
|
|
171
185
|
const fieldKeys = Object.keys(counts)
|
|
186
|
+
let sumRequests = 0
|
|
187
|
+
const samples = []
|
|
172
188
|
for (const field of fieldKeys) {
|
|
173
189
|
const count = parseInt(counts[field], 10)
|
|
174
190
|
if (!count || count < 1) {
|
|
@@ -180,6 +196,10 @@ class HttpMetricsRedisStore {
|
|
|
180
196
|
continue
|
|
181
197
|
}
|
|
182
198
|
const [m, route, statusStr, aid, did] = parts
|
|
199
|
+
sumRequests += count
|
|
200
|
+
if (samples.length < 3) {
|
|
201
|
+
samples.push(`${m} ${route} ${statusStr} x${count}`)
|
|
202
|
+
}
|
|
183
203
|
const labels = {
|
|
184
204
|
method: m,
|
|
185
205
|
route,
|
|
@@ -192,6 +212,11 @@ class HttpMetricsRedisStore {
|
|
|
192
212
|
applyDuration(labels, dur)
|
|
193
213
|
}
|
|
194
214
|
}
|
|
215
|
+
console.warn(
|
|
216
|
+
`${LOG} 3_redis_drain pid=${process.pid} countKey=${this.countKey} hash_fields=${
|
|
217
|
+
fieldKeys.length
|
|
218
|
+
} sum_requests=${sumRequests} sample=${samples.join(' | ') || '—'}`
|
|
219
|
+
)
|
|
195
220
|
} catch (e) {
|
|
196
221
|
console.error(
|
|
197
222
|
'[HttpMetricsRedisStore] flush apply failed:',
|
|
@@ -4,8 +4,8 @@
|
|
|
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** for API traffic is fixed **`web`**
|
|
8
|
+
* (`HttpMetricsRedisCollector` / `HttpMetricsRedisRecorder`), not `BUILD_DYNO_PROCESS_TYPE`.
|
|
9
9
|
*
|
|
10
10
|
* @module metrics/metricsProcessTypeUtils
|
|
11
11
|
*/
|
|
@@ -25,9 +25,6 @@ const METRICS_PROCESS_TYPE_WEB = 'web'
|
|
|
25
25
|
/** Build/compile workers and similar (e.g. backend `worker:`) — no HTTP server metrics here. */
|
|
26
26
|
const METRICS_PROCESS_TYPE_WORKER = 'worker'
|
|
27
27
|
|
|
28
|
-
/** HTTP Redis **drain + push** process (e.g. backend `http-metrics:` / `http-metrics-collector.ts`). */
|
|
29
|
-
const METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR = 'http-metrics'
|
|
30
|
-
|
|
31
28
|
/**
|
|
32
29
|
* Parent {@link RedisMetricsClient} allows either redis-only or queue stack (`QueueRedisMetricsClient`).
|
|
33
30
|
* @type {readonly string[]}
|
|
@@ -100,6 +97,5 @@ module.exports = {
|
|
|
100
97
|
METRICS_PROCESS_TYPE_REDIS,
|
|
101
98
|
METRICS_PROCESS_TYPE_WEB,
|
|
102
99
|
METRICS_PROCESS_TYPE_WORKER,
|
|
103
|
-
METRICS_PROCESS_TYPE_HTTP_METRICS_COLLECTOR,
|
|
104
100
|
REDIS_METRICS_CLIENT_ALLOWED_PROCESS_TYPES,
|
|
105
101
|
}
|