@platformatic/watt-extra 1.2.1-alpha.5 → 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
@@ -1,10 +1,8 @@
1
- import { readFile, writeFile } from 'node:fs/promises'
2
- import { join, resolve, dirname } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
1
+ import { readFile } from 'node:fs/promises'
2
+ import { join, resolve } from 'node:path'
4
3
  import { createRequire } from 'node:module'
5
4
 
6
5
  const require = createRequire(import.meta.url)
7
- const __dirname = dirname(fileURLToPath(import.meta.url))
8
6
 
9
7
  // Simple replacement for ensureLoggableError
10
8
  function ensureLoggableError (err) {
@@ -65,16 +63,6 @@ class Watt {
65
63
  )
66
64
  throw err
67
65
  }
68
-
69
- const { eventLoopUtilization } = require('node:perf_hooks').performance
70
- // Print eventloop utilization every second
71
- let elu1 = eventLoopUtilization()
72
- setInterval(() => {
73
- const elu2 = eventLoopUtilization()
74
- const current = eventLoopUtilization(elu1)
75
- console.log('Watt-extra event loop utilization:', current)
76
- elu1 = elu2
77
- }, 1000)
78
66
  }
79
67
 
80
68
  async close () {
@@ -112,15 +100,6 @@ class Watt {
112
100
 
113
101
  async #createRuntime () {
114
102
  this.#logger.info('Creating runtime')
115
- const runtimeDir = dirname(this.#require.resolve('@platformatic/runtime'))
116
- const runtimeModule = join(runtimeDir, 'lib', 'runtime.js')
117
- const runtimePatch = join(__dirname, 'runtime-patch.js')
118
- const patchedRuntime = await readFile(runtimePatch, 'utf8')
119
- await writeFile(runtimeModule, patchedRuntime)
120
-
121
- const patchedRuntimeModule = await readFile(runtimeModule, 'utf8')
122
- process._rawDebug('------------', patchedRuntimeModule)
123
-
124
103
  const { create, transform } = this.#require('@platformatic/runtime')
125
104
 
126
105
  this.#logger.info('Building runtime')
@@ -172,7 +151,7 @@ class Watt {
172
151
  config.server = {
173
152
  ...serverConfig,
174
153
  hostname: this.#env.PLT_APP_HOSTNAME || serverConfig.hostname,
175
- port: this.#env.PLT_APP_PORT || serverConfig.port
154
+ port: this.#env.PLT_APP_PORT || serverConfig.port,
176
155
  }
177
156
 
178
157
  config.hotReload = false
@@ -180,14 +159,14 @@ class Watt {
180
159
  config.metrics = {
181
160
  server: 'hide',
182
161
  defaultMetrics: {
183
- enabled: true
162
+ enabled: true,
184
163
  },
185
164
  hostname: this.#env.PLT_APP_HOSTNAME || '0.0.0.0',
186
165
  port: this.#env.PLT_METRICS_PORT || 9090,
187
166
  labels: {
188
167
  serviceId: 'main',
189
168
  applicationId: this.#instanceConfig?.applicationId,
190
- instanceId: this.#instanceId
169
+ instanceId: this.#instanceId,
191
170
  },
192
171
  applicationLabel: this.#instanceConfig?.applicationMetricsLabel ?? 'serviceId'
193
172
  }
@@ -201,7 +180,7 @@ class Watt {
201
180
  }
202
181
 
203
182
  this.#configureUndici(config)
204
- config.managementApi = false
183
+ config.managementApi = true
205
184
  }
206
185
 
207
186
  #getUndiciConfig () {
@@ -252,20 +231,20 @@ class Watt {
252
231
  ),
253
232
  options: {
254
233
  labels: {
255
- applicationId: this.#instanceConfig.applicationId
234
+ applicationId: this.#instanceConfig.applicationId,
256
235
  },
257
236
  bloomFilter: {
258
237
  size: 100000,
259
- errorRate: 0.01
238
+ errorRate: 0.01,
260
239
  },
261
240
  maxResponseSize: 5 * 1024 * 1024, // 5MB
262
241
  trafficInspectorOptions: {
263
242
  url: trafficInspectorOrigin,
264
243
  pathSendBody: join(trafficInspectorPath, '/requests'),
265
- pathSendMeta: join(trafficInspectorPath, '/requests/hash')
244
+ pathSendMeta: join(trafficInspectorPath, '/requests/hash'),
266
245
  },
267
- matchingDomains: [this.#env.PLT_APP_INTERNAL_SUB_DOMAIN]
268
- }
246
+ matchingDomains: [this.#env.PLT_APP_INTERNAL_SUB_DOMAIN],
247
+ },
269
248
  }
270
249
  }
271
250
 
@@ -276,9 +255,9 @@ class Watt {
276
255
  rules: [
277
256
  {
278
257
  routeToMatch: 'http://plt.slicer.default/',
279
- headers: {}
280
- }
281
- ]
258
+ headers: {},
259
+ },
260
+ ],
282
261
  }
283
262
 
284
263
  // This is the cache config from ICC
@@ -330,7 +309,7 @@ class Watt {
330
309
 
331
310
  return {
332
311
  module: require.resolve('undici-slicer-interceptor'),
333
- options: cacheConfig
312
+ options: cacheConfig,
334
313
  }
335
314
  }
336
315
 
@@ -362,7 +341,7 @@ class Watt {
362
341
  applicationName: `${this.#applicationName}`,
363
342
  skip: [
364
343
  { method: 'GET', path: '/documentation' },
365
- { method: 'GET', path: '/documentation/json' }
344
+ { method: 'GET', path: '/documentation/json' },
366
345
  ],
367
346
  exporter: {
368
347
  type: 'otlp',
@@ -370,14 +349,14 @@ class Watt {
370
349
  url:
371
350
  this.#instanceConfig?.iccServices?.riskEngine?.url + '/v1/traces',
372
351
  headers: {
373
- 'x-platformatic-application-id': this.#instanceConfig?.applicationId
352
+ 'x-platformatic-application-id': this.#instanceConfig?.applicationId,
374
353
  },
375
354
  keepAlive: true,
376
355
  httpAgentOptions: {
377
- rejectUnauthorized: false
378
- }
379
- }
380
- }
356
+ rejectUnauthorized: false,
357
+ },
358
+ },
359
+ },
381
360
  }
382
361
  }
383
362
 
@@ -396,17 +375,16 @@ class Watt {
396
375
  ...config.httpCache,
397
376
  cacheTagsHeader,
398
377
  store: require.resolve('undici-cache-redis'),
399
- clientOpts: httpCache
378
+ clientOpts: httpCache,
400
379
  }
401
380
  }
402
381
 
403
382
  #configureHealth (config) {
404
383
  config.health = {
405
384
  ...config.health,
406
- gracePeriod: 1000,
407
385
  enabled: true,
408
386
  interval: 1000,
409
- maxUnhealthyChecks: 30
387
+ maxUnhealthyChecks: 30,
410
388
  }
411
389
  }
412
390
 
@@ -416,7 +394,7 @@ class Watt {
416
394
  if (config.scheduler) {
417
395
  config.scheduler = config.scheduler.map((scheduler) => ({
418
396
  ...scheduler,
419
- enabled: false
397
+ enabled: false,
420
398
  }))
421
399
  }
422
400
  }
@@ -435,7 +413,7 @@ class Watt {
435
413
  [
436
414
  '@platformatic/service',
437
415
  '@platformatic/composer',
438
- '@platformatic/db'
416
+ '@platformatic/db',
439
417
  ].includes(app.type)
440
418
  ) {
441
419
  await this.#configurePlatformaticServices(runtime, app)
@@ -479,8 +457,8 @@ class Watt {
479
457
  adapter: 'valkey',
480
458
  url: `valkey://${username}:${password}@${host}:${port}`,
481
459
  prefix: keyPrefix,
482
- maxTTL: 604800 // 86400 * 7
483
- }
460
+ maxTTL: 604800, // 86400 * 7
461
+ },
484
462
  })
485
463
  }
486
464
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/watt-extra",
3
- "version": "1.2.1-alpha.5",
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",
@@ -36,7 +36,7 @@
36
36
  "dependencies": {
37
37
  "@datadog/pprof": "^5.9.0",
38
38
  "@fastify/error": "^4.2.0",
39
- "@platformatic/runtime": "^3.11.0",
39
+ "@platformatic/runtime": "^3.8.0",
40
40
  "@platformatic/wattpm-pprof-capture": "^3.8.0",
41
41
  "avvio": "^9.1.0",
42
42
  "chalk": "^4.1.2",
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
+ })