@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.
Files changed (95) hide show
  1. package/README.md +87 -0
  2. package/app.js +124 -0
  3. package/cli.js +141 -0
  4. package/clients/compliance/compliance-types.d.ts +887 -0
  5. package/clients/compliance/compliance.mjs +1049 -0
  6. package/clients/compliance/compliance.openapi.json +6127 -0
  7. package/clients/control-plane/control-plane-types.d.ts +2696 -0
  8. package/clients/control-plane/control-plane.mjs +3051 -0
  9. package/clients/control-plane/control-plane.openapi.json +13693 -0
  10. package/clients/cron/cron-types.d.ts +1479 -0
  11. package/clients/cron/cron.mjs +872 -0
  12. package/clients/cron/cron.openapi.json +9330 -0
  13. package/compliance/index.js +21 -0
  14. package/compliance/rules/dependencies.js +76 -0
  15. package/compliance/rules/utils.js +12 -0
  16. package/eslint.config.js +11 -0
  17. package/help/start.txt +12 -0
  18. package/help/watt-extra.txt +12 -0
  19. package/index.js +45 -0
  20. package/lib/banner.js +22 -0
  21. package/lib/errors.js +34 -0
  22. package/lib/utils.js +34 -0
  23. package/lib/wattpro.js +580 -0
  24. package/package.json +50 -0
  25. package/plugins/alerts.js +115 -0
  26. package/plugins/auth.js +89 -0
  27. package/plugins/compliancy.js +70 -0
  28. package/plugins/env.js +58 -0
  29. package/plugins/flamegraphs.js +100 -0
  30. package/plugins/init.js +70 -0
  31. package/plugins/metadata.js +84 -0
  32. package/plugins/scheduler.js +48 -0
  33. package/plugins/update.js +128 -0
  34. package/renovate.json +6 -0
  35. package/test/alerts.test.js +607 -0
  36. package/test/auth.test.js +128 -0
  37. package/test/auto-cache.test.js +401 -0
  38. package/test/cli.test.js +75 -0
  39. package/test/compliancy.test.js +87 -0
  40. package/test/fixtures/runtime-domains/alpha/package.json +5 -0
  41. package/test/fixtures/runtime-domains/alpha/platformatic.json +6 -0
  42. package/test/fixtures/runtime-domains/alpha/plugin.js +16 -0
  43. package/test/fixtures/runtime-domains/beta/package.json +5 -0
  44. package/test/fixtures/runtime-domains/beta/platformatic.json +6 -0
  45. package/test/fixtures/runtime-domains/beta/plugin.js +7 -0
  46. package/test/fixtures/runtime-domains/composer/package.json +5 -0
  47. package/test/fixtures/runtime-domains/composer/platformatic.json +19 -0
  48. package/test/fixtures/runtime-domains/package.json +1 -0
  49. package/test/fixtures/runtime-domains/platformatic.json +27 -0
  50. package/test/fixtures/runtime-health/package.json +20 -0
  51. package/test/fixtures/runtime-health/platformatic.json +16 -0
  52. package/test/fixtures/runtime-health/services/service-1/package.json +17 -0
  53. package/test/fixtures/runtime-health/services/service-1/platformatic.json +16 -0
  54. package/test/fixtures/runtime-health/services/service-1/plugins/example.js +6 -0
  55. package/test/fixtures/runtime-health/services/service-1/routes/root.cjs +8 -0
  56. package/test/fixtures/runtime-health/services/service-2/package.json +17 -0
  57. package/test/fixtures/runtime-health/services/service-2/platformatic.json +16 -0
  58. package/test/fixtures/runtime-health/services/service-2/plugins/example.js +6 -0
  59. package/test/fixtures/runtime-health/services/service-2/routes/root.cjs +8 -0
  60. package/test/fixtures/runtime-next/package.json +5 -0
  61. package/test/fixtures/runtime-next/platformatic.json +9 -0
  62. package/test/fixtures/runtime-next/web/next/next.config.js +2 -0
  63. package/test/fixtures/runtime-next/web/next/package.json +7 -0
  64. package/test/fixtures/runtime-next/web/next/platformatic.json +9 -0
  65. package/test/fixtures/runtime-next/web/next/src/app/direct/route.js +3 -0
  66. package/test/fixtures/runtime-next/web/next/src/app/layout.jsx +7 -0
  67. package/test/fixtures/runtime-next/web/next/src/app/page.jsx +3 -0
  68. package/test/fixtures/runtime-scheduler/main/package.json +5 -0
  69. package/test/fixtures/runtime-scheduler/main/platformatic.json +9 -0
  70. package/test/fixtures/runtime-scheduler/main/routes/root.cjs +11 -0
  71. package/test/fixtures/runtime-scheduler/package.json +1 -0
  72. package/test/fixtures/runtime-scheduler/platformatic.json +27 -0
  73. package/test/fixtures/runtime-service/main/package.json +5 -0
  74. package/test/fixtures/runtime-service/main/platformatic.json +12 -0
  75. package/test/fixtures/runtime-service/main/routes/root.cjs +11 -0
  76. package/test/fixtures/runtime-service/package.json +1 -0
  77. package/test/fixtures/runtime-service/platformatic.json +19 -0
  78. package/test/fixtures/service-1/package.json +7 -0
  79. package/test/fixtures/service-1/platformatic.json +18 -0
  80. package/test/fixtures/service-1/routes/root.cjs +48 -0
  81. package/test/fixtures/service-2/platformatic.json +21 -0
  82. package/test/fixtures/service-2/routes/root.cjs +5 -0
  83. package/test/fixtures/service-3/package.json +5 -0
  84. package/test/fixtures/service-3/platformatic.json +21 -0
  85. package/test/fixtures/service-3/routes/root.cjs +8 -0
  86. package/test/health.test.js +44 -0
  87. package/test/helper.js +274 -0
  88. package/test/init.test.js +243 -0
  89. package/test/patch-config.test.js +434 -0
  90. package/test/scheduler.test.js +71 -0
  91. package/test/send-to-icc-retry.test.js +138 -0
  92. package/test/shared-context.test.js +82 -0
  93. package/test/spawn.test.js +110 -0
  94. package/test/trigger-flamegraphs.test.js +226 -0
  95. 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
+ })