@platformatic/watt-extra 1.7.0 → 1.7.1-alpha.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.
@@ -90,9 +90,6 @@ test('should send alert when service becomes unhealthy', async (t) => {
90
90
  await icc.close()
91
91
  })
92
92
 
93
- // Wait for the first flamegraph to be generated
94
- await sleep(5000)
95
-
96
93
  // Manually trigger health event with unhealthy state
97
94
  const healthInfo = {
98
95
  id: 'main:0',
@@ -133,6 +130,9 @@ test('should send alert when service becomes unhealthy', async (t) => {
133
130
  assert.strictEqual(alertReceived.healthHistory[0].application, 'main')
134
131
  assert.strictEqual(alertReceived.healthHistory[0].service, 'main')
135
132
 
133
+ // Wait for flamegraph to be generated (duration is 2 seconds)
134
+ await sleep(2500)
135
+
136
136
  assert.ok(flamegraphReceived, 'Flamegraph should have been received')
137
137
 
138
138
  const profile = Profile.decode(flamegraphReceived)
@@ -526,8 +526,6 @@ test('should send alert when flamegraphs are disabled', async (t) => {
526
526
  await icc.close()
527
527
  })
528
528
 
529
- await sleep(5000)
530
-
531
529
  // Manually trigger health event with unhealthy state
532
530
  const healthInfo = {
533
531
  id: 'main:0',
@@ -611,8 +609,6 @@ test('should send alert when failed to send a flamegraph', async (t) => {
611
609
  await icc.close()
612
610
  })
613
611
 
614
- await sleep(5000)
615
-
616
612
  // Manually trigger health event with unhealthy state
617
613
  const healthInfo = {
618
614
  id: 'main:0',
@@ -799,9 +795,6 @@ test('should attach one flamegraph to multiple alerts', async (t) => {
799
795
  await icc.close()
800
796
  })
801
797
 
802
- // Wait for the first flamegraph to be generated
803
- await sleep(5000)
804
-
805
798
  // Manually trigger health event with unhealthy state
806
799
  const healthInfo = {
807
800
  id: 'main:0',
@@ -827,8 +820,8 @@ test('should attach one flamegraph to multiple alerts', async (t) => {
827
820
  await sleep(1000)
828
821
  emitHealthEvent(app, healthInfo)
829
822
 
830
- // Wait for flamegraphs to be sent
831
- await sleep(1000)
823
+ // Wait for flamegraph to be generated (duration is 5 seconds) and sent
824
+ await sleep(5500)
832
825
 
833
826
  assert.strictEqual(receivedAlerts.length, 2)
834
827
  const alert1 = receivedAlerts[0]
@@ -902,9 +895,6 @@ test('should send flamegraphs if attaching fails', async (t) => {
902
895
  await icc.close()
903
896
  })
904
897
 
905
- // Wait for the first flamegraph to be generated
906
- await sleep(5000)
907
-
908
898
  // Manually trigger health event with unhealthy state
909
899
  const healthInfo = {
910
900
  id: 'main:0',
@@ -930,8 +920,8 @@ test('should send flamegraphs if attaching fails', async (t) => {
930
920
  await sleep(1000)
931
921
  emitHealthEvent(app, healthInfo)
932
922
 
933
- // Wait for flamegraphs to be sent
934
- await sleep(1000)
923
+ // Wait for flamegraph to be generated (duration is 5 seconds) and sent
924
+ await sleep(5500)
935
925
 
936
926
  assert.strictEqual(receivedAlerts.length, 2)
937
927
  const alert1 = receivedAlerts[0]
@@ -31,7 +31,7 @@ test('should send health signals when service becomes unhealthy', async (t) => {
31
31
  processSignals: (req) => {
32
32
  assert.equal(req.headers.authorization, 'Bearer test-token')
33
33
  receivedSignalReqs.push(req.body)
34
- return { id: 'test-alert-id' }
34
+ return { alertId: 'test-alert-id' }
35
35
  },
36
36
  processFlamegraphs: (req) => {
37
37
  const alertId = req.query.alertId
@@ -58,9 +58,6 @@ test('should send health signals when service becomes unhealthy', async (t) => {
58
58
  await icc.close()
59
59
  })
60
60
 
61
- // Wait for the first flamegraph to be generated
62
- await sleep(5000)
63
-
64
61
  {
65
62
  const { statusCode } = await request('http://127.0.0.1:3042/custom-health-signal', {
66
63
  method: 'POST',
@@ -119,8 +116,8 @@ test('should send health signals when service becomes unhealthy', async (t) => {
119
116
  assert.ok(receivedSignal.timestamp > 0)
120
117
  }
121
118
 
122
- // Wait for the second flamegraph to be generated
123
- await sleep(2000)
119
+ // Wait for flamegraph to be generated (duration is 2 seconds)
120
+ await sleep(2500)
124
121
 
125
122
  // assert.strictEqual(receivedFlamegraphReqs.length, 1)
126
123
 
@@ -0,0 +1,443 @@
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
+ import { Profiler } from '../plugins/flamegraphs.js'
11
+
12
+ const __filename = fileURLToPath(import.meta.url)
13
+ const __dirname = dirname(__filename)
14
+
15
+ async function setupApp (t) {
16
+ const applicationName = 'test-app'
17
+ const applicationId = randomUUID()
18
+ const applicationPath = join(__dirname, 'fixtures', 'service-1')
19
+
20
+ const icc = await startICC(t, {
21
+ applicationId,
22
+ applicationName
23
+ })
24
+
25
+ setUpEnvironment({
26
+ PLT_APP_NAME: applicationName,
27
+ PLT_APP_DIR: applicationPath,
28
+ PLT_ICC_URL: 'http://127.0.0.1:3000'
29
+ })
30
+
31
+ const app = await start()
32
+
33
+ t.after(async () => {
34
+ await app.close()
35
+ await icc.close()
36
+ })
37
+
38
+ return app
39
+ }
40
+
41
+ test('Profiler should start profiling and generate profile on first request', async (t) => {
42
+ const app = await setupApp(t)
43
+
44
+ let profileReceived = false
45
+ const profiler = new Profiler({
46
+ app,
47
+ workerId: 'main:0',
48
+ type: 'cpu',
49
+ duration: 1000,
50
+ sourceMaps: false,
51
+ onProfile: (err, profile, requests) => {
52
+ assert.strictEqual(err, null)
53
+ assert.ok(profile)
54
+ assert.ok(profile.data)
55
+ assert.ok(profile.timestamp)
56
+ assert.strictEqual(requests.length, 1)
57
+ assert.strictEqual(requests[0].alertId, 'test-alert')
58
+ profileReceived = true
59
+ }
60
+ })
61
+
62
+ t.after(async () => {
63
+ await profiler.stop()
64
+ })
65
+
66
+ // Request a profile
67
+ await profiler.requestProfile({ alertId: 'test-alert' })
68
+
69
+ // Wait for profile to be generated (duration is 1 second)
70
+ await sleep(1500)
71
+
72
+ assert.ok(profileReceived, 'Profile should have been generated')
73
+ })
74
+
75
+ test('Profiler should queue multiple requests and associate them with the profile', async (t) => {
76
+ const app = await setupApp(t)
77
+
78
+ let profileReceived = false
79
+ const profiler = new Profiler({
80
+ app,
81
+ workerId: 'main:0',
82
+ type: 'cpu',
83
+ duration: 1000,
84
+ sourceMaps: false,
85
+ onProfile: (err, profile, requests) => {
86
+ assert.strictEqual(err, null)
87
+ assert.ok(profile)
88
+ assert.strictEqual(requests.length, 3)
89
+ assert.strictEqual(requests[0].alertId, 'alert-1')
90
+ assert.strictEqual(requests[1].alertId, 'alert-2')
91
+ assert.strictEqual(requests[2].alertId, 'alert-3')
92
+ profileReceived = true
93
+ }
94
+ })
95
+
96
+ t.after(async () => {
97
+ await profiler.stop()
98
+ })
99
+
100
+ // Queue multiple requests
101
+ await profiler.requestProfile({ alertId: 'alert-1' })
102
+ await profiler.requestProfile({ alertId: 'alert-2' })
103
+ await profiler.requestProfile({ alertId: 'alert-3' })
104
+
105
+ // Wait for profile to be generated
106
+ await sleep(1500)
107
+
108
+ assert.ok(profileReceived, 'Profile should have been generated with all requests')
109
+ })
110
+
111
+ test('Profiler should filter requests by timestamp', async (t) => {
112
+ const app = await setupApp(t)
113
+
114
+ const profilesReceived = []
115
+ const profiler = new Profiler({
116
+ app,
117
+ workerId: 'main:0',
118
+ type: 'cpu',
119
+ duration: 1000,
120
+ sourceMaps: false,
121
+ onProfile: (err, profile, requests) => {
122
+ if (err) return
123
+ profilesReceived.push({ profile, requests })
124
+ }
125
+ })
126
+
127
+ t.after(async () => {
128
+ await profiler.stop()
129
+ })
130
+
131
+ // Request first profile
132
+ await profiler.requestProfile({ alertId: 'alert-1' })
133
+
134
+ // Wait for first profile
135
+ await sleep(1500)
136
+
137
+ // Request second profile after some delay
138
+ await profiler.requestProfile({ alertId: 'alert-2' })
139
+
140
+ // Wait for second profile
141
+ await sleep(1500)
142
+
143
+ assert.strictEqual(profilesReceived.length, 2)
144
+ assert.strictEqual(profilesReceived[0].requests.length, 1)
145
+ assert.strictEqual(profilesReceived[0].requests[0].alertId, 'alert-1')
146
+ assert.strictEqual(profilesReceived[1].requests.length, 1)
147
+ assert.strictEqual(profilesReceived[1].requests[0].alertId, 'alert-2')
148
+ })
149
+
150
+ test('Profiler should auto-stop after idle period', async (t) => {
151
+ const app = await setupApp(t)
152
+
153
+ let stopCalled = false
154
+ const originalSendCommand = app.watt.runtime.sendCommandToApplication
155
+ app.watt.runtime.sendCommandToApplication = async (workerId, command, options) => {
156
+ if (command === 'stopProfiling') {
157
+ stopCalled = true
158
+ }
159
+ return originalSendCommand.call(app.watt.runtime, workerId, command, options)
160
+ }
161
+
162
+ const profiler = new Profiler({
163
+ app,
164
+ workerId: 'main:0',
165
+ type: 'cpu',
166
+ duration: 1000,
167
+ sourceMaps: false,
168
+ onProfile: () => {}
169
+ })
170
+
171
+ t.after(async () => {
172
+ await profiler.stop()
173
+ })
174
+
175
+ // Request a profile
176
+ await profiler.requestProfile({ alertId: 'test-alert' })
177
+
178
+ // Wait for profile generation + idle timeout (duration + duration/2)
179
+ await sleep(2000)
180
+
181
+ assert.ok(stopCalled, 'Profiler should have stopped after idle period')
182
+ })
183
+
184
+ test('Profiler should not stop if new requests arrive', async (t) => {
185
+ const app = await setupApp(t)
186
+
187
+ let stopCalled = false
188
+ const originalSendCommand = app.watt.runtime.sendCommandToApplication
189
+ app.watt.runtime.sendCommandToApplication = async (workerId, command, options) => {
190
+ if (command === 'stopProfiling') {
191
+ stopCalled = true
192
+ }
193
+ return originalSendCommand.call(app.watt.runtime, workerId, command, options)
194
+ }
195
+
196
+ const profilesReceived = []
197
+ const profiler = new Profiler({
198
+ app,
199
+ workerId: 'main:0',
200
+ type: 'cpu',
201
+ duration: 1000,
202
+ sourceMaps: false,
203
+ onProfile: (err, profile, requests) => {
204
+ if (err) return
205
+ profilesReceived.push({ profile, requests })
206
+ }
207
+ })
208
+
209
+ t.after(async () => {
210
+ await profiler.stop()
211
+ })
212
+
213
+ // Request first profile
214
+ await profiler.requestProfile({ alertId: 'alert-1' })
215
+
216
+ // Wait for first profile
217
+ await sleep(1200)
218
+
219
+ // Request second profile before idle timeout
220
+ await profiler.requestProfile({ alertId: 'alert-2' })
221
+
222
+ // Wait for second profile
223
+ await sleep(1200)
224
+
225
+ assert.strictEqual(profilesReceived.length, 2)
226
+ assert.strictEqual(stopCalled, false, 'Profiler should not have stopped yet')
227
+ })
228
+
229
+ test('Profiler should handle errors when starting profiling', async (t) => {
230
+ const app = await setupApp(t)
231
+
232
+ // Mock sendCommandToApplication to throw error on startProfiling
233
+ const originalSendCommand = app.watt.runtime.sendCommandToApplication
234
+ app.watt.runtime.sendCommandToApplication = async (workerId, command, options) => {
235
+ if (command === 'startProfiling') {
236
+ throw new Error('Failed to start profiling')
237
+ }
238
+ return originalSendCommand.call(app.watt.runtime, workerId, command, options)
239
+ }
240
+
241
+ let errorReceived = false
242
+ const profiler = new Profiler({
243
+ app,
244
+ workerId: 'main:0',
245
+ type: 'cpu',
246
+ duration: 1000,
247
+ sourceMaps: false,
248
+ onProfile: (err, profile, requests) => {
249
+ assert.ok(err)
250
+ assert.strictEqual(err.message, 'Failed to start profiling')
251
+ assert.strictEqual(profile, null)
252
+ assert.strictEqual(requests.length, 1)
253
+ errorReceived = true
254
+ }
255
+ })
256
+
257
+ t.after(async () => {
258
+ await profiler.stop()
259
+ })
260
+
261
+ // Request a profile
262
+ await profiler.requestProfile({ alertId: 'test-alert' })
263
+
264
+ // Wait for error callback
265
+ await sleep(200)
266
+
267
+ assert.ok(errorReceived, 'Error should have been handled')
268
+ })
269
+
270
+ test('Profiler should handle errors when getting profile', async (t) => {
271
+ const app = await setupApp(t)
272
+
273
+ // Mock sendCommandToApplication to throw error on getLastProfile
274
+ const originalSendCommand = app.watt.runtime.sendCommandToApplication
275
+ app.watt.runtime.sendCommandToApplication = async (workerId, command, options) => {
276
+ if (command === 'getLastProfile') {
277
+ throw new Error('Failed to get profile')
278
+ }
279
+ return originalSendCommand.call(app.watt.runtime, workerId, command, options)
280
+ }
281
+
282
+ let errorReceived = false
283
+ const profiler = new Profiler({
284
+ app,
285
+ workerId: 'main:0',
286
+ type: 'cpu',
287
+ duration: 1000,
288
+ sourceMaps: false,
289
+ onProfile: (err, profile, requests) => {
290
+ assert.ok(err)
291
+ assert.strictEqual(err.message, 'Failed to get profile')
292
+ assert.strictEqual(profile, null)
293
+ assert.strictEqual(requests.length, 1)
294
+ errorReceived = true
295
+ }
296
+ })
297
+
298
+ t.after(async () => {
299
+ await profiler.stop()
300
+ })
301
+
302
+ // Request a profile
303
+ await profiler.requestProfile({ alertId: 'test-alert' })
304
+
305
+ // Wait for profile generation attempt
306
+ await sleep(1500)
307
+
308
+ assert.ok(errorReceived, 'Error should have been handled')
309
+ })
310
+
311
+ test('Profiler should pass sourceMaps option correctly', async (t) => {
312
+ const app = await setupApp(t)
313
+
314
+ let sourceMapsOptionReceived = null
315
+ const originalSendCommand = app.watt.runtime.sendCommandToApplication
316
+ app.watt.runtime.sendCommandToApplication = async (workerId, command, options) => {
317
+ if (command === 'startProfiling') {
318
+ sourceMapsOptionReceived = options.sourceMaps
319
+ }
320
+ return originalSendCommand.call(app.watt.runtime, workerId, command, options)
321
+ }
322
+
323
+ const profiler = new Profiler({
324
+ app,
325
+ workerId: 'main:0',
326
+ type: 'cpu',
327
+ duration: 1000,
328
+ sourceMaps: true,
329
+ onProfile: () => {}
330
+ })
331
+
332
+ t.after(async () => {
333
+ await profiler.stop()
334
+ })
335
+
336
+ // Request a profile
337
+ await profiler.requestProfile({ alertId: 'test-alert' })
338
+
339
+ // Wait for profiling to start
340
+ await sleep(200)
341
+
342
+ assert.strictEqual(sourceMapsOptionReceived, true, 'sourceMaps option should be passed correctly')
343
+ })
344
+
345
+ test('Profiler should manually stop profiling when stop() is called', async (t) => {
346
+ const app = await setupApp(t)
347
+
348
+ let stopCalled = false
349
+ const originalSendCommand = app.watt.runtime.sendCommandToApplication
350
+ app.watt.runtime.sendCommandToApplication = async (workerId, command, options) => {
351
+ if (command === 'stopProfiling') {
352
+ stopCalled = true
353
+ }
354
+ return originalSendCommand.call(app.watt.runtime, workerId, command, options)
355
+ }
356
+
357
+ const profiler = new Profiler({
358
+ app,
359
+ workerId: 'main:0',
360
+ type: 'cpu',
361
+ duration: 1000,
362
+ sourceMaps: false,
363
+ onProfile: () => {}
364
+ })
365
+
366
+ // Request a profile
367
+ await profiler.requestProfile({ alertId: 'test-alert' })
368
+
369
+ // Wait a bit
370
+ await sleep(200)
371
+
372
+ // Manually stop
373
+ await profiler.stop()
374
+
375
+ assert.ok(stopCalled, 'stopProfiling should have been called')
376
+ })
377
+
378
+ test('Profiler should handle requests with custom timestamps', async (t) => {
379
+ const app = await setupApp(t)
380
+
381
+ let profileReceived = false
382
+ const profiler = new Profiler({
383
+ app,
384
+ workerId: 'main:0',
385
+ type: 'cpu',
386
+ duration: 1000,
387
+ sourceMaps: false,
388
+ onProfile: (err, profile, requests) => {
389
+ assert.strictEqual(err, null)
390
+ assert.ok(profile)
391
+ assert.strictEqual(requests.length, 1)
392
+ assert.strictEqual(requests[0].timestamp, 123456)
393
+ profileReceived = true
394
+ }
395
+ })
396
+
397
+ t.after(async () => {
398
+ await profiler.stop()
399
+ })
400
+
401
+ // Request with custom timestamp
402
+ await profiler.requestProfile({ alertId: 'test-alert', timestamp: 123456 })
403
+
404
+ // Wait for profile to be generated
405
+ await sleep(1500)
406
+
407
+ assert.ok(profileReceived, 'Profile should have been generated with custom timestamp')
408
+ })
409
+
410
+ test('Profiler should generate valid pprof profile', async (t) => {
411
+ const app = await setupApp(t)
412
+
413
+ let profileData = null
414
+ const profiler = new Profiler({
415
+ app,
416
+ workerId: 'main:0',
417
+ type: 'cpu',
418
+ duration: 1000,
419
+ sourceMaps: false,
420
+ onProfile: (err, profile, requests) => {
421
+ if (!err && profile) {
422
+ profileData = profile.data
423
+ }
424
+ }
425
+ })
426
+
427
+ t.after(async () => {
428
+ await profiler.stop()
429
+ })
430
+
431
+ // Request a profile
432
+ await profiler.requestProfile({ alertId: 'test-alert' })
433
+
434
+ // Wait for profile to be generated
435
+ await sleep(1500)
436
+
437
+ assert.ok(profileData, 'Profile data should exist')
438
+
439
+ // Verify it's a valid pprof format
440
+ const profile = Profile.decode(profileData)
441
+ assert.ok(profile, 'Profile should be decodable')
442
+ assert.ok(profile.sample, 'Profile should have samples')
443
+ })