@platformatic/runtime 2.0.0-alpha.6 → 2.0.0-alpha.8
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/build-server.js +8 -4
- package/lib/errors.js +1 -0
- package/lib/runtime.js +88 -46
- package/lib/schema.js +92 -96
- package/lib/start.js +19 -8
- package/lib/worker/app.js +31 -7
- package/lib/worker/default-stackable.js +5 -1
- package/lib/worker/itc.js +11 -2
- package/lib/worker/main.js +11 -10
- package/lib/worker/metrics.js +106 -0
- package/package.json +16 -14
- 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 HttpsSchemasPlatformaticDevPlatformaticRuntime200Alpha8Json = {
|
|
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/build-server.js
CHANGED
|
@@ -9,7 +9,7 @@ const { platformaticRuntime } = require('./config')
|
|
|
9
9
|
const { buildRuntime } = require('./start')
|
|
10
10
|
const { loadConfig } = require('./utils')
|
|
11
11
|
|
|
12
|
-
async function buildServerRuntime (options = {}) {
|
|
12
|
+
async function buildServerRuntime (options = {}, args = undefined) {
|
|
13
13
|
const { serviceMap } = options
|
|
14
14
|
|
|
15
15
|
if (!options.configManager) {
|
|
@@ -18,7 +18,7 @@ async function buildServerRuntime (options = {}) {
|
|
|
18
18
|
// Instantiate a new config manager from the current options.
|
|
19
19
|
const cm = new ConfigManager({
|
|
20
20
|
...platformaticRuntime.configManagerConfig,
|
|
21
|
-
source: options
|
|
21
|
+
source: options
|
|
22
22
|
})
|
|
23
23
|
await cm.parseAndValidate()
|
|
24
24
|
|
|
@@ -31,10 +31,14 @@ async function buildServerRuntime (options = {}) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
if (args) {
|
|
35
|
+
options.configManager.args = args
|
|
36
|
+
}
|
|
37
|
+
|
|
34
38
|
return buildRuntime(options.configManager, options.env)
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
async function buildServer (options) {
|
|
41
|
+
async function buildServer (options, args) {
|
|
38
42
|
if (typeof options === 'string') {
|
|
39
43
|
const config = await loadConfig({}, ['-c', options])
|
|
40
44
|
options = config.configManager.current
|
|
@@ -47,7 +51,7 @@ async function buildServer (options) {
|
|
|
47
51
|
delete options.app
|
|
48
52
|
|
|
49
53
|
if (app === platformaticRuntime || !app) {
|
|
50
|
-
return buildServerRuntime(options)
|
|
54
|
+
return buildServerRuntime(options, args)
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
if (app.buildServer) {
|
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
|
@@ -9,7 +9,10 @@ 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 {
|
|
12
|
+
const {
|
|
13
|
+
errors: { ensureLoggableError },
|
|
14
|
+
executeWithTimeout
|
|
15
|
+
} = require('@platformatic/utils')
|
|
13
16
|
const ts = require('tail-file-stream')
|
|
14
17
|
const { createThreadInterceptor } = require('undici-thread-interceptor')
|
|
15
18
|
|
|
@@ -64,7 +67,7 @@ class Runtime extends EventEmitter {
|
|
|
64
67
|
this.#servicesIds = []
|
|
65
68
|
this.#url = undefined
|
|
66
69
|
// Note: nothing hits the main thread so there is no reason to set the globalDispatcher here
|
|
67
|
-
this.#interceptor = createThreadInterceptor({ domain: '.plt.local' })
|
|
70
|
+
this.#interceptor = createThreadInterceptor({ domain: '.plt.local', timeout: true })
|
|
68
71
|
this.#status = undefined
|
|
69
72
|
this.#startedServices = new Map()
|
|
70
73
|
this.#restartPromises = new Map()
|
|
@@ -148,10 +151,11 @@ class Runtime extends EventEmitter {
|
|
|
148
151
|
this.startCollectingMetrics()
|
|
149
152
|
}
|
|
150
153
|
|
|
154
|
+
this.logger.info(`Platformatic is now listening at ${this.#url}`)
|
|
151
155
|
return this.#url
|
|
152
156
|
}
|
|
153
157
|
|
|
154
|
-
async stop () {
|
|
158
|
+
async stop (silent = false) {
|
|
155
159
|
if (this.#status === 'starting') {
|
|
156
160
|
await once(this, 'started')
|
|
157
161
|
}
|
|
@@ -159,7 +163,7 @@ class Runtime extends EventEmitter {
|
|
|
159
163
|
this.#updateStatus('stopping')
|
|
160
164
|
this.#startedServices.clear()
|
|
161
165
|
|
|
162
|
-
await Promise.all(this.#servicesIds.map(service => this._stopService(service)))
|
|
166
|
+
await Promise.all(this.#servicesIds.map(service => this._stopService(service, silent)))
|
|
163
167
|
|
|
164
168
|
this.#updateStatus('stopped')
|
|
165
169
|
}
|
|
@@ -172,15 +176,16 @@ class Runtime extends EventEmitter {
|
|
|
172
176
|
|
|
173
177
|
this.emit('restarted')
|
|
174
178
|
|
|
179
|
+
this.logger.info(`Platformatic is now listening at ${this.#url}`)
|
|
175
180
|
return this.#url
|
|
176
181
|
}
|
|
177
182
|
|
|
178
|
-
async close (fromManagementApi) {
|
|
183
|
+
async close (fromManagementApi = false, silent = false) {
|
|
179
184
|
this.#updateStatus('closing')
|
|
180
185
|
|
|
181
186
|
clearInterval(this.#metricsTimeout)
|
|
182
187
|
|
|
183
|
-
await this.stop()
|
|
188
|
+
await this.stop(silent)
|
|
184
189
|
|
|
185
190
|
if (this.#managementApi) {
|
|
186
191
|
if (fromManagementApi) {
|
|
@@ -236,7 +241,7 @@ class Runtime extends EventEmitter {
|
|
|
236
241
|
// TODO: handle port allocation error here
|
|
237
242
|
if (error.code === 'EADDRINUSE') throw error
|
|
238
243
|
|
|
239
|
-
this.logger.error({ error }, `Failed to start service "${id}".`)
|
|
244
|
+
this.logger.error({ error: ensureLoggableError(error) }, `Failed to start service "${id}".`)
|
|
240
245
|
|
|
241
246
|
const config = this.#configManager.current
|
|
242
247
|
const restartOnError = config.restartOnError
|
|
@@ -263,7 +268,7 @@ class Runtime extends EventEmitter {
|
|
|
263
268
|
}
|
|
264
269
|
|
|
265
270
|
// Do not rename to #stopService as this is used in tests
|
|
266
|
-
async _stopService (id) {
|
|
271
|
+
async _stopService (id, silent) {
|
|
267
272
|
const service = await this.#getServiceById(id, false, false)
|
|
268
273
|
|
|
269
274
|
if (!service) {
|
|
@@ -272,17 +277,24 @@ class Runtime extends EventEmitter {
|
|
|
272
277
|
|
|
273
278
|
this.#startedServices.set(id, false)
|
|
274
279
|
|
|
275
|
-
|
|
280
|
+
if (!silent) {
|
|
281
|
+
this.logger?.info(`Stopping service "${id}"...`)
|
|
282
|
+
}
|
|
276
283
|
|
|
277
284
|
// Always send the stop message, it will shut down workers that only had ITC and interceptors setup
|
|
278
285
|
try {
|
|
279
|
-
await
|
|
286
|
+
await executeWithTimeout(sendViaITC(service, 'stop'), 10000)
|
|
280
287
|
} catch (error) {
|
|
281
|
-
this.logger?.info(
|
|
288
|
+
this.logger?.info(
|
|
289
|
+
{ error: ensureLoggableError(error) },
|
|
290
|
+
`Failed to stop service "${id}". Killing a worker thread.`
|
|
291
|
+
)
|
|
292
|
+
} finally {
|
|
293
|
+
service[kITC].close()
|
|
282
294
|
}
|
|
283
295
|
|
|
284
296
|
// Wait for the worker thread to finish, we're going to create a new one if the service is ever restarted
|
|
285
|
-
const res = await
|
|
297
|
+
const res = await executeWithTimeout(once(service, 'exit'), 10000)
|
|
286
298
|
|
|
287
299
|
// If the worker didn't exit in time, kill it
|
|
288
300
|
if (res === 'timeout') {
|
|
@@ -290,6 +302,25 @@ class Runtime extends EventEmitter {
|
|
|
290
302
|
}
|
|
291
303
|
}
|
|
292
304
|
|
|
305
|
+
async buildService (id) {
|
|
306
|
+
const service = this.#services.get(id)
|
|
307
|
+
|
|
308
|
+
if (!service) {
|
|
309
|
+
throw new errors.ServiceNotFoundError(id, Array.from(this.#services.keys()).join(', '))
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
return await sendViaITC(service, 'build')
|
|
314
|
+
} catch (e) {
|
|
315
|
+
// The service exports no meta, return an empty object
|
|
316
|
+
if (e.code === 'PLT_ITC_HANDLER_NOT_FOUND') {
|
|
317
|
+
return {}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
throw e
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
293
324
|
async inject (id, injectParams) {
|
|
294
325
|
const service = await this.#getServiceById(id, true)
|
|
295
326
|
return sendViaITC(service, 'inject', injectParams)
|
|
@@ -524,7 +555,6 @@ class Runtime extends EventEmitter {
|
|
|
524
555
|
}
|
|
525
556
|
|
|
526
557
|
const serviceMetrics = await sendViaITC(service, 'getMetrics', format)
|
|
527
|
-
|
|
528
558
|
if (serviceMetrics) {
|
|
529
559
|
if (metrics === null) {
|
|
530
560
|
metrics = format === 'json' ? [] : ''
|
|
@@ -576,17 +606,24 @@ class Runtime extends EventEmitter {
|
|
|
576
606
|
let p99Value = 0
|
|
577
607
|
|
|
578
608
|
const metricName = 'http_request_all_summary_seconds'
|
|
579
|
-
const httpLatencyMetrics = metrics.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
609
|
+
const httpLatencyMetrics = metrics.filter(metric => metric.name === metricName)
|
|
610
|
+
|
|
611
|
+
if (httpLatencyMetrics) {
|
|
612
|
+
const entrypointMetrics = httpLatencyMetrics.find(
|
|
613
|
+
metric => metric.values?.[0]?.labels?.serviceId === this.#entrypointId
|
|
614
|
+
)
|
|
615
|
+
if (entrypointMetrics) {
|
|
616
|
+
p50Value = entrypointMetrics.values.find(value => value.labels.quantile === 0.5)?.value || 0
|
|
617
|
+
p90Value = entrypointMetrics.values.find(value => value.labels.quantile === 0.9)?.value || 0
|
|
618
|
+
p95Value = entrypointMetrics.values.find(value => value.labels.quantile === 0.95)?.value || 0
|
|
619
|
+
p99Value = entrypointMetrics.values.find(value => value.labels.quantile === 0.99)?.value || 0
|
|
620
|
+
|
|
621
|
+
p50Value = Math.round(p50Value * 1000)
|
|
622
|
+
p90Value = Math.round(p90Value * 1000)
|
|
623
|
+
p95Value = Math.round(p95Value * 1000)
|
|
624
|
+
p99Value = Math.round(p99Value * 1000)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
590
627
|
|
|
591
628
|
const cpu = cpuMetric.values[0].value
|
|
592
629
|
const rss = rssMetric.values[0].value
|
|
@@ -625,6 +662,25 @@ class Runtime extends EventEmitter {
|
|
|
625
662
|
}
|
|
626
663
|
}
|
|
627
664
|
|
|
665
|
+
async getServiceMeta (id) {
|
|
666
|
+
const service = this.#services.get(id)
|
|
667
|
+
|
|
668
|
+
if (!service) {
|
|
669
|
+
throw new errors.ServiceNotFoundError(id, Array.from(this.#services.keys()).join(', '))
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
return await sendViaITC(service, 'getServiceMeta')
|
|
674
|
+
} catch (e) {
|
|
675
|
+
// The service exports no meta, return an empty object
|
|
676
|
+
if (e.code === 'PLT_ITC_HANDLER_NOT_FOUND') {
|
|
677
|
+
return {}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
throw e
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
628
684
|
async getLogIds (runtimePID) {
|
|
629
685
|
runtimePID = runtimePID ?? process.pid
|
|
630
686
|
|
|
@@ -685,7 +741,10 @@ class Runtime extends EventEmitter {
|
|
|
685
741
|
const service = new Worker(kWorkerFile, {
|
|
686
742
|
workerData: {
|
|
687
743
|
config,
|
|
688
|
-
serviceConfig
|
|
744
|
+
serviceConfig: {
|
|
745
|
+
...serviceConfig,
|
|
746
|
+
isProduction: this.#configManager.args?.production ?? false
|
|
747
|
+
},
|
|
689
748
|
dirname: this.#configManager.dirname,
|
|
690
749
|
runtimeLogsDir: this.#runtimeLogsDir,
|
|
691
750
|
loggingPort
|
|
@@ -712,6 +771,7 @@ class Runtime extends EventEmitter {
|
|
|
712
771
|
const started = this.#startedServices.get(id)
|
|
713
772
|
this.#services.delete(id)
|
|
714
773
|
loggerDestination.close()
|
|
774
|
+
service[kITC].close()
|
|
715
775
|
loggingPort.close()
|
|
716
776
|
|
|
717
777
|
if (this.#status === 'stopping') return
|
|
@@ -725,7 +785,7 @@ class Runtime extends EventEmitter {
|
|
|
725
785
|
if (restartOnError > 0) {
|
|
726
786
|
this.logger.warn(`Restarting a service "${id}" in ${restartOnError}ms...`)
|
|
727
787
|
this.#restartCrashedService(id).catch(err => {
|
|
728
|
-
this.logger.error({ err }, `Failed to restart service "${id}".`)
|
|
788
|
+
this.logger.error({ err: ensureLoggableError(err) }, `Failed to restart service "${id}".`)
|
|
729
789
|
})
|
|
730
790
|
} else {
|
|
731
791
|
this.logger.warn(`The "${id}" service is no longer available.`)
|
|
@@ -739,6 +799,7 @@ class Runtime extends EventEmitter {
|
|
|
739
799
|
|
|
740
800
|
// Setup ITC
|
|
741
801
|
service[kITC] = new ITC({
|
|
802
|
+
name: id + '-runtime',
|
|
742
803
|
port: service,
|
|
743
804
|
handlers: {
|
|
744
805
|
getServiceMeta: this.getServiceMeta.bind(this)
|
|
@@ -846,25 +907,6 @@ class Runtime extends EventEmitter {
|
|
|
846
907
|
return service
|
|
847
908
|
}
|
|
848
909
|
|
|
849
|
-
async getServiceMeta (id) {
|
|
850
|
-
const service = this.#services.get(id)
|
|
851
|
-
|
|
852
|
-
if (!service) {
|
|
853
|
-
throw new errors.ServiceNotFoundError(id, Array.from(this.#services.keys()).join(', '))
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
try {
|
|
857
|
-
return await service[kITC].send('getServiceMeta')
|
|
858
|
-
} catch (e) {
|
|
859
|
-
// The service exports no meta, return an empty object
|
|
860
|
-
if (e.code === 'PLT_ITC_HANDLER_NOT_FOUND') {
|
|
861
|
-
return {}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
throw e
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
910
|
async #getRuntimePackageJson () {
|
|
869
911
|
const runtimeDir = this.#configManager.dirname
|
|
870
912
|
const packageJsonPath = join(runtimeDir, 'package.json')
|
|
@@ -893,7 +935,7 @@ class Runtime extends EventEmitter {
|
|
|
893
935
|
try {
|
|
894
936
|
await access(this.#runtimeTmpDir)
|
|
895
937
|
} catch (err) {
|
|
896
|
-
this.logger.error({ err }, 'Cannot access temporary folder.')
|
|
938
|
+
this.logger.error({ err: ensureLoggableError(err) }, 'Cannot access temporary folder.')
|
|
897
939
|
return []
|
|
898
940
|
}
|
|
899
941
|
|
package/lib/schema.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
4
|
const telemetry = require('@platformatic/telemetry').schema
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
schemaComponents: { server }
|
|
7
|
+
} = require('@platformatic/utils')
|
|
6
8
|
|
|
7
9
|
const pkg = require('../package.json')
|
|
8
10
|
const platformaticRuntimeSchema = {
|
|
@@ -11,11 +13,11 @@ const platformaticRuntimeSchema = {
|
|
|
11
13
|
type: 'object',
|
|
12
14
|
properties: {
|
|
13
15
|
$schema: {
|
|
14
|
-
type: 'string'
|
|
16
|
+
type: 'string'
|
|
15
17
|
},
|
|
16
18
|
preload: {
|
|
17
19
|
type: 'string',
|
|
18
|
-
resolvePath: true
|
|
20
|
+
resolvePath: true
|
|
19
21
|
},
|
|
20
22
|
autoload: {
|
|
21
23
|
type: 'object',
|
|
@@ -24,14 +26,14 @@ const platformaticRuntimeSchema = {
|
|
|
24
26
|
properties: {
|
|
25
27
|
path: {
|
|
26
28
|
type: 'string',
|
|
27
|
-
resolvePath: true
|
|
29
|
+
resolvePath: true
|
|
28
30
|
},
|
|
29
31
|
exclude: {
|
|
30
32
|
type: 'array',
|
|
31
33
|
default: [],
|
|
32
34
|
items: {
|
|
33
|
-
type: 'string'
|
|
34
|
-
}
|
|
35
|
+
type: 'string'
|
|
36
|
+
}
|
|
35
37
|
},
|
|
36
38
|
mappings: {
|
|
37
39
|
type: 'object',
|
|
@@ -41,89 +43,92 @@ const platformaticRuntimeSchema = {
|
|
|
41
43
|
required: ['id'],
|
|
42
44
|
properties: {
|
|
43
45
|
id: {
|
|
44
|
-
type: 'string'
|
|
46
|
+
type: 'string'
|
|
45
47
|
},
|
|
46
48
|
config: {
|
|
47
|
-
type: 'string'
|
|
49
|
+
type: 'string'
|
|
48
50
|
},
|
|
49
51
|
useHttp: {
|
|
50
|
-
type: 'boolean'
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
52
|
+
type: 'boolean'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
56
58
|
},
|
|
57
59
|
telemetry,
|
|
58
60
|
server,
|
|
59
61
|
entrypoint: {
|
|
60
|
-
type: 'string'
|
|
62
|
+
type: 'string'
|
|
61
63
|
},
|
|
62
64
|
watch: {
|
|
63
65
|
anyOf: [
|
|
64
66
|
{
|
|
65
|
-
type: 'boolean'
|
|
67
|
+
type: 'boolean'
|
|
66
68
|
},
|
|
67
69
|
{
|
|
68
|
-
type: 'string'
|
|
69
|
-
}
|
|
70
|
-
]
|
|
70
|
+
type: 'string'
|
|
71
|
+
}
|
|
72
|
+
]
|
|
71
73
|
},
|
|
72
74
|
inspectorOptions: {
|
|
73
75
|
type: 'object',
|
|
74
76
|
properties: {
|
|
75
77
|
host: {
|
|
76
|
-
type: 'string'
|
|
78
|
+
type: 'string'
|
|
77
79
|
},
|
|
78
80
|
port: {
|
|
79
|
-
type: 'number'
|
|
81
|
+
type: 'number'
|
|
80
82
|
},
|
|
81
83
|
breakFirstLine: {
|
|
82
|
-
type: 'boolean'
|
|
84
|
+
type: 'boolean'
|
|
83
85
|
},
|
|
84
86
|
watchDisabled: {
|
|
85
|
-
type: 'boolean'
|
|
86
|
-
}
|
|
87
|
-
}
|
|
87
|
+
type: 'boolean'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
88
90
|
},
|
|
89
91
|
undici: {
|
|
90
92
|
type: 'object',
|
|
91
93
|
properties: {
|
|
92
94
|
agentOptions: {
|
|
93
95
|
type: 'object',
|
|
94
|
-
additionalProperties: true
|
|
96
|
+
additionalProperties: true
|
|
95
97
|
},
|
|
96
98
|
interceptors: {
|
|
97
|
-
anyOf: [
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
anyOf: [
|
|
100
|
+
{
|
|
101
|
+
type: 'array',
|
|
102
|
+
items: {
|
|
103
|
+
$ref: '#/$defs/undiciInterceptor'
|
|
104
|
+
}
|
|
101
105
|
},
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
},
|
|
111
|
-
Pool: {
|
|
112
|
-
type: 'array',
|
|
113
|
-
items: {
|
|
114
|
-
$ref: '#/$defs/undiciInterceptor',
|
|
106
|
+
{
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
Client: {
|
|
110
|
+
type: 'array',
|
|
111
|
+
items: {
|
|
112
|
+
$ref: '#/$defs/undiciInterceptor'
|
|
113
|
+
}
|
|
115
114
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
Pool: {
|
|
116
|
+
type: 'array',
|
|
117
|
+
items: {
|
|
118
|
+
$ref: '#/$defs/undiciInterceptor'
|
|
119
|
+
}
|
|
121
120
|
},
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
Agent: {
|
|
122
|
+
type: 'array',
|
|
123
|
+
items: {
|
|
124
|
+
$ref: '#/$defs/undiciInterceptor'
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
127
132
|
},
|
|
128
133
|
managementApi: {
|
|
129
134
|
anyOf: [
|
|
@@ -136,14 +141,14 @@ const platformaticRuntimeSchema = {
|
|
|
136
141
|
maxSize: {
|
|
137
142
|
type: 'number',
|
|
138
143
|
minimum: 5,
|
|
139
|
-
default: 200
|
|
140
|
-
}
|
|
141
|
-
}
|
|
144
|
+
default: 200
|
|
145
|
+
}
|
|
146
|
+
}
|
|
142
147
|
},
|
|
143
|
-
additionalProperties: false
|
|
144
|
-
}
|
|
148
|
+
additionalProperties: false
|
|
149
|
+
}
|
|
145
150
|
],
|
|
146
|
-
default: true
|
|
151
|
+
default: true
|
|
147
152
|
},
|
|
148
153
|
metrics: {
|
|
149
154
|
anyOf: [
|
|
@@ -152,10 +157,7 @@ const platformaticRuntimeSchema = {
|
|
|
152
157
|
type: 'object',
|
|
153
158
|
properties: {
|
|
154
159
|
port: {
|
|
155
|
-
anyOf: [
|
|
156
|
-
{ type: 'integer' },
|
|
157
|
-
{ type: 'string' },
|
|
158
|
-
],
|
|
160
|
+
anyOf: [{ type: 'integer' }, { type: 'string' }]
|
|
159
161
|
},
|
|
160
162
|
hostname: { type: 'string' },
|
|
161
163
|
endpoint: { type: 'string' },
|
|
@@ -163,19 +165,19 @@ const platformaticRuntimeSchema = {
|
|
|
163
165
|
type: 'object',
|
|
164
166
|
properties: {
|
|
165
167
|
username: { type: 'string' },
|
|
166
|
-
password: { type: 'string' }
|
|
168
|
+
password: { type: 'string' }
|
|
167
169
|
},
|
|
168
170
|
additionalProperties: false,
|
|
169
|
-
required: ['username', 'password']
|
|
171
|
+
required: ['username', 'password']
|
|
170
172
|
},
|
|
171
173
|
labels: {
|
|
172
174
|
type: 'object',
|
|
173
|
-
additionalProperties: { type: 'string' }
|
|
174
|
-
}
|
|
175
|
+
additionalProperties: { type: 'string' }
|
|
176
|
+
}
|
|
175
177
|
},
|
|
176
|
-
additionalProperties: false
|
|
177
|
-
}
|
|
178
|
-
]
|
|
178
|
+
additionalProperties: false
|
|
179
|
+
}
|
|
180
|
+
]
|
|
179
181
|
},
|
|
180
182
|
restartOnError: {
|
|
181
183
|
default: true,
|
|
@@ -183,59 +185,53 @@ const platformaticRuntimeSchema = {
|
|
|
183
185
|
{ type: 'boolean' },
|
|
184
186
|
{
|
|
185
187
|
type: 'number',
|
|
186
|
-
minimum: 100
|
|
187
|
-
}
|
|
188
|
-
]
|
|
188
|
+
minimum: 100
|
|
189
|
+
}
|
|
190
|
+
]
|
|
189
191
|
},
|
|
190
192
|
services: {
|
|
191
193
|
type: 'array',
|
|
192
194
|
items: {
|
|
193
195
|
type: 'object',
|
|
194
|
-
anyOf: [
|
|
195
|
-
{ required: ['id', 'path'] },
|
|
196
|
-
{ required: ['id', 'url'] },
|
|
197
|
-
],
|
|
196
|
+
anyOf: [{ required: ['id', 'path'] }, { required: ['id', 'url'] }],
|
|
198
197
|
properties: {
|
|
199
198
|
id: {
|
|
200
|
-
type: 'string'
|
|
199
|
+
type: 'string'
|
|
201
200
|
},
|
|
202
201
|
path: {
|
|
203
202
|
type: 'string',
|
|
204
|
-
resolvePath: true
|
|
203
|
+
resolvePath: true
|
|
205
204
|
},
|
|
206
205
|
config: {
|
|
207
|
-
type: 'string'
|
|
206
|
+
type: 'string'
|
|
208
207
|
},
|
|
209
208
|
url: {
|
|
210
|
-
type: 'string'
|
|
209
|
+
type: 'string'
|
|
211
210
|
},
|
|
212
211
|
useHttp: {
|
|
213
|
-
type: 'boolean'
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
212
|
+
type: 'boolean'
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
218
217
|
},
|
|
219
|
-
anyOf: [
|
|
220
|
-
{ required: ['autoload', 'entrypoint'] },
|
|
221
|
-
{ required: ['services', 'entrypoint'] },
|
|
222
|
-
],
|
|
218
|
+
anyOf: [{ required: ['autoload', 'entrypoint'] }, { required: ['services', 'entrypoint'] }],
|
|
223
219
|
additionalProperties: false,
|
|
224
220
|
$defs: {
|
|
225
221
|
undiciInterceptor: {
|
|
226
222
|
type: 'object',
|
|
227
223
|
properties: {
|
|
228
224
|
module: {
|
|
229
|
-
type: 'string'
|
|
225
|
+
type: 'string'
|
|
230
226
|
},
|
|
231
227
|
options: {
|
|
232
228
|
type: 'object',
|
|
233
|
-
additionalProperties: true
|
|
234
|
-
}
|
|
229
|
+
additionalProperties: true
|
|
230
|
+
}
|
|
235
231
|
},
|
|
236
|
-
required: ['module', 'options']
|
|
237
|
-
}
|
|
238
|
-
}
|
|
232
|
+
required: ['module', 'options']
|
|
233
|
+
}
|
|
234
|
+
}
|
|
239
235
|
}
|
|
240
236
|
|
|
241
237
|
module.exports.schema = platformaticRuntimeSchema
|
package/lib/start.js
CHANGED
|
@@ -5,6 +5,9 @@ const { writeFile } = require('node:fs/promises')
|
|
|
5
5
|
const { join, resolve, dirname } = require('node:path')
|
|
6
6
|
|
|
7
7
|
const { printConfigValidationErrors } = require('@platformatic/config')
|
|
8
|
+
const {
|
|
9
|
+
errors: { ensureLoggableError }
|
|
10
|
+
} = require('@platformatic/utils')
|
|
8
11
|
const closeWithGrace = require('close-with-grace')
|
|
9
12
|
const pino = require('pino')
|
|
10
13
|
const pretty = require('pino-pretty')
|
|
@@ -38,7 +41,7 @@ async function buildRuntime (configManager, env) {
|
|
|
38
41
|
try {
|
|
39
42
|
await runtime.restart()
|
|
40
43
|
} catch (err) {
|
|
41
|
-
runtime.logger.error({ err }, 'Failed to restart services.')
|
|
44
|
+
runtime.logger.error({ err: ensureLoggableError(err) }, 'Failed to restart services.')
|
|
42
45
|
}
|
|
43
46
|
})
|
|
44
47
|
|
|
@@ -96,7 +99,7 @@ async function setupAndStartRuntime (config) {
|
|
|
96
99
|
const logger = pino(
|
|
97
100
|
pretty({
|
|
98
101
|
translateTime: 'SYS:HH:MM:ss',
|
|
99
|
-
ignore: 'hostname,pid'
|
|
102
|
+
ignore: 'hostname,pid'
|
|
100
103
|
})
|
|
101
104
|
)
|
|
102
105
|
logger.warn(`Port: ${originalPort} is already in use!`)
|
|
@@ -107,7 +110,15 @@ async function setupAndStartRuntime (config) {
|
|
|
107
110
|
|
|
108
111
|
async function startCommand (args) {
|
|
109
112
|
try {
|
|
110
|
-
const config = await loadConfig(
|
|
113
|
+
const config = await loadConfig(
|
|
114
|
+
{
|
|
115
|
+
alias: {
|
|
116
|
+
p: 'production'
|
|
117
|
+
},
|
|
118
|
+
boolean: ['p', 'production']
|
|
119
|
+
},
|
|
120
|
+
args
|
|
121
|
+
)
|
|
111
122
|
|
|
112
123
|
const startResult = await setupAndStartRuntime(config)
|
|
113
124
|
|
|
@@ -130,16 +141,16 @@ async function startCommand (args) {
|
|
|
130
141
|
hostname: '127.0.0.1',
|
|
131
142
|
port: 3042,
|
|
132
143
|
logger: {
|
|
133
|
-
level: 'info'
|
|
134
|
-
}
|
|
144
|
+
level: 'info'
|
|
145
|
+
}
|
|
135
146
|
},
|
|
136
147
|
plugins: {
|
|
137
|
-
paths: [args[0]]
|
|
148
|
+
paths: [args[0]]
|
|
138
149
|
},
|
|
139
150
|
service: {
|
|
140
|
-
openapi: true
|
|
151
|
+
openapi: true
|
|
141
152
|
},
|
|
142
|
-
watch: true
|
|
153
|
+
watch: true
|
|
143
154
|
}
|
|
144
155
|
const toWrite = join(dirname(resolve(args[0])), 'platformatic.service.json')
|
|
145
156
|
console.log(`No config file found, creating ${join(dirname(args[0]), 'platformatic.service.json')}`)
|
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,17 +29,18 @@ 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,
|
|
33
36
|
directory: this.appConfig.path,
|
|
34
37
|
isEntrypoint: this.appConfig.entrypoint,
|
|
35
|
-
isProduction:
|
|
38
|
+
isProduction: this.appConfig.isProduction,
|
|
36
39
|
telemetryConfig,
|
|
37
40
|
metricsConfig,
|
|
38
41
|
serverConfig,
|
|
39
42
|
hasManagementApi: !!hasManagementApi,
|
|
40
|
-
localServiceEnvVars: this.appConfig.localServiceEnvVars
|
|
43
|
+
localServiceEnvVars: this.appConfig.localServiceEnvVars
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -68,7 +71,7 @@ class PlatformaticApp extends EventEmitter {
|
|
|
68
71
|
appConfig.path,
|
|
69
72
|
{
|
|
70
73
|
onMissingEnv: this.#fetchServiceUrl,
|
|
71
|
-
context: appConfig
|
|
74
|
+
context: appConfig
|
|
72
75
|
},
|
|
73
76
|
true
|
|
74
77
|
)
|
|
@@ -78,7 +81,7 @@ class PlatformaticApp extends EventEmitter {
|
|
|
78
81
|
['-c', appConfig.config],
|
|
79
82
|
{
|
|
80
83
|
onMissingEnv: this.#fetchServiceUrl,
|
|
81
|
-
context: appConfig
|
|
84
|
+
context: appConfig
|
|
82
85
|
},
|
|
83
86
|
true
|
|
84
87
|
)
|
|
@@ -86,13 +89,26 @@ class PlatformaticApp extends EventEmitter {
|
|
|
86
89
|
|
|
87
90
|
const app = loadedConfig.app
|
|
88
91
|
|
|
92
|
+
if (appConfig.isProduction && !process.env.NODE_ENV) {
|
|
93
|
+
process.env.NODE_ENV = 'production'
|
|
94
|
+
}
|
|
95
|
+
|
|
89
96
|
const stackable = await app.buildStackable({
|
|
90
97
|
onMissingEnv: this.#fetchServiceUrl,
|
|
91
98
|
config: this.appConfig.config,
|
|
92
|
-
context: this.#context
|
|
99
|
+
context: this.#context
|
|
93
100
|
})
|
|
94
101
|
this.stackable = this.#wrapStackable(stackable)
|
|
95
102
|
|
|
103
|
+
const metricsConfig = this.#context.metricsConfig
|
|
104
|
+
if (metricsConfig !== false) {
|
|
105
|
+
this.#metricsRegistry = await collectMetrics(
|
|
106
|
+
this.stackable,
|
|
107
|
+
this.appConfig.id,
|
|
108
|
+
metricsConfig
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
96
112
|
this.#updateDispatcher()
|
|
97
113
|
} catch (err) {
|
|
98
114
|
this.#logAndExit(err)
|
|
@@ -164,6 +180,14 @@ class PlatformaticApp extends EventEmitter {
|
|
|
164
180
|
await this.stackable.start({ listen: true })
|
|
165
181
|
}
|
|
166
182
|
|
|
183
|
+
async getMetrics ({ format }) {
|
|
184
|
+
if (!this.#metricsRegistry) return null
|
|
185
|
+
|
|
186
|
+
return format === 'json'
|
|
187
|
+
? this.#metricsRegistry.getMetricsAsJSON()
|
|
188
|
+
: this.#metricsRegistry.metrics()
|
|
189
|
+
}
|
|
190
|
+
|
|
167
191
|
#fetchServiceUrl (key, { parent, context: service }) {
|
|
168
192
|
if (service.localServiceEnvVars.has(key)) {
|
|
169
193
|
return service.localServiceEnvVars.get(key)
|
|
@@ -183,7 +207,7 @@ class PlatformaticApp extends EventEmitter {
|
|
|
183
207
|
path: watch.path,
|
|
184
208
|
/* c8 ignore next 2 */
|
|
185
209
|
allowToWatch: watch?.allow,
|
|
186
|
-
watchIgnore: watch?.ignore || []
|
|
210
|
+
watchIgnore: watch?.ignore || []
|
|
187
211
|
})
|
|
188
212
|
|
|
189
213
|
fileWatcher.on('update', this.#debouncedRestart)
|
|
@@ -226,7 +250,7 @@ class PlatformaticApp extends EventEmitter {
|
|
|
226
250
|
if (telemetryId) {
|
|
227
251
|
opts.headers = {
|
|
228
252
|
...opts.headers,
|
|
229
|
-
'x-plt-telemetry-id': telemetryId
|
|
253
|
+
'x-plt-telemetry-id': telemetryId
|
|
230
254
|
}
|
|
231
255
|
}
|
|
232
256
|
return dispatch(opts, handler)
|
|
@@ -6,6 +6,7 @@ const defaultStackable = {
|
|
|
6
6
|
throw new Error('Stackable start not implemented')
|
|
7
7
|
},
|
|
8
8
|
stop: () => {},
|
|
9
|
+
build: () => {},
|
|
9
10
|
getUrl: () => null,
|
|
10
11
|
updateContext: () => {},
|
|
11
12
|
getConfig: () => null,
|
|
@@ -14,7 +15,10 @@ const defaultStackable = {
|
|
|
14
15
|
getOpenapiSchema: () => null,
|
|
15
16
|
getGraphqlSchema: () => null,
|
|
16
17
|
getMeta: () => ({}),
|
|
17
|
-
|
|
18
|
+
collectMetrics: () => ({
|
|
19
|
+
defaultMetrics: true,
|
|
20
|
+
httpMetrics: true,
|
|
21
|
+
}),
|
|
18
22
|
inject: () => {
|
|
19
23
|
throw new Error('Stackable inject not implemented')
|
|
20
24
|
},
|
package/lib/worker/itc.js
CHANGED
|
@@ -44,6 +44,7 @@ async function sendViaITC (worker, name, message) {
|
|
|
44
44
|
|
|
45
45
|
function setupITC (app, service, dispatcher) {
|
|
46
46
|
const itc = new ITC({
|
|
47
|
+
name: app.appConfig.id + '-worker',
|
|
47
48
|
port: parentPort,
|
|
48
49
|
handlers: {
|
|
49
50
|
async start () {
|
|
@@ -82,6 +83,10 @@ function setupITC (app, service, dispatcher) {
|
|
|
82
83
|
itc.close()
|
|
83
84
|
},
|
|
84
85
|
|
|
86
|
+
async build () {
|
|
87
|
+
return app.stackable.build()
|
|
88
|
+
},
|
|
89
|
+
|
|
85
90
|
getStatus () {
|
|
86
91
|
return app.getStatus()
|
|
87
92
|
},
|
|
@@ -120,8 +125,12 @@ function setupITC (app, service, dispatcher) {
|
|
|
120
125
|
}
|
|
121
126
|
},
|
|
122
127
|
|
|
123
|
-
getMetrics (format) {
|
|
124
|
-
|
|
128
|
+
async getMetrics (format) {
|
|
129
|
+
try {
|
|
130
|
+
return await app.getMetrics({ format })
|
|
131
|
+
} catch (err) {
|
|
132
|
+
throw new errors.FailedToRetrieveMetricsError(service.id, err.message)
|
|
133
|
+
}
|
|
125
134
|
},
|
|
126
135
|
|
|
127
136
|
inject (injectParams) {
|
package/lib/worker/main.js
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const { createRequire } = require('node:module')
|
|
4
4
|
const { join } = require('node:path')
|
|
5
|
-
const { setTimeout: sleep } = require('node:timers/promises')
|
|
6
5
|
const { parentPort, workerData, threadId } = require('node:worker_threads')
|
|
7
6
|
const { pathToFileURL } = require('node:url')
|
|
8
7
|
|
|
9
|
-
const { Unpromise } = require('@watchable/unpromise')
|
|
10
8
|
const pino = require('pino')
|
|
11
9
|
const { fetch, setGlobalDispatcher, Agent } = require('undici')
|
|
12
10
|
const { wire } = require('undici-thread-interceptor')
|
|
@@ -14,7 +12,7 @@ const { wire } = require('undici-thread-interceptor')
|
|
|
14
12
|
const { PlatformaticApp } = require('./app')
|
|
15
13
|
const { setupITC } = require('./itc')
|
|
16
14
|
const loadInterceptors = require('./interceptors')
|
|
17
|
-
const { MessagePortWritable, createPinoWritable } = require('@platformatic/utils')
|
|
15
|
+
const { MessagePortWritable, createPinoWritable, executeWithTimeout, errors } = require('@platformatic/utils')
|
|
18
16
|
const { kId, kITC } = require('./symbols')
|
|
19
17
|
|
|
20
18
|
process.on('uncaughtException', handleUnhandled.bind(null, 'uncaught exception'))
|
|
@@ -25,12 +23,15 @@ globalThis[kId] = threadId
|
|
|
25
23
|
|
|
26
24
|
let app
|
|
27
25
|
const config = workerData.config
|
|
28
|
-
|
|
26
|
+
globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, { logger: createLogger() })
|
|
29
27
|
|
|
30
28
|
function handleUnhandled (type, err) {
|
|
31
|
-
logger.error(
|
|
29
|
+
globalThis.platformatic.logger.error(
|
|
30
|
+
{ err: errors.ensureLoggableError(err) },
|
|
31
|
+
`Service ${workerData.serviceConfig.id} threw an ${type}.`
|
|
32
|
+
)
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
executeWithTimeout(app?.stop(), 1000)
|
|
34
35
|
.catch()
|
|
35
36
|
.finally(() => {
|
|
36
37
|
process.exit(1)
|
|
@@ -73,13 +74,13 @@ async function main () {
|
|
|
73
74
|
|
|
74
75
|
const globalDispatcher = new Agent({
|
|
75
76
|
...config.undici,
|
|
76
|
-
interceptors
|
|
77
|
+
interceptors
|
|
77
78
|
}).compose(composedInterceptors)
|
|
78
79
|
|
|
79
80
|
setGlobalDispatcher(globalDispatcher)
|
|
80
81
|
|
|
81
82
|
// Setup mesh networker
|
|
82
|
-
const threadDispatcher = wire({ port: parentPort, useNetwork: service.useHttp })
|
|
83
|
+
const threadDispatcher = wire({ port: parentPort, useNetwork: service.useHttp, timeout: true })
|
|
83
84
|
|
|
84
85
|
// If the service is an entrypoint and runtime server config is defined, use it.
|
|
85
86
|
let serverConfig = null
|
|
@@ -89,7 +90,7 @@ async function main () {
|
|
|
89
90
|
serverConfig = {
|
|
90
91
|
port: 0,
|
|
91
92
|
hostname: '127.0.0.1',
|
|
92
|
-
keepAliveTimeout: 5000
|
|
93
|
+
keepAliveTimeout: 5000
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
|
|
@@ -97,7 +98,7 @@ async function main () {
|
|
|
97
98
|
if (telemetryConfig) {
|
|
98
99
|
telemetryConfig = {
|
|
99
100
|
...telemetryConfig,
|
|
100
|
-
serviceName: `${telemetryConfig.serviceName}-${service.id}
|
|
101
|
+
serviceName: `${telemetryConfig.serviceName}-${service.id}`
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
|
|
@@ -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.8",
|
|
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/service": "2.0.0-alpha.
|
|
39
|
-
"@platformatic/sql-graphql": "2.0.0-alpha.
|
|
40
|
-
"@platformatic/sql-mapper": "2.0.0-alpha.
|
|
41
|
-
"@platformatic/db": "2.0.0-alpha.
|
|
37
|
+
"@platformatic/composer": "2.0.0-alpha.8",
|
|
38
|
+
"@platformatic/service": "2.0.0-alpha.8",
|
|
39
|
+
"@platformatic/sql-graphql": "2.0.0-alpha.8",
|
|
40
|
+
"@platformatic/sql-mapper": "2.0.0-alpha.8",
|
|
41
|
+
"@platformatic/db": "2.0.0-alpha.8"
|
|
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
|
-
"undici-thread-interceptor": "^0.
|
|
70
|
+
"undici-thread-interceptor": "^0.6.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.8",
|
|
73
|
+
"@platformatic/config": "2.0.0-alpha.8",
|
|
74
|
+
"@platformatic/generators": "2.0.0-alpha.8",
|
|
75
|
+
"@platformatic/itc": "2.0.0-alpha.8",
|
|
76
|
+
"@platformatic/telemetry": "2.0.0-alpha.8",
|
|
77
|
+
"@platformatic/ts-compiler": "2.0.0-alpha.8",
|
|
78
|
+
"@platformatic/utils": "2.0.0-alpha.8"
|
|
77
79
|
},
|
|
78
80
|
"scripts": {
|
|
79
81
|
"test": "npm run lint && borp --concurrency=1 --timeout=180000 && tsd",
|
package/schema.json
CHANGED