@platformatic/runtime 1.46.0 → 1.48.0

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.
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.20.0/runtime",
3
+ "entrypoint": "serviceApp",
4
+ "allowCycles": true,
5
+ "hotReload": false,
6
+ "managementApi": false,
7
+ "autoload": {
8
+ "path": "../monorepo",
9
+ "exclude": [
10
+ "docs",
11
+ "composerApp"
12
+ ],
13
+ "mappings": {
14
+ "serviceAppWithLogger": {
15
+ "id": "with-logger",
16
+ "config": "platformatic.service.json"
17
+ },
18
+ "serviceAppWithMultiplePlugins": {
19
+ "id": "multi-plugin-service",
20
+ "config": "platformatic.service.json"
21
+ },
22
+ "dbApp": {
23
+ "id": "db-app",
24
+ "config": "platformatic.db.json"
25
+ }
26
+ }
27
+ },
28
+ "server":{
29
+ "hostname":"127.0.0.1",
30
+ "port":"{PORT}"
31
+ }
32
+ }
33
+
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "test-runtime-package",
3
+ "version": "1.0.42"
4
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v1.22.0/runtime",
3
+ "entrypoint": "service-1",
4
+ "allowCycles": true,
5
+ "hotReload": false,
6
+ "autoload": {
7
+ "path": "./services"
8
+ },
9
+ "server": {
10
+ "hostname": "127.0.0.1",
11
+ "port": 0
12
+ },
13
+ "managementApi": {
14
+ "logs": {
15
+ "maxSize": 15
16
+ }
17
+ },
18
+ "metrics": {
19
+ "labels": {
20
+ "foo": "bar"
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v1.22.0/service",
3
+ "service": {
4
+ "openapi": true
5
+ },
6
+ "plugins": {
7
+ "paths": [
8
+ "plugin.js"
9
+ ]
10
+ },
11
+ "watch": true
12
+ }
@@ -0,0 +1,15 @@
1
+ 'use strict'
2
+
3
+ /** @param {import('fastify').FastifyInstance} app */
4
+ module.exports = async function (app) {
5
+ app.get('/hello', async () => {
6
+ return { service: 'service-2' }
7
+ })
8
+
9
+ app.get('/large-logs', async (req) => {
10
+ const largeLog = 'a'.repeat(100)
11
+ for (let i = 0; i < 500000; i++) {
12
+ app.log.trace(largeLog)
13
+ }
14
+ })
15
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v1.22.0/service",
3
+ "service": {
4
+ "openapi": true
5
+ },
6
+ "plugins": {
7
+ "paths": ["plugin.js"]
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ 'use strict'
2
+
3
+ /** @param {import('fastify').FastifyInstance} app */
4
+ module.exports = async function (app, options) {
5
+ app.get('/hello', async () => {
6
+ return { service: 'service-2' }
7
+ })
8
+ }
@@ -0,0 +1,4 @@
1
+ CREATE TABLE IF NOT EXISTS movies (
2
+ id INTEGER PRIMARY KEY,
3
+ title TEXT NOT NULL
4
+ );
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@myscope/myname",
3
+ "version": "0.0.1",
4
+ "description": "test package.json"
5
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.22.0/db",
3
+ "server": {
4
+ "hostname": "127.0.0.1",
5
+ "port": 0
6
+ },
7
+ "migrations": {
8
+ "dir": "migrations",
9
+ "table": "versions",
10
+ "autoApply": true
11
+ },
12
+ "types": {
13
+ "autogenerate": false
14
+ },
15
+ "db": {
16
+ "connectionString": "sqlite://db.sqlite",
17
+ "graphql": true,
18
+ "ignore": {
19
+ "versions": true
20
+ },
21
+ "events": false
22
+ },
23
+ "plugins": {
24
+ "paths": [
25
+ "plugin.js"
26
+ ]
27
+ },
28
+ "watch": false
29
+ }
@@ -0,0 +1,12 @@
1
+ 'use strict'
2
+
3
+ /** @param {import('fastify').FastifyInstance} app */
4
+ module.exports = async function (app) {
5
+ app.get('/async_crash', async () => {
6
+ setImmediate(() => {
7
+ throw new Error('boom')
8
+ })
9
+
10
+ return 'ok'
11
+ })
12
+ }
package/lib/api-client.js CHANGED
@@ -265,6 +265,7 @@ class RuntimeApiClient extends EventEmitter {
265
265
  metrics = await this.getFormattedMetrics()
266
266
  } catch (error) {
267
267
  if (!(error instanceof errors.RuntimeExitedError)) {
268
+ // TODO(mcollina): use the logger
268
269
  console.error('Error collecting metrics', error)
269
270
  }
270
271
  return
@@ -449,19 +450,22 @@ class RuntimeApiClient extends EventEmitter {
449
450
 
450
451
  async #sendCommand (command, params = {}) {
451
452
  const operationId = randomUUID()
452
-
453
453
  this.worker.postMessage({ operationId, command, params })
454
454
  const [message] = await Promise.race(
455
455
  [once(this, operationId), this.#exitPromise]
456
456
  )
457
457
 
458
458
  if (this.#exitCode !== undefined) {
459
+ if (this.exitCode === 1) {
460
+ throw new errors.AddressInUseError()
461
+ }
459
462
  throw new errors.RuntimeExitedError()
460
463
  }
461
-
462
- const { error, data } = message
464
+ const { error, data, code } = message
463
465
  if (error !== null) {
464
- throw new Error(error)
466
+ const err = new Error(error)
467
+ err.code = code
468
+ throw err
465
469
  }
466
470
 
467
471
  return JSON.parse(data)
package/lib/api.js CHANGED
@@ -6,21 +6,30 @@ const { createFastifyInterceptor } = require('fastify-undici-dispatcher')
6
6
  const { PlatformaticApp } = require('./app')
7
7
  const errors = require('./errors')
8
8
  const { printSchema } = require('graphql')
9
+ const { Bus } = require('@platformatic/bus')
9
10
 
10
11
  class RuntimeApi {
11
12
  #services
12
13
  #dispatcher
13
14
  #interceptor
14
15
  #logger
16
+ #bus
15
17
 
16
18
  constructor (config, logger, loaderPort, composedInterceptors = []) {
17
19
  this.#services = new Map()
18
20
  this.#logger = logger
21
+ this.#setupBus()
19
22
  const telemetryConfig = config.telemetry
23
+ const metricsConfig = config.metrics
20
24
 
21
25
  for (let i = 0; i < config.services.length; ++i) {
22
26
  const service = config.services[i]
23
- const serviceTelemetryConfig = telemetryConfig ? { ...telemetryConfig, serviceName: `${telemetryConfig.serviceName}-${service.id}` } : null
27
+ const serviceTelemetryConfig = telemetryConfig
28
+ ? {
29
+ ...telemetryConfig,
30
+ serviceName: `${telemetryConfig.serviceName}-${service.id}`
31
+ }
32
+ : null
24
33
 
25
34
  // If the service is an entrypoint and runtime server config is defined, use it.
26
35
  let serverConfig = null
@@ -34,7 +43,7 @@ class RuntimeApi {
34
43
  }
35
44
  }
36
45
 
37
- const app = new PlatformaticApp(service, loaderPort, logger, serviceTelemetryConfig, serverConfig, !!config.managementApi)
46
+ const app = new PlatformaticApp(service, loaderPort, logger, serviceTelemetryConfig, serverConfig, !!config.managementApi, metricsConfig)
38
47
 
39
48
  this.#services.set(service.id, app)
40
49
  }
@@ -105,7 +114,7 @@ class RuntimeApi {
105
114
  const res = await this.#runCommandHandler(command, params)
106
115
  return { operationId, error: null, data: JSON.stringify(res || null) }
107
116
  } catch (err) {
108
- return { operationId, error: err.message }
117
+ return { operationId, error: err.message, code: err.code }
109
118
  }
110
119
  }
111
120
 
@@ -155,6 +164,7 @@ class RuntimeApi {
155
164
  const serviceUrl = new URL(service.appConfig.localUrl)
156
165
  this.#interceptor.route(serviceUrl.host, service.server)
157
166
  }
167
+ this.#bus.broadcast('runtime:services:started')
158
168
  return entrypointUrl
159
169
  }
160
170
 
@@ -172,6 +182,7 @@ class RuntimeApi {
172
182
  }
173
183
  }
174
184
  await Promise.all(stopServiceReqs)
185
+ this.#bus.broadcast('runtime:services:stopped')
175
186
  }
176
187
 
177
188
  async #restartServices () {
@@ -191,6 +202,7 @@ class RuntimeApi {
191
202
  const serviceUrl = new URL(service.appConfig.localUrl)
192
203
  this.#interceptor.route(serviceUrl.host, service.server)
193
204
  }
205
+ this.#bus.broadcast('runtime:services:restarted')
194
206
  return entrypointUrl
195
207
  }
196
208
 
@@ -308,30 +320,30 @@ class RuntimeApi {
308
320
  }
309
321
 
310
322
  async #getMetrics ({ format }) {
311
- let entrypoint = null
323
+ let metrics = null
324
+
312
325
  for (const service of this.#services.values()) {
313
- if (service.appConfig.entrypoint) {
314
- entrypoint = service
315
- break
326
+ const serviceId = service.appConfig.id
327
+ const serviceStatus = service.getStatus()
328
+ if (serviceStatus !== 'started') {
329
+ throw new errors.ServiceNotStartedError(serviceId)
316
330
  }
317
- }
318
331
 
319
- const entrypointStatus = entrypoint.getStatus()
320
- if (entrypointStatus !== 'started') {
321
- throw new errors.ServiceNotStartedError(entrypoint.id)
322
- }
332
+ const promRegister = service.server.metrics?.client?.register
323
333
 
324
- const promRegister = entrypoint.server.metrics?.client?.register
325
- if (!promRegister) {
326
- return { metrics: null }
334
+ if (promRegister) {
335
+ if (metrics === null) {
336
+ metrics = format === 'json' ? [] : ''
337
+ }
338
+ if (format === 'json') {
339
+ metrics.push(...await promRegister.getMetricsAsJSON())
340
+ } else {
341
+ const serviceMetrics = await promRegister.metrics()
342
+ metrics += serviceMetrics
343
+ }
344
+ }
327
345
  }
328
346
 
329
- // All runtime services shares the same metrics registry.
330
- // Getting metrics from the entrypoint returns all metrics.
331
- const metrics = format === 'json'
332
- ? await promRegister.getMetricsAsJSON()
333
- : await promRegister.metrics()
334
-
335
347
  return { metrics }
336
348
  }
337
349
 
@@ -344,6 +356,7 @@ class RuntimeApi {
344
356
  } else {
345
357
  await service.start()
346
358
  }
359
+ this.#bus.broadcast('runtime:service:started', id)
347
360
  }
348
361
 
349
362
  async #stopService ({ id }) {
@@ -355,6 +368,7 @@ class RuntimeApi {
355
368
  }
356
369
 
357
370
  await service.stop()
371
+ this.#bus.broadcast('runtime:service:stopped', id)
358
372
  }
359
373
 
360
374
  async #inject ({ id, injectParams }) {
@@ -374,6 +388,10 @@ class RuntimeApi {
374
388
  body: res.body
375
389
  }
376
390
  }
391
+
392
+ #setupBus (config) {
393
+ this.#bus = new Bus('$root')
394
+ }
377
395
  }
378
396
 
379
397
  module.exports = RuntimeApi
package/lib/app.js CHANGED
@@ -21,8 +21,9 @@ class PlatformaticApp extends EventEmitter {
21
21
  #serverConfig
22
22
  #debouncedRestart
23
23
  #hasManagementApi
24
+ #metricsConfig
24
25
 
25
- constructor (appConfig, loaderPort, logger, telemetryConfig, serverConfig, hasManagementApi) {
26
+ constructor (appConfig, loaderPort, logger, telemetryConfig, serverConfig, hasManagementApi, metricsConfig) {
26
27
  super()
27
28
  this.appConfig = appConfig
28
29
  this.config = null
@@ -38,6 +39,7 @@ class PlatformaticApp extends EventEmitter {
38
39
  name: this.appConfig.id
39
40
  })
40
41
  this.#telemetryConfig = telemetryConfig
42
+ this.#metricsConfig = metricsConfig
41
43
  this.#serverConfig = serverConfig
42
44
 
43
45
  /* c8 ignore next 4 */
@@ -111,6 +113,7 @@ class PlatformaticApp extends EventEmitter {
111
113
  this.server = await buildServer({
112
114
  app: this.config.app,
113
115
  ...config,
116
+ id: this.appConfig.id,
114
117
  configManager
115
118
  })
116
119
  } catch (err) {
@@ -133,7 +136,8 @@ class PlatformaticApp extends EventEmitter {
133
136
  /* c8 ignore next 5 */
134
137
  } catch (err) {
135
138
  this.server.log.error({ err })
136
- process.exit(1)
139
+ this.#starting = false
140
+ throw err
137
141
  }
138
142
  } else {
139
143
  // Make sure the server has run all the onReady hooks before returning.
@@ -249,9 +253,17 @@ class PlatformaticApp extends EventEmitter {
249
253
  const { configManager } = this.config
250
254
  configManager.update({
251
255
  ...configManager.current,
252
- telemetry: this.#telemetryConfig
256
+ telemetry: this.#telemetryConfig,
257
+ metrics: this.#metricsConfig
253
258
  })
254
259
 
260
+ if (configManager.current.metrics !== false) {
261
+ configManager.update({
262
+ ...configManager.current,
263
+ metrics: this.#metricsConfig
264
+ })
265
+ }
266
+
255
267
  if (this.#serverConfig) {
256
268
  configManager.update({
257
269
  ...configManager.current,
package/lib/errors.js CHANGED
@@ -5,6 +5,7 @@ const createError = require('@fastify/error')
5
5
  const ERROR_PREFIX = 'PLT_RUNTIME'
6
6
 
7
7
  module.exports = {
8
+ AddressInUseError: createError(`${ERROR_PREFIX}_EADDR_IN_USE`, 'The current port is in use by another application'),
8
9
  RuntimeExitedError: createError(`${ERROR_PREFIX}_RUNTIME_EXIT`, 'The runtime exited before the operation completed'),
9
10
  UnknownRuntimeAPICommandError: createError(`${ERROR_PREFIX}_UNKNOWN_RUNTIME_API_COMMAND`, 'Unknown Runtime API command "%s"'),
10
11
  ServiceNotFoundError: createError(`${ERROR_PREFIX}_SERVICE_NOT_FOUND`, 'Service not found. Available services are: %s'),
package/lib/schema.js CHANGED
@@ -170,6 +170,10 @@ const platformaticRuntimeSchema = {
170
170
  },
171
171
  additionalProperties: false,
172
172
  required: ['username', 'password']
173
+ },
174
+ labels: {
175
+ type: 'object',
176
+ additionalProperties: { type: 'string' }
173
177
  }
174
178
  },
175
179
  additionalProperties: false
package/lib/start.js CHANGED
@@ -16,6 +16,8 @@ const { parseInspectorOptions, wrapConfigInRuntimeConfig } = require('./config')
16
16
  const { RuntimeApiClient, getRuntimeLogsDir } = require('./api-client.js')
17
17
  const errors = require('./errors')
18
18
  const pkg = require('../package.json')
19
+ const pino = require('pino')
20
+ const pretty = require('pino-pretty')
19
21
 
20
22
  const kLoaderFile = pathToFileURL(join(__dirname, 'loader.mjs')).href
21
23
  const kWorkerFile = join(__dirname, 'worker.js')
@@ -138,21 +140,58 @@ async function start (args) {
138
140
  return serviceStart(config.app, args)
139
141
  }
140
142
 
143
+ async function setupAndStartRuntime (config) {
144
+ const MAX_PORT = 65535
145
+ let runtimeConfig
146
+
147
+ if (config.configType === 'runtime') {
148
+ config.configManager.args = config.args
149
+ runtimeConfig = config.configManager
150
+ } else {
151
+ const wrappedConfig = await wrapConfigInRuntimeConfig(config)
152
+ wrappedConfig.args = config.args
153
+ runtimeConfig = wrappedConfig
154
+ }
155
+
156
+ let runtime = await buildRuntime(runtimeConfig)
157
+
158
+ let address = null
159
+ let startErr = null
160
+ const originalPort = runtimeConfig.current.server?.port || 0
161
+ while (address === null) {
162
+ try {
163
+ address = await runtime.start()
164
+ } catch (err) {
165
+ startErr = err
166
+ if (err.code === 'EADDRINUSE') {
167
+ await runtime.close()
168
+ if (runtimeConfig.current.server.port > MAX_PORT) throw err
169
+ runtimeConfig.current.server.port++
170
+ runtime = await buildRuntime(runtimeConfig)
171
+ } else {
172
+ throw err
173
+ }
174
+ }
175
+ }
176
+ if (startErr?.code === 'PLT_RUNTIME_EADDR_IN_USE') {
177
+ const logger = pino(pretty({
178
+ translateTime: 'SYS:HH:MM:ss',
179
+ ignore: 'hostname,pid'
180
+ }))
181
+ logger.warn(`Port: ${originalPort} is already in use!`)
182
+ logger.warn(`Starting service on port: ${runtimeConfig.current.server.port}`)
183
+ }
184
+ return { address, runtime }
185
+ }
186
+
141
187
  async function startCommand (args) {
142
188
  try {
143
189
  const config = await loadConfig({}, args)
144
- let runtime
145
-
146
- if (config.configType === 'runtime') {
147
- config.configManager.args = config.args
148
- runtime = await buildRuntime(config.configManager)
149
- } else {
150
- const wrappedConfig = await wrapConfigInRuntimeConfig(config)
151
- wrappedConfig.args = config.args
152
- runtime = await buildRuntime(wrappedConfig)
153
- }
154
190
 
155
- const res = await runtime.start()
191
+ const startResult = await setupAndStartRuntime(config)
192
+
193
+ const runtime = startResult.runtime
194
+ const res = startResult.address
156
195
 
157
196
  closeWithGrace(async (event) => {
158
197
  if (event.err instanceof Error) {
@@ -206,4 +245,4 @@ async function startCommand (args) {
206
245
  }
207
246
  }
208
247
 
209
- module.exports = { buildRuntime, start, startCommand }
248
+ module.exports = { buildRuntime, start, startCommand, setupAndStartRuntime }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "1.46.0",
3
+ "version": "1.48.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "@fastify/formbody": "^7.4.0",
22
22
  "@matteo.collina/tspl": "^0.1.1",
23
23
  "borp": "^0.15.0",
24
- "c8": "^9.1.0",
24
+ "c8": "^10.0.0",
25
25
  "execa": "^8.0.1",
26
26
  "express": "^4.18.3",
27
27
  "fast-jwt": "^4.0.0",
@@ -34,8 +34,8 @@
34
34
  "typescript": "^5.4.2",
35
35
  "undici-oidc-interceptor": "^0.5.0",
36
36
  "why-is-node-running": "^2.2.2",
37
- "@platformatic/sql-mapper": "1.46.0",
38
- "@platformatic/sql-graphql": "1.46.0"
37
+ "@platformatic/sql-mapper": "1.48.0",
38
+ "@platformatic/sql-graphql": "1.48.0"
39
39
  },
40
40
  "dependencies": {
41
41
  "@fastify/error": "^3.4.1",
@@ -63,13 +63,14 @@
63
63
  "undici": "^6.9.0",
64
64
  "why-is-node-running": "^2.2.2",
65
65
  "ws": "^8.16.0",
66
- "@platformatic/composer": "1.46.0",
67
- "@platformatic/config": "1.46.0",
68
- "@platformatic/service": "1.46.0",
69
- "@platformatic/telemetry": "1.46.0",
70
- "@platformatic/db": "1.46.0",
71
- "@platformatic/generators": "1.46.0",
72
- "@platformatic/utils": "1.46.0"
66
+ "@platformatic/bus": "1.48.0",
67
+ "@platformatic/composer": "1.48.0",
68
+ "@platformatic/db": "1.48.0",
69
+ "@platformatic/config": "1.48.0",
70
+ "@platformatic/generators": "1.48.0",
71
+ "@platformatic/service": "1.48.0",
72
+ "@platformatic/telemetry": "1.48.0",
73
+ "@platformatic/utils": "1.48.0"
73
74
  },
74
75
  "standard": {
75
76
  "ignore": [