@platformatic/runtime 2.0.0-alpha.5 → 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/management-api.js +0 -14
- package/lib/runtime.js +46 -31
- package/lib/worker/app.js +20 -0
- package/lib/worker/default-stackable.js +4 -1
- package/lib/worker/itc.js +91 -83
- package/lib/worker/main.js +2 -1
- package/lib/worker/metrics.js +106 -0
- package/package.json +16 -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/management-api.js
CHANGED
|
@@ -72,20 +72,6 @@ async function managementApiPlugin (app, opts) {
|
|
|
72
72
|
return runtime.getServiceGraphqlSchema(id)
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
-
app.get('/services/:id/connection-strings', async request => {
|
|
76
|
-
const { id } = request.params
|
|
77
|
-
try {
|
|
78
|
-
app.log.debug('get connection strings', { id })
|
|
79
|
-
const meta = await runtime.getServiceMeta(id)
|
|
80
|
-
if (meta.db) {
|
|
81
|
-
return meta.db
|
|
82
|
-
}
|
|
83
|
-
return null
|
|
84
|
-
} catch (err) {
|
|
85
|
-
throw new errors.FailedToRetrieveMetaError(id, err.message)
|
|
86
|
-
}
|
|
87
|
-
})
|
|
88
|
-
|
|
89
75
|
app.post('/services/:id/start', async request => {
|
|
90
76
|
const { id } = request.params
|
|
91
77
|
app.log.debug('start service', { id })
|
package/lib/runtime.js
CHANGED
|
@@ -9,6 +9,7 @@ const { setTimeout: sleep } = require('node:timers/promises')
|
|
|
9
9
|
const { Worker } = require('node:worker_threads')
|
|
10
10
|
|
|
11
11
|
const { ITC } = require('@platformatic/itc')
|
|
12
|
+
const { Unpromise } = require('@watchable/unpromise')
|
|
12
13
|
const ts = require('tail-file-stream')
|
|
13
14
|
const { createThreadInterceptor } = require('undici-thread-interceptor')
|
|
14
15
|
|
|
@@ -150,7 +151,7 @@ class Runtime extends EventEmitter {
|
|
|
150
151
|
return this.#url
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
async stop () {
|
|
154
|
+
async stop (silent = false) {
|
|
154
155
|
if (this.#status === 'starting') {
|
|
155
156
|
await once(this, 'started')
|
|
156
157
|
}
|
|
@@ -158,7 +159,7 @@ class Runtime extends EventEmitter {
|
|
|
158
159
|
this.#updateStatus('stopping')
|
|
159
160
|
this.#startedServices.clear()
|
|
160
161
|
|
|
161
|
-
await Promise.all(this.#servicesIds.map(service => this._stopService(service)))
|
|
162
|
+
await Promise.all(this.#servicesIds.map(service => this._stopService(service, silent)))
|
|
162
163
|
|
|
163
164
|
this.#updateStatus('stopped')
|
|
164
165
|
}
|
|
@@ -174,12 +175,12 @@ class Runtime extends EventEmitter {
|
|
|
174
175
|
return this.#url
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
async close (fromManagementApi) {
|
|
178
|
+
async close (fromManagementApi = false, silent = false) {
|
|
178
179
|
this.#updateStatus('closing')
|
|
179
180
|
|
|
180
181
|
clearInterval(this.#metricsTimeout)
|
|
181
182
|
|
|
182
|
-
await this.stop()
|
|
183
|
+
await this.stop(silent)
|
|
183
184
|
|
|
184
185
|
if (this.#managementApi) {
|
|
185
186
|
if (fromManagementApi) {
|
|
@@ -262,7 +263,7 @@ class Runtime extends EventEmitter {
|
|
|
262
263
|
}
|
|
263
264
|
|
|
264
265
|
// Do not rename to #stopService as this is used in tests
|
|
265
|
-
async _stopService (id) {
|
|
266
|
+
async _stopService (id, silent) {
|
|
266
267
|
const service = await this.#getServiceById(id, false, false)
|
|
267
268
|
|
|
268
269
|
if (!service) {
|
|
@@ -271,17 +272,19 @@ class Runtime extends EventEmitter {
|
|
|
271
272
|
|
|
272
273
|
this.#startedServices.set(id, false)
|
|
273
274
|
|
|
274
|
-
|
|
275
|
+
if (!silent) {
|
|
276
|
+
this.logger?.info(`Stopping service "${id}"...`)
|
|
277
|
+
}
|
|
275
278
|
|
|
276
279
|
// Always send the stop message, it will shut down workers that only had ITC and interceptors setup
|
|
277
280
|
try {
|
|
278
|
-
await
|
|
281
|
+
await Unpromise.race([sendViaITC(service, 'stop'), sleep(10000, 'timeout', { ref: false })])
|
|
279
282
|
} catch (error) {
|
|
280
283
|
this.logger?.info(`Failed to stop service "${id}". Killing a worker thread.`, error)
|
|
281
284
|
}
|
|
282
285
|
|
|
283
286
|
// Wait for the worker thread to finish, we're going to create a new one if the service is ever restarted
|
|
284
|
-
const res = await
|
|
287
|
+
const res = await Unpromise.race([once(service, 'exit'), sleep(10000, 'timeout', { ref: false })])
|
|
285
288
|
|
|
286
289
|
// If the worker didn't exit in time, kill it
|
|
287
290
|
if (res === 'timeout') {
|
|
@@ -422,7 +425,7 @@ class Runtime extends EventEmitter {
|
|
|
422
425
|
packageName: packageJson.name ?? null,
|
|
423
426
|
packageVersion: packageJson.version ?? null,
|
|
424
427
|
url: entrypointDetails?.url ?? null,
|
|
425
|
-
platformaticVersion
|
|
428
|
+
platformaticVersion
|
|
426
429
|
}
|
|
427
430
|
}
|
|
428
431
|
|
|
@@ -449,7 +452,7 @@ class Runtime extends EventEmitter {
|
|
|
449
452
|
async getServices () {
|
|
450
453
|
return {
|
|
451
454
|
entrypoint: this.#entrypointId,
|
|
452
|
-
services: await Promise.all(this.#servicesIds.map(id => this.getServiceDetails(id)))
|
|
455
|
+
services: await Promise.all(this.#servicesIds.map(id => this.getServiceDetails(id)))
|
|
453
456
|
}
|
|
454
457
|
}
|
|
455
458
|
|
|
@@ -478,7 +481,7 @@ class Runtime extends EventEmitter {
|
|
|
478
481
|
version,
|
|
479
482
|
localUrl,
|
|
480
483
|
entrypoint,
|
|
481
|
-
dependencies
|
|
484
|
+
dependencies
|
|
482
485
|
}
|
|
483
486
|
|
|
484
487
|
if (entrypoint) {
|
|
@@ -523,7 +526,6 @@ class Runtime extends EventEmitter {
|
|
|
523
526
|
}
|
|
524
527
|
|
|
525
528
|
const serviceMetrics = await sendViaITC(service, 'getMetrics', format)
|
|
526
|
-
|
|
527
529
|
if (serviceMetrics) {
|
|
528
530
|
if (metrics === null) {
|
|
529
531
|
metrics = format === 'json' ? [] : ''
|
|
@@ -575,17 +577,26 @@ class Runtime extends EventEmitter {
|
|
|
575
577
|
let p99Value = 0
|
|
576
578
|
|
|
577
579
|
const metricName = 'http_request_all_summary_seconds'
|
|
578
|
-
const httpLatencyMetrics = metrics.
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
p90Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.9).value || 0
|
|
582
|
-
p95Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.95).value || 0
|
|
583
|
-
p99Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.99).value || 0
|
|
580
|
+
const httpLatencyMetrics = metrics.filter(
|
|
581
|
+
metric => metric.name === metricName
|
|
582
|
+
)
|
|
584
583
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
+
}
|
|
589
600
|
|
|
590
601
|
const cpu = cpuMetric.values[0].value
|
|
591
602
|
const rss = rssMetric.values[0].value
|
|
@@ -610,9 +621,9 @@ class Runtime extends EventEmitter {
|
|
|
610
621
|
p50: p50Value,
|
|
611
622
|
p90: p90Value,
|
|
612
623
|
p95: p95Value,
|
|
613
|
-
p99: p99Value
|
|
614
|
-
}
|
|
615
|
-
}
|
|
624
|
+
p99: p99Value
|
|
625
|
+
}
|
|
626
|
+
}
|
|
616
627
|
}
|
|
617
628
|
|
|
618
629
|
return formattedMetrics
|
|
@@ -649,7 +660,7 @@ class Runtime extends EventEmitter {
|
|
|
649
660
|
}
|
|
650
661
|
runtimesLogsIds.push({
|
|
651
662
|
pid: runtime.runtimePID,
|
|
652
|
-
indexes: runtimeLogIds
|
|
663
|
+
indexes: runtimeLogIds
|
|
653
664
|
})
|
|
654
665
|
}
|
|
655
666
|
|
|
@@ -687,7 +698,7 @@ class Runtime extends EventEmitter {
|
|
|
687
698
|
serviceConfig,
|
|
688
699
|
dirname: this.#configManager.dirname,
|
|
689
700
|
runtimeLogsDir: this.#runtimeLogsDir,
|
|
690
|
-
loggingPort
|
|
701
|
+
loggingPort
|
|
691
702
|
},
|
|
692
703
|
execArgv: [], // Avoid side effects
|
|
693
704
|
env: this.#env,
|
|
@@ -700,7 +711,7 @@ class Runtime extends EventEmitter {
|
|
|
700
711
|
The author of this (Paolo and Matteo) are not proud of the solution. Forgive us.
|
|
701
712
|
*/
|
|
702
713
|
stdout: true,
|
|
703
|
-
stderr: true
|
|
714
|
+
stderr: true
|
|
704
715
|
})
|
|
705
716
|
|
|
706
717
|
// Make sure the listener can handle a lot of API requests at once before raising a warning
|
|
@@ -737,9 +748,13 @@ class Runtime extends EventEmitter {
|
|
|
737
748
|
service[kConfig] = serviceConfig
|
|
738
749
|
|
|
739
750
|
// Setup ITC
|
|
740
|
-
service[kITC] = new ITC({
|
|
751
|
+
service[kITC] = new ITC({
|
|
752
|
+
port: service,
|
|
753
|
+
handlers: {
|
|
754
|
+
getServiceMeta: this.getServiceMeta.bind(this)
|
|
755
|
+
}
|
|
756
|
+
})
|
|
741
757
|
service[kITC].listen()
|
|
742
|
-
service[kITC].handle('getServiceMeta', this.getServiceMeta.bind(this))
|
|
743
758
|
|
|
744
759
|
// Handle services changes
|
|
745
760
|
// This is not purposely activated on when this.#configManager.current.watch === true
|
|
@@ -904,7 +919,7 @@ class Runtime extends EventEmitter {
|
|
|
904
919
|
runtimesLogFiles.push({
|
|
905
920
|
runtimePID: parseInt(runtimePID),
|
|
906
921
|
runtimeLogFiles,
|
|
907
|
-
lastModified
|
|
922
|
+
lastModified
|
|
908
923
|
})
|
|
909
924
|
}
|
|
910
925
|
|
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
|
@@ -4,6 +4,7 @@ const { once } = require('node:events')
|
|
|
4
4
|
const { parentPort } = require('node:worker_threads')
|
|
5
5
|
|
|
6
6
|
const { ITC } = require('@platformatic/itc')
|
|
7
|
+
const { Unpromise } = require('@watchable/unpromise')
|
|
7
8
|
|
|
8
9
|
const errors = require('../errors')
|
|
9
10
|
const { kITC, kId } = require('./symbols')
|
|
@@ -14,11 +15,11 @@ async function sendViaITC (worker, name, message) {
|
|
|
14
15
|
const ac = new AbortController()
|
|
15
16
|
let exitCode
|
|
16
17
|
|
|
17
|
-
const response = await
|
|
18
|
+
const response = await Unpromise.race([
|
|
18
19
|
worker[kITC].send(name, message),
|
|
19
20
|
once(worker, 'exit', { signal: ac.signal }).then(([code]) => {
|
|
20
21
|
exitCode = code
|
|
21
|
-
})
|
|
22
|
+
})
|
|
22
23
|
])
|
|
23
24
|
|
|
24
25
|
if (typeof exitCode === 'number') {
|
|
@@ -42,90 +43,97 @@ async function sendViaITC (worker, name, message) {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function setupITC (app, service, dispatcher) {
|
|
45
|
-
const itc = new ITC({
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
46
|
+
const itc = new ITC({
|
|
47
|
+
port: parentPort,
|
|
48
|
+
handlers: {
|
|
49
|
+
async start () {
|
|
50
|
+
const status = app.getStatus()
|
|
51
|
+
|
|
52
|
+
if (status === 'starting') {
|
|
53
|
+
await once(app, 'start')
|
|
54
|
+
} else {
|
|
55
|
+
await app.start()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (service.entrypoint) {
|
|
59
|
+
await app.listen()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const url = app.stackable.getUrl()
|
|
63
|
+
|
|
64
|
+
const dispatchFunc = await app.stackable.getDispatchFunc()
|
|
65
|
+
dispatcher.replaceServer(url ?? dispatchFunc)
|
|
66
|
+
|
|
67
|
+
return service.entrypoint ? url : null
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async stop () {
|
|
71
|
+
const status = app.getStatus()
|
|
72
|
+
|
|
73
|
+
if (status === 'starting') {
|
|
74
|
+
await once(app, 'start')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (status !== 'stopped') {
|
|
78
|
+
await app.stop()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
dispatcher.interceptor.close()
|
|
82
|
+
itc.close()
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
getStatus () {
|
|
86
|
+
return app.getStatus()
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
getServiceInfo () {
|
|
90
|
+
return app.stackable.getInfo()
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
async getServiceConfig () {
|
|
94
|
+
const current = await app.stackable.getConfig()
|
|
95
|
+
// Remove all undefined keys from the config
|
|
96
|
+
return JSON.parse(JSON.stringify(current))
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async getServiceOpenAPISchema () {
|
|
100
|
+
try {
|
|
101
|
+
return await app.stackable.getOpenapiSchema()
|
|
102
|
+
} catch (err) {
|
|
103
|
+
throw new errors.FailedToRetrieveOpenAPISchemaError(service.id, err.message)
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async getServiceGraphQLSchema () {
|
|
108
|
+
try {
|
|
109
|
+
return await app.stackable.getGraphqlSchema()
|
|
110
|
+
} catch (err) {
|
|
111
|
+
throw new errors.FailedToRetrieveGraphQLSchemaError(service.id, err.message)
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async getServiceMeta () {
|
|
116
|
+
try {
|
|
117
|
+
return await app.stackable.getMeta()
|
|
118
|
+
} catch (err) {
|
|
119
|
+
throw new errors.FailedToRetrieveMetaError(service.id, err.message)
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
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
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
inject (injectParams) {
|
|
132
|
+
return app.stackable.inject(injectParams)
|
|
133
|
+
}
|
|
102
134
|
}
|
|
103
135
|
})
|
|
104
136
|
|
|
105
|
-
itc.handle('getServiceGraphQLSchema', async () => {
|
|
106
|
-
try {
|
|
107
|
-
return app.stackable.getGraphqlSchema()
|
|
108
|
-
} catch (err) {
|
|
109
|
-
throw new errors.FailedToRetrieveGraphQLSchemaError(service.id, err.message)
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
itc.handle('getServiceMeta', async () => {
|
|
114
|
-
try {
|
|
115
|
-
return app.stackable.getMeta()
|
|
116
|
-
} catch (err) {
|
|
117
|
-
throw new errors.FailedToRetrieveMetaError(service.id, err.message)
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
itc.handle('getMetrics', async format => {
|
|
122
|
-
return app.stackable.getMetrics({ format })
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
itc.handle('inject', async injectParams => {
|
|
126
|
-
return app.stackable.inject(injectParams)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
137
|
app.on('changed', () => {
|
|
130
138
|
itc.notify('changed')
|
|
131
139
|
})
|
package/lib/worker/main.js
CHANGED
|
@@ -6,6 +6,7 @@ const { setTimeout: sleep } = require('node:timers/promises')
|
|
|
6
6
|
const { parentPort, workerData, threadId } = require('node:worker_threads')
|
|
7
7
|
const { pathToFileURL } = require('node:url')
|
|
8
8
|
|
|
9
|
+
const { Unpromise } = require('@watchable/unpromise')
|
|
9
10
|
const pino = require('pino')
|
|
10
11
|
const { fetch, setGlobalDispatcher, Agent } = require('undici')
|
|
11
12
|
const { wire } = require('undici-thread-interceptor')
|
|
@@ -29,7 +30,7 @@ const logger = createLogger()
|
|
|
29
30
|
function handleUnhandled (type, err) {
|
|
30
31
|
logger.error({ err }, `application ${type}`)
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
Unpromise.race([app?.stop(), sleep(1000, 'timeout', { ref: false })])
|
|
33
34
|
.catch()
|
|
34
35
|
.finally(() => {
|
|
35
36
|
process.exit(1)
|
|
@@ -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,18 @@
|
|
|
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/db": "2.0.0-alpha.
|
|
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",
|
|
48
|
+
"@watchable/unpromise": "^1.0.2",
|
|
47
49
|
"boring-name-generator": "^1.0.3",
|
|
48
50
|
"change-case-all": "^2.1.0",
|
|
49
51
|
"close-with-grace": "^2.0.0",
|
|
@@ -61,18 +63,19 @@
|
|
|
61
63
|
"pino": "^8.19.0",
|
|
62
64
|
"pino-pretty": "^11.0.0",
|
|
63
65
|
"pino-roll": "^1.0.0",
|
|
66
|
+
"prom-client": "^15.1.2",
|
|
64
67
|
"semgrator": "^0.3.0",
|
|
65
68
|
"tail-file-stream": "^0.2.0",
|
|
66
69
|
"undici": "^6.9.0",
|
|
67
70
|
"undici-thread-interceptor": "^0.5.1",
|
|
68
71
|
"ws": "^8.16.0",
|
|
69
|
-
"@platformatic/basic": "2.0.0-alpha.
|
|
70
|
-
"@platformatic/
|
|
71
|
-
"@platformatic/generators": "2.0.0-alpha.
|
|
72
|
-
"@platformatic/
|
|
73
|
-
"@platformatic/
|
|
74
|
-
"@platformatic/
|
|
75
|
-
"@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"
|
|
76
79
|
},
|
|
77
80
|
"scripts": {
|
|
78
81
|
"test": "npm run lint && borp --concurrency=1 --timeout=180000 && tsd",
|
package/schema.json
CHANGED