@platformatic/watt-extra 1.2.1-alpha.4 → 1.3.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 +9 -0
- package/.github/workflows/test.yml +1 -1
- package/lib/watt.js +28 -41
- package/package.json +2 -2
- package/plugins/auth.js +3 -3
- package/plugins/env.js +2 -0
- package/plugins/flamegraphs.js +23 -5
- package/plugins/update.js +8 -1
- package/test/auth.test.js +44 -0
- package/test/trigger-flamegraphs.test.js +219 -13
|
@@ -41,7 +41,7 @@ jobs:
|
|
|
41
41
|
version: 10
|
|
42
42
|
|
|
43
43
|
- name: Use Node.js ${{ matrix.node-version }}
|
|
44
|
-
uses: actions/setup-node@
|
|
44
|
+
uses: actions/setup-node@v6
|
|
45
45
|
with:
|
|
46
46
|
node-version: ${{ matrix.node-version }}
|
|
47
47
|
# https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md#use-private-packages
|
package/lib/watt.js
CHANGED
|
@@ -63,16 +63,6 @@ class Watt {
|
|
|
63
63
|
)
|
|
64
64
|
throw err
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
const { eventLoopUtilization } = require('node:perf_hooks').performance
|
|
68
|
-
// Print eventloop utilization every second
|
|
69
|
-
let elu1 = eventLoopUtilization()
|
|
70
|
-
setInterval(() => {
|
|
71
|
-
const elu2 = eventLoopUtilization()
|
|
72
|
-
const current = eventLoopUtilization(elu1)
|
|
73
|
-
console.log('Watt-extra event loop utilization:', current)
|
|
74
|
-
elu1 = elu2
|
|
75
|
-
}, 1000)
|
|
76
66
|
}
|
|
77
67
|
|
|
78
68
|
async close () {
|
|
@@ -161,25 +151,22 @@ class Watt {
|
|
|
161
151
|
config.server = {
|
|
162
152
|
...serverConfig,
|
|
163
153
|
hostname: this.#env.PLT_APP_HOSTNAME || serverConfig.hostname,
|
|
164
|
-
port: this.#env.PLT_APP_PORT || serverConfig.port
|
|
154
|
+
port: this.#env.PLT_APP_PORT || serverConfig.port,
|
|
165
155
|
}
|
|
166
156
|
|
|
167
157
|
config.hotReload = false
|
|
168
158
|
config.restartOnError = 1000
|
|
169
|
-
config.metrics = {
|
|
170
|
-
enabled: false
|
|
171
|
-
}
|
|
172
159
|
config.metrics = {
|
|
173
160
|
server: 'hide',
|
|
174
161
|
defaultMetrics: {
|
|
175
|
-
enabled: true
|
|
162
|
+
enabled: true,
|
|
176
163
|
},
|
|
177
164
|
hostname: this.#env.PLT_APP_HOSTNAME || '0.0.0.0',
|
|
178
165
|
port: this.#env.PLT_METRICS_PORT || 9090,
|
|
179
166
|
labels: {
|
|
180
167
|
serviceId: 'main',
|
|
181
168
|
applicationId: this.#instanceConfig?.applicationId,
|
|
182
|
-
instanceId: this.#instanceId
|
|
169
|
+
instanceId: this.#instanceId,
|
|
183
170
|
},
|
|
184
171
|
applicationLabel: this.#instanceConfig?.applicationMetricsLabel ?? 'serviceId'
|
|
185
172
|
}
|
|
@@ -193,7 +180,7 @@ class Watt {
|
|
|
193
180
|
}
|
|
194
181
|
|
|
195
182
|
this.#configureUndici(config)
|
|
196
|
-
config.managementApi =
|
|
183
|
+
config.managementApi = true
|
|
197
184
|
}
|
|
198
185
|
|
|
199
186
|
#getUndiciConfig () {
|
|
@@ -244,20 +231,20 @@ class Watt {
|
|
|
244
231
|
),
|
|
245
232
|
options: {
|
|
246
233
|
labels: {
|
|
247
|
-
applicationId: this.#instanceConfig.applicationId
|
|
234
|
+
applicationId: this.#instanceConfig.applicationId,
|
|
248
235
|
},
|
|
249
236
|
bloomFilter: {
|
|
250
237
|
size: 100000,
|
|
251
|
-
errorRate: 0.01
|
|
238
|
+
errorRate: 0.01,
|
|
252
239
|
},
|
|
253
240
|
maxResponseSize: 5 * 1024 * 1024, // 5MB
|
|
254
241
|
trafficInspectorOptions: {
|
|
255
242
|
url: trafficInspectorOrigin,
|
|
256
243
|
pathSendBody: join(trafficInspectorPath, '/requests'),
|
|
257
|
-
pathSendMeta: join(trafficInspectorPath, '/requests/hash')
|
|
244
|
+
pathSendMeta: join(trafficInspectorPath, '/requests/hash'),
|
|
258
245
|
},
|
|
259
|
-
matchingDomains: [this.#env.PLT_APP_INTERNAL_SUB_DOMAIN]
|
|
260
|
-
}
|
|
246
|
+
matchingDomains: [this.#env.PLT_APP_INTERNAL_SUB_DOMAIN],
|
|
247
|
+
},
|
|
261
248
|
}
|
|
262
249
|
}
|
|
263
250
|
|
|
@@ -268,9 +255,9 @@ class Watt {
|
|
|
268
255
|
rules: [
|
|
269
256
|
{
|
|
270
257
|
routeToMatch: 'http://plt.slicer.default/',
|
|
271
|
-
headers: {}
|
|
272
|
-
}
|
|
273
|
-
]
|
|
258
|
+
headers: {},
|
|
259
|
+
},
|
|
260
|
+
],
|
|
274
261
|
}
|
|
275
262
|
|
|
276
263
|
// This is the cache config from ICC
|
|
@@ -322,7 +309,7 @@ class Watt {
|
|
|
322
309
|
|
|
323
310
|
return {
|
|
324
311
|
module: require.resolve('undici-slicer-interceptor'),
|
|
325
|
-
options: cacheConfig
|
|
312
|
+
options: cacheConfig,
|
|
326
313
|
}
|
|
327
314
|
}
|
|
328
315
|
|
|
@@ -354,7 +341,7 @@ class Watt {
|
|
|
354
341
|
applicationName: `${this.#applicationName}`,
|
|
355
342
|
skip: [
|
|
356
343
|
{ method: 'GET', path: '/documentation' },
|
|
357
|
-
{ method: 'GET', path: '/documentation/json' }
|
|
344
|
+
{ method: 'GET', path: '/documentation/json' },
|
|
358
345
|
],
|
|
359
346
|
exporter: {
|
|
360
347
|
type: 'otlp',
|
|
@@ -362,14 +349,14 @@ class Watt {
|
|
|
362
349
|
url:
|
|
363
350
|
this.#instanceConfig?.iccServices?.riskEngine?.url + '/v1/traces',
|
|
364
351
|
headers: {
|
|
365
|
-
'x-platformatic-application-id': this.#instanceConfig?.applicationId
|
|
352
|
+
'x-platformatic-application-id': this.#instanceConfig?.applicationId,
|
|
366
353
|
},
|
|
367
354
|
keepAlive: true,
|
|
368
355
|
httpAgentOptions: {
|
|
369
|
-
rejectUnauthorized: false
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
356
|
+
rejectUnauthorized: false,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
},
|
|
373
360
|
}
|
|
374
361
|
}
|
|
375
362
|
|
|
@@ -388,16 +375,16 @@ class Watt {
|
|
|
388
375
|
...config.httpCache,
|
|
389
376
|
cacheTagsHeader,
|
|
390
377
|
store: require.resolve('undici-cache-redis'),
|
|
391
|
-
clientOpts: httpCache
|
|
378
|
+
clientOpts: httpCache,
|
|
392
379
|
}
|
|
393
380
|
}
|
|
394
381
|
|
|
395
382
|
#configureHealth (config) {
|
|
396
383
|
config.health = {
|
|
397
|
-
|
|
398
|
-
enabled:
|
|
399
|
-
|
|
400
|
-
|
|
384
|
+
...config.health,
|
|
385
|
+
enabled: true,
|
|
386
|
+
interval: 1000,
|
|
387
|
+
maxUnhealthyChecks: 30,
|
|
401
388
|
}
|
|
402
389
|
}
|
|
403
390
|
|
|
@@ -407,7 +394,7 @@ class Watt {
|
|
|
407
394
|
if (config.scheduler) {
|
|
408
395
|
config.scheduler = config.scheduler.map((scheduler) => ({
|
|
409
396
|
...scheduler,
|
|
410
|
-
enabled: false
|
|
397
|
+
enabled: false,
|
|
411
398
|
}))
|
|
412
399
|
}
|
|
413
400
|
}
|
|
@@ -426,7 +413,7 @@ class Watt {
|
|
|
426
413
|
[
|
|
427
414
|
'@platformatic/service',
|
|
428
415
|
'@platformatic/composer',
|
|
429
|
-
'@platformatic/db'
|
|
416
|
+
'@platformatic/db',
|
|
430
417
|
].includes(app.type)
|
|
431
418
|
) {
|
|
432
419
|
await this.#configurePlatformaticServices(runtime, app)
|
|
@@ -470,8 +457,8 @@ class Watt {
|
|
|
470
457
|
adapter: 'valkey',
|
|
471
458
|
url: `valkey://${username}:${password}@${host}:${port}`,
|
|
472
459
|
prefix: keyPrefix,
|
|
473
|
-
maxTTL: 604800 // 86400 * 7
|
|
474
|
-
}
|
|
460
|
+
maxTTL: 604800, // 86400 * 7
|
|
461
|
+
},
|
|
475
462
|
})
|
|
476
463
|
}
|
|
477
464
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/watt-extra",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "The Platformatic runtime manager",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@platformatic/next": "^3.8.0",
|
|
24
24
|
"@platformatic/node": "^3.8.0",
|
|
25
25
|
"@platformatic/service": "^3.8.0",
|
|
26
|
-
"borp": "^0.
|
|
26
|
+
"borp": "^0.21.0",
|
|
27
27
|
"eslint": "9",
|
|
28
28
|
"fastify": "^5.4.0",
|
|
29
29
|
"fastify-plugin": "^5.0.1",
|
package/plugins/auth.js
CHANGED
|
@@ -48,7 +48,7 @@ async function authPlugin (app) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const getAuthorizationHeader = async (headers = {}) => {
|
|
51
|
-
if (isTokenExpired(app.token, offset)) {
|
|
51
|
+
if (app.token && isTokenExpired(app.token, offset)) {
|
|
52
52
|
app.log.info('JWT token expired, reloading')
|
|
53
53
|
app.token = await loadToken()
|
|
54
54
|
|
|
@@ -74,11 +74,11 @@ async function authPlugin (app) {
|
|
|
74
74
|
|
|
75
75
|
app.token = await loadToken()
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
setInterval(async () => {
|
|
78
78
|
// Check if token is expired to propagate it to the runtime
|
|
79
79
|
// via the shared context
|
|
80
80
|
await getAuthorizationHeader()
|
|
81
|
-
}, offset * 1000 / 2).unref()
|
|
81
|
+
}, offset ? offset * 1000 / 2 : 30000).unref()
|
|
82
82
|
|
|
83
83
|
// We cannot change the global dispatcher because it's shared with the runtime main thread.
|
|
84
84
|
const wattDispatcher = new Agent()
|
package/plugins/env.js
CHANGED
|
@@ -19,6 +19,8 @@ const schema = {
|
|
|
19
19
|
PLT_CACHE_CONFIG: { type: 'string' },
|
|
20
20
|
PLT_DISABLE_FLAMEGRAPHS: { type: 'boolean', default: false },
|
|
21
21
|
PLT_FLAMEGRAPHS_INTERVAL_SEC: { type: 'number', default: 60 },
|
|
22
|
+
PLT_FLAMEGRAPHS_ELU_THRESHOLD: { type: 'number', default: 0.4 },
|
|
23
|
+
PLT_FLAMEGRAPHS_GRACE_PERIOD: { type: 'number', default: 3000 },
|
|
22
24
|
PLT_JWT_EXPIRATION_OFFSET_SEC: { type: 'number', default: 60 },
|
|
23
25
|
PLT_UPDATES_RECONNECT_INTERVAL_SEC: { type: 'number', default: 1 }
|
|
24
26
|
}
|
package/plugins/flamegraphs.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
3
4
|
import { request } from 'undici'
|
|
4
5
|
|
|
5
6
|
async function flamegraphs (app, _opts) {
|
|
6
7
|
const isFlamegraphsDisabled = app.env.PLT_DISABLE_FLAMEGRAPHS
|
|
7
8
|
const flamegraphsIntervalSec = app.env.PLT_FLAMEGRAPHS_INTERVAL_SEC
|
|
9
|
+
const flamegraphsELUThreshold = app.env.PLT_FLAMEGRAPHS_ELU_THRESHOLD
|
|
10
|
+
const flamegraphsGracePeriod = app.env.PLT_FLAMEGRAPHS_GRACE_PERIOD
|
|
8
11
|
|
|
9
12
|
const durationMillis = parseInt(flamegraphsIntervalSec) * 1000
|
|
13
|
+
const eluThreshold = parseInt(flamegraphsELUThreshold)
|
|
14
|
+
const gracePeriod = parseInt(flamegraphsGracePeriod)
|
|
10
15
|
|
|
11
16
|
app.setupFlamegraphs = async () => {
|
|
12
17
|
if (isFlamegraphsDisabled) {
|
|
@@ -16,6 +21,8 @@ async function flamegraphs (app, _opts) {
|
|
|
16
21
|
|
|
17
22
|
app.log.info('Start profiling services')
|
|
18
23
|
|
|
24
|
+
await sleep(gracePeriod)
|
|
25
|
+
|
|
19
26
|
const runtime = app.watt.runtime
|
|
20
27
|
const { applications } = await runtime.getApplications()
|
|
21
28
|
|
|
@@ -24,7 +31,7 @@ async function flamegraphs (app, _opts) {
|
|
|
24
31
|
const promise = runtime.sendCommandToApplication(
|
|
25
32
|
application.id,
|
|
26
33
|
'startProfiling',
|
|
27
|
-
{ durationMillis }
|
|
34
|
+
{ durationMillis, eluThreshold }
|
|
28
35
|
)
|
|
29
36
|
promises.push(promise)
|
|
30
37
|
}
|
|
@@ -43,7 +50,7 @@ async function flamegraphs (app, _opts) {
|
|
|
43
50
|
return
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
let { serviceIds, alertId } = options
|
|
53
|
+
let { serviceIds, alertId, profileType = 'cpu' } = options
|
|
47
54
|
|
|
48
55
|
const scalerUrl = app.instanceConfig?.iccServices?.scaler?.url
|
|
49
56
|
if (!scalerUrl) {
|
|
@@ -71,7 +78,12 @@ async function flamegraphs (app, _opts) {
|
|
|
71
78
|
|
|
72
79
|
const url = `${scalerUrl}/pods/${podId}/services/${serviceId}/flamegraph`
|
|
73
80
|
|
|
74
|
-
app.log.info({ serviceId, podId }, 'Sending flamegraph')
|
|
81
|
+
app.log.info({ serviceId, podId, profileType }, 'Sending flamegraph')
|
|
82
|
+
|
|
83
|
+
const query = { profileType }
|
|
84
|
+
if (alertId) {
|
|
85
|
+
query.alertId = alertId
|
|
86
|
+
}
|
|
75
87
|
|
|
76
88
|
const { statusCode, body } = await request(url, {
|
|
77
89
|
method: 'POST',
|
|
@@ -79,7 +91,7 @@ async function flamegraphs (app, _opts) {
|
|
|
79
91
|
'Content-Type': 'application/octet-stream',
|
|
80
92
|
...authHeaders
|
|
81
93
|
},
|
|
82
|
-
query
|
|
94
|
+
query,
|
|
83
95
|
body: profile
|
|
84
96
|
})
|
|
85
97
|
|
|
@@ -89,7 +101,13 @@ async function flamegraphs (app, _opts) {
|
|
|
89
101
|
throw new Error(`Failed to send flamegraph: ${error}`)
|
|
90
102
|
}
|
|
91
103
|
} catch (err) {
|
|
92
|
-
|
|
104
|
+
if (err.code === 'PLT_PPROF_NO_PROFILE_AVAILABLE') {
|
|
105
|
+
app.log.info({ serviceId, podId }, 'No profile available for the service')
|
|
106
|
+
} else if (err.code === 'PLT_PPROF_NOT_ENOUGH_ELU') {
|
|
107
|
+
app.log.info({ serviceId, podId }, 'ELU low, CPU profiling not active')
|
|
108
|
+
} else {
|
|
109
|
+
app.log.warn({ err, serviceId, podId }, 'Failed to send flamegraph from service')
|
|
110
|
+
}
|
|
93
111
|
}
|
|
94
112
|
})
|
|
95
113
|
|
package/plugins/update.js
CHANGED
|
@@ -23,7 +23,14 @@ async function updatePlugin (app) {
|
|
|
23
23
|
// Handle trigger-flamegraph command from ICC
|
|
24
24
|
if (command === 'trigger-flamegraph') {
|
|
25
25
|
app.log.info({ command }, 'Received trigger-flamegraph command from ICC')
|
|
26
|
-
await app.sendFlamegraphs()
|
|
26
|
+
await app.sendFlamegraphs({ profileType: 'cpu' })
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Handle trigger-heapprofile command from ICC
|
|
31
|
+
if (command === 'trigger-heapprofile') {
|
|
32
|
+
app.log.info({ command }, 'Received trigger-heapprofile command from ICC')
|
|
33
|
+
await app.sendFlamegraphs({ profileType: 'heap' })
|
|
27
34
|
return
|
|
28
35
|
}
|
|
29
36
|
|
package/test/auth.test.js
CHANGED
|
@@ -126,3 +126,47 @@ test('auth plugin reloads expired token', async (t) => {
|
|
|
126
126
|
const reloadLogMessage = logMessages.find(msg => msg === 'JWT token expired, reloading')
|
|
127
127
|
equal(!!reloadLogMessage, true, 'Should log message about token reload')
|
|
128
128
|
})
|
|
129
|
+
|
|
130
|
+
test('auth plugin does not reload when token is undefined', async (t) => {
|
|
131
|
+
const originalEnv = { ...process.env }
|
|
132
|
+
|
|
133
|
+
t.after(() => {
|
|
134
|
+
process.env = originalEnv
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
delete process.env.PLT_TEST_TOKEN
|
|
138
|
+
|
|
139
|
+
const logMessages = []
|
|
140
|
+
const app = {
|
|
141
|
+
log: {
|
|
142
|
+
info: (msg) => {
|
|
143
|
+
if (typeof msg === 'string') {
|
|
144
|
+
logMessages.push(msg)
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
warn: () => {},
|
|
148
|
+
error: () => {}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const server = fastify()
|
|
153
|
+
server.get('/', async (request) => {
|
|
154
|
+
return { headers: request.headers }
|
|
155
|
+
})
|
|
156
|
+
await server.listen({ port: 0 })
|
|
157
|
+
const url = `http://localhost:${server.server.address().port}`
|
|
158
|
+
|
|
159
|
+
t.after(async () => server.close())
|
|
160
|
+
|
|
161
|
+
await authPlugin(app)
|
|
162
|
+
|
|
163
|
+
equal(app.token, undefined, 'Token should be undefined when not available')
|
|
164
|
+
|
|
165
|
+
const response = await request(url, { dispatcher: app.dispatcher })
|
|
166
|
+
const responseBody = await response.body.json()
|
|
167
|
+
|
|
168
|
+
equal(response.statusCode, 200)
|
|
169
|
+
equal(responseBody.headers.authorization, 'Bearer undefined')
|
|
170
|
+
const reloadLogMessage = logMessages.find(msg => msg === 'JWT token expired, reloading')
|
|
171
|
+
equal(reloadLogMessage, undefined, 'Should not attempt to reload undefined token')
|
|
172
|
+
})
|
|
@@ -39,9 +39,9 @@ function createMockApp (port, includeScalerUrl = true) {
|
|
|
39
39
|
const mockWatt = {
|
|
40
40
|
runtime: {
|
|
41
41
|
getApplications: () => ({
|
|
42
|
-
applications: [{ id: 'service-1' }, { id: 'service-2' }]
|
|
43
|
-
})
|
|
44
|
-
}
|
|
42
|
+
applications: [{ id: 'service-1' }, { id: 'service-2' }]
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const app = {
|
|
@@ -49,10 +49,10 @@ function createMockApp (port, includeScalerUrl = true) {
|
|
|
49
49
|
info: () => {},
|
|
50
50
|
error: () => {},
|
|
51
51
|
warn: () => {},
|
|
52
|
-
debug: () => {}
|
|
52
|
+
debug: () => {}
|
|
53
53
|
},
|
|
54
54
|
instanceConfig: {
|
|
55
|
-
applicationId: 'test-application-id'
|
|
55
|
+
applicationId: 'test-application-id'
|
|
56
56
|
},
|
|
57
57
|
instanceId: 'test-pod-123',
|
|
58
58
|
getAuthorizationHeader: async () => {
|
|
@@ -63,16 +63,18 @@ function createMockApp (port, includeScalerUrl = true) {
|
|
|
63
63
|
PLT_APP_DIR: '/path/to/app',
|
|
64
64
|
PLT_ICC_URL: `http://localhost:${port}`,
|
|
65
65
|
PLT_DISABLE_FLAMEGRAPHS: false,
|
|
66
|
-
PLT_FLAMEGRAPHS_INTERVAL_SEC: 1
|
|
66
|
+
PLT_FLAMEGRAPHS_INTERVAL_SEC: 1,
|
|
67
|
+
PLT_FLAMEGRAPHS_ELU_THRESHOLD: 0,
|
|
68
|
+
PLT_FLAMEGRAPHS_GRACE_PERIOD: 0
|
|
67
69
|
},
|
|
68
|
-
watt: mockWatt
|
|
70
|
+
watt: mockWatt
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
if (includeScalerUrl) {
|
|
72
74
|
app.instanceConfig.iccServices = {
|
|
73
75
|
scaler: {
|
|
74
|
-
url: `http://localhost:${port}/scaler
|
|
75
|
-
}
|
|
76
|
+
url: `http://localhost:${port}/scaler`
|
|
77
|
+
}
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -111,7 +113,7 @@ test('should handle trigger-flamegraph command and upload flamegraphs from servi
|
|
|
111
113
|
if (getFlamegraphReqs.length === 2) {
|
|
112
114
|
uploadResolve()
|
|
113
115
|
}
|
|
114
|
-
return
|
|
116
|
+
return new Uint8Array([1, 2, 3, 4, 5])
|
|
115
117
|
}
|
|
116
118
|
return { success: false }
|
|
117
119
|
}
|
|
@@ -125,7 +127,7 @@ test('should handle trigger-flamegraph command and upload flamegraphs from servi
|
|
|
125
127
|
await waitForClientSubscription
|
|
126
128
|
|
|
127
129
|
const triggerFlamegraphMessage = {
|
|
128
|
-
command: 'trigger-flamegraph'
|
|
130
|
+
command: 'trigger-flamegraph'
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
getWs().send(JSON.stringify(triggerFlamegraphMessage))
|
|
@@ -169,7 +171,7 @@ test('should handle trigger-flamegraph when no runtime is available', async (t)
|
|
|
169
171
|
await waitForClientSubscription
|
|
170
172
|
|
|
171
173
|
const triggerFlamegraphMessage = {
|
|
172
|
-
command: 'trigger-flamegraph'
|
|
174
|
+
command: 'trigger-flamegraph'
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
getWs().send(JSON.stringify(triggerFlamegraphMessage))
|
|
@@ -215,7 +217,7 @@ test('should handle trigger-flamegraph when flamegraph upload fails', async (t)
|
|
|
215
217
|
await waitForClientSubscription
|
|
216
218
|
|
|
217
219
|
const triggerFlamegraphMessage = {
|
|
218
|
-
command: 'trigger-flamegraph'
|
|
220
|
+
command: 'trigger-flamegraph'
|
|
219
221
|
}
|
|
220
222
|
|
|
221
223
|
getWs().send(JSON.stringify(triggerFlamegraphMessage))
|
|
@@ -224,3 +226,207 @@ test('should handle trigger-flamegraph when flamegraph upload fails', async (t)
|
|
|
224
226
|
|
|
225
227
|
await app.closeUpdates()
|
|
226
228
|
})
|
|
229
|
+
|
|
230
|
+
test('should handle trigger-heapprofile command and upload heap profiles from services', async (t) => {
|
|
231
|
+
setUpEnvironment()
|
|
232
|
+
|
|
233
|
+
const receivedMessages = []
|
|
234
|
+
const getHeapProfileReqs = []
|
|
235
|
+
let uploadResolve
|
|
236
|
+
const allUploadsComplete = new Promise((resolve) => {
|
|
237
|
+
uploadResolve = resolve
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const wss = new WebSocketServer({ port: port + 3 })
|
|
241
|
+
t.after(async () => wss.close())
|
|
242
|
+
|
|
243
|
+
const { waitForClientSubscription, getWs } = setupMockIccServer(
|
|
244
|
+
wss,
|
|
245
|
+
receivedMessages,
|
|
246
|
+
true
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
const app = createMockApp(port + 3)
|
|
250
|
+
|
|
251
|
+
app.watt.runtime.sendCommandToApplication = async (
|
|
252
|
+
serviceId,
|
|
253
|
+
command
|
|
254
|
+
) => {
|
|
255
|
+
if (command === 'getLastProfile') {
|
|
256
|
+
getHeapProfileReqs.push({ serviceId })
|
|
257
|
+
if (getHeapProfileReqs.length === 2) {
|
|
258
|
+
uploadResolve()
|
|
259
|
+
}
|
|
260
|
+
return new Uint8Array([1, 2, 3, 4, 5])
|
|
261
|
+
}
|
|
262
|
+
return { success: false }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await updatePlugin(app)
|
|
266
|
+
await flamegraphsPlugin(app)
|
|
267
|
+
|
|
268
|
+
await app.connectToUpdates()
|
|
269
|
+
await app.setupFlamegraphs()
|
|
270
|
+
|
|
271
|
+
await waitForClientSubscription
|
|
272
|
+
|
|
273
|
+
const triggerHeapProfileMessage = {
|
|
274
|
+
command: 'trigger-heapprofile'
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getWs().send(JSON.stringify(triggerHeapProfileMessage))
|
|
278
|
+
|
|
279
|
+
await allUploadsComplete
|
|
280
|
+
|
|
281
|
+
equal(getHeapProfileReqs.length, 2)
|
|
282
|
+
|
|
283
|
+
const service1Req = getHeapProfileReqs.find(
|
|
284
|
+
(f) => f.serviceId === 'service-1'
|
|
285
|
+
)
|
|
286
|
+
const service2Req = getHeapProfileReqs.find(
|
|
287
|
+
(f) => f.serviceId === 'service-2'
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
equal(service1Req.serviceId, 'service-1')
|
|
291
|
+
equal(service2Req.serviceId, 'service-2')
|
|
292
|
+
|
|
293
|
+
await app.closeUpdates()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('should handle PLT_PPROF_NO_PROFILE_AVAILABLE error with info log', async (t) => {
|
|
297
|
+
setUpEnvironment()
|
|
298
|
+
|
|
299
|
+
const receivedMessages = []
|
|
300
|
+
const infoLogs = []
|
|
301
|
+
let errorCount = 0
|
|
302
|
+
let uploadResolve
|
|
303
|
+
const allUploadsComplete = new Promise((resolve) => {
|
|
304
|
+
uploadResolve = resolve
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
const wss = new WebSocketServer({ port: port + 4 })
|
|
308
|
+
t.after(async () => wss.close())
|
|
309
|
+
|
|
310
|
+
const { waitForClientSubscription, getWs } = setupMockIccServer(
|
|
311
|
+
wss,
|
|
312
|
+
receivedMessages,
|
|
313
|
+
true
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
const app = createMockApp(port + 4)
|
|
317
|
+
const originalInfo = app.log.info
|
|
318
|
+
app.log.info = (...args) => {
|
|
319
|
+
originalInfo(...args)
|
|
320
|
+
if (args[1] && args[1].includes('No profile available for the service')) {
|
|
321
|
+
infoLogs.push(args)
|
|
322
|
+
errorCount++
|
|
323
|
+
if (errorCount === 2) {
|
|
324
|
+
uploadResolve()
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
app.watt.runtime.sendCommandToApplication = async (
|
|
330
|
+
serviceId,
|
|
331
|
+
command
|
|
332
|
+
) => {
|
|
333
|
+
if (command === 'getLastProfile') {
|
|
334
|
+
const error = new Error('No profile available - wait for profiling to complete or trigger manual capture')
|
|
335
|
+
error.code = 'PLT_PPROF_NO_PROFILE_AVAILABLE'
|
|
336
|
+
throw error
|
|
337
|
+
}
|
|
338
|
+
return { success: false }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
await updatePlugin(app)
|
|
342
|
+
await flamegraphsPlugin(app)
|
|
343
|
+
|
|
344
|
+
await app.connectToUpdates()
|
|
345
|
+
await app.setupFlamegraphs()
|
|
346
|
+
|
|
347
|
+
await waitForClientSubscription
|
|
348
|
+
|
|
349
|
+
const triggerFlamegraphMessage = {
|
|
350
|
+
command: 'trigger-flamegraph'
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
getWs().send(JSON.stringify(triggerFlamegraphMessage))
|
|
354
|
+
|
|
355
|
+
await allUploadsComplete
|
|
356
|
+
|
|
357
|
+
equal(infoLogs.length, 2)
|
|
358
|
+
equal(infoLogs[0][0].serviceId, 'service-1')
|
|
359
|
+
equal(infoLogs[0][0].podId, 'test-pod-123')
|
|
360
|
+
equal(infoLogs[0][1], 'No profile available for the service')
|
|
361
|
+
|
|
362
|
+
await app.closeUpdates()
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('should handle PLT_PPROF_NOT_ENOUGH_ELU error with info log', async (t) => {
|
|
366
|
+
setUpEnvironment()
|
|
367
|
+
|
|
368
|
+
const receivedMessages = []
|
|
369
|
+
const infoLogs = []
|
|
370
|
+
let errorCount = 0
|
|
371
|
+
let uploadResolve
|
|
372
|
+
const allUploadsComplete = new Promise((resolve) => {
|
|
373
|
+
uploadResolve = resolve
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
const wss = new WebSocketServer({ port: port + 5 })
|
|
377
|
+
t.after(async () => wss.close())
|
|
378
|
+
|
|
379
|
+
const { waitForClientSubscription, getWs } = setupMockIccServer(
|
|
380
|
+
wss,
|
|
381
|
+
receivedMessages,
|
|
382
|
+
true
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
const app = createMockApp(port + 5)
|
|
386
|
+
const originalInfo = app.log.info
|
|
387
|
+
app.log.info = (...args) => {
|
|
388
|
+
originalInfo(...args)
|
|
389
|
+
if (args[1] && args[1].includes('ELU low, CPU profiling not active')) {
|
|
390
|
+
infoLogs.push(args)
|
|
391
|
+
errorCount++
|
|
392
|
+
if (errorCount === 2) {
|
|
393
|
+
uploadResolve()
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
app.watt.runtime.sendCommandToApplication = async (
|
|
399
|
+
serviceId,
|
|
400
|
+
command
|
|
401
|
+
) => {
|
|
402
|
+
if (command === 'getLastProfile') {
|
|
403
|
+
const error = new Error('No profile available - event loop utilization has been below threshold for too long')
|
|
404
|
+
error.code = 'PLT_PPROF_NOT_ENOUGH_ELU'
|
|
405
|
+
throw error
|
|
406
|
+
}
|
|
407
|
+
return { success: false }
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
await updatePlugin(app)
|
|
411
|
+
await flamegraphsPlugin(app)
|
|
412
|
+
|
|
413
|
+
await app.connectToUpdates()
|
|
414
|
+
await app.setupFlamegraphs()
|
|
415
|
+
|
|
416
|
+
await waitForClientSubscription
|
|
417
|
+
|
|
418
|
+
const triggerFlamegraphMessage = {
|
|
419
|
+
command: 'trigger-flamegraph'
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
getWs().send(JSON.stringify(triggerFlamegraphMessage))
|
|
423
|
+
|
|
424
|
+
await allUploadsComplete
|
|
425
|
+
|
|
426
|
+
equal(infoLogs.length, 2)
|
|
427
|
+
equal(infoLogs[0][0].serviceId, 'service-1')
|
|
428
|
+
equal(infoLogs[0][0].podId, 'test-pod-123')
|
|
429
|
+
equal(infoLogs[0][1], 'ELU low, CPU profiling not active')
|
|
430
|
+
|
|
431
|
+
await app.closeUpdates()
|
|
432
|
+
})
|