@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 CHANGED
@@ -5,7 +5,7 @@
5
5
  * and run json-schema-to-typescript to regenerate this file.
6
6
  */
7
7
 
8
- export type HttpsSchemasPlatformaticDevPlatformaticRuntime200Alpha6Json = {
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/1.52.0.json",
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
@@ -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 { Unpromise } = require('@watchable/unpromise')
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
- this.logger?.info(`Stopping service "${id}"...`)
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 Unpromise.race([sendViaITC(service, 'stop'), sleep(10000, 'timeout', { ref: false })])
286
+ await executeWithTimeout(sendViaITC(service, 'stop'), 10000)
280
287
  } catch (error) {
281
- this.logger?.info(`Failed to stop service "${id}". Killing a worker thread.`, error)
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 Unpromise.race([once(service, 'exit'), sleep(10000, 'timeout', { ref: false })])
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.find(metric => metric.name === metricName)
580
-
581
- p50Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.5).value || 0
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
585
-
586
- p50Value = Math.round(p50Value * 1000)
587
- p90Value = Math.round(p90Value * 1000)
588
- p95Value = Math.round(p95Value * 1000)
589
- p99Value = Math.round(p99Value * 1000)
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 { schemas: { server } } = require('@platformatic/utils')
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
- type: 'array',
99
- items: {
100
- $ref: '#/$defs/undiciInterceptor',
99
+ anyOf: [
100
+ {
101
+ type: 'array',
102
+ items: {
103
+ $ref: '#/$defs/undiciInterceptor'
104
+ }
101
105
  },
102
- }, {
103
- type: 'object',
104
- properties: {
105
- Client: {
106
- type: 'array',
107
- items: {
108
- $ref: '#/$defs/undiciInterceptor',
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
- Agent: {
118
- type: 'array',
119
- items: {
120
- $ref: '#/$defs/undiciInterceptor',
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({}, args)
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: false,
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
- getMetrics: () => null,
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
- return app.stackable.getMetrics({ format })
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) {
@@ -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
- const logger = createLogger()
26
+ globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, { logger: createLogger() })
29
27
 
30
28
  function handleUnhandled (type, err) {
31
- logger.error({ err }, `application ${type}`)
29
+ globalThis.platformatic.logger.error(
30
+ { err: errors.ensureLoggableError(err) },
31
+ `Service ${workerData.serviceConfig.id} threw an ${type}.`
32
+ )
32
33
 
33
- Unpromise.race([app?.stop(), sleep(1000, 'timeout', { ref: false })])
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.6",
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.6",
38
- "@platformatic/service": "2.0.0-alpha.6",
39
- "@platformatic/sql-graphql": "2.0.0-alpha.6",
40
- "@platformatic/sql-mapper": "2.0.0-alpha.6",
41
- "@platformatic/db": "2.0.0-alpha.6"
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.5.1",
70
+ "undici-thread-interceptor": "^0.6.1",
69
71
  "ws": "^8.16.0",
70
- "@platformatic/basic": "2.0.0-alpha.6",
71
- "@platformatic/generators": "2.0.0-alpha.6",
72
- "@platformatic/config": "2.0.0-alpha.6",
73
- "@platformatic/utils": "2.0.0-alpha.6",
74
- "@platformatic/itc": "2.0.0-alpha.6",
75
- "@platformatic/ts-compiler": "2.0.0-alpha.6",
76
- "@platformatic/telemetry": "2.0.0-alpha.6"
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.0.0-alpha.6.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.0.0-alpha.8.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "type": "object",
5
5
  "properties": {