@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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(//work/workspaces/workspace-platformatic/platformatic/**)"
5
+ ],
6
+ "deny": [],
7
+ "ask": []
8
+ }
9
+ }
@@ -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@v5
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 = false
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
- // ...config.health,
398
- enabled: false
399
- // interval: 1000,
400
- // maxUnhealthyChecks: 30
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.2.1-alpha.4",
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.20.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
- await setInterval(async () => {
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
  }
@@ -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: alertId ? { alertId } : {},
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
- app.log.warn({ err, serviceId, podId }, 'Failed to send flamegraph from service')
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 { success: true }
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
+ })