@platformatic/watt-extra 1.4.0-alpha.3 → 1.4.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 +11 -0
- package/app.js +3 -0
- package/lib/watt.js +34 -28
- package/package.json +1 -1
- package/plugins/flamegraphs.js +35 -1
- package/test/alerts.test.js +0 -14
- package/test/metrics.test.js +37 -4
package/app.js
CHANGED
|
@@ -112,6 +112,9 @@ async function buildApp (logger) {
|
|
|
112
112
|
|
|
113
113
|
app.close = async function close () {
|
|
114
114
|
app.log.info('Closing runtime')
|
|
115
|
+
if (app.cleanupFlamegraphs) {
|
|
116
|
+
await app.cleanupFlamegraphs()
|
|
117
|
+
}
|
|
115
118
|
if (app.watt.runtime) {
|
|
116
119
|
await app.watt.close()
|
|
117
120
|
}
|
package/lib/watt.js
CHANGED
|
@@ -151,7 +151,17 @@ class Watt {
|
|
|
151
151
|
config.server = {
|
|
152
152
|
...serverConfig,
|
|
153
153
|
hostname: this.#env.PLT_APP_HOSTNAME || serverConfig.hostname,
|
|
154
|
-
port: this.#env.PLT_APP_PORT || serverConfig.port
|
|
154
|
+
port: this.#env.PLT_APP_PORT || serverConfig.port
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const labels = {
|
|
158
|
+
serviceId: 'main',
|
|
159
|
+
instanceId: this.#instanceId
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const applicationId = this.#instanceConfig?.applicationId
|
|
163
|
+
if (applicationId) {
|
|
164
|
+
labels.applicationId = applicationId
|
|
155
165
|
}
|
|
156
166
|
|
|
157
167
|
config.hotReload = false
|
|
@@ -159,15 +169,11 @@ class Watt {
|
|
|
159
169
|
config.metrics = {
|
|
160
170
|
server: 'hide',
|
|
161
171
|
defaultMetrics: {
|
|
162
|
-
enabled: true
|
|
172
|
+
enabled: true
|
|
163
173
|
},
|
|
164
174
|
hostname: this.#env.PLT_APP_HOSTNAME || '0.0.0.0',
|
|
165
175
|
port: this.#env.PLT_METRICS_PORT || 9090,
|
|
166
|
-
labels
|
|
167
|
-
serviceId: 'main',
|
|
168
|
-
applicationId: this.#instanceConfig?.applicationId,
|
|
169
|
-
instanceId: this.#instanceId,
|
|
170
|
-
},
|
|
176
|
+
labels,
|
|
171
177
|
applicationLabel: this.#instanceConfig?.applicationMetricsLabel ?? 'serviceId'
|
|
172
178
|
}
|
|
173
179
|
|
|
@@ -231,20 +237,20 @@ class Watt {
|
|
|
231
237
|
),
|
|
232
238
|
options: {
|
|
233
239
|
labels: {
|
|
234
|
-
applicationId: this.#instanceConfig.applicationId
|
|
240
|
+
applicationId: this.#instanceConfig.applicationId
|
|
235
241
|
},
|
|
236
242
|
bloomFilter: {
|
|
237
243
|
size: 100000,
|
|
238
|
-
errorRate: 0.01
|
|
244
|
+
errorRate: 0.01
|
|
239
245
|
},
|
|
240
246
|
maxResponseSize: 5 * 1024 * 1024, // 5MB
|
|
241
247
|
trafficInspectorOptions: {
|
|
242
248
|
url: trafficInspectorOrigin,
|
|
243
249
|
pathSendBody: join(trafficInspectorPath, '/requests'),
|
|
244
|
-
pathSendMeta: join(trafficInspectorPath, '/requests/hash')
|
|
250
|
+
pathSendMeta: join(trafficInspectorPath, '/requests/hash')
|
|
245
251
|
},
|
|
246
|
-
matchingDomains: [this.#env.PLT_APP_INTERNAL_SUB_DOMAIN]
|
|
247
|
-
}
|
|
252
|
+
matchingDomains: [this.#env.PLT_APP_INTERNAL_SUB_DOMAIN]
|
|
253
|
+
}
|
|
248
254
|
}
|
|
249
255
|
}
|
|
250
256
|
|
|
@@ -255,9 +261,9 @@ class Watt {
|
|
|
255
261
|
rules: [
|
|
256
262
|
{
|
|
257
263
|
routeToMatch: 'http://plt.slicer.default/',
|
|
258
|
-
headers: {}
|
|
259
|
-
}
|
|
260
|
-
]
|
|
264
|
+
headers: {}
|
|
265
|
+
}
|
|
266
|
+
]
|
|
261
267
|
}
|
|
262
268
|
|
|
263
269
|
// This is the cache config from ICC
|
|
@@ -309,7 +315,7 @@ class Watt {
|
|
|
309
315
|
|
|
310
316
|
return {
|
|
311
317
|
module: require.resolve('undici-slicer-interceptor'),
|
|
312
|
-
options: cacheConfig
|
|
318
|
+
options: cacheConfig
|
|
313
319
|
}
|
|
314
320
|
}
|
|
315
321
|
|
|
@@ -341,7 +347,7 @@ class Watt {
|
|
|
341
347
|
applicationName: `${this.#applicationName}`,
|
|
342
348
|
skip: [
|
|
343
349
|
{ method: 'GET', path: '/documentation' },
|
|
344
|
-
{ method: 'GET', path: '/documentation/json' }
|
|
350
|
+
{ method: 'GET', path: '/documentation/json' }
|
|
345
351
|
],
|
|
346
352
|
exporter: {
|
|
347
353
|
type: 'otlp',
|
|
@@ -349,14 +355,14 @@ class Watt {
|
|
|
349
355
|
url:
|
|
350
356
|
this.#instanceConfig?.iccServices?.riskEngine?.url + '/v1/traces',
|
|
351
357
|
headers: {
|
|
352
|
-
'x-platformatic-application-id': this.#instanceConfig?.applicationId
|
|
358
|
+
'x-platformatic-application-id': this.#instanceConfig?.applicationId
|
|
353
359
|
},
|
|
354
360
|
keepAlive: true,
|
|
355
361
|
httpAgentOptions: {
|
|
356
|
-
rejectUnauthorized: false
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
362
|
+
rejectUnauthorized: false
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
360
366
|
}
|
|
361
367
|
}
|
|
362
368
|
|
|
@@ -375,7 +381,7 @@ class Watt {
|
|
|
375
381
|
...config.httpCache,
|
|
376
382
|
cacheTagsHeader,
|
|
377
383
|
store: require.resolve('undici-cache-redis'),
|
|
378
|
-
clientOpts: httpCache
|
|
384
|
+
clientOpts: httpCache
|
|
379
385
|
}
|
|
380
386
|
}
|
|
381
387
|
|
|
@@ -384,7 +390,7 @@ class Watt {
|
|
|
384
390
|
...config.health,
|
|
385
391
|
enabled: true,
|
|
386
392
|
interval: 1000,
|
|
387
|
-
maxUnhealthyChecks: 30
|
|
393
|
+
maxUnhealthyChecks: 30
|
|
388
394
|
}
|
|
389
395
|
}
|
|
390
396
|
|
|
@@ -394,7 +400,7 @@ class Watt {
|
|
|
394
400
|
if (config.scheduler) {
|
|
395
401
|
config.scheduler = config.scheduler.map((scheduler) => ({
|
|
396
402
|
...scheduler,
|
|
397
|
-
enabled: false
|
|
403
|
+
enabled: false
|
|
398
404
|
}))
|
|
399
405
|
}
|
|
400
406
|
}
|
|
@@ -413,7 +419,7 @@ class Watt {
|
|
|
413
419
|
[
|
|
414
420
|
'@platformatic/service',
|
|
415
421
|
'@platformatic/composer',
|
|
416
|
-
'@platformatic/db'
|
|
422
|
+
'@platformatic/db'
|
|
417
423
|
].includes(app.type)
|
|
418
424
|
) {
|
|
419
425
|
await this.#configurePlatformaticServices(runtime, app)
|
|
@@ -457,8 +463,8 @@ class Watt {
|
|
|
457
463
|
adapter: 'valkey',
|
|
458
464
|
url: `valkey://${username}:${password}@${host}:${port}`,
|
|
459
465
|
prefix: keyPrefix,
|
|
460
|
-
maxTTL: 604800
|
|
461
|
-
}
|
|
466
|
+
maxTTL: 604800 // 86400 * 7
|
|
467
|
+
}
|
|
462
468
|
})
|
|
463
469
|
}
|
|
464
470
|
}
|
package/package.json
CHANGED
package/plugins/flamegraphs.js
CHANGED
|
@@ -80,11 +80,45 @@ async function flamegraphs (app, _opts) {
|
|
|
80
80
|
runtime.on('application:worker:started', workerStartedListener)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
app.cleanupFlamegraphs = () => {
|
|
83
|
+
app.cleanupFlamegraphs = async () => {
|
|
84
84
|
if (workerStartedListener && app.watt?.runtime) {
|
|
85
85
|
app.watt.runtime.removeListener('application:worker:started', workerStartedListener)
|
|
86
86
|
workerStartedListener = null
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
// Explicitly stop all active profiling sessions to avoid memory corruption
|
|
90
|
+
if (!isFlamegraphsDisabled && app.watt?.runtime) {
|
|
91
|
+
try {
|
|
92
|
+
const workers = await app.watt.runtime.getWorkers()
|
|
93
|
+
const stopPromises = []
|
|
94
|
+
for (const workerFullId of Object.keys(workers)) {
|
|
95
|
+
// Stop both CPU and heap profiling on each worker
|
|
96
|
+
stopPromises.push(
|
|
97
|
+
app.watt.runtime.sendCommandToApplication(workerFullId, 'stopProfiling', { type: 'cpu' })
|
|
98
|
+
.catch(err => {
|
|
99
|
+
// Ignore errors if profiling wasn't running
|
|
100
|
+
if (err.code !== 'PLT_PPROF_PROFILING_NOT_STARTED') {
|
|
101
|
+
app.log.warn({ err, workerFullId }, 'Failed to stop CPU profiling')
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
stopPromises.push(
|
|
106
|
+
app.watt.runtime.sendCommandToApplication(workerFullId, 'stopProfiling', { type: 'heap' })
|
|
107
|
+
.catch(err => {
|
|
108
|
+
// Ignore errors if profiling wasn't running
|
|
109
|
+
if (err.code !== 'PLT_PPROF_PROFILING_NOT_STARTED') {
|
|
110
|
+
app.log.warn({ err, workerFullId }, 'Failed to stop heap profiling')
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
await Promise.all(stopPromises)
|
|
116
|
+
// Small delay to ensure native cleanup completes
|
|
117
|
+
await sleep(100)
|
|
118
|
+
} catch (err) {
|
|
119
|
+
app.log.warn({ err }, 'Failed to stop profiling during cleanup')
|
|
120
|
+
}
|
|
121
|
+
}
|
|
88
122
|
}
|
|
89
123
|
|
|
90
124
|
app.sendFlamegraphs = async (options = {}) => {
|
package/test/alerts.test.js
CHANGED
|
@@ -52,8 +52,6 @@ test('should send alert when service becomes unhealthy', async (t) => {
|
|
|
52
52
|
const app = await start()
|
|
53
53
|
app.getAuthorizationHeader = getAuthorizationHeader
|
|
54
54
|
|
|
55
|
-
await app.setupAlerts()
|
|
56
|
-
|
|
57
55
|
t.after(async () => {
|
|
58
56
|
await app.close()
|
|
59
57
|
await icc.close()
|
|
@@ -131,8 +129,6 @@ test('should not send alert when service is healthy', async (t) => {
|
|
|
131
129
|
const app = await start()
|
|
132
130
|
app.getAuthorizationHeader = getAuthorizationHeader
|
|
133
131
|
|
|
134
|
-
await app.setupAlerts()
|
|
135
|
-
|
|
136
132
|
t.after(async () => {
|
|
137
133
|
await app.close()
|
|
138
134
|
await icc.close()
|
|
@@ -199,8 +195,6 @@ test('should cache health data and include it in alerts', async (t) => {
|
|
|
199
195
|
const app = await start()
|
|
200
196
|
app.getAuthorizationHeader = getAuthorizationHeader
|
|
201
197
|
|
|
202
|
-
await app.setupAlerts()
|
|
203
|
-
|
|
204
198
|
t.after(async () => {
|
|
205
199
|
await app.close()
|
|
206
200
|
await icc.close()
|
|
@@ -314,8 +308,6 @@ test('should not fail when health info is missing', async (t) => {
|
|
|
314
308
|
const app = await start()
|
|
315
309
|
app.getAuthorizationHeader = getAuthorizationHeader
|
|
316
310
|
|
|
317
|
-
await app.setupAlerts()
|
|
318
|
-
|
|
319
311
|
t.after(async () => {
|
|
320
312
|
await app.close()
|
|
321
313
|
await icc.close()
|
|
@@ -365,8 +357,6 @@ test('should respect alert retention window', async (t) => {
|
|
|
365
357
|
|
|
366
358
|
app.getAuthorizationHeader = getAuthorizationHeader
|
|
367
359
|
|
|
368
|
-
await app.setupAlerts()
|
|
369
|
-
|
|
370
360
|
t.after(async () => {
|
|
371
361
|
await app.close()
|
|
372
362
|
await icc.close()
|
|
@@ -489,8 +479,6 @@ test('should send alert when flamegraphs are disabled', async (t) => {
|
|
|
489
479
|
const app = await start()
|
|
490
480
|
app.getAuthorizationHeader = getAuthorizationHeader
|
|
491
481
|
|
|
492
|
-
await app.setupAlerts()
|
|
493
|
-
|
|
494
482
|
t.after(async () => {
|
|
495
483
|
await app.close()
|
|
496
484
|
await icc.close()
|
|
@@ -568,8 +556,6 @@ test('should send alert when failed to send a flamegraph', async (t) => {
|
|
|
568
556
|
const app = await start()
|
|
569
557
|
app.getAuthorizationHeader = getAuthorizationHeader
|
|
570
558
|
|
|
571
|
-
await app.setupAlerts()
|
|
572
|
-
|
|
573
559
|
t.after(async () => {
|
|
574
560
|
await app.close()
|
|
575
561
|
await icc.close()
|
package/test/metrics.test.js
CHANGED
|
@@ -28,7 +28,7 @@ test('should generate metrics with a correct labels', async (t) => {
|
|
|
28
28
|
setUpEnvironment({
|
|
29
29
|
PLT_APP_NAME: applicationName,
|
|
30
30
|
PLT_APP_DIR: applicationPath,
|
|
31
|
-
PLT_ICC_URL: 'http://127.0.0.1:3000'
|
|
31
|
+
PLT_ICC_URL: 'http://127.0.0.1:3000'
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
const app = await start()
|
|
@@ -40,7 +40,7 @@ test('should generate metrics with a correct labels', async (t) => {
|
|
|
40
40
|
|
|
41
41
|
const { statusCode, body } = await request('http://127.0.0.1:9090/metrics', {
|
|
42
42
|
headers: {
|
|
43
|
-
accept: 'application/json'
|
|
43
|
+
accept: 'application/json'
|
|
44
44
|
}
|
|
45
45
|
})
|
|
46
46
|
assert.strictEqual(statusCode, 200)
|
|
@@ -77,7 +77,7 @@ test('should generate metrics with a custom metrics label', async (t) => {
|
|
|
77
77
|
setUpEnvironment({
|
|
78
78
|
PLT_APP_NAME: applicationName,
|
|
79
79
|
PLT_APP_DIR: applicationPath,
|
|
80
|
-
PLT_ICC_URL: 'http://127.0.0.1:3000'
|
|
80
|
+
PLT_ICC_URL: 'http://127.0.0.1:3000'
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
const app = await start()
|
|
@@ -89,7 +89,7 @@ test('should generate metrics with a custom metrics label', async (t) => {
|
|
|
89
89
|
|
|
90
90
|
const { statusCode, body } = await request('http://127.0.0.1:9090/metrics', {
|
|
91
91
|
headers: {
|
|
92
|
-
accept: 'application/json'
|
|
92
|
+
accept: 'application/json'
|
|
93
93
|
}
|
|
94
94
|
})
|
|
95
95
|
assert.strictEqual(statusCode, 200)
|
|
@@ -105,3 +105,36 @@ test('should generate metrics with a custom metrics label', async (t) => {
|
|
|
105
105
|
assert.strictEqual(labels[applicationMetricsLabel], 'main')
|
|
106
106
|
}
|
|
107
107
|
})
|
|
108
|
+
|
|
109
|
+
test('should not set an applicationId label if it is undefined', async (t) => {
|
|
110
|
+
const applicationName = 'test-app'
|
|
111
|
+
const applicationPath = join(__dirname, 'fixtures', 'service-1')
|
|
112
|
+
|
|
113
|
+
delete process.env.PLT_ICC_URL
|
|
114
|
+
|
|
115
|
+
process.env.PLT_TEST_APP_1_URL = 'http://test-app-1:3042'
|
|
116
|
+
t.after(() => {
|
|
117
|
+
delete process.env.PLT_TEST_APP_1_URL
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
setUpEnvironment({
|
|
121
|
+
PLT_APP_NAME: applicationName,
|
|
122
|
+
PLT_APP_DIR: applicationPath
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const app = await start()
|
|
126
|
+
|
|
127
|
+
t.after(async () => {
|
|
128
|
+
await app.close()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const { statusCode, body } = await request('http://127.0.0.1:9090/metrics')
|
|
132
|
+
assert.strictEqual(statusCode, 200)
|
|
133
|
+
|
|
134
|
+
const metrics = await body.text()
|
|
135
|
+
const lines = metrics.split('\n')
|
|
136
|
+
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
assert.ok(!line.includes('applicationId'), 'applicationId label should not be set:' + line)
|
|
139
|
+
}
|
|
140
|
+
})
|