@adalo/metrics 0.0.0-staging.3 → 0.0.0-staging.31

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.
Files changed (64) hide show
  1. package/README.md +8 -14
  2. package/__tests__/httpMetricsRedisCollector.test.js +203 -0
  3. package/__tests__/httpMetricsRedisRecorder.test.js +60 -0
  4. package/__tests__/httpMetricsRedisStore.test.js +431 -0
  5. package/docs/http-metrics-redis.md +19 -0
  6. package/lib/health/healthCheckClient.d.ts +8 -0
  7. package/lib/health/healthCheckClient.d.ts.map +1 -1
  8. package/lib/health/healthCheckClient.js +36 -5
  9. package/lib/health/healthCheckClient.js.map +1 -1
  10. package/lib/health/healthCheckWorker.d.ts.map +1 -1
  11. package/lib/health/healthCheckWorker.js +15 -1
  12. package/lib/health/healthCheckWorker.js.map +1 -1
  13. package/lib/index.d.ts +4 -0
  14. package/lib/index.d.ts.map +1 -1
  15. package/lib/index.js +44 -0
  16. package/lib/index.js.map +1 -1
  17. package/lib/metrics/baseMetricsClient.d.ts +18 -1
  18. package/lib/metrics/baseMetricsClient.d.ts.map +1 -1
  19. package/lib/metrics/baseMetricsClient.js +28 -5
  20. package/lib/metrics/baseMetricsClient.js.map +1 -1
  21. package/lib/metrics/httpMetricsRedisCollector.d.ts +50 -0
  22. package/lib/metrics/httpMetricsRedisCollector.d.ts.map +1 -0
  23. package/lib/metrics/httpMetricsRedisCollector.js +115 -0
  24. package/lib/metrics/httpMetricsRedisCollector.js.map +1 -0
  25. package/lib/metrics/httpMetricsRedisRecorder.d.ts +48 -0
  26. package/lib/metrics/httpMetricsRedisRecorder.d.ts.map +1 -0
  27. package/lib/metrics/httpMetricsRedisRecorder.js +86 -0
  28. package/lib/metrics/httpMetricsRedisRecorder.js.map +1 -0
  29. package/lib/metrics/httpMetricsRedisStore.d.ts +88 -0
  30. package/lib/metrics/httpMetricsRedisStore.d.ts.map +1 -0
  31. package/lib/metrics/httpMetricsRedisStore.js +223 -0
  32. package/lib/metrics/httpMetricsRedisStore.js.map +1 -0
  33. package/lib/metrics/metricsClient.d.ts +34 -27
  34. package/lib/metrics/metricsClient.d.ts.map +1 -1
  35. package/lib/metrics/metricsClient.js +37 -37
  36. package/lib/metrics/metricsClient.js.map +1 -1
  37. package/lib/metrics/metricsDatabaseClient.d.ts +1 -1
  38. package/lib/metrics/metricsDatabaseClient.d.ts.map +1 -1
  39. package/lib/metrics/metricsDatabaseClient.js +9 -4
  40. package/lib/metrics/metricsDatabaseClient.js.map +1 -1
  41. package/lib/metrics/metricsProcessTypeUtils.d.ts +58 -0
  42. package/lib/metrics/metricsProcessTypeUtils.d.ts.map +1 -0
  43. package/lib/metrics/metricsProcessTypeUtils.js +86 -0
  44. package/lib/metrics/metricsProcessTypeUtils.js.map +1 -0
  45. package/lib/metrics/metricsQueueRedisClient.d.ts +1 -1
  46. package/lib/metrics/metricsQueueRedisClient.d.ts.map +1 -1
  47. package/lib/metrics/metricsQueueRedisClient.js +8 -3
  48. package/lib/metrics/metricsQueueRedisClient.js.map +1 -1
  49. package/lib/metrics/metricsRedisClient.d.ts.map +1 -1
  50. package/lib/metrics/metricsRedisClient.js +7 -1
  51. package/lib/metrics/metricsRedisClient.js.map +1 -1
  52. package/package.json +6 -6
  53. package/src/health/healthCheckClient.js +40 -5
  54. package/src/health/healthCheckWorker.js +18 -1
  55. package/src/index.ts +4 -0
  56. package/src/metrics/baseMetricsClient.js +33 -3
  57. package/src/metrics/httpMetricsRedisCollector.js +121 -0
  58. package/src/metrics/httpMetricsRedisRecorder.js +74 -0
  59. package/src/metrics/httpMetricsRedisStore.js +208 -0
  60. package/src/metrics/metricsClient.js +38 -55
  61. package/src/metrics/metricsDatabaseClient.js +10 -4
  62. package/src/metrics/metricsProcessTypeUtils.js +98 -0
  63. package/src/metrics/metricsQueueRedisClient.js +9 -3
  64. package/src/metrics/metricsRedisClient.js +12 -1
package/README.md CHANGED
@@ -25,8 +25,15 @@ new MetricsClient({
25
25
  pushgatewayUrl, // defaults: process.env.METRICS_PUSHGATEWAY_URL || ''
26
26
  pushgatewaySecret, // defaults: process.env.METRICS_PUSHGATEWAY_SECRET || '' (Base64 of user:password)
27
27
  intervalSec, // defaults: process.env.METRICS_INTERVAL_SEC || 15
28
+ httpMetricsEnabled, // defaults: METRICS_HTTP_ENABLED === 'true' (in-process HTTP counters only)
28
29
  })
29
30
  ```
31
+
32
+ ### HTTP metrics: `MetricsClient` vs Redis
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`. See **[`docs/http-metrics-redis.md`](docs/http-metrics-redis.md)** (quick Redis reference) and the repo guide **[`../docs/METRICS.md`](../docs/METRICS.md)** (full architecture for devs and AI assistants).
35
+
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.
30
37
  ## Example Usage
31
38
  ```ts
32
39
  import { MetricsClient } from '@adalo/metrics-js'
@@ -117,17 +124,4 @@ Environment variables:
117
124
  secret env was created as
118
125
  ```js
119
126
  Buffer.from(`${username}:${password}`).toString('base64')
120
- ```
121
-
122
- ## Troubleshooting (consumers)
123
-
124
- **TypeScript build errors in `node_modules/@types/node`** (e.g. `TS1005: '?' expected` in `crypto.d.ts` or `http.d.ts`):
125
- These come from a version of `@types/node` that requires TypeScript 4.7+. If your app uses TypeScript 4.6 or older, force an older `@types/node` by adding to your app’s `package.json`:
126
-
127
- ```json
128
- "resolutions": {
129
- "@types/node": "^16.0.0"
130
- }
131
- ```
132
-
133
- Then run `yarn install` again and rebuild. Alternatively, upgrade your app to TypeScript 4.7+.
127
+ ```
@@ -0,0 +1,203 @@
1
+ const { HttpMetricsRedisCollector } = require('../src/metrics/httpMetricsRedisCollector')
2
+ const { buildFieldKey } = require('../src/metrics/httpMetricsRedisStore')
3
+
4
+ function appRequestTotalsByRoute(metricsJson) {
5
+ const reqTotal = metricsJson.find(m => m.name === 'app_requests_total')
6
+ if (!reqTotal || !reqTotal.values) {
7
+ return {}
8
+ }
9
+ return Object.fromEntries(reqTotal.values.map(v => [v.labels.route, v.value]))
10
+ }
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
+ eval: jest.fn(),
25
+ _multiChain: multiChain,
26
+ }
27
+ }
28
+
29
+ describe('HttpMetricsRedisCollector', () => {
30
+ const originalEnv = process.env
31
+
32
+ beforeEach(() => {
33
+ process.env = { ...originalEnv, METRICS_DISABLE_PUSHGATEWAY: 'true' }
34
+ })
35
+
36
+ afterEach(() => {
37
+ process.env = originalEnv
38
+ })
39
+
40
+ it('throws without redisClient', () => {
41
+ expect(() => new HttpMetricsRedisCollector({ appName: 'a' })).toThrow('redisClient is required')
42
+ })
43
+
44
+ it('pushMetrics drains Redis then completes without network push', async () => {
45
+ const redis = createRedisV3Mock()
46
+ const field = buildFieldKey('GET', '/health', 200)
47
+ redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
48
+ cb(null, [
49
+ [field, '1'],
50
+ [field, '10'],
51
+ ])
52
+ })
53
+
54
+ const collector = new HttpMetricsRedisCollector({
55
+ redisClient: redis,
56
+ appName: 'test-app',
57
+ dynoId: 'dyno-1',
58
+ processType: 'http-metrics',
59
+ redisProcessTypeForKeys: 'web',
60
+ disablePushgateway: true,
61
+ })
62
+
63
+ const pushSpy = jest.spyOn(collector, 'gatewayPush').mockResolvedValue()
64
+
65
+ await collector.pushMetrics()
66
+
67
+ expect(redis.eval).toHaveBeenCalled()
68
+ expect(pushSpy).toHaveBeenCalledTimes(1)
69
+ const metrics = await collector._registry.getMetricsAsJSON()
70
+ const total = metrics.find(m => m.name === 'app_requests_total')
71
+ expect(total).toBeDefined()
72
+ const totalDur = metrics.find(m => m.name === 'app_requests_total_duration')
73
+ expect(totalDur).toBeDefined()
74
+
75
+ pushSpy.mockRestore()
76
+ })
77
+
78
+ it('primes new label sets with 0 then applies count (extra push only when labels are new)', async () => {
79
+ const redis = createRedisV3Mock()
80
+ const field = buildFieldKey('GET', '/health', 200)
81
+
82
+ const collector = new HttpMetricsRedisCollector({
83
+ redisClient: redis,
84
+ appName: 'test-app',
85
+ dynoId: 'dyno-1',
86
+ processType: 'http-metrics',
87
+ redisProcessTypeForKeys: 'web',
88
+ disablePushgateway: true,
89
+ })
90
+
91
+ const pushSpy = jest.spyOn(collector, 'gatewayPush').mockResolvedValue()
92
+
93
+ redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
94
+ cb(null, [
95
+ [field, '2'],
96
+ [field, '0'],
97
+ ])
98
+ })
99
+ await collector.pushMetrics()
100
+ expect(pushSpy).toHaveBeenCalledTimes(1)
101
+
102
+ redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
103
+ cb(null, [
104
+ [field, '1'],
105
+ [field, '0'],
106
+ ])
107
+ })
108
+ await collector.pushMetrics()
109
+ expect(pushSpy).toHaveBeenCalledTimes(1)
110
+
111
+ pushSpy.mockRestore()
112
+ })
113
+
114
+ it('prime push for new route a leaves existing route b unchanged (a=0, b=18), then a=1, b=38', async () => {
115
+ const redis = createRedisV3Mock()
116
+ const fieldB = buildFieldKey('GET', '/b', 200)
117
+ const fieldA = buildFieldKey('GET', '/a', 200)
118
+
119
+ const collector = new HttpMetricsRedisCollector({
120
+ redisClient: redis,
121
+ appName: 'test-app',
122
+ dynoId: 'dyno-1',
123
+ processType: 'http-metrics',
124
+ redisProcessTypeForKeys: 'web',
125
+ disablePushgateway: true,
126
+ })
127
+
128
+ let primePushCount = 0
129
+ const pushSpy = jest.spyOn(collector, 'gatewayPush').mockImplementation(async () => {
130
+ primePushCount++
131
+ if (primePushCount === 2) {
132
+ const metrics = await collector._registry.getMetricsAsJSON()
133
+ const byRoute = appRequestTotalsByRoute(metrics)
134
+ expect(byRoute['/a']).toBe(0)
135
+ expect(byRoute['/b']).toBe(18)
136
+ }
137
+ })
138
+
139
+ redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
140
+ cb(null, [
141
+ [fieldB, '18'],
142
+ [fieldB, '0'],
143
+ ])
144
+ })
145
+ await collector.pushMetrics()
146
+
147
+ redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
148
+ cb(null, [
149
+ [fieldA, '1', fieldB, '20'],
150
+ [fieldA, '0', fieldB, '0'],
151
+ ])
152
+ })
153
+ await collector.pushMetrics()
154
+
155
+ const final = appRequestTotalsByRoute(await collector._registry.getMetricsAsJSON())
156
+ expect(final['/a']).toBe(1)
157
+ expect(final['/b']).toBe(38)
158
+ expect(pushSpy).toHaveBeenCalledTimes(2)
159
+
160
+ pushSpy.mockRestore()
161
+ })
162
+
163
+ it('does not prime POST when drain only has already-seen routes (was a:1 b:18, then a:1 b:20)', async () => {
164
+ const redis = createRedisV3Mock()
165
+ const fieldA = buildFieldKey('GET', '/a', 200)
166
+ const fieldB = buildFieldKey('GET', '/b', 200)
167
+
168
+ const collector = new HttpMetricsRedisCollector({
169
+ redisClient: redis,
170
+ appName: 'test-app',
171
+ dynoId: 'dyno-1',
172
+ processType: 'http-metrics',
173
+ redisProcessTypeForKeys: 'web',
174
+ disablePushgateway: true,
175
+ })
176
+
177
+ const pushSpy = jest.spyOn(collector, 'gatewayPush').mockResolvedValue()
178
+
179
+ redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
180
+ cb(null, [
181
+ [fieldA, '1', fieldB, '18'],
182
+ [fieldA, '0', fieldB, '0'],
183
+ ])
184
+ })
185
+ await collector.pushMetrics()
186
+ expect(pushSpy).toHaveBeenCalledTimes(1)
187
+
188
+ redis.eval.mockImplementation((lua, numKeys, k1, k2, cb) => {
189
+ cb(null, [
190
+ [fieldA, '1', fieldB, '20'],
191
+ [fieldA, '0', fieldB, '0'],
192
+ ])
193
+ })
194
+ await collector.pushMetrics()
195
+ expect(pushSpy).toHaveBeenCalledTimes(1)
196
+
197
+ const final = appRequestTotalsByRoute(await collector._registry.getMetricsAsJSON())
198
+ expect(final['/a']).toBe(2)
199
+ expect(final['/b']).toBe(38)
200
+
201
+ pushSpy.mockRestore()
202
+ })
203
+ })
@@ -0,0 +1,60 @@
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
+ const originalEnv = process.env
27
+
28
+ beforeEach(() => {
29
+ process.env = { ...originalEnv, BUILD_APP_NAME: 'app' }
30
+ })
31
+
32
+ afterEach(() => {
33
+ process.env = originalEnv
34
+ })
35
+
36
+ it('throws without redisClient', () => {
37
+ expect(() => new HttpMetricsRedisRecorder({})).toThrow('redisClient is required')
38
+ })
39
+
40
+ it('trackHttpRequest forwards to Redis multi/hincrby', async () => {
41
+ const redis = createRedisV3Mock()
42
+ const rec = new HttpMetricsRedisRecorder({
43
+ redisClient: redis,
44
+ })
45
+ const countKey = rec._store.countKey
46
+ const durKey = rec._store.durKey
47
+ const field = buildFieldKey('GET', '/p', 404)
48
+
49
+ rec.trackHttpRequest({
50
+ method: 'GET',
51
+ route: '/p',
52
+ status_code: 404,
53
+ duration: 5,
54
+ })
55
+ await flushMicrotasks()
56
+
57
+ expect(redis._multiChain.hincrby).toHaveBeenCalledWith(countKey, field, 1)
58
+ expect(redis._multiChain.hincrby).toHaveBeenCalledWith(durKey, field, 5)
59
+ })
60
+ })