@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.
- package/.claude/settings.local.json +9 -0
- package/.github/workflows/test.yml +1 -1
- package/lib/watt.js +27 -49
- package/package.json +3 -3
- 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
- package/lib/runtime-patch.js +0 -2712
|
@@ -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
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { readFile
|
|
2
|
-
import { join, resolve
|
|
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 =
|
|
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.
|
|
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",
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
+
})
|