@platformatic/watt-extra 1.5.3 → 1.6.0-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.
- package/.claude/settings.local.json +5 -1
- package/lib/watt.js +23 -5
- package/package.json +9 -9
- package/plugins/alerts.js +40 -2
- package/plugins/flamegraphs.js +8 -2
- package/test/alerts.test.js +151 -19
- package/test/fixtures/runtime-health-custom/package.json +20 -0
- package/test/fixtures/runtime-health-custom/platformatic.json +21 -0
- package/test/fixtures/runtime-health-custom/services/service-1/package.json +17 -0
- package/test/fixtures/runtime-health-custom/services/service-1/platformatic.json +16 -0
- package/test/fixtures/runtime-health-custom/services/service-1/plugins/example.js +6 -0
- package/test/fixtures/runtime-health-custom/services/service-1/routes/root.cjs +8 -0
- package/test/fixtures/runtime-health-custom/services/service-2/package.json +17 -0
- package/test/fixtures/runtime-health-custom/services/service-2/platformatic.json +16 -0
- package/test/fixtures/runtime-health-custom/services/service-2/plugins/example.js +6 -0
- package/test/fixtures/runtime-health-custom/services/service-2/routes/root.cjs +8 -0
- package/test/fixtures/runtime-health-disabled/package.json +20 -0
- package/test/fixtures/runtime-health-disabled/platformatic.json +20 -0
- package/test/fixtures/runtime-health-disabled/services/service-1/package.json +17 -0
- package/test/fixtures/runtime-health-disabled/services/service-1/platformatic.json +16 -0
- package/test/fixtures/runtime-health-disabled/services/service-1/plugins/example.js +6 -0
- package/test/fixtures/runtime-health-disabled/services/service-1/routes/root.cjs +8 -0
- package/test/fixtures/runtime-health-disabled/services/service-2/package.json +17 -0
- package/test/fixtures/runtime-health-disabled/services/service-2/platformatic.json +16 -0
- package/test/fixtures/runtime-health-disabled/services/service-2/plugins/example.js +6 -0
- package/test/fixtures/runtime-health-disabled/services/service-2/routes/root.cjs +8 -0
- package/test/health.test.js +85 -2
- package/test/patch-config.test.js +117 -2
- package/test/trigger-flamegraphs.test.js +431 -9
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { test } from 'node:test'
|
|
2
|
-
import { equal } from 'node:assert'
|
|
2
|
+
import { equal, ok, deepEqual } from 'node:assert'
|
|
3
3
|
import { once } from 'node:events'
|
|
4
4
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
5
5
|
import { WebSocketServer } from 'ws'
|
|
@@ -67,8 +67,16 @@ function createMockApp (port, includeScalerUrl = true) {
|
|
|
67
67
|
for (const listener of listeners) {
|
|
68
68
|
listener(...args)
|
|
69
69
|
}
|
|
70
|
-
}
|
|
71
|
-
|
|
70
|
+
},
|
|
71
|
+
getApplicationDetails: async (id) => {
|
|
72
|
+
// Default implementation, can be overridden in tests
|
|
73
|
+
return { id, config: {} }
|
|
74
|
+
},
|
|
75
|
+
getRuntimeConfig: async () => {
|
|
76
|
+
// Default implementation, can be overridden in tests
|
|
77
|
+
return {}
|
|
78
|
+
},
|
|
79
|
+
},
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
const app = {
|
|
@@ -110,6 +118,296 @@ function createMockApp (port, includeScalerUrl = true) {
|
|
|
110
118
|
|
|
111
119
|
const port = 14000
|
|
112
120
|
|
|
121
|
+
test('setupFlamegraphs should pass sourceMaps from application config to startProfiling', async (t) => {
|
|
122
|
+
setUpEnvironment()
|
|
123
|
+
|
|
124
|
+
const app = createMockApp(port)
|
|
125
|
+
const startProfilingCalls = []
|
|
126
|
+
|
|
127
|
+
// Mock getApplicationDetails to return config with sourceMaps for worker IDs
|
|
128
|
+
app.watt.runtime.getApplicationDetails = async (workerFullId) => {
|
|
129
|
+
if (workerFullId.startsWith('service-1')) {
|
|
130
|
+
return { id: workerFullId, config: { sourceMaps: true } }
|
|
131
|
+
} else if (workerFullId.startsWith('service-2')) {
|
|
132
|
+
return { id: workerFullId, config: { sourceMaps: false } }
|
|
133
|
+
}
|
|
134
|
+
return { id: workerFullId, config: {} }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
app.watt.runtime.getRuntimeConfig = async () => {
|
|
138
|
+
return {}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
app.watt.runtime.sendCommandToApplication = async (workerFullId, command, options) => {
|
|
142
|
+
if (command === 'startProfiling') {
|
|
143
|
+
startProfilingCalls.push({ workerFullId, command, options })
|
|
144
|
+
return { success: true }
|
|
145
|
+
}
|
|
146
|
+
return { success: false }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await flamegraphsPlugin(app)
|
|
150
|
+
await app.setupFlamegraphs()
|
|
151
|
+
|
|
152
|
+
// Should call startProfiling 4 times: 2 workers × 2 profile types (cpu + heap)
|
|
153
|
+
equal(startProfilingCalls.length, 4, 'Should call startProfiling for both workers with cpu and heap')
|
|
154
|
+
|
|
155
|
+
const service1CpuCall = startProfilingCalls.find(c => c.workerFullId === 'service-1:0' && c.options.type === 'cpu')
|
|
156
|
+
const service1HeapCall = startProfilingCalls.find(c => c.workerFullId === 'service-1:0' && c.options.type === 'heap')
|
|
157
|
+
const service2CpuCall = startProfilingCalls.find(c => c.workerFullId === 'service-2:0' && c.options.type === 'cpu')
|
|
158
|
+
const service2HeapCall = startProfilingCalls.find(c => c.workerFullId === 'service-2:0' && c.options.type === 'heap')
|
|
159
|
+
|
|
160
|
+
ok(service1CpuCall, 'Should have called startProfiling for service-1 CPU')
|
|
161
|
+
ok(service1HeapCall, 'Should have called startProfiling for service-1 heap')
|
|
162
|
+
ok(service2CpuCall, 'Should have called startProfiling for service-2 CPU')
|
|
163
|
+
ok(service2HeapCall, 'Should have called startProfiling for service-2 heap')
|
|
164
|
+
|
|
165
|
+
equal(service1CpuCall.options.sourceMaps, true, 'Should pass sourceMaps=true for service-1 CPU')
|
|
166
|
+
equal(service1HeapCall.options.sourceMaps, true, 'Should pass sourceMaps=true for service-1 heap')
|
|
167
|
+
equal(service2CpuCall.options.sourceMaps, false, 'Should pass sourceMaps=false for service-2 CPU')
|
|
168
|
+
equal(service2HeapCall.options.sourceMaps, false, 'Should pass sourceMaps=false for service-2 heap')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('setupFlamegraphs should handle missing sourceMaps in application config', async (t) => {
|
|
172
|
+
setUpEnvironment()
|
|
173
|
+
|
|
174
|
+
const app = createMockApp(port)
|
|
175
|
+
const startProfilingCalls = []
|
|
176
|
+
|
|
177
|
+
// Mock getApplicationDetails to return config without sourceMaps
|
|
178
|
+
app.watt.runtime.getApplicationDetails = async (workerFullId) => {
|
|
179
|
+
return { id: workerFullId, config: {} }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
app.watt.runtime.getRuntimeConfig = async () => {
|
|
183
|
+
return {}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
app.watt.runtime.sendCommandToApplication = async (workerFullId, command, options) => {
|
|
187
|
+
if (command === 'startProfiling') {
|
|
188
|
+
startProfilingCalls.push({ workerFullId, command, options })
|
|
189
|
+
return { success: true }
|
|
190
|
+
}
|
|
191
|
+
return { success: false }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await flamegraphsPlugin(app)
|
|
195
|
+
await app.setupFlamegraphs()
|
|
196
|
+
|
|
197
|
+
equal(startProfilingCalls.length, 4, 'Should call startProfiling for both workers with cpu and heap')
|
|
198
|
+
|
|
199
|
+
for (const call of startProfilingCalls) {
|
|
200
|
+
equal(call.options.sourceMaps, undefined, 'sourceMaps should be undefined when not in config')
|
|
201
|
+
equal(call.options.durationMillis, 1000, 'Should still pass duration')
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('setupFlamegraphs should skip profiling when PLT_DISABLE_FLAMEGRAPHS is set', async (t) => {
|
|
206
|
+
setUpEnvironment()
|
|
207
|
+
|
|
208
|
+
const app = createMockApp(port)
|
|
209
|
+
app.env.PLT_DISABLE_FLAMEGRAPHS = true
|
|
210
|
+
|
|
211
|
+
const startProfilingCalls = []
|
|
212
|
+
|
|
213
|
+
app.watt.runtime.sendCommandToApplication = async (serviceId, command, options) => {
|
|
214
|
+
if (command === 'startProfiling') {
|
|
215
|
+
startProfilingCalls.push({ serviceId, command, options })
|
|
216
|
+
return { success: true }
|
|
217
|
+
}
|
|
218
|
+
return { success: false }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await flamegraphsPlugin(app)
|
|
222
|
+
await app.setupFlamegraphs()
|
|
223
|
+
|
|
224
|
+
equal(startProfilingCalls.length, 0, 'Should not call startProfiling when disabled')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('setupFlamegraphs should handle errors when starting profiling', async (t) => {
|
|
228
|
+
setUpEnvironment()
|
|
229
|
+
|
|
230
|
+
const app = createMockApp(port)
|
|
231
|
+
const errors = []
|
|
232
|
+
|
|
233
|
+
app.log.error = (result) => {
|
|
234
|
+
errors.push(result)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
app.watt.runtime.getApplicationDetails = async (workerFullId) => {
|
|
238
|
+
return { id: workerFullId, config: { sourceMaps: true } }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
app.watt.runtime.getRuntimeConfig = async () => {
|
|
242
|
+
return {}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
app.watt.runtime.sendCommandToApplication = async (workerFullId, command, options) => {
|
|
246
|
+
if (command === 'startProfiling') {
|
|
247
|
+
throw new Error(`Failed to start profiling for ${workerFullId}`)
|
|
248
|
+
}
|
|
249
|
+
return { success: false }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await flamegraphsPlugin(app)
|
|
253
|
+
await app.setupFlamegraphs()
|
|
254
|
+
|
|
255
|
+
// Should log 4 errors (2 workers, each logged twice: once in startProfilingOnWorker, once in setupFlamegraphs)
|
|
256
|
+
equal(errors.length, 4, 'Should log errors for both failed workers')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('sendFlamegraphs should upload flamegraphs from all services', async (t) => {
|
|
260
|
+
setUpEnvironment()
|
|
261
|
+
|
|
262
|
+
const uploadedFlamegraphs = []
|
|
263
|
+
const httpPort = port + 100
|
|
264
|
+
|
|
265
|
+
const app = createMockApp(httpPort)
|
|
266
|
+
|
|
267
|
+
const mockProfile = new Uint8Array([1, 2, 3, 4, 5])
|
|
268
|
+
|
|
269
|
+
app.watt.runtime.sendCommandToApplication = async (serviceId, command) => {
|
|
270
|
+
if (command === 'getLastProfile') {
|
|
271
|
+
return mockProfile
|
|
272
|
+
}
|
|
273
|
+
return { success: false }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Mock HTTP server to receive flamegraphs
|
|
277
|
+
const { createServer } = await import('node:http')
|
|
278
|
+
const server = createServer((req, res) => {
|
|
279
|
+
const body = []
|
|
280
|
+
req.on('data', chunk => body.push(chunk))
|
|
281
|
+
req.on('end', () => {
|
|
282
|
+
const buffer = Buffer.concat(body)
|
|
283
|
+
uploadedFlamegraphs.push({
|
|
284
|
+
url: req.url,
|
|
285
|
+
headers: req.headers,
|
|
286
|
+
body: buffer
|
|
287
|
+
})
|
|
288
|
+
res.writeHead(200)
|
|
289
|
+
res.end()
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
await new Promise(resolve => server.listen(httpPort, resolve))
|
|
294
|
+
t.after(() => server.close())
|
|
295
|
+
|
|
296
|
+
await flamegraphsPlugin(app)
|
|
297
|
+
await app.sendFlamegraphs()
|
|
298
|
+
|
|
299
|
+
equal(uploadedFlamegraphs.length, 2, 'Should upload flamegraphs for both services')
|
|
300
|
+
|
|
301
|
+
const service1Upload = uploadedFlamegraphs.find(u => u.url.includes('service-1'))
|
|
302
|
+
const service2Upload = uploadedFlamegraphs.find(u => u.url.includes('service-2'))
|
|
303
|
+
|
|
304
|
+
ok(service1Upload, 'Should upload flamegraph for service-1')
|
|
305
|
+
ok(service2Upload, 'Should upload flamegraph for service-2')
|
|
306
|
+
|
|
307
|
+
equal(service1Upload.headers['content-type'], 'application/octet-stream')
|
|
308
|
+
equal(service1Upload.headers.authorization, 'Bearer test-token')
|
|
309
|
+
deepEqual(service1Upload.body, Buffer.from(mockProfile))
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test('sendFlamegraphs should handle missing profile data', async (t) => {
|
|
313
|
+
setUpEnvironment()
|
|
314
|
+
|
|
315
|
+
const app = createMockApp(port + 11)
|
|
316
|
+
const errors = []
|
|
317
|
+
|
|
318
|
+
app.log.error = (obj) => {
|
|
319
|
+
errors.push(obj)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
app.watt.runtime.sendCommandToApplication = async (serviceId, command) => {
|
|
323
|
+
if (command === 'getLastProfile') {
|
|
324
|
+
// Return invalid data (not Uint8Array)
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
return { success: false }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await flamegraphsPlugin(app)
|
|
331
|
+
await app.sendFlamegraphs()
|
|
332
|
+
|
|
333
|
+
equal(errors.length, 2, 'Should log errors for both services with missing profiles')
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
test('sendFlamegraphs should filter by serviceIds when provided', async (t) => {
|
|
337
|
+
setUpEnvironment()
|
|
338
|
+
|
|
339
|
+
const app = createMockApp(port + 12)
|
|
340
|
+
const getProfileCalls = []
|
|
341
|
+
|
|
342
|
+
app.watt.runtime.sendCommandToApplication = async (serviceId, command) => {
|
|
343
|
+
if (command === 'getLastProfile') {
|
|
344
|
+
getProfileCalls.push(serviceId)
|
|
345
|
+
return new Uint8Array([1, 2, 3])
|
|
346
|
+
}
|
|
347
|
+
return { success: false }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Mock HTTP server
|
|
351
|
+
const { createServer } = await import('node:http')
|
|
352
|
+
const server = createServer((req, res) => {
|
|
353
|
+
const body = []
|
|
354
|
+
req.on('data', chunk => body.push(chunk))
|
|
355
|
+
req.on('end', () => {
|
|
356
|
+
res.writeHead(200)
|
|
357
|
+
res.end()
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
await new Promise(resolve => server.listen(port + 12, resolve))
|
|
362
|
+
t.after(() => server.close())
|
|
363
|
+
|
|
364
|
+
await flamegraphsPlugin(app)
|
|
365
|
+
await app.sendFlamegraphs({ serviceIds: ['service-1'] })
|
|
366
|
+
|
|
367
|
+
equal(getProfileCalls.length, 1, 'Should only request profile for specified service')
|
|
368
|
+
equal(getProfileCalls[0], 'service-1', 'Should request profile for service-1')
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
test('sendFlamegraphs should skip when PLT_DISABLE_FLAMEGRAPHS is set', async (t) => {
|
|
372
|
+
setUpEnvironment()
|
|
373
|
+
|
|
374
|
+
const app = createMockApp(port + 13)
|
|
375
|
+
app.env.PLT_DISABLE_FLAMEGRAPHS = true
|
|
376
|
+
|
|
377
|
+
const getProfileCalls = []
|
|
378
|
+
|
|
379
|
+
app.watt.runtime.sendCommandToApplication = async (serviceId, command) => {
|
|
380
|
+
if (command === 'getLastProfile') {
|
|
381
|
+
getProfileCalls.push(serviceId)
|
|
382
|
+
return new Uint8Array([1, 2, 3])
|
|
383
|
+
}
|
|
384
|
+
return { success: false }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
await flamegraphsPlugin(app)
|
|
388
|
+
await app.sendFlamegraphs()
|
|
389
|
+
|
|
390
|
+
equal(getProfileCalls.length, 0, 'Should not request profiles when disabled')
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('sendFlamegraphs should throw error when scaler URL is missing', async (t) => {
|
|
394
|
+
setUpEnvironment()
|
|
395
|
+
|
|
396
|
+
const app = createMockApp(port + 14, false) // Don't include scaler URL
|
|
397
|
+
|
|
398
|
+
await flamegraphsPlugin(app)
|
|
399
|
+
|
|
400
|
+
let errorThrown = false
|
|
401
|
+
try {
|
|
402
|
+
await app.sendFlamegraphs()
|
|
403
|
+
} catch (err) {
|
|
404
|
+
errorThrown = true
|
|
405
|
+
ok(err.message.includes('No scaler URL'), 'Should throw error about missing scaler URL')
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
ok(errorThrown, 'Should throw error when scaler URL is missing')
|
|
409
|
+
})
|
|
410
|
+
|
|
113
411
|
test('should handle trigger-flamegraph command and upload flamegraphs from services', async (t) => {
|
|
114
412
|
setUpEnvironment()
|
|
115
413
|
|
|
@@ -120,7 +418,7 @@ test('should handle trigger-flamegraph command and upload flamegraphs from servi
|
|
|
120
418
|
uploadResolve = resolve
|
|
121
419
|
})
|
|
122
420
|
|
|
123
|
-
const wss = new WebSocketServer({ port })
|
|
421
|
+
const wss = new WebSocketServer({ port: port + 15 })
|
|
124
422
|
t.after(async () => wss.close())
|
|
125
423
|
|
|
126
424
|
const { waitForClientSubscription, getWs } = setupMockIccServer(
|
|
@@ -129,7 +427,7 @@ test('should handle trigger-flamegraph command and upload flamegraphs from servi
|
|
|
129
427
|
true
|
|
130
428
|
)
|
|
131
429
|
|
|
132
|
-
const app = createMockApp(port)
|
|
430
|
+
const app = createMockApp(port + 15)
|
|
133
431
|
|
|
134
432
|
app.watt.runtime.sendCommandToApplication = async (
|
|
135
433
|
serviceId,
|
|
@@ -185,7 +483,7 @@ test('should handle trigger-flamegraph when no runtime is available', async (t)
|
|
|
185
483
|
|
|
186
484
|
const receivedMessages = []
|
|
187
485
|
|
|
188
|
-
const wss = new WebSocketServer({ port: port +
|
|
486
|
+
const wss = new WebSocketServer({ port: port + 16 })
|
|
189
487
|
t.after(async () => wss.close())
|
|
190
488
|
|
|
191
489
|
const { waitForClientSubscription, getWs } = setupMockIccServer(
|
|
@@ -194,7 +492,7 @@ test('should handle trigger-flamegraph when no runtime is available', async (t)
|
|
|
194
492
|
false
|
|
195
493
|
)
|
|
196
494
|
|
|
197
|
-
const app = createMockApp(port +
|
|
495
|
+
const app = createMockApp(port + 16)
|
|
198
496
|
app.watt.runtime = null
|
|
199
497
|
|
|
200
498
|
await updatePlugin(app)
|
|
@@ -218,7 +516,7 @@ test('should handle trigger-flamegraph when flamegraph upload fails', async (t)
|
|
|
218
516
|
|
|
219
517
|
const receivedMessages = []
|
|
220
518
|
|
|
221
|
-
const wss = new WebSocketServer({ port: port +
|
|
519
|
+
const wss = new WebSocketServer({ port: port + 17 })
|
|
222
520
|
t.after(async () => wss.close())
|
|
223
521
|
|
|
224
522
|
const { waitForClientSubscription, getWs } = setupMockIccServer(
|
|
@@ -227,7 +525,7 @@ test('should handle trigger-flamegraph when flamegraph upload fails', async (t)
|
|
|
227
525
|
false
|
|
228
526
|
)
|
|
229
527
|
|
|
230
|
-
const app = createMockApp(port +
|
|
528
|
+
const app = createMockApp(port + 17)
|
|
231
529
|
|
|
232
530
|
app.watt.runtime.sendCommandToApplication = async (
|
|
233
531
|
serviceId,
|
|
@@ -603,3 +901,127 @@ test('should not start profiling on new workers when flamegraphs are disabled',
|
|
|
603
901
|
if (app.cleanupFlamegraphs) app.cleanupFlamegraphs()
|
|
604
902
|
await app.closeUpdates()
|
|
605
903
|
})
|
|
904
|
+
|
|
905
|
+
test('sendFlamegraphs should include alertId in query params when provided', async (t) => {
|
|
906
|
+
setUpEnvironment()
|
|
907
|
+
|
|
908
|
+
const uploadedRequests = []
|
|
909
|
+
|
|
910
|
+
const app = createMockApp(port + 18)
|
|
911
|
+
|
|
912
|
+
app.watt.runtime.sendCommandToApplication = async (serviceId, command) => {
|
|
913
|
+
if (command === 'getLastProfile') {
|
|
914
|
+
return new Uint8Array([1, 2, 3])
|
|
915
|
+
}
|
|
916
|
+
return { success: false }
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Mock HTTP server to capture requests
|
|
920
|
+
const { createServer } = await import('node:http')
|
|
921
|
+
const server = createServer((req, res) => {
|
|
922
|
+
const body = []
|
|
923
|
+
req.on('data', chunk => body.push(chunk))
|
|
924
|
+
req.on('end', () => {
|
|
925
|
+
uploadedRequests.push({
|
|
926
|
+
url: req.url,
|
|
927
|
+
method: req.method
|
|
928
|
+
})
|
|
929
|
+
res.writeHead(200)
|
|
930
|
+
res.end()
|
|
931
|
+
})
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
await new Promise(resolve => server.listen(port + 18, resolve))
|
|
935
|
+
t.after(() => server.close())
|
|
936
|
+
|
|
937
|
+
await flamegraphsPlugin(app)
|
|
938
|
+
await app.sendFlamegraphs({ alertId: 'test-alert-123' })
|
|
939
|
+
|
|
940
|
+
equal(uploadedRequests.length, 2, 'Should upload flamegraphs for both services')
|
|
941
|
+
|
|
942
|
+
for (const req of uploadedRequests) {
|
|
943
|
+
ok(req.url.includes('alertId=test-alert-123'), 'URL should include alertId query param')
|
|
944
|
+
}
|
|
945
|
+
})
|
|
946
|
+
|
|
947
|
+
test('setupFlamegraphs should use runtime-level sourceMaps as fallback', async (t) => {
|
|
948
|
+
setUpEnvironment()
|
|
949
|
+
|
|
950
|
+
const app = createMockApp(port)
|
|
951
|
+
const startProfilingCalls = []
|
|
952
|
+
|
|
953
|
+
// Mock getApplicationDetails to return config WITHOUT service-level sourceMaps
|
|
954
|
+
app.watt.runtime.getApplicationDetails = async (workerFullId) => {
|
|
955
|
+
return { id: workerFullId, config: {} } // No sourceMaps at service level
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Mock getRuntimeConfig to return runtime-level sourceMaps
|
|
959
|
+
app.watt.runtime.getRuntimeConfig = async () => {
|
|
960
|
+
return { sourceMaps: true } // Runtime-level default
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
app.watt.runtime.sendCommandToApplication = async (workerFullId, command, options) => {
|
|
964
|
+
if (command === 'startProfiling') {
|
|
965
|
+
startProfilingCalls.push({ workerFullId, command, options })
|
|
966
|
+
return { success: true }
|
|
967
|
+
}
|
|
968
|
+
return { success: false }
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
await flamegraphsPlugin(app)
|
|
972
|
+
await app.setupFlamegraphs()
|
|
973
|
+
|
|
974
|
+
equal(startProfilingCalls.length, 4, 'Should call startProfiling for both workers with cpu and heap')
|
|
975
|
+
|
|
976
|
+
for (const call of startProfilingCalls) {
|
|
977
|
+
equal(call.options.sourceMaps, true, 'Should use runtime-level sourceMaps as fallback')
|
|
978
|
+
equal(call.options.durationMillis, 1000, 'Should still pass duration')
|
|
979
|
+
}
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
test('setupFlamegraphs should prefer service-level over runtime-level sourceMaps', async (t) => {
|
|
983
|
+
setUpEnvironment()
|
|
984
|
+
|
|
985
|
+
const app = createMockApp(port)
|
|
986
|
+
const startProfilingCalls = []
|
|
987
|
+
|
|
988
|
+
// Mock getApplicationDetails - service-1 has explicit false, service-2 has no setting
|
|
989
|
+
app.watt.runtime.getApplicationDetails = async (workerFullId) => {
|
|
990
|
+
if (workerFullId.startsWith('service-1')) {
|
|
991
|
+
return { id: workerFullId, config: { sourceMaps: false } } // Explicitly set to false
|
|
992
|
+
}
|
|
993
|
+
return { id: workerFullId, config: {} } // No setting
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Mock getRuntimeConfig to return runtime-level sourceMaps = true
|
|
997
|
+
app.watt.runtime.getRuntimeConfig = async () => {
|
|
998
|
+
return { sourceMaps: true } // Runtime-level default
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
app.watt.runtime.sendCommandToApplication = async (workerFullId, command, options) => {
|
|
1002
|
+
if (command === 'startProfiling') {
|
|
1003
|
+
startProfilingCalls.push({ workerFullId, command, options })
|
|
1004
|
+
return { success: true }
|
|
1005
|
+
}
|
|
1006
|
+
return { success: false }
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
await flamegraphsPlugin(app)
|
|
1010
|
+
await app.setupFlamegraphs()
|
|
1011
|
+
|
|
1012
|
+
equal(startProfilingCalls.length, 4, 'Should call startProfiling for both workers with cpu and heap')
|
|
1013
|
+
|
|
1014
|
+
const service1Calls = startProfilingCalls.filter(c => c.workerFullId === 'service-1:0')
|
|
1015
|
+
const service2Calls = startProfilingCalls.filter(c => c.workerFullId === 'service-2:0')
|
|
1016
|
+
|
|
1017
|
+
equal(service1Calls.length, 2, 'Should have 2 calls for service-1 (cpu + heap)')
|
|
1018
|
+
equal(service2Calls.length, 2, 'Should have 2 calls for service-2 (cpu + heap)')
|
|
1019
|
+
|
|
1020
|
+
for (const call of service1Calls) {
|
|
1021
|
+
equal(call.options.sourceMaps, false, 'Service-1 should use explicit false, not runtime default')
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
for (const call of service2Calls) {
|
|
1025
|
+
equal(call.options.sourceMaps, true, 'Service-2 should inherit runtime-level true')
|
|
1026
|
+
}
|
|
1027
|
+
})
|