@platformatic/watt-extra 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/app.js +124 -0
- package/cli.js +141 -0
- package/clients/compliance/compliance-types.d.ts +887 -0
- package/clients/compliance/compliance.mjs +1049 -0
- package/clients/compliance/compliance.openapi.json +6127 -0
- package/clients/control-plane/control-plane-types.d.ts +2696 -0
- package/clients/control-plane/control-plane.mjs +3051 -0
- package/clients/control-plane/control-plane.openapi.json +13693 -0
- package/clients/cron/cron-types.d.ts +1479 -0
- package/clients/cron/cron.mjs +872 -0
- package/clients/cron/cron.openapi.json +9330 -0
- package/compliance/index.js +21 -0
- package/compliance/rules/dependencies.js +76 -0
- package/compliance/rules/utils.js +12 -0
- package/eslint.config.js +11 -0
- package/help/start.txt +12 -0
- package/help/watt-extra.txt +12 -0
- package/index.js +45 -0
- package/lib/banner.js +22 -0
- package/lib/errors.js +34 -0
- package/lib/utils.js +34 -0
- package/lib/wattpro.js +580 -0
- package/package.json +50 -0
- package/plugins/alerts.js +115 -0
- package/plugins/auth.js +89 -0
- package/plugins/compliancy.js +70 -0
- package/plugins/env.js +58 -0
- package/plugins/flamegraphs.js +100 -0
- package/plugins/init.js +70 -0
- package/plugins/metadata.js +84 -0
- package/plugins/scheduler.js +48 -0
- package/plugins/update.js +128 -0
- package/renovate.json +6 -0
- package/test/alerts.test.js +607 -0
- package/test/auth.test.js +128 -0
- package/test/auto-cache.test.js +401 -0
- package/test/cli.test.js +75 -0
- package/test/compliancy.test.js +87 -0
- package/test/fixtures/runtime-domains/alpha/package.json +5 -0
- package/test/fixtures/runtime-domains/alpha/platformatic.json +6 -0
- package/test/fixtures/runtime-domains/alpha/plugin.js +16 -0
- package/test/fixtures/runtime-domains/beta/package.json +5 -0
- package/test/fixtures/runtime-domains/beta/platformatic.json +6 -0
- package/test/fixtures/runtime-domains/beta/plugin.js +7 -0
- package/test/fixtures/runtime-domains/composer/package.json +5 -0
- package/test/fixtures/runtime-domains/composer/platformatic.json +19 -0
- package/test/fixtures/runtime-domains/package.json +1 -0
- package/test/fixtures/runtime-domains/platformatic.json +27 -0
- package/test/fixtures/runtime-health/package.json +20 -0
- package/test/fixtures/runtime-health/platformatic.json +16 -0
- package/test/fixtures/runtime-health/services/service-1/package.json +17 -0
- package/test/fixtures/runtime-health/services/service-1/platformatic.json +16 -0
- package/test/fixtures/runtime-health/services/service-1/plugins/example.js +6 -0
- package/test/fixtures/runtime-health/services/service-1/routes/root.cjs +8 -0
- package/test/fixtures/runtime-health/services/service-2/package.json +17 -0
- package/test/fixtures/runtime-health/services/service-2/platformatic.json +16 -0
- package/test/fixtures/runtime-health/services/service-2/plugins/example.js +6 -0
- package/test/fixtures/runtime-health/services/service-2/routes/root.cjs +8 -0
- package/test/fixtures/runtime-next/package.json +5 -0
- package/test/fixtures/runtime-next/platformatic.json +9 -0
- package/test/fixtures/runtime-next/web/next/next.config.js +2 -0
- package/test/fixtures/runtime-next/web/next/package.json +7 -0
- package/test/fixtures/runtime-next/web/next/platformatic.json +9 -0
- package/test/fixtures/runtime-next/web/next/src/app/direct/route.js +3 -0
- package/test/fixtures/runtime-next/web/next/src/app/layout.jsx +7 -0
- package/test/fixtures/runtime-next/web/next/src/app/page.jsx +3 -0
- package/test/fixtures/runtime-scheduler/main/package.json +5 -0
- package/test/fixtures/runtime-scheduler/main/platformatic.json +9 -0
- package/test/fixtures/runtime-scheduler/main/routes/root.cjs +11 -0
- package/test/fixtures/runtime-scheduler/package.json +1 -0
- package/test/fixtures/runtime-scheduler/platformatic.json +27 -0
- package/test/fixtures/runtime-service/main/package.json +5 -0
- package/test/fixtures/runtime-service/main/platformatic.json +12 -0
- package/test/fixtures/runtime-service/main/routes/root.cjs +11 -0
- package/test/fixtures/runtime-service/package.json +1 -0
- package/test/fixtures/runtime-service/platformatic.json +19 -0
- package/test/fixtures/service-1/package.json +7 -0
- package/test/fixtures/service-1/platformatic.json +18 -0
- package/test/fixtures/service-1/routes/root.cjs +48 -0
- package/test/fixtures/service-2/platformatic.json +21 -0
- package/test/fixtures/service-2/routes/root.cjs +5 -0
- package/test/fixtures/service-3/package.json +5 -0
- package/test/fixtures/service-3/platformatic.json +21 -0
- package/test/fixtures/service-3/routes/root.cjs +8 -0
- package/test/health.test.js +44 -0
- package/test/helper.js +274 -0
- package/test/init.test.js +243 -0
- package/test/patch-config.test.js +434 -0
- package/test/scheduler.test.js +71 -0
- package/test/send-to-icc-retry.test.js +138 -0
- package/test/shared-context.test.js +82 -0
- package/test/spawn.test.js +110 -0
- package/test/trigger-flamegraphs.test.js +226 -0
- package/test/update.test.js +519 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
import { join, dirname } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
7
|
+
import { Profile } from 'pprof-format'
|
|
8
|
+
import { setUpEnvironment, startICC } from './helper.js'
|
|
9
|
+
import { start } from '../index.js'
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
12
|
+
const __dirname = dirname(__filename)
|
|
13
|
+
|
|
14
|
+
test('should send alert when service becomes unhealthy', async (t) => {
|
|
15
|
+
const applicationName = 'test-app'
|
|
16
|
+
const applicationId = randomUUID()
|
|
17
|
+
const applicationPath = join(__dirname, 'fixtures', 'service-1')
|
|
18
|
+
|
|
19
|
+
let alertReceived = null
|
|
20
|
+
let flamegraphReceived = null
|
|
21
|
+
|
|
22
|
+
const getAuthorizationHeader = async (headers) => {
|
|
23
|
+
return { ...headers, authorization: 'Bearer test-token' }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const icc = await startICC(t, {
|
|
27
|
+
applicationId,
|
|
28
|
+
applicationName,
|
|
29
|
+
processAlerts: (req) => {
|
|
30
|
+
const alert = req.body
|
|
31
|
+
assert.equal(req.headers.authorization, 'Bearer test-token')
|
|
32
|
+
alertReceived = alert
|
|
33
|
+
return { id: 'test-alert-id', ...alert }
|
|
34
|
+
},
|
|
35
|
+
processFlamegraphs: (req) => {
|
|
36
|
+
const alertId = req.query.alertId
|
|
37
|
+
assert.strictEqual(alertId, 'test-alert-id')
|
|
38
|
+
assert.strictEqual(req.headers.authorization, 'Bearer test-token')
|
|
39
|
+
flamegraphReceived = req.body
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
setUpEnvironment({
|
|
44
|
+
PLT_APP_NAME: applicationName,
|
|
45
|
+
PLT_APP_DIR: applicationPath,
|
|
46
|
+
PLT_ICC_URL: 'http://127.0.0.1:3000',
|
|
47
|
+
PLT_DISABLE_FLAMEGRAPHS: false,
|
|
48
|
+
PLT_FLAMEGRAPHS_INTERVAL_SEC: 2
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const app = await start()
|
|
52
|
+
app.getAuthorizationHeader = getAuthorizationHeader
|
|
53
|
+
|
|
54
|
+
await app.setupAlerts()
|
|
55
|
+
|
|
56
|
+
t.after(async () => {
|
|
57
|
+
await app.close()
|
|
58
|
+
await icc.close()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Wait for the first flamegraph to be generated
|
|
62
|
+
await sleep(5000)
|
|
63
|
+
|
|
64
|
+
// Manually trigger health event with unhealthy state
|
|
65
|
+
const healthInfo = {
|
|
66
|
+
id: 'main:0',
|
|
67
|
+
service: 'main',
|
|
68
|
+
currentHealth: {
|
|
69
|
+
elu: 0.95,
|
|
70
|
+
heapUsed: 76798040,
|
|
71
|
+
heapTotal: 99721216
|
|
72
|
+
},
|
|
73
|
+
unhealthy: true,
|
|
74
|
+
healthConfig: {
|
|
75
|
+
enabled: true,
|
|
76
|
+
interval: 1000,
|
|
77
|
+
gracePeriod: 1000,
|
|
78
|
+
maxUnhealthyChecks: 10,
|
|
79
|
+
maxELU: 0.99,
|
|
80
|
+
maxHeapUsed: 0.99,
|
|
81
|
+
maxHeapTotal: 4294967296
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
app.wattpro.runtime.emit('health', healthInfo)
|
|
86
|
+
|
|
87
|
+
await sleep(200)
|
|
88
|
+
|
|
89
|
+
assert.ok(alertReceived, 'Alert should have been received')
|
|
90
|
+
assert.strictEqual(alertReceived.applicationId, applicationId)
|
|
91
|
+
assert.deepStrictEqual(alertReceived.alert, healthInfo)
|
|
92
|
+
assert.ok(Array.isArray(alertReceived.healthHistory), 'Health history should be an array')
|
|
93
|
+
assert.ok(alertReceived.healthHistory.length > 0, 'Health history should not be empty')
|
|
94
|
+
assert.strictEqual(alertReceived.healthHistory[0].service, 'main')
|
|
95
|
+
|
|
96
|
+
assert.ok(flamegraphReceived, 'Flamegraph should have been received')
|
|
97
|
+
|
|
98
|
+
const profile = Profile.decode(flamegraphReceived)
|
|
99
|
+
assert.ok(profile, 'Profile should be decoded')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('should not send alert when service is healthy', async (t) => {
|
|
103
|
+
const applicationName = 'test-app'
|
|
104
|
+
const applicationId = randomUUID()
|
|
105
|
+
const applicationPath = join(__dirname, 'fixtures', 'service-1')
|
|
106
|
+
|
|
107
|
+
let alertReceived = null
|
|
108
|
+
|
|
109
|
+
const getAuthorizationHeader = async (headers) => {
|
|
110
|
+
return { ...headers, authorization: 'Bearer test-token' }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const icc = await startICC(t, {
|
|
114
|
+
applicationId,
|
|
115
|
+
applicationName,
|
|
116
|
+
processAlerts: (req) => {
|
|
117
|
+
const alert = req.body
|
|
118
|
+
assert.equal(req.headers.authorization, 'Bearer test-token')
|
|
119
|
+
alertReceived = alert
|
|
120
|
+
return { id: 'test-alert-id', ...alert }
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
setUpEnvironment({
|
|
125
|
+
PLT_APP_NAME: applicationName,
|
|
126
|
+
PLT_APP_DIR: applicationPath,
|
|
127
|
+
PLT_ICC_URL: 'http://127.0.0.1:3000'
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const app = await start()
|
|
131
|
+
app.getAuthorizationHeader = getAuthorizationHeader
|
|
132
|
+
|
|
133
|
+
await app.setupAlerts()
|
|
134
|
+
|
|
135
|
+
t.after(async () => {
|
|
136
|
+
await app.close()
|
|
137
|
+
await icc.close()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Manually trigger health event with healthy state
|
|
141
|
+
const healthInfo = {
|
|
142
|
+
id: 'service-1',
|
|
143
|
+
service: 'service-1',
|
|
144
|
+
currentHealth: {
|
|
145
|
+
elu: 0.5,
|
|
146
|
+
heapUsed: 76798040,
|
|
147
|
+
heapTotal: 99721216
|
|
148
|
+
},
|
|
149
|
+
unhealthy: false,
|
|
150
|
+
healthConfig: {
|
|
151
|
+
enabled: true,
|
|
152
|
+
interval: 1000,
|
|
153
|
+
gracePeriod: 1000,
|
|
154
|
+
maxUnhealthyChecks: 10,
|
|
155
|
+
maxELU: 0.99,
|
|
156
|
+
maxHeapUsed: 0.99,
|
|
157
|
+
maxHeapTotal: 4294967296
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
app.wattpro.runtime.emit('health', healthInfo)
|
|
162
|
+
|
|
163
|
+
await sleep(200)
|
|
164
|
+
|
|
165
|
+
// Verify no alert was sent
|
|
166
|
+
assert.strictEqual(alertReceived, null, 'No alert should have been received')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('should cache health data and include it in alerts', async (t) => {
|
|
170
|
+
const applicationName = 'test-app'
|
|
171
|
+
const applicationId = randomUUID()
|
|
172
|
+
const applicationPath = join(__dirname, 'fixtures', 'service-1')
|
|
173
|
+
|
|
174
|
+
let alertReceived = null
|
|
175
|
+
|
|
176
|
+
const getAuthorizationHeader = async (headers) => {
|
|
177
|
+
return { ...headers, authorization: 'Bearer test-token' }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const icc = await startICC(t, {
|
|
181
|
+
applicationId,
|
|
182
|
+
applicationName,
|
|
183
|
+
processAlerts: (req) => {
|
|
184
|
+
const alert = req.body
|
|
185
|
+
assert.equal(req.headers.authorization, 'Bearer test-token')
|
|
186
|
+
alertReceived = alert
|
|
187
|
+
return { id: 'test-alert-id', ...alert }
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
setUpEnvironment({
|
|
192
|
+
PLT_APP_NAME: applicationName,
|
|
193
|
+
PLT_APP_DIR: applicationPath,
|
|
194
|
+
PLT_ICC_URL: 'http://127.0.0.1:3000',
|
|
195
|
+
PLT_ALERT_CACHE_WINDOW: '2000' // 2 seconds for faster testing
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const app = await start()
|
|
199
|
+
app.getAuthorizationHeader = getAuthorizationHeader
|
|
200
|
+
|
|
201
|
+
await app.setupAlerts()
|
|
202
|
+
|
|
203
|
+
t.after(async () => {
|
|
204
|
+
await app.close()
|
|
205
|
+
await icc.close()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Send 3 healthy events first
|
|
209
|
+
for (let i = 0; i < 3; i++) {
|
|
210
|
+
const healthyInfo = {
|
|
211
|
+
id: 'service-1',
|
|
212
|
+
service: 'service-1',
|
|
213
|
+
currentHealth: {
|
|
214
|
+
elu: 0.5 + (i * 0.1), // Different values to distinguish them
|
|
215
|
+
heapUsed: 76798040,
|
|
216
|
+
heapTotal: 99721216
|
|
217
|
+
},
|
|
218
|
+
unhealthy: false,
|
|
219
|
+
healthConfig: {
|
|
220
|
+
enabled: true,
|
|
221
|
+
interval: 1000,
|
|
222
|
+
gracePeriod: 1000,
|
|
223
|
+
maxUnhealthyChecks: 10,
|
|
224
|
+
maxELU: 0.99,
|
|
225
|
+
maxHeapUsed: 0.99,
|
|
226
|
+
maxHeapTotal: 4294967296
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
app.wattpro.runtime.emit('health', healthyInfo)
|
|
231
|
+
await sleep(100) // Small delay between events
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Now send an unhealthy event to trigger alert
|
|
235
|
+
const unhealthyInfo = {
|
|
236
|
+
id: 'service-1',
|
|
237
|
+
service: 'service-1',
|
|
238
|
+
currentHealth: {
|
|
239
|
+
elu: 0.95,
|
|
240
|
+
heapUsed: 76798040,
|
|
241
|
+
heapTotal: 99721216
|
|
242
|
+
},
|
|
243
|
+
unhealthy: true,
|
|
244
|
+
healthConfig: {
|
|
245
|
+
enabled: true,
|
|
246
|
+
interval: 1000,
|
|
247
|
+
gracePeriod: 1000,
|
|
248
|
+
maxUnhealthyChecks: 10,
|
|
249
|
+
maxELU: 0.99,
|
|
250
|
+
maxHeapUsed: 0.99,
|
|
251
|
+
maxHeapTotal: 4294967296
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
app.wattpro.runtime.emit('health', unhealthyInfo)
|
|
256
|
+
await sleep(200)
|
|
257
|
+
|
|
258
|
+
assert.ok(alertReceived, 'Alert should have been received')
|
|
259
|
+
assert.strictEqual(alertReceived.applicationId, applicationId)
|
|
260
|
+
|
|
261
|
+
assert.strictEqual(alertReceived.alert.id, unhealthyInfo.id)
|
|
262
|
+
assert.strictEqual(alertReceived.alert.service, unhealthyInfo.service)
|
|
263
|
+
assert.strictEqual(alertReceived.alert.unhealthy, unhealthyInfo.unhealthy)
|
|
264
|
+
|
|
265
|
+
assert.ok(Array.isArray(alertReceived.healthHistory), 'Health history should be an array')
|
|
266
|
+
assert.ok(alertReceived.healthHistory.length >= 4, 'Should contain at least our 4 health events')
|
|
267
|
+
|
|
268
|
+
for (const entry of alertReceived.healthHistory) {
|
|
269
|
+
assert.ok('unhealthy' in entry, 'Entry should have unhealthy property')
|
|
270
|
+
assert.ok('currentHealth' in entry, 'Entry should have currentHealth property')
|
|
271
|
+
assert.ok('timestamp' in entry, 'Entry should have timestamp property')
|
|
272
|
+
assert.ok('service' in entry || 'id' in entry, 'Entry should have service or id property')
|
|
273
|
+
assert.ok(!('healthConfig' in entry), 'Entry should not have healthConfig property')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
assert.strictEqual(alertReceived.healthHistory[alertReceived.healthHistory.length - 1].unhealthy, true, 'Last event should be unhealthy')
|
|
277
|
+
|
|
278
|
+
let healthyCount = 0
|
|
279
|
+
for (const entry of alertReceived.healthHistory) {
|
|
280
|
+
if (!entry.unhealthy) healthyCount++
|
|
281
|
+
}
|
|
282
|
+
assert.ok(healthyCount >= 3, 'Should contain at least 3 healthy events')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('should not fail when health info is missing', async (t) => {
|
|
286
|
+
const applicationName = 'test-app'
|
|
287
|
+
const applicationId = randomUUID()
|
|
288
|
+
const applicationPath = join(__dirname, 'fixtures', 'service-1')
|
|
289
|
+
|
|
290
|
+
let alertReceived = null
|
|
291
|
+
|
|
292
|
+
const getAuthorizationHeader = async (headers) => {
|
|
293
|
+
return { ...headers, authorization: 'Bearer test-token' }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const icc = await startICC(t, {
|
|
297
|
+
applicationId,
|
|
298
|
+
applicationName,
|
|
299
|
+
processAlerts: (req) => {
|
|
300
|
+
const alert = req.body
|
|
301
|
+
assert.equal(req.headers.authorization, 'Bearer test-token')
|
|
302
|
+
alertReceived = alert
|
|
303
|
+
return { id: 'test-alert-id', ...alert }
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
setUpEnvironment({
|
|
308
|
+
PLT_APP_NAME: applicationName,
|
|
309
|
+
PLT_APP_DIR: applicationPath,
|
|
310
|
+
PLT_ICC_URL: 'http://127.0.0.1:3000'
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
const app = await start()
|
|
314
|
+
app.getAuthorizationHeader = getAuthorizationHeader
|
|
315
|
+
|
|
316
|
+
await app.setupAlerts()
|
|
317
|
+
|
|
318
|
+
t.after(async () => {
|
|
319
|
+
await app.close()
|
|
320
|
+
await icc.close()
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
app.wattpro.runtime.emit('health', null)
|
|
324
|
+
|
|
325
|
+
await sleep(200)
|
|
326
|
+
|
|
327
|
+
assert.strictEqual(alertReceived, null, 'No alert should have been received')
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('should respect alert retention window', async (t) => {
|
|
331
|
+
const applicationName = 'test-app'
|
|
332
|
+
const applicationId = randomUUID()
|
|
333
|
+
const applicationPath = join(__dirname, 'fixtures', 'service-1')
|
|
334
|
+
|
|
335
|
+
const alertsReceived = []
|
|
336
|
+
|
|
337
|
+
const getAuthorizationHeader = async (headers) => {
|
|
338
|
+
return { ...headers, authorization: 'Bearer test-token' }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const icc = await startICC(t, {
|
|
342
|
+
applicationId,
|
|
343
|
+
applicationName,
|
|
344
|
+
iccConfig: {
|
|
345
|
+
scaler: {
|
|
346
|
+
alertRetentionWindow: 500
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
processAlerts: (req) => {
|
|
350
|
+
const alert = req.body
|
|
351
|
+
assert.equal(req.headers.authorization, 'Bearer test-token')
|
|
352
|
+
alertsReceived.push(alert)
|
|
353
|
+
return { id: 'test-alert-id', ...alert }
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
setUpEnvironment({
|
|
358
|
+
PLT_APP_NAME: applicationName,
|
|
359
|
+
PLT_APP_DIR: applicationPath,
|
|
360
|
+
PLT_ICC_URL: 'http://127.0.0.1:3000'
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
const app = await start()
|
|
364
|
+
|
|
365
|
+
app.getAuthorizationHeader = getAuthorizationHeader
|
|
366
|
+
|
|
367
|
+
await app.setupAlerts()
|
|
368
|
+
|
|
369
|
+
t.after(async () => {
|
|
370
|
+
await app.close()
|
|
371
|
+
await icc.close()
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
// Create a health info template
|
|
375
|
+
const createHealthInfo = (unhealthy = true) => ({
|
|
376
|
+
id: 'service-1',
|
|
377
|
+
service: 'service-1',
|
|
378
|
+
currentHealth: {
|
|
379
|
+
elu: unhealthy ? 0.95 : 0.5,
|
|
380
|
+
heapUsed: 76798040,
|
|
381
|
+
heapTotal: 99721216
|
|
382
|
+
},
|
|
383
|
+
unhealthy,
|
|
384
|
+
healthConfig: {
|
|
385
|
+
enabled: true,
|
|
386
|
+
interval: 1000,
|
|
387
|
+
gracePeriod: 1000,
|
|
388
|
+
maxUnhealthyChecks: 10,
|
|
389
|
+
maxELU: 0.99,
|
|
390
|
+
maxHeapUsed: 0.99,
|
|
391
|
+
maxHeapTotal: 4294967296
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// Send first unhealthy event - should trigger alert
|
|
396
|
+
app.wattpro.runtime.emit('health', createHealthInfo(true))
|
|
397
|
+
await sleep(100)
|
|
398
|
+
|
|
399
|
+
// Send second unhealthy event immediately - should be ignored due to retention window
|
|
400
|
+
app.wattpro.runtime.emit('health', createHealthInfo(true))
|
|
401
|
+
await sleep(100)
|
|
402
|
+
|
|
403
|
+
assert.strictEqual(alertsReceived.length, 1, 'Only one alert should be sent within retention window')
|
|
404
|
+
|
|
405
|
+
await sleep(500)
|
|
406
|
+
|
|
407
|
+
// Send third unhealthy event - should trigger second alert
|
|
408
|
+
app.wattpro.runtime.emit('health', createHealthInfo(true))
|
|
409
|
+
await sleep(100)
|
|
410
|
+
|
|
411
|
+
assert.strictEqual(alertsReceived.length, 2, 'Second alert should be sent after retention window expires')
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
test('should not set up alerts when scaler URL is missing', async (t) => {
|
|
415
|
+
const applicationName = 'test-app'
|
|
416
|
+
const applicationId = randomUUID()
|
|
417
|
+
const applicationPath = join(__dirname, 'fixtures', 'service-1')
|
|
418
|
+
|
|
419
|
+
const icc = await startICC(t, {
|
|
420
|
+
applicationId,
|
|
421
|
+
applicationName,
|
|
422
|
+
iccServices: {} // No data scaler URL
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
setUpEnvironment({
|
|
426
|
+
PLT_APP_NAME: applicationName,
|
|
427
|
+
PLT_APP_DIR: applicationPath,
|
|
428
|
+
PLT_ICC_URL: 'http://127.0.0.1:3000'
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
const app = await start()
|
|
432
|
+
|
|
433
|
+
t.after(async () => {
|
|
434
|
+
await app.close()
|
|
435
|
+
await icc.close()
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
const warnings = []
|
|
439
|
+
const originalWarn = app.log.warn
|
|
440
|
+
app.log.warn = function (message) {
|
|
441
|
+
warnings.push(typeof message === 'string' ? message : JSON.stringify(message))
|
|
442
|
+
return originalWarn.apply(this, arguments)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
await app.setupAlerts()
|
|
446
|
+
|
|
447
|
+
// Verify warning about missing scaler URL
|
|
448
|
+
const hasWarning = warnings.some(warning =>
|
|
449
|
+
warning.includes('No scaler URL found in ICC services, health alerts disabled')
|
|
450
|
+
)
|
|
451
|
+
assert.ok(hasWarning, 'Should log warning about missing scaler URL')
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
test('should send alert when flamegraphs are disabled', async (t) => {
|
|
455
|
+
const applicationName = 'test-app'
|
|
456
|
+
const applicationId = randomUUID()
|
|
457
|
+
const applicationPath = join(__dirname, 'fixtures', 'service-1')
|
|
458
|
+
|
|
459
|
+
let alertReceived = null
|
|
460
|
+
|
|
461
|
+
const getAuthorizationHeader = async (headers) => {
|
|
462
|
+
return { ...headers, authorization: 'Bearer test-token' }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const icc = await startICC(t, {
|
|
466
|
+
applicationId,
|
|
467
|
+
applicationName,
|
|
468
|
+
processAlerts: (req) => {
|
|
469
|
+
const alert = req.body
|
|
470
|
+
assert.equal(req.headers.authorization, 'Bearer test-token')
|
|
471
|
+
alertReceived = alert
|
|
472
|
+
return { id: 'test-alert-id', ...alert }
|
|
473
|
+
}
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
setUpEnvironment({
|
|
477
|
+
PLT_APP_NAME: applicationName,
|
|
478
|
+
PLT_APP_DIR: applicationPath,
|
|
479
|
+
PLT_ICC_URL: 'http://127.0.0.1:3000',
|
|
480
|
+
PLT_DISABLE_FLAMEGRAPHS: true,
|
|
481
|
+
PLT_FLAMEGRAPHS_INTERVAL_SEC: 2
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
const app = await start()
|
|
485
|
+
app.getAuthorizationHeader = getAuthorizationHeader
|
|
486
|
+
|
|
487
|
+
await app.setupAlerts()
|
|
488
|
+
|
|
489
|
+
t.after(async () => {
|
|
490
|
+
await app.close()
|
|
491
|
+
await icc.close()
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
await sleep(5000)
|
|
495
|
+
|
|
496
|
+
// Manually trigger health event with unhealthy state
|
|
497
|
+
const healthInfo = {
|
|
498
|
+
id: 'main:0',
|
|
499
|
+
service: 'main',
|
|
500
|
+
currentHealth: {
|
|
501
|
+
elu: 0.95,
|
|
502
|
+
heapUsed: 76798040,
|
|
503
|
+
heapTotal: 99721216
|
|
504
|
+
},
|
|
505
|
+
unhealthy: true,
|
|
506
|
+
healthConfig: {
|
|
507
|
+
enabled: true,
|
|
508
|
+
interval: 1000,
|
|
509
|
+
gracePeriod: 1000,
|
|
510
|
+
maxUnhealthyChecks: 10,
|
|
511
|
+
maxELU: 0.99,
|
|
512
|
+
maxHeapUsed: 0.99,
|
|
513
|
+
maxHeapTotal: 4294967296
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
app.wattpro.runtime.emit('health', healthInfo)
|
|
518
|
+
|
|
519
|
+
await sleep(200)
|
|
520
|
+
|
|
521
|
+
assert.ok(alertReceived, 'Alert should have been received')
|
|
522
|
+
assert.strictEqual(alertReceived.applicationId, applicationId)
|
|
523
|
+
assert.deepStrictEqual(alertReceived.alert, healthInfo)
|
|
524
|
+
assert.ok(Array.isArray(alertReceived.healthHistory), 'Health history should be an array')
|
|
525
|
+
assert.ok(alertReceived.healthHistory.length > 0, 'Health history should not be empty')
|
|
526
|
+
assert.strictEqual(alertReceived.healthHistory[0].service, 'main')
|
|
527
|
+
assert.equal(alertReceived.flamegraph, null, 'Flamegraph should be null')
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
test('should send alert when failed to send a flamegraph', async (t) => {
|
|
531
|
+
const applicationName = 'test-app'
|
|
532
|
+
const applicationId = randomUUID()
|
|
533
|
+
const applicationPath = join(__dirname, 'fixtures', 'service-1')
|
|
534
|
+
|
|
535
|
+
let alertReceived = null
|
|
536
|
+
|
|
537
|
+
const getAuthorizationHeader = async (headers) => {
|
|
538
|
+
return { ...headers, authorization: 'Bearer test-token' }
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const icc = await startICC(t, {
|
|
542
|
+
applicationId,
|
|
543
|
+
applicationName,
|
|
544
|
+
processAlerts: (req) => {
|
|
545
|
+
const alert = req.body
|
|
546
|
+
assert.equal(req.headers.authorization, 'Bearer test-token')
|
|
547
|
+
alertReceived = alert
|
|
548
|
+
return { id: 'test-alert-id', ...alert }
|
|
549
|
+
},
|
|
550
|
+
processFlamegraphs: ({ alertId, flamegraph }) => {
|
|
551
|
+
throw new Error('Failed to send flamegraph')
|
|
552
|
+
}
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
setUpEnvironment({
|
|
556
|
+
PLT_APP_NAME: applicationName,
|
|
557
|
+
PLT_APP_DIR: applicationPath,
|
|
558
|
+
PLT_ICC_URL: 'http://127.0.0.1:3000',
|
|
559
|
+
PLT_DISABLE_FLAMEGRAPHS: false,
|
|
560
|
+
PLT_FLAMEGRAPHS_INTERVAL_SEC: 2
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
const app = await start()
|
|
564
|
+
app.getAuthorizationHeader = getAuthorizationHeader
|
|
565
|
+
|
|
566
|
+
await app.setupAlerts()
|
|
567
|
+
|
|
568
|
+
t.after(async () => {
|
|
569
|
+
await app.close()
|
|
570
|
+
await icc.close()
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
await sleep(5000)
|
|
574
|
+
|
|
575
|
+
// Manually trigger health event with unhealthy state
|
|
576
|
+
const healthInfo = {
|
|
577
|
+
id: 'main:0',
|
|
578
|
+
service: 'main',
|
|
579
|
+
currentHealth: {
|
|
580
|
+
elu: 0.95,
|
|
581
|
+
heapUsed: 76798040,
|
|
582
|
+
heapTotal: 99721216
|
|
583
|
+
},
|
|
584
|
+
unhealthy: true,
|
|
585
|
+
healthConfig: {
|
|
586
|
+
enabled: true,
|
|
587
|
+
interval: 1000,
|
|
588
|
+
gracePeriod: 1000,
|
|
589
|
+
maxUnhealthyChecks: 10,
|
|
590
|
+
maxELU: 0.99,
|
|
591
|
+
maxHeapUsed: 0.99,
|
|
592
|
+
maxHeapTotal: 4294967296
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
app.wattpro.runtime.emit('health', healthInfo)
|
|
597
|
+
|
|
598
|
+
await sleep(200)
|
|
599
|
+
|
|
600
|
+
assert.ok(alertReceived, 'Alert should have been received')
|
|
601
|
+
assert.strictEqual(alertReceived.applicationId, applicationId)
|
|
602
|
+
assert.deepStrictEqual(alertReceived.alert, healthInfo)
|
|
603
|
+
assert.ok(Array.isArray(alertReceived.healthHistory), 'Health history should be an array')
|
|
604
|
+
assert.ok(alertReceived.healthHistory.length > 0, 'Health history should not be empty')
|
|
605
|
+
assert.strictEqual(alertReceived.healthHistory[0].service, 'main')
|
|
606
|
+
assert.equal(alertReceived.flamegraph, null, 'Flamegraph should be null')
|
|
607
|
+
})
|