@platformatic/watt-extra 1.4.0 → 1.5.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.
package/app.js CHANGED
@@ -8,11 +8,12 @@ import scheduler from './plugins/scheduler.js'
8
8
  import auth from './plugins/auth.js'
9
9
  import update from './plugins/update.js'
10
10
  import alert from './plugins/alerts.js'
11
+ import healthSignals from './plugins/health-signals.js'
11
12
  import flamegraphs from './plugins/flamegraphs.js'
12
13
 
13
14
  async function buildApp (logger) {
14
15
  const app = {
15
- log: logger,
16
+ log: logger
16
17
  }
17
18
 
18
19
  avvio(app)
@@ -22,6 +23,7 @@ async function buildApp (logger) {
22
23
  .use(auth)
23
24
  .use(init)
24
25
  .use(alert)
26
+ .use(healthSignals)
25
27
  .use(metadata)
26
28
  .use(compliancy)
27
29
  .use(scheduler)
@@ -101,7 +103,7 @@ async function buildApp (logger) {
101
103
  {
102
104
  err: err.message,
103
105
  attemptNumber: retries,
104
- nextRetryMs: currentRetryInterval,
106
+ nextRetryMs: currentRetryInterval
105
107
  },
106
108
  `Failed to send info to ICC, retrying in ${currentRetryInterval}ms`
107
109
  )
package/index.js CHANGED
@@ -22,6 +22,7 @@ async function start () {
22
22
 
23
23
  app.log.info('Setup health check')
24
24
  await app.setupAlerts()
25
+ await app.setupHealthSignals()
25
26
  await app.setupFlamegraphs()
26
27
 
27
28
  app.log.info('Sending info to ICC')
package/lib/watt.js CHANGED
@@ -98,6 +98,11 @@ class Watt {
98
98
  await this.runtime?.updateSharedContext?.({ context })
99
99
  }
100
100
 
101
+ getRuntimeVersion () {
102
+ const { version } = this.#require('@platformatic/runtime/package.json')
103
+ return version
104
+ }
105
+
101
106
  async #createRuntime () {
102
107
  this.#logger.info('Creating runtime')
103
108
  const { create, transform } = this.#require('@platformatic/runtime')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/watt-extra",
3
- "version": "1.4.0",
3
+ "version": "1.5.0-alpha.1",
4
4
  "description": "The Platformatic runtime manager",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -19,25 +19,27 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "@fastify/websocket": "^11.1.0",
22
- "@platformatic/composer": "^3.8.0",
23
- "@platformatic/next": "^3.8.0",
24
- "@platformatic/node": "^3.8.0",
25
- "@platformatic/service": "^3.8.0",
22
+ "@platformatic/composer": "^3.14.0",
23
+ "@platformatic/next": "^3.14.0",
24
+ "@platformatic/node": "^3.14.0",
25
+ "@platformatic/service": "^3.14.0",
26
+ "atomic-sleep": "^1.0.0",
26
27
  "borp": "^0.21.0",
27
28
  "eslint": "9",
28
29
  "fastify": "^5.4.0",
29
30
  "fastify-plugin": "^5.0.1",
30
31
  "neostandard": "^0.12.0",
31
32
  "next": "^15.3.4",
32
- "platformatic": "^3.8.0",
33
+ "platformatic": "^3.14.0",
33
34
  "pprof-format": "^2.1.0",
34
35
  "why-is-node-running": "^2.3.0"
35
36
  },
36
37
  "dependencies": {
37
38
  "@datadog/pprof": "^5.9.0",
38
39
  "@fastify/error": "^4.2.0",
39
- "@platformatic/runtime": "^3.12.0",
40
- "@platformatic/wattpm-pprof-capture": "^3.12.0",
40
+ "@platformatic/foundation": "^3.14.0",
41
+ "@platformatic/runtime": "^3.14.0",
42
+ "@platformatic/wattpm-pprof-capture": "^3.14.0",
41
43
  "avvio": "^9.1.0",
42
44
  "chalk": "^4.1.2",
43
45
  "commist": "^3.2.0",
@@ -47,6 +49,7 @@
47
49
  "minimist": "^1.2.8",
48
50
  "pino": "^10.0.0",
49
51
  "pino-pretty": "^13.0.0",
52
+ "semver": "^7.7.3",
50
53
  "undici": "^7.11.0",
51
54
  "undici-cache-redis": "^1.0.0",
52
55
  "undici-slicer-interceptor": "^0.4.1",
package/plugins/alerts.js CHANGED
@@ -10,6 +10,9 @@ async function alerts (app, _opts) {
10
10
  const lastServicesAlertTime = {}
11
11
 
12
12
  async function setupAlerts () {
13
+ const scalerAlgorithmVersion = app.instanceConfig?.scaler?.version ?? 'v1'
14
+ if (scalerAlgorithmVersion !== 'v1') return
15
+
13
16
  // Skip alerts setup if ICC is not configured
14
17
  if (!app.env.PLT_ICC_URL) {
15
18
  app.log.info('PLT_ICC_URL not set, skipping alerts setup')
@@ -85,13 +88,13 @@ async function alerts (app, _opts) {
85
88
  method: 'POST',
86
89
  headers: {
87
90
  'Content-Type': 'application/json',
88
- ...authHeaders,
91
+ ...authHeaders
89
92
  },
90
93
  body: JSON.stringify({
91
94
  applicationId: app.instanceConfig?.applicationId,
92
95
  alert: healthInfo,
93
- healthHistory: healthCache,
94
- }),
96
+ healthHistory: healthCache
97
+ })
95
98
  })
96
99
 
97
100
  if (statusCode !== 200) {
package/plugins/env.js CHANGED
@@ -22,7 +22,9 @@ const schema = {
22
22
  PLT_FLAMEGRAPHS_ELU_THRESHOLD: { type: 'number', default: 0.4 },
23
23
  PLT_FLAMEGRAPHS_GRACE_PERIOD: { type: 'number', default: 3000 },
24
24
  PLT_JWT_EXPIRATION_OFFSET_SEC: { type: 'number', default: 60 },
25
- PLT_UPDATES_RECONNECT_INTERVAL_SEC: { type: 'number', default: 1 }
25
+ PLT_UPDATES_RECONNECT_INTERVAL_SEC: { type: 'number', default: 1 },
26
+ PLT_ELU_HEALTH_SIGNAL_THRESHOLD: { type: 'number', default: 0.9 },
27
+ PLT_HEAP_HEALTH_SIGNAL_THRESHOLD: { type: ['number', 'string'], default: '4GB' }
26
28
  }
27
29
  }
28
30
 
@@ -0,0 +1,191 @@
1
+ import { request } from 'undici'
2
+ import semver from 'semver'
3
+ import { parseMemorySize } from '@platformatic/foundation'
4
+
5
+ class HealthSignalsCache {
6
+ #signals = []
7
+ #size = 100
8
+
9
+ constructor () {
10
+ this.#signals = []
11
+ }
12
+
13
+ add (signals) {
14
+ for (const signal of signals) {
15
+ this.#signals.push(signal)
16
+ }
17
+ if (this.#signals.length > this.#size) {
18
+ this.#signals.splice(0, this.#signals.length - this.#size)
19
+ }
20
+ }
21
+
22
+ getAll () {
23
+ const values = this.#signals
24
+ this.#signals = []
25
+ return values
26
+ }
27
+ }
28
+
29
+ async function healthSignals (app, _opts) {
30
+ const signalsCaches = {}
31
+ const servicesSendingStatuses = {}
32
+
33
+ // TODO: needed to the UI compatibility
34
+ // remove after depricating the Scaler v1 UI
35
+ const servicesMetrics = {}
36
+
37
+ async function setupHealthSignals () {
38
+ const scalerAlgorithmVersion = app.instanceConfig?.scaler?.version ?? 'v1'
39
+ if (scalerAlgorithmVersion !== 'v2') return
40
+
41
+ const runtimeVersion = app.watt.getRuntimeVersion()
42
+ if (semver.lt(runtimeVersion, '1.4.0')) {
43
+ app.log.warn(
44
+ `Watt version "${runtimeVersion}" does not support health signals for the Signal Scaler Algorithm.` +
45
+ 'Please update your watt-extra to version 1.4.0 or higher.'
46
+ )
47
+ return
48
+ }
49
+
50
+ const eluThreshold = app.env.PLT_ELU_HEALTH_SIGNAL_THRESHOLD
51
+
52
+ let heapThreshold = app.env.PLT_HEAP_HEALTH_SIGNAL_THRESHOLD
53
+ if (typeof heapThreshold === 'string') {
54
+ heapThreshold = parseMemorySize(heapThreshold)
55
+ }
56
+
57
+ // Skip alerts setup if ICC is not configured
58
+ if (!app.env.PLT_ICC_URL) {
59
+ app.log.info('PLT_ICC_URL not set, skipping alerts setup')
60
+ return
61
+ }
62
+
63
+ const scalerUrl = app.instanceConfig?.iccServices?.scaler?.url
64
+ const runtime = app.watt.runtime
65
+
66
+ if (!scalerUrl) {
67
+ app.log.warn(
68
+ 'No scaler URL found in ICC services, health alerts disabled'
69
+ )
70
+ return
71
+ }
72
+
73
+ runtime.on('application:worker:health:metrics', async (healthInfo) => {
74
+ if (!healthInfo) {
75
+ app.log.error('No health metrics info received')
76
+ }
77
+
78
+ const {
79
+ application: serviceId,
80
+ currentHealth,
81
+ healthSignals
82
+ } = healthInfo
83
+
84
+ const { elu, heapUsed, heapTotal } = currentHealth
85
+
86
+ if (elu > eluThreshold) {
87
+ healthSignals.push({
88
+ type: 'elu',
89
+ value: currentHealth.elu,
90
+ description:
91
+ `The ${serviceId} has an ELU of ${(elu * 100).toFixed(2)} %, ` +
92
+ `above the maximum allowed usage of ${(eluThreshold * 100).toFixed(2)} %`,
93
+ timestamp: Date.now()
94
+ })
95
+ }
96
+
97
+ if (heapThreshold && heapUsed > heapThreshold) {
98
+ const usedHeapMb = Math.round(heapUsed / 1024 / 1024)
99
+ const heapThresholdMb = Math.round(heapThreshold / 1024 / 1024)
100
+
101
+ healthSignals.push({
102
+ type: 'heapUsed',
103
+ value: currentHealth.heapUsed,
104
+ description:
105
+ `The ${serviceId} is using ${usedHeapMb} MB of heap, ` +
106
+ `above the maximum allowed usage of ${heapThresholdMb} MB`,
107
+ timestamp: Date.now()
108
+ })
109
+ }
110
+
111
+ // TODO: needed to the UI compatibility
112
+ // remove after depricating the Scaler v1 UI
113
+ servicesMetrics[serviceId] ??= { elu: 0, heapUsed: 0, heapTotal: 0 }
114
+ const metrics = servicesMetrics[serviceId]
115
+ if (elu > metrics.elu) {
116
+ metrics.elu = elu
117
+ }
118
+ if (heapUsed > metrics.heapUsed) {
119
+ metrics.heapUsed = heapUsed
120
+ metrics.heapTotal = heapTotal
121
+ }
122
+
123
+ if (healthSignals.length > 0) {
124
+ await sendHealthSignalsWithTimeout(serviceId, healthSignals)
125
+ }
126
+ })
127
+ }
128
+ app.setupHealthSignals = setupHealthSignals
129
+
130
+ async function sendHealthSignalsWithTimeout (serviceId, signals) {
131
+ signalsCaches[serviceId] ??= new HealthSignalsCache()
132
+ servicesSendingStatuses[serviceId] ??= false
133
+
134
+ const signalsCache = signalsCaches[serviceId]
135
+ signalsCache.add(signals)
136
+
137
+ if (!servicesSendingStatuses[serviceId]) {
138
+ servicesSendingStatuses[serviceId] = true
139
+ setTimeout(async () => {
140
+ servicesSendingStatuses[serviceId] = false
141
+
142
+ const metrics = servicesMetrics[serviceId]
143
+ servicesMetrics[serviceId] = null
144
+
145
+ try {
146
+ const signals = signalsCache.getAll()
147
+ await sendHealthSignals(serviceId, signals, metrics)
148
+ } catch (err) {
149
+ app.log.error({ err }, 'Failed to send health signals to scaler')
150
+ }
151
+ }, 5000).unref()
152
+ }
153
+ }
154
+
155
+ async function sendHealthSignals (serviceId, signals, metrics) {
156
+ const scalerUrl = app.instanceConfig?.iccServices?.scaler?.url
157
+ const applicationId = app.instanceConfig?.applicationId
158
+ const authHeaders = await app.getAuthorizationHeader()
159
+
160
+ const { statusCode, body } = await request(`${scalerUrl}/signals`, {
161
+ method: 'POST',
162
+ headers: {
163
+ 'Content-Type': 'application/json',
164
+ ...authHeaders
165
+ },
166
+ body: JSON.stringify({
167
+ applicationId,
168
+ serviceId,
169
+ signals,
170
+ elu: metrics.elu,
171
+ heapUsed: metrics.heapUsed,
172
+ heapTotal: metrics.heapTotal
173
+ })
174
+ })
175
+
176
+ if (statusCode !== 200) {
177
+ const error = await body.text()
178
+ app.log.error({ error }, 'Failed to send health signals to scaler')
179
+ }
180
+
181
+ const alert = await body.json()
182
+
183
+ try {
184
+ await app.sendFlamegraphs({ serviceIds: [serviceId], alertId: alert.id })
185
+ } catch (err) {
186
+ app.log.error({ err }, 'Failed to send a flamegraph')
187
+ }
188
+ }
189
+ }
190
+
191
+ export default healthSignals
package/plugins/init.js CHANGED
@@ -40,6 +40,8 @@ async function initPlugin (app) {
40
40
  const instanceConfig = await initApplicationInstance(instanceId, applicationName)
41
41
  app.log.info({ applicationId: instanceConfig.applicationId }, 'Got application info')
42
42
 
43
+ instanceConfig.scaler ??= { version: 'v1' }
44
+
43
45
  // Use the application name from the ICC response if not provided
44
46
  applicationName = applicationName || instanceConfig.applicationName
45
47
  app.log.info({ applicationName }, 'Application name resolved')
@@ -42,15 +42,17 @@ async function metadata (app, _opts) {
42
42
  )
43
43
  )
44
44
 
45
- const verticalScalerConfig = runtimeConfig.verticalScaler
46
- if (verticalScalerConfig?.enabled) {
47
- for (const applicationId in verticalScalerConfig.applications) {
48
- const service = services.find((s) => s.id === applicationId)
49
- if (service) {
50
- const appScalerConfig = verticalScalerConfig.applications[applicationId]
51
- service.maxWorkers = appScalerConfig.maxWorkers
52
- service.minWorkers = appScalerConfig.minWorkers
53
- }
45
+ const workersCount = getWorkersCount(runtimeConfig)
46
+ for (const service of services) {
47
+ const serviceWorkers = workersCount[service.id]
48
+ if (serviceWorkers?.workers) {
49
+ service.workers = serviceWorkers.workers
50
+ }
51
+ if (serviceWorkers?.minWorkers) {
52
+ service.minWorkers = serviceWorkers.minWorkers
53
+ }
54
+ if (serviceWorkers?.maxWorkers) {
55
+ service.maxWorkers = serviceWorkers.maxWorkers
54
56
  }
55
57
  }
56
58
 
@@ -91,6 +93,42 @@ async function metadata (app, _opts) {
91
93
  }
92
94
  }
93
95
  app.sendMetadata = sendMetadata
96
+
97
+ function getWorkersCount (runtimeConfig) {
98
+ const verticalScalerConfig = runtimeConfig.verticalScaler
99
+ const serviceWorkers = {}
100
+
101
+ for (const application of runtimeConfig.applications) {
102
+ const { workers } = application
103
+ if (!workers) continue
104
+
105
+ if (typeof workers === 'number') {
106
+ serviceWorkers[application.id] = {
107
+ workers,
108
+ minWorkers: workers,
109
+ maxWorkers: workers
110
+ }
111
+ }
112
+ if (typeof workers === 'object') {
113
+ serviceWorkers[application.id] = {
114
+ workers: workers.static,
115
+ minWorkers: workers.minimum ?? workers.static,
116
+ maxWorkers: workers.maximum ?? workers.static
117
+ }
118
+ }
119
+ }
120
+
121
+ if (verticalScalerConfig?.enabled) {
122
+ for (const applicationId in verticalScalerConfig.applications) {
123
+ const scalingConfig = verticalScalerConfig.applications[applicationId]
124
+ serviceWorkers[applicationId] ??= {}
125
+ serviceWorkers[applicationId].maxWorkers ??= scalingConfig.maxWorkers
126
+ serviceWorkers[applicationId].minWorkers ??= scalingConfig.minWorkers
127
+ }
128
+ }
129
+
130
+ return serviceWorkers
131
+ }
94
132
  }
95
133
 
96
134
  export default metadata
@@ -1,48 +1,66 @@
1
- "use strict";
1
+ 'use strict'
2
2
 
3
- const { join } = require("node:path");
4
- const { readFile } = require("node:fs/promises");
5
- const { request } = require("undici");
3
+ const { join } = require('node:path')
4
+ const { readFile } = require('node:fs/promises')
5
+ const { request } = require('undici')
6
+ const atomicSleep = require('atomic-sleep')
6
7
 
7
8
  module.exports = async function (fastify) {
8
- fastify.get("/example", async () => {
9
- return { hello: "world" };
10
- });
9
+ fastify.get('/example', async () => {
10
+ return { hello: 'world' }
11
+ })
11
12
 
12
- fastify.get("/config", async () => {
13
- return fastify.platformatic.config;
14
- });
13
+ fastify.get('/config', async () => {
14
+ return fastify.platformatic.config
15
+ })
15
16
 
16
- fastify.get("/preprocess", async () => {
17
+ fastify.get('/preprocess', async () => {
17
18
  return {
18
- base: "~PLT_BASE_PATH",
19
- leadingSlash: "/~PLT_BASE_PATH",
20
- withPrefix: "~PLT_BASE_PATH/foo",
21
- externalUrl: "~PLT_EXTERNAL_APP_URL",
22
- };
23
- });
24
-
25
- fastify.get("/custom-ext-file", async () => {
26
- const customExtFilePath = join(__dirname, "..", "file.custom");
27
- const customExtFile = await readFile(customExtFilePath, "utf8");
28
- return { data: customExtFile };
29
- });
30
-
31
- fastify.get("/env", async () => {
32
- return { env: process.env };
33
- });
34
-
35
- fastify.post("/request", async (req) => {
36
- const { method, url } = req.body;
19
+ base: '~PLT_BASE_PATH',
20
+ leadingSlash: '/~PLT_BASE_PATH',
21
+ withPrefix: '~PLT_BASE_PATH/foo',
22
+ externalUrl: '~PLT_EXTERNAL_APP_URL'
23
+ }
24
+ })
25
+
26
+ fastify.get('/custom-ext-file', async () => {
27
+ const customExtFilePath = join(__dirname, '..', 'file.custom')
28
+ const customExtFile = await readFile(customExtFilePath, 'utf8')
29
+ return { data: customExtFile }
30
+ })
31
+
32
+ fastify.get('/env', async () => {
33
+ return { env: process.env }
34
+ })
35
+
36
+ fastify.post('/request', async (req) => {
37
+ const { method, url } = req.body
37
38
 
38
39
  const { statusCode, headers, body } = await request(url, {
39
- method: method ?? "GET",
40
+ method: method ?? 'GET',
40
41
  headers: {
41
- "content-type": "application/json",
42
- },
43
- });
44
- const data = await body.text();
45
-
46
- return { statusCode, headers, data };
47
- });
48
- };
42
+ 'content-type': 'application/json'
43
+ }
44
+ })
45
+ const data = await body.text()
46
+
47
+ return { statusCode, headers, data }
48
+ })
49
+
50
+ fastify.post('/cpu-intensive', async (req) => {
51
+ // Simulate a CPU intensive operation
52
+ const timeout = req.query.timeout || 10000
53
+ atomicSleep(timeout)
54
+
55
+ return { status: 'ok' }
56
+ })
57
+
58
+ fastify.post('/custom-health-signal', async (req) => {
59
+ const { type, value, description } = req.body
60
+ await globalThis.platformatic.sendHealthSignal({
61
+ type,
62
+ value,
63
+ description
64
+ })
65
+ })
66
+ }
@@ -0,0 +1,130 @@
1
+ import assert from 'node:assert'
2
+ import { test } from 'node:test'
3
+ import { randomUUID } from 'node:crypto'
4
+ import { join, dirname } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { setTimeout as sleep } from 'node:timers/promises'
7
+ import { Profile } from 'pprof-format'
8
+ import { request } from 'undici'
9
+ import { setUpEnvironment, startICC } from './helper.js'
10
+ import { start } from '../index.js'
11
+
12
+ const __filename = fileURLToPath(import.meta.url)
13
+ const __dirname = dirname(__filename)
14
+
15
+ test('should send health signals when service becomes unhealthy', async (t) => {
16
+ const applicationName = 'test-app'
17
+ const applicationId = randomUUID()
18
+ const applicationPath = join(__dirname, 'fixtures', 'service-1')
19
+
20
+ const receivedSignalReqs = []
21
+ const receivedFlamegraphReqs = []
22
+
23
+ const getAuthorizationHeader = async (headers) => {
24
+ return { ...headers, authorization: 'Bearer test-token' }
25
+ }
26
+
27
+ const icc = await startICC(t, {
28
+ applicationId,
29
+ applicationName,
30
+ scaler: { version: 'v2' },
31
+ processSignals: (req) => {
32
+ assert.equal(req.headers.authorization, 'Bearer test-token')
33
+ receivedSignalReqs.push(req.body)
34
+ return { id: 'test-alert-id' }
35
+ },
36
+ processFlamegraphs: (req) => {
37
+ const alertId = req.query.alertId
38
+ assert.strictEqual(alertId, 'test-alert-id')
39
+ assert.strictEqual(req.headers.authorization, 'Bearer test-token')
40
+ receivedFlamegraphReqs.push(req.body)
41
+ }
42
+ })
43
+
44
+ setUpEnvironment({
45
+ PLT_APP_NAME: applicationName,
46
+ PLT_APP_DIR: applicationPath,
47
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
48
+ PLT_DISABLE_FLAMEGRAPHS: false,
49
+ PLT_FLAMEGRAPHS_INTERVAL_SEC: 2,
50
+ PLT_FLAMEGRAPHS_ELU_THRESHOLD: 0
51
+ })
52
+
53
+ const app = await start()
54
+ app.getAuthorizationHeader = getAuthorizationHeader
55
+
56
+ t.after(async () => {
57
+ await app.close()
58
+ await icc.close()
59
+ })
60
+
61
+ // Wait for the first flamegraph to be generated
62
+ await sleep(5000)
63
+
64
+ {
65
+ const { statusCode } = await request('http://127.0.0.1:3042/custom-health-signal', {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/json'
69
+ },
70
+ body: JSON.stringify({
71
+ type: 'custom',
72
+ value: 42,
73
+ description: 'This is a custom health signal'
74
+ })
75
+ })
76
+ assert.strictEqual(statusCode, 200)
77
+ }
78
+
79
+ {
80
+ const { statusCode } = await request('http://127.0.0.1:3042/cpu-intensive', {
81
+ method: 'POST',
82
+ headers: {
83
+ 'Content-Type': 'application/json'
84
+ },
85
+ body: JSON.stringify({ timeout: 3000 })
86
+ })
87
+ assert.strictEqual(statusCode, 200)
88
+ }
89
+
90
+ assert.strictEqual(receivedSignalReqs.length, 1)
91
+
92
+ const receivedSignalReq = receivedSignalReqs[0]
93
+ assert.ok(receivedSignalReq, 'Alert should have been received')
94
+ assert.strictEqual(receivedSignalReq.applicationId, applicationId)
95
+ assert.strictEqual(receivedSignalReq.serviceId, 'main')
96
+ assert.ok(receivedSignalReq.elu > 0.9)
97
+ assert.ok(receivedSignalReq.heapUsed > 0)
98
+ assert.ok(receivedSignalReq.heapTotal > 0)
99
+
100
+ const receivedSignals = receivedSignalReq.signals
101
+ assert.ok(receivedSignals.length > 5)
102
+
103
+ const eluSignals = receivedSignals.filter(
104
+ (signal) => signal.type === 'elu'
105
+ )
106
+ const customSignals = receivedSignals.filter(
107
+ (signal) => signal.type === 'custom'
108
+ )
109
+ assert.strictEqual(customSignals.length, 1)
110
+
111
+ for (const receivedSignal of eluSignals) {
112
+ assert.strictEqual(receivedSignal.type, 'elu')
113
+ assert.ok(receivedSignal.value > 0.9)
114
+ assert.ok(receivedSignal.timestamp > 0)
115
+ }
116
+ for (const receivedSignal of customSignals) {
117
+ assert.strictEqual(receivedSignal.type, 'custom')
118
+ assert.strictEqual(receivedSignal.value, 42)
119
+ assert.ok(receivedSignal.timestamp > 0)
120
+ }
121
+
122
+ // Wait for the second flamegraph to be generated
123
+ await sleep(2000)
124
+
125
+ // assert.strictEqual(receivedFlamegraphReqs.length, 1)
126
+
127
+ const receivedFlamegraph = receivedFlamegraphReqs[0]
128
+ const profile = Profile.decode(receivedFlamegraph)
129
+ assert.ok(profile, 'Profile should be decoded')
130
+ })
package/test/helper.js CHANGED
@@ -35,6 +35,7 @@ async function startICC (t, opts = {}) {
35
35
  applicationId,
36
36
  applicationName,
37
37
  applicationMetricsLabel,
38
+ scaler,
38
39
  iccServices,
39
40
  iccConfig = {},
40
41
  enableOpenTelemetry = false,
@@ -111,6 +112,7 @@ async function startICC (t, opts = {}) {
111
112
  applicationName,
112
113
  applicationMetricsLabel,
113
114
  iccServices,
115
+ scaler,
114
116
  config: iccConfig,
115
117
  enableOpenTelemetry,
116
118
  enableSlicerInterceptor,
@@ -191,6 +193,9 @@ async function startICC (t, opts = {}) {
191
193
  icc.post('/alerts', async (req) => {
192
194
  return opts.processAlerts?.(req)
193
195
  })
196
+ icc.post('/signals', async (req) => {
197
+ return opts.processSignals?.(req)
198
+ })
194
199
  icc.post('/pods/:podId/services/:serviceId/flamegraph', async (req) => {
195
200
  return opts.processFlamegraphs?.(req)
196
201
  })
@@ -1,11 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Read(//work/workspaces/workspace-platformatic/platformatic/**)",
5
- "Bash(npx borp:*)",
6
- "Bash(timeout 30 npx borp -c 1 --timeout=20000 ./test/trigger-flamegraphs.test.js)"
7
- ],
8
- "deny": [],
9
- "ask": []
10
- }
11
- }