@platformatic/watt-extra 1.5.3 → 1.6.0-alpha.1

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.
Files changed (29) hide show
  1. package/.claude/settings.local.json +5 -1
  2. package/lib/watt.js +23 -5
  3. package/package.json +9 -9
  4. package/plugins/alerts.js +41 -2
  5. package/plugins/flamegraphs.js +8 -2
  6. package/test/alerts.test.js +151 -19
  7. package/test/fixtures/runtime-health-custom/package.json +20 -0
  8. package/test/fixtures/runtime-health-custom/platformatic.json +21 -0
  9. package/test/fixtures/runtime-health-custom/services/service-1/package.json +17 -0
  10. package/test/fixtures/runtime-health-custom/services/service-1/platformatic.json +16 -0
  11. package/test/fixtures/runtime-health-custom/services/service-1/plugins/example.js +6 -0
  12. package/test/fixtures/runtime-health-custom/services/service-1/routes/root.cjs +8 -0
  13. package/test/fixtures/runtime-health-custom/services/service-2/package.json +17 -0
  14. package/test/fixtures/runtime-health-custom/services/service-2/platformatic.json +16 -0
  15. package/test/fixtures/runtime-health-custom/services/service-2/plugins/example.js +6 -0
  16. package/test/fixtures/runtime-health-custom/services/service-2/routes/root.cjs +8 -0
  17. package/test/fixtures/runtime-health-disabled/package.json +20 -0
  18. package/test/fixtures/runtime-health-disabled/platformatic.json +20 -0
  19. package/test/fixtures/runtime-health-disabled/services/service-1/package.json +17 -0
  20. package/test/fixtures/runtime-health-disabled/services/service-1/platformatic.json +16 -0
  21. package/test/fixtures/runtime-health-disabled/services/service-1/plugins/example.js +6 -0
  22. package/test/fixtures/runtime-health-disabled/services/service-1/routes/root.cjs +8 -0
  23. package/test/fixtures/runtime-health-disabled/services/service-2/package.json +17 -0
  24. package/test/fixtures/runtime-health-disabled/services/service-2/platformatic.json +16 -0
  25. package/test/fixtures/runtime-health-disabled/services/service-2/plugins/example.js +6 -0
  26. package/test/fixtures/runtime-health-disabled/services/service-2/routes/root.cjs +8 -0
  27. package/test/health.test.js +85 -2
  28. package/test/patch-config.test.js +117 -2
  29. package/test/trigger-flamegraphs.test.js +431 -9
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://schemas.platformatic.dev/@platformatic/runtime/2.62.1.json",
3
+ "entrypoint": "service-1",
4
+ "watch": true,
5
+ "restartOnError": true,
6
+ "autoload": {
7
+ "path": "services",
8
+ "exclude": ["docs"]
9
+ },
10
+ "logger": {
11
+ "level": "info"
12
+ },
13
+ "server": {
14
+ "hostname": "127.0.0.1",
15
+ "port": "3042"
16
+ },
17
+ "health": {
18
+ "enabled": false
19
+ }
20
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "service-1",
3
+ "scripts": {
4
+ "start": "platformatic start",
5
+ "test": "borp"
6
+ },
7
+ "devDependencies": {
8
+ "fastify": "^5.0.0",
9
+ "borp": "^0.19.0"
10
+ },
11
+ "dependencies": {
12
+ "@platformatic/service": "^2.70.0"
13
+ },
14
+ "engines": {
15
+ "node": "^18.8.0 || >=20.6.0"
16
+ }
17
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://schemas.platformatic.dev/@platformatic/service/2.62.1.json",
3
+ "service": {
4
+ "openapi": true
5
+ },
6
+ "watch": true,
7
+ "plugins": {
8
+ "paths": [
9
+ {
10
+ "path": "./plugins",
11
+ "encapsulate": false
12
+ },
13
+ "./routes"
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,6 @@
1
+ /// <reference path="../global.d.ts" />
2
+ 'use strict'
3
+ /** @param {import('fastify').FastifyInstance} fastify */
4
+ module.exports = async function (fastify, opts) {
5
+ fastify.decorate('example', 'foobar')
6
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference path="../global.d.ts" />
2
+ 'use strict'
3
+ /** @param {import('fastify').FastifyInstance} fastify */
4
+ module.exports = async function (fastify, opts) {
5
+ fastify.get('/example', async (request, reply) => {
6
+ return { hello: fastify.example }
7
+ })
8
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "service-2",
3
+ "scripts": {
4
+ "start": "platformatic start",
5
+ "test": "borp"
6
+ },
7
+ "devDependencies": {
8
+ "fastify": "^5.0.0",
9
+ "borp": "^0.19.0"
10
+ },
11
+ "dependencies": {
12
+ "@platformatic/service": "^2.70.0"
13
+ },
14
+ "engines": {
15
+ "node": "^18.8.0 || >=20.6.0"
16
+ }
17
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://schemas.platformatic.dev/@platformatic/service/2.62.1.json",
3
+ "service": {
4
+ "openapi": true
5
+ },
6
+ "watch": true,
7
+ "plugins": {
8
+ "paths": [
9
+ {
10
+ "path": "./plugins",
11
+ "encapsulate": false
12
+ },
13
+ "./routes"
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,6 @@
1
+ /// <reference path="../global.d.ts" />
2
+ 'use strict'
3
+ /** @param {import('fastify').FastifyInstance} fastify */
4
+ module.exports = async function (fastify, opts) {
5
+ fastify.decorate('example', 'foobar')
6
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference path="../global.d.ts" />
2
+ 'use strict'
3
+ /** @param {import('fastify').FastifyInstance} fastify */
4
+ module.exports = async function (fastify, opts) {
5
+ fastify.get('/example', async (request, reply) => {
6
+ return { hello: fastify.example }
7
+ })
8
+ }
@@ -3,6 +3,7 @@ import { test } from 'node:test'
3
3
  import { randomUUID } from 'node:crypto'
4
4
  import { join, dirname } from 'node:path'
5
5
  import { fileURLToPath } from 'node:url'
6
+ import { setTimeout } from 'node:timers/promises'
6
7
 
7
8
  import {
8
9
  setUpEnvironment,
@@ -37,8 +38,90 @@ test('check that health is configured in runtime', async (t) => {
37
38
 
38
39
  const runtimeConfig = await app.watt.runtime.getRuntimeConfig()
39
40
 
41
+ // Runtime applies its own defaults on top of ours, so we just verify health is enabled
42
+ // and has expected properties
40
43
  assert.ok(runtimeConfig.health, 'Health configuration should be present')
41
44
  assert.strictEqual(runtimeConfig.health.enabled, true, 'Health monitoring should be enabled')
42
- assert.strictEqual(runtimeConfig.health.interval, 1000)
43
- assert.strictEqual(runtimeConfig.health.gracePeriod, 30000)
45
+ assert.ok(runtimeConfig.health.interval, 'Health interval should be set')
46
+ assert.ok(runtimeConfig.health.maxUnhealthyChecks, 'Health maxUnhealthyChecks should be set')
47
+ })
48
+
49
+ test('check that custom health configuration is not overridden', async (t) => {
50
+ const appName = 'test-health-custom'
51
+ const applicationId = randomUUID()
52
+ const applicationPath = join(__dirname, 'fixtures', 'runtime-health-custom')
53
+
54
+ const icc = await startICC(t, {
55
+ applicationId
56
+ })
57
+
58
+ setUpEnvironment({
59
+ PLT_APP_NAME: appName,
60
+ PLT_APP_DIR: applicationPath,
61
+ PLT_ICC_URL: 'http://127.0.0.1:3000'
62
+ })
63
+
64
+ const app = await start()
65
+
66
+ t.after(async () => {
67
+ await app.close()
68
+ await icc.close()
69
+ })
70
+
71
+ const runtimeConfig = await app.watt.runtime.getRuntimeConfig()
72
+
73
+ assert.ok(runtimeConfig.health, 'Health configuration should be present')
74
+ assert.strictEqual(runtimeConfig.health.enabled, true, 'Health monitoring should be enabled')
75
+ assert.strictEqual(runtimeConfig.health.interval, 2500, 'Custom interval should be preserved')
76
+ assert.strictEqual(runtimeConfig.health.maxUnhealthyChecks, 50, 'Custom maxUnhealthyChecks should be preserved')
77
+ })
78
+
79
+ test('should force health enabled and set defaults when app tries to disable it', async (t) => {
80
+ const appName = 'test-health-disabled'
81
+ const applicationId = randomUUID()
82
+ const applicationPath = join(__dirname, 'fixtures', 'runtime-health-disabled')
83
+
84
+ const icc = await startICC(t, {
85
+ applicationId
86
+ })
87
+
88
+ setUpEnvironment({
89
+ PLT_APP_NAME: appName,
90
+ PLT_APP_DIR: applicationPath,
91
+ PLT_ICC_URL: 'http://127.0.0.1:3000'
92
+ })
93
+
94
+ const app = await start()
95
+
96
+ t.after(async () => {
97
+ await app.close()
98
+ await icc.close()
99
+ })
100
+
101
+ const runtimeConfig = await app.watt.runtime.getRuntimeConfig()
102
+
103
+ assert.ok(runtimeConfig.health, 'Health configuration should be present')
104
+ assert.strictEqual(runtimeConfig.health.enabled, true, 'Health should be forced to enabled even if app config says false')
105
+ assert.ok(runtimeConfig.health.interval, 'Health interval should be set')
106
+ assert.ok(runtimeConfig.health.maxUnhealthyChecks, 'Health maxUnhealthyChecks should be set')
107
+
108
+ // Wait for health events
109
+ let healthEventReceived = false
110
+ const eventName = app.watt.runtimeSupportsNewHealthMetrics()
111
+ ? 'application:worker:health:metrics'
112
+ : 'application:worker:health'
113
+
114
+ app.watt.runtime.on(eventName, (healthInfo) => {
115
+ healthEventReceived = true
116
+ })
117
+
118
+ // Wait up to 5 seconds for a health event
119
+ const startTime = Date.now()
120
+ /* eslint-disable no-unmodified-loop-condition */
121
+ while (!healthEventReceived && (Date.now() - startTime) < 5000) {
122
+ await setTimeout(100)
123
+ }
124
+ /* eslint-enable no-unmodified-loop-condition */
125
+
126
+ assert.ok(healthEventReceived, 'Should receive health events since enabled is forced to true')
44
127
  })
@@ -151,9 +151,13 @@ test('should configure health options', async (t) => {
151
151
  const runtimeConfig = await app.watt.runtime.getRuntimeConfig(true)
152
152
 
153
153
  const { health } = runtimeConfig
154
+ // Runtime applies its own defaults on top of ours, so we just verify health is configured
154
155
  assert.strictEqual(health.enabled, true)
155
- assert.strictEqual(health.interval, 1000)
156
- assert.strictEqual(health.maxUnhealthyChecks, 30)
156
+ assert.ok(health.interval, 'Health interval should be set')
157
+ assert.strictEqual(health.maxUnhealthyChecks, 10, 'Health maxUnhealthyChecks should have default value')
158
+ assert.strictEqual(health.maxELU, 0.99, 'Health maxELU should have default value')
159
+ assert.strictEqual(health.maxHeapUsed, 0.99, 'Health maxHeapUsed should have default value')
160
+ assert.strictEqual(health.gracePeriod, 30000, 'Health gracePeriod should have default value')
157
161
  })
158
162
 
159
163
  test('should not set opentelemetry if it is disabled', async (t) => {
@@ -214,3 +218,114 @@ test('should not set opentelemetry if it is disabled', async (t) => {
214
218
  const mainConfig = await body.json()
215
219
  assert.deepStrictEqual(mainConfig.telemetry, expectedTelemetry)
216
220
  })
221
+ test('should expose runtimeSupportsNewHealthMetrics method', async (t) => {
222
+ const applicationName = 'test-application'
223
+ const applicationId = randomUUID()
224
+ const applicationPath = join(__dirname, 'fixtures', 'service-1')
225
+
226
+ const icc = await startICC(t, {
227
+ applicationId,
228
+ applicationName
229
+ })
230
+
231
+ setUpEnvironment({
232
+ PLT_APP_NAME: applicationName,
233
+ PLT_APP_DIR: applicationPath,
234
+ PLT_ICC_URL: 'http://127.0.0.1:3000'
235
+ })
236
+
237
+ const app = await start()
238
+
239
+ t.after(async () => {
240
+ await app.close()
241
+ await icc.close()
242
+ })
243
+
244
+ assert.strictEqual(typeof app.watt.runtimeSupportsNewHealthMetrics, 'function', 'runtimeSupportsNewHealthMetrics should be a function')
245
+
246
+ const supportsNewMetrics = app.watt.runtimeSupportsNewHealthMetrics()
247
+ assert.strictEqual(typeof supportsNewMetrics, 'boolean', 'runtimeSupportsNewHealthMetrics should return a boolean')
248
+
249
+ const runtimeVersion = app.watt.getRuntimeVersion()
250
+ assert.ok(runtimeVersion, 'Runtime version should be available')
251
+ })
252
+
253
+ test('should expose getHealthConfig method', async (t) => {
254
+ const applicationName = 'test-application'
255
+ const applicationId = randomUUID()
256
+ const applicationPath = join(__dirname, 'fixtures', 'service-1')
257
+
258
+ const icc = await startICC(t, {
259
+ applicationId,
260
+ applicationName
261
+ })
262
+
263
+ setUpEnvironment({
264
+ PLT_APP_NAME: applicationName,
265
+ PLT_APP_DIR: applicationPath,
266
+ PLT_ICC_URL: 'http://127.0.0.1:3000'
267
+ })
268
+
269
+ const app = await start()
270
+
271
+ t.after(async () => {
272
+ await app.close()
273
+ await icc.close()
274
+ })
275
+
276
+ assert.strictEqual(typeof app.watt.getHealthConfig, 'function', 'getHealthConfig should be a function')
277
+
278
+ const healthConfig = app.watt.getHealthConfig()
279
+ assert.ok(healthConfig, 'Health config should be returned')
280
+ assert.strictEqual(typeof healthConfig, 'object', 'Health config should be an object')
281
+ assert.strictEqual(healthConfig.enabled, true, 'Health should be enabled')
282
+ assert.ok(healthConfig.interval, 'Health config should have interval')
283
+ assert.strictEqual(healthConfig.maxUnhealthyChecks, 10, 'Health config should have maxUnhealthyChecks default')
284
+ assert.strictEqual(healthConfig.maxELU, 0.99, 'Health config should have maxELU default')
285
+ assert.strictEqual(healthConfig.maxHeapUsed, 0.99, 'Health config should have maxHeapUsed default')
286
+ assert.strictEqual(healthConfig.gracePeriod, 30000, 'Health config should have gracePeriod default')
287
+ })
288
+
289
+ test('should configure health based on runtime version', async (t) => {
290
+ const appName = 'test-app'
291
+ const applicationId = randomUUID()
292
+ const applicationPath = join(__dirname, 'fixtures', 'runtime-service')
293
+
294
+ await installDeps(t, applicationPath)
295
+
296
+ const icc = await startICC(t, {
297
+ applicationId
298
+ })
299
+
300
+ setUpEnvironment({
301
+ PLT_APP_NAME: appName,
302
+ PLT_APP_DIR: applicationPath,
303
+ PLT_ICC_URL: 'http://127.0.0.1:3000'
304
+ })
305
+
306
+ const app = await start()
307
+
308
+ t.after(async () => {
309
+ await app.close()
310
+ await icc.close()
311
+ })
312
+
313
+ const runtimeConfig = app.watt.runtime.getRuntimeConfig(true)
314
+ const { health } = runtimeConfig
315
+
316
+ // Health should always be enabled regardless of runtime version
317
+ assert.strictEqual(health.enabled, true, 'Health should be enabled')
318
+
319
+ const supportsNewMetrics = app.watt.runtimeSupportsNewHealthMetrics()
320
+
321
+ if (supportsNewMetrics) {
322
+ // For new runtime (>= 3.18.0), we only force enabled: true
323
+ // Other values come from app config or runtime defaults
324
+ assert.ok(health.interval, 'Health interval should be set')
325
+ assert.ok(health.maxUnhealthyChecks, 'Health maxUnhealthyChecks should be set')
326
+ } else {
327
+ // For old runtime (< 3.18.0), we set specific defaults
328
+ assert.ok(health.interval, 'Health interval should be set')
329
+ assert.ok(health.maxUnhealthyChecks, 'Health maxUnhealthyChecks should be set')
330
+ }
331
+ })