@platformatic/runtime 2.0.0-alpha.6 → 2.0.0-alpha.7
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/config.d.ts +1 -1
- package/fixtures/management-api/platformatic.json +6 -1
- package/index.js +2 -1
- package/lib/errors.js +1 -0
- package/lib/runtime.js +27 -17
- package/lib/worker/app.js +20 -0
- package/lib/worker/default-stackable.js +4 -1
- package/lib/worker/itc.js +6 -2
- package/lib/worker/metrics.js +106 -0
- package/package.json +15 -13
- package/schema.json +1 -1
package/config.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and run json-schema-to-typescript to regenerate this file.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export type
|
|
8
|
+
export type HttpsSchemasPlatformaticDevPlatformaticRuntime200Alpha7Json = {
|
|
9
9
|
[k: string]: unknown;
|
|
10
10
|
} & {
|
|
11
11
|
$schema?: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://schemas.platformatic.dev/@platformatic/runtime/
|
|
2
|
+
"$schema": "https://schemas.platformatic.dev/@platformatic/runtime/2.0.0-alpha.5.json",
|
|
3
3
|
"entrypoint": "service-1",
|
|
4
4
|
"watch": false,
|
|
5
5
|
"autoload": {
|
|
@@ -13,5 +13,10 @@
|
|
|
13
13
|
"logs": {
|
|
14
14
|
"maxSize": 6
|
|
15
15
|
}
|
|
16
|
+
},
|
|
17
|
+
"metrics": {
|
|
18
|
+
"labels": {
|
|
19
|
+
"custom_label": "custom-value"
|
|
20
|
+
}
|
|
16
21
|
}
|
|
17
22
|
}
|
package/index.js
CHANGED
|
@@ -6,11 +6,12 @@ const errors = require('./lib/errors')
|
|
|
6
6
|
const { platformaticRuntime, wrapConfigInRuntimeConfig } = require('./lib/config')
|
|
7
7
|
const RuntimeGenerator = require('./lib/generator/runtime-generator')
|
|
8
8
|
const { Runtime } = require('./lib/runtime')
|
|
9
|
-
const { start, startCommand } = require('./lib/start')
|
|
9
|
+
const { buildRuntime, start, startCommand } = require('./lib/start')
|
|
10
10
|
const symbols = require('./lib/worker/symbols')
|
|
11
11
|
const { loadConfig, getRuntimeLogsDir } = require('./lib/utils')
|
|
12
12
|
|
|
13
13
|
module.exports.buildServer = buildServer
|
|
14
|
+
module.exports.buildRuntime = buildRuntime
|
|
14
15
|
module.exports.compile = compile
|
|
15
16
|
module.exports.errors = errors
|
|
16
17
|
module.exports.Generator = RuntimeGenerator
|
package/lib/errors.js
CHANGED
|
@@ -14,6 +14,7 @@ module.exports = {
|
|
|
14
14
|
FailedToRetrieveOpenAPISchemaError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_OPENAPI_SCHEMA`, 'Failed to retrieve OpenAPI schema for service with id "%s": %s'),
|
|
15
15
|
FailedToRetrieveGraphQLSchemaError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_GRAPHQL_SCHEMA`, 'Failed to retrieve GraphQL schema for service with id "%s": %s'),
|
|
16
16
|
FailedToRetrieveMetaError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_META`, 'Failed to retrieve metadata for service with id "%s": %s'),
|
|
17
|
+
FailedToRetrieveMetricsError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_METRICS`, 'Failed to retrieve metrics for service with id "%s": %s'),
|
|
17
18
|
ApplicationAlreadyStartedError: createError(`${ERROR_PREFIX}_APPLICATION_ALREADY_STARTED`, 'Application is already started'),
|
|
18
19
|
ApplicationNotStartedError: createError(`${ERROR_PREFIX}_APPLICATION_NOT_STARTED`, 'Application has not been started'),
|
|
19
20
|
ConfigPathMustBeStringError: createError(`${ERROR_PREFIX}_CONFIG_PATH_MUST_BE_STRING`, 'Config path must be a string'),
|
package/lib/runtime.js
CHANGED
|
@@ -151,7 +151,7 @@ class Runtime extends EventEmitter {
|
|
|
151
151
|
return this.#url
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
async stop () {
|
|
154
|
+
async stop (silent = false) {
|
|
155
155
|
if (this.#status === 'starting') {
|
|
156
156
|
await once(this, 'started')
|
|
157
157
|
}
|
|
@@ -159,7 +159,7 @@ class Runtime extends EventEmitter {
|
|
|
159
159
|
this.#updateStatus('stopping')
|
|
160
160
|
this.#startedServices.clear()
|
|
161
161
|
|
|
162
|
-
await Promise.all(this.#servicesIds.map(service => this._stopService(service)))
|
|
162
|
+
await Promise.all(this.#servicesIds.map(service => this._stopService(service, silent)))
|
|
163
163
|
|
|
164
164
|
this.#updateStatus('stopped')
|
|
165
165
|
}
|
|
@@ -175,12 +175,12 @@ class Runtime extends EventEmitter {
|
|
|
175
175
|
return this.#url
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
async close (fromManagementApi) {
|
|
178
|
+
async close (fromManagementApi = false, silent = false) {
|
|
179
179
|
this.#updateStatus('closing')
|
|
180
180
|
|
|
181
181
|
clearInterval(this.#metricsTimeout)
|
|
182
182
|
|
|
183
|
-
await this.stop()
|
|
183
|
+
await this.stop(silent)
|
|
184
184
|
|
|
185
185
|
if (this.#managementApi) {
|
|
186
186
|
if (fromManagementApi) {
|
|
@@ -263,7 +263,7 @@ class Runtime extends EventEmitter {
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
// Do not rename to #stopService as this is used in tests
|
|
266
|
-
async _stopService (id) {
|
|
266
|
+
async _stopService (id, silent) {
|
|
267
267
|
const service = await this.#getServiceById(id, false, false)
|
|
268
268
|
|
|
269
269
|
if (!service) {
|
|
@@ -272,7 +272,9 @@ class Runtime extends EventEmitter {
|
|
|
272
272
|
|
|
273
273
|
this.#startedServices.set(id, false)
|
|
274
274
|
|
|
275
|
-
|
|
275
|
+
if (!silent) {
|
|
276
|
+
this.logger?.info(`Stopping service "${id}"...`)
|
|
277
|
+
}
|
|
276
278
|
|
|
277
279
|
// Always send the stop message, it will shut down workers that only had ITC and interceptors setup
|
|
278
280
|
try {
|
|
@@ -524,7 +526,6 @@ class Runtime extends EventEmitter {
|
|
|
524
526
|
}
|
|
525
527
|
|
|
526
528
|
const serviceMetrics = await sendViaITC(service, 'getMetrics', format)
|
|
527
|
-
|
|
528
529
|
if (serviceMetrics) {
|
|
529
530
|
if (metrics === null) {
|
|
530
531
|
metrics = format === 'json' ? [] : ''
|
|
@@ -576,17 +577,26 @@ class Runtime extends EventEmitter {
|
|
|
576
577
|
let p99Value = 0
|
|
577
578
|
|
|
578
579
|
const metricName = 'http_request_all_summary_seconds'
|
|
579
|
-
const httpLatencyMetrics = metrics.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
p90Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.9).value || 0
|
|
583
|
-
p95Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.95).value || 0
|
|
584
|
-
p99Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.99).value || 0
|
|
580
|
+
const httpLatencyMetrics = metrics.filter(
|
|
581
|
+
metric => metric.name === metricName
|
|
582
|
+
)
|
|
585
583
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
584
|
+
if (httpLatencyMetrics) {
|
|
585
|
+
const entrypointMetrics = httpLatencyMetrics.find(
|
|
586
|
+
metric => metric.values?.[0]?.labels?.serviceId === this.#entrypointId
|
|
587
|
+
)
|
|
588
|
+
if (entrypointMetrics) {
|
|
589
|
+
p50Value = entrypointMetrics.values.find(value => value.labels.quantile === 0.5)?.value || 0
|
|
590
|
+
p90Value = entrypointMetrics.values.find(value => value.labels.quantile === 0.9)?.value || 0
|
|
591
|
+
p95Value = entrypointMetrics.values.find(value => value.labels.quantile === 0.95)?.value || 0
|
|
592
|
+
p99Value = entrypointMetrics.values.find(value => value.labels.quantile === 0.99)?.value || 0
|
|
593
|
+
|
|
594
|
+
p50Value = Math.round(p50Value * 1000)
|
|
595
|
+
p90Value = Math.round(p90Value * 1000)
|
|
596
|
+
p95Value = Math.round(p95Value * 1000)
|
|
597
|
+
p99Value = Math.round(p99Value * 1000)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
590
600
|
|
|
591
601
|
const cpu = cpuMetric.values[0].value
|
|
592
602
|
const rss = rssMetric.values[0].value
|
package/lib/worker/app.js
CHANGED
|
@@ -7,6 +7,7 @@ const debounce = require('debounce')
|
|
|
7
7
|
|
|
8
8
|
const errors = require('../errors')
|
|
9
9
|
const defaultStackable = require('./default-stackable')
|
|
10
|
+
const { collectMetrics } = require('./metrics')
|
|
10
11
|
const { getServiceUrl, loadConfig, loadEmptyConfig } = require('../utils')
|
|
11
12
|
|
|
12
13
|
class PlatformaticApp extends EventEmitter {
|
|
@@ -15,6 +16,7 @@ class PlatformaticApp extends EventEmitter {
|
|
|
15
16
|
#listening
|
|
16
17
|
#watch
|
|
17
18
|
#fileWatcher
|
|
19
|
+
#metricsRegistry
|
|
18
20
|
#debouncedRestart
|
|
19
21
|
#context
|
|
20
22
|
|
|
@@ -27,6 +29,7 @@ class PlatformaticApp extends EventEmitter {
|
|
|
27
29
|
this.#listening = false
|
|
28
30
|
this.stackable = null
|
|
29
31
|
this.#fileWatcher = null
|
|
32
|
+
this.#metricsRegistry = null
|
|
30
33
|
|
|
31
34
|
this.#context = {
|
|
32
35
|
serviceId: this.appConfig.id,
|
|
@@ -93,6 +96,15 @@ class PlatformaticApp extends EventEmitter {
|
|
|
93
96
|
})
|
|
94
97
|
this.stackable = this.#wrapStackable(stackable)
|
|
95
98
|
|
|
99
|
+
const metricsConfig = this.#context.metricsConfig
|
|
100
|
+
if (metricsConfig !== false) {
|
|
101
|
+
this.#metricsRegistry = await collectMetrics(
|
|
102
|
+
this.stackable,
|
|
103
|
+
this.appConfig.id,
|
|
104
|
+
metricsConfig
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
96
108
|
this.#updateDispatcher()
|
|
97
109
|
} catch (err) {
|
|
98
110
|
this.#logAndExit(err)
|
|
@@ -164,6 +176,14 @@ class PlatformaticApp extends EventEmitter {
|
|
|
164
176
|
await this.stackable.start({ listen: true })
|
|
165
177
|
}
|
|
166
178
|
|
|
179
|
+
async getMetrics ({ format }) {
|
|
180
|
+
if (!this.#metricsRegistry) return null
|
|
181
|
+
|
|
182
|
+
return format === 'json'
|
|
183
|
+
? this.#metricsRegistry.getMetricsAsJSON()
|
|
184
|
+
: this.#metricsRegistry.metrics()
|
|
185
|
+
}
|
|
186
|
+
|
|
167
187
|
#fetchServiceUrl (key, { parent, context: service }) {
|
|
168
188
|
if (service.localServiceEnvVars.has(key)) {
|
|
169
189
|
return service.localServiceEnvVars.get(key)
|
|
@@ -14,7 +14,10 @@ const defaultStackable = {
|
|
|
14
14
|
getOpenapiSchema: () => null,
|
|
15
15
|
getGraphqlSchema: () => null,
|
|
16
16
|
getMeta: () => ({}),
|
|
17
|
-
|
|
17
|
+
collectMetrics: () => ({
|
|
18
|
+
defaultMetrics: true,
|
|
19
|
+
httpMetrics: true,
|
|
20
|
+
}),
|
|
18
21
|
inject: () => {
|
|
19
22
|
throw new Error('Stackable inject not implemented')
|
|
20
23
|
},
|
package/lib/worker/itc.js
CHANGED
|
@@ -120,8 +120,12 @@ function setupITC (app, service, dispatcher) {
|
|
|
120
120
|
}
|
|
121
121
|
},
|
|
122
122
|
|
|
123
|
-
getMetrics (format) {
|
|
124
|
-
|
|
123
|
+
async getMetrics (format) {
|
|
124
|
+
try {
|
|
125
|
+
return await app.getMetrics({ format })
|
|
126
|
+
} catch (err) {
|
|
127
|
+
throw new errors.FailedToRetrieveMetricsError(service.id, err.message)
|
|
128
|
+
}
|
|
125
129
|
},
|
|
126
130
|
|
|
127
131
|
inject (injectParams) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const os = require('node:os')
|
|
4
|
+
const { eventLoopUtilization } = require('node:perf_hooks').performance
|
|
5
|
+
const { Registry, Gauge, collectDefaultMetrics } = require('prom-client')
|
|
6
|
+
const collectHttpMetrics = require('@platformatic/http-metrics')
|
|
7
|
+
|
|
8
|
+
async function collectMetrics (stackable, serviceId, opts = {}) {
|
|
9
|
+
const registry = new Registry()
|
|
10
|
+
const metricsConfig = await stackable.collectMetrics({ registry })
|
|
11
|
+
|
|
12
|
+
const labels = opts.labels ?? {}
|
|
13
|
+
registry.setDefaultLabels({ ...labels, serviceId })
|
|
14
|
+
|
|
15
|
+
if (metricsConfig.defaultMetrics) {
|
|
16
|
+
collectDefaultMetrics({ register: registry })
|
|
17
|
+
collectEluMetric(registry)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (metricsConfig.httpMetrics) {
|
|
21
|
+
collectHttpMetrics(registry, {
|
|
22
|
+
customLabels: ['telemetry_id'],
|
|
23
|
+
getCustomLabels: (req) => {
|
|
24
|
+
const telemetryId = req.headers['x-plt-telemetry-id'] ?? 'unknown'
|
|
25
|
+
return { telemetry_id: telemetryId }
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// TODO: check if it's a nodejs environment
|
|
30
|
+
// Needed for the Meraki metrics
|
|
31
|
+
collectHttpMetrics(registry, {
|
|
32
|
+
customLabels: ['telemetry_id'],
|
|
33
|
+
getCustomLabels: (req) => {
|
|
34
|
+
const telemetryId = req.headers['x-plt-telemetry-id'] ?? 'unknown'
|
|
35
|
+
return { telemetry_id: telemetryId }
|
|
36
|
+
},
|
|
37
|
+
histogram: {
|
|
38
|
+
name: 'http_request_all_duration_seconds',
|
|
39
|
+
help: 'request duration in seconds summary for all requests',
|
|
40
|
+
collect: function () {
|
|
41
|
+
process.nextTick(() => this.reset())
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
summary: {
|
|
45
|
+
name: 'http_request_all_summary_seconds',
|
|
46
|
+
help: 'request duration in seconds histogram for all requests',
|
|
47
|
+
collect: function () {
|
|
48
|
+
process.nextTick(() => this.reset())
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return registry
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function collectEluMetric (register) {
|
|
58
|
+
let startELU = eventLoopUtilization()
|
|
59
|
+
const eluMetric = new Gauge({
|
|
60
|
+
name: 'nodejs_eventloop_utilization',
|
|
61
|
+
help: 'The event loop utilization as a fraction of the loop time. 1 is fully utilized, 0 is fully idle.',
|
|
62
|
+
collect: () => {
|
|
63
|
+
const endELU = eventLoopUtilization()
|
|
64
|
+
const result = eventLoopUtilization(endELU, startELU).utilization
|
|
65
|
+
eluMetric.set(result)
|
|
66
|
+
startELU = endELU
|
|
67
|
+
},
|
|
68
|
+
registers: [register],
|
|
69
|
+
})
|
|
70
|
+
register.registerMetric(eluMetric)
|
|
71
|
+
|
|
72
|
+
let previousIdleTime = 0
|
|
73
|
+
let previousTotalTime = 0
|
|
74
|
+
const cpuMetric = new Gauge({
|
|
75
|
+
name: 'process_cpu_percent_usage',
|
|
76
|
+
help: 'The process CPU percent usage.',
|
|
77
|
+
collect: () => {
|
|
78
|
+
const cpus = os.cpus()
|
|
79
|
+
let idleTime = 0
|
|
80
|
+
let totalTime = 0
|
|
81
|
+
|
|
82
|
+
cpus.forEach(cpu => {
|
|
83
|
+
for (const type in cpu.times) {
|
|
84
|
+
totalTime += cpu.times[type]
|
|
85
|
+
if (type === 'idle') {
|
|
86
|
+
idleTime += cpu.times[type]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const idleDiff = idleTime - previousIdleTime
|
|
92
|
+
const totalDiff = totalTime - previousTotalTime
|
|
93
|
+
|
|
94
|
+
const usagePercent = 100 - ((100 * idleDiff) / totalDiff)
|
|
95
|
+
const roundedUsage = Math.round(usagePercent * 100) / 100
|
|
96
|
+
cpuMetric.set(roundedUsage)
|
|
97
|
+
|
|
98
|
+
previousIdleTime = idleTime
|
|
99
|
+
previousTotalTime = totalTime
|
|
100
|
+
},
|
|
101
|
+
registers: [register],
|
|
102
|
+
})
|
|
103
|
+
register.registerMetric(cpuMetric)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { collectMetrics }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.7",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -34,16 +34,17 @@
|
|
|
34
34
|
"typescript": "^5.5.4",
|
|
35
35
|
"undici-oidc-interceptor": "^0.5.0",
|
|
36
36
|
"why-is-node-running": "^2.2.2",
|
|
37
|
-
"@platformatic/composer": "2.0.0-alpha.
|
|
38
|
-
"@platformatic/
|
|
39
|
-
"@platformatic/
|
|
40
|
-
"@platformatic/sql-
|
|
41
|
-
"@platformatic/
|
|
37
|
+
"@platformatic/composer": "2.0.0-alpha.7",
|
|
38
|
+
"@platformatic/db": "2.0.0-alpha.7",
|
|
39
|
+
"@platformatic/service": "2.0.0-alpha.7",
|
|
40
|
+
"@platformatic/sql-graphql": "2.0.0-alpha.7",
|
|
41
|
+
"@platformatic/sql-mapper": "2.0.0-alpha.7"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@fastify/error": "^3.4.1",
|
|
45
45
|
"@fastify/websocket": "^10.0.0",
|
|
46
46
|
"@hapi/topo": "^6.0.2",
|
|
47
|
+
"@platformatic/http-metrics": "^0.1.0",
|
|
47
48
|
"@watchable/unpromise": "^1.0.2",
|
|
48
49
|
"boring-name-generator": "^1.0.3",
|
|
49
50
|
"change-case-all": "^2.1.0",
|
|
@@ -62,18 +63,19 @@
|
|
|
62
63
|
"pino": "^8.19.0",
|
|
63
64
|
"pino-pretty": "^11.0.0",
|
|
64
65
|
"pino-roll": "^1.0.0",
|
|
66
|
+
"prom-client": "^15.1.2",
|
|
65
67
|
"semgrator": "^0.3.0",
|
|
66
68
|
"tail-file-stream": "^0.2.0",
|
|
67
69
|
"undici": "^6.9.0",
|
|
68
70
|
"undici-thread-interceptor": "^0.5.1",
|
|
69
71
|
"ws": "^8.16.0",
|
|
70
|
-
"@platformatic/basic": "2.0.0-alpha.
|
|
71
|
-
"@platformatic/
|
|
72
|
-
"@platformatic/
|
|
73
|
-
"@platformatic/
|
|
74
|
-
"@platformatic/
|
|
75
|
-
"@platformatic/ts-compiler": "2.0.0-alpha.
|
|
76
|
-
"@platformatic/
|
|
72
|
+
"@platformatic/basic": "2.0.0-alpha.7",
|
|
73
|
+
"@platformatic/config": "2.0.0-alpha.7",
|
|
74
|
+
"@platformatic/generators": "2.0.0-alpha.7",
|
|
75
|
+
"@platformatic/itc": "2.0.0-alpha.7",
|
|
76
|
+
"@platformatic/telemetry": "2.0.0-alpha.7",
|
|
77
|
+
"@platformatic/ts-compiler": "2.0.0-alpha.7",
|
|
78
|
+
"@platformatic/utils": "2.0.0-alpha.7"
|
|
77
79
|
},
|
|
78
80
|
"scripts": {
|
|
79
81
|
"test": "npm run lint && borp --concurrency=1 --timeout=180000 && tsd",
|
package/schema.json
CHANGED