@platformatic/runtime 1.28.0 → 1.29.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.
@@ -10,5 +10,9 @@
10
10
  "hostname": "127.0.0.1",
11
11
  "port": 0
12
12
  },
13
- "managementApi": true
13
+ "managementApi": {
14
+ "logs": {
15
+ "maxSize": 15
16
+ }
17
+ }
14
18
  }
@@ -5,4 +5,11 @@ module.exports = async function (app) {
5
5
  app.get('/hello', async () => {
6
6
  return { service: 'service-2' }
7
7
  })
8
+
9
+ app.get('/large-logs', async (req) => {
10
+ const largeLog = 'a'.repeat(5 * 1024 * 1024)
11
+ for (let i = 0; i < 10; i++) {
12
+ app.log.trace(largeLog)
13
+ }
14
+ })
8
15
  }
@@ -16,8 +16,5 @@
16
16
  "plugin.js"
17
17
  ]
18
18
  },
19
- "watch": true,
20
- "metrics": {
21
- "server": "parent"
22
- }
19
+ "watch": true
23
20
  }
@@ -0,0 +1,17 @@
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
+ "metrics": {
14
+ "hostname": "127.0.0.1",
15
+ "port": 9090
16
+ }
17
+ }
@@ -0,0 +1,13 @@
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
+ "watch": true,
10
+ "metrics": {
11
+ "server": "parent"
12
+ }
13
+ }
@@ -0,0 +1,8 @@
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
+ }
@@ -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": ["plugin.js"]
8
+ },
9
+ "metrics": {
10
+ "server": "parent"
11
+ }
12
+ }
@@ -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,9 @@
1
+ PLT_SERVER_HOSTNAME=0.0.0.0
2
+ PORT=3042
3
+ PLT_SERVER_LOGGER_LEVEL=info
4
+ PLT_RIVAL_TYPESCRIPT=true
5
+ PLT_RIVAL_FST_PLUGIN_OAUTH2_NAME=googleOAuth2
6
+ PLT_RIVAL_FST_PLUGIN_OAUTH2_CREDENTIALS_CLIENT_ID=sample_client_id
7
+ PLT_RIVAL_FST_PLUGIN_OAUTH2_CREDENTIALS_CLIENT_SECRET=sample_client_secret
8
+ PLT_RIVAL_FST_PLUGIN_OAUTH2_REDIRECT_PATH=/login/google
9
+ PLT_RIVAL_FST_PLUGIN_OAUTH2_CALLBACK_URI=http://localhost:3000/login/google/callback
@@ -0,0 +1,16 @@
1
+ {
2
+ "scripts": {
3
+ "start": "platformatic start",
4
+ "test": "node --test test/*/*.test.js"
5
+ },
6
+ "devDependencies": {
7
+ "fastify": "^4.26.0"
8
+ },
9
+ "dependencies": {
10
+ "platformatic": "^1.25.0",
11
+ "@fastify/oauth2": "7.8.0"
12
+ },
13
+ "engines": {
14
+ "node": "^18.8.0 || >=20.6.0"
15
+ }
16
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v1.25.0/runtime",
3
+ "entrypoint": "rival",
4
+ "allowCycles": false,
5
+ "hotReload": true,
6
+ "autoload": {
7
+ "path": "services",
8
+ "exclude": [
9
+ "docs"
10
+ ]
11
+ },
12
+ "server": {
13
+ "hostname": "{PLT_SERVER_HOSTNAME}",
14
+ "port": "{PORT}",
15
+ "logger": {
16
+ "level": "{PLT_SERVER_LOGGER_LEVEL}"
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "scripts": {
3
+ "start": "platformatic start",
4
+ "test": "node --test test/**"
5
+ },
6
+ "devDependencies": {
7
+ "fastify": "^4.26.0"
8
+ },
9
+ "dependencies": {
10
+ "platformatic": "^1.25.0",
11
+ "@platformatic/service": "^1.25.0"
12
+ },
13
+ "engines": {
14
+ "node": "^18.8.0 || >=20.6.0"
15
+ }
16
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v1.25.0/service",
3
+ "service": {
4
+ "openapi": true
5
+ },
6
+ "watch": true,
7
+ "plugins": {
8
+ "paths": [
9
+ {
10
+ "path": "./plugins",
11
+ "encapsulate": false
12
+ },
13
+ "./routes"
14
+ ],
15
+ "typescript": "{PLT_RIVAL_TYPESCRIPT}",
16
+ "packages": [
17
+ {
18
+ "name": "@fastify/oauth2",
19
+ "options": {
20
+ "name": "{PLT_RIVAL_FST_PLUGIN_OAUTH2_NAME}",
21
+ "credentials": {
22
+ "client": {
23
+ "id": "{PLT_RIVAL_FST_PLUGIN_OAUTH2_CREDENTIALS_CLIENT_ID}",
24
+ "secret": "{PLT_RIVAL_FST_PLUGIN_OAUTH2_CREDENTIALS_CLIENT_SECRET}"
25
+ }
26
+ },
27
+ "startRedirectPath": "{PLT_RIVAL_FST_PLUGIN_OAUTH2_REDIRECT_PATH}",
28
+ "callbackUri": "{PLT_RIVAL_FST_PLUGIN_OAUTH2_CALLBACK_URI}"
29
+ }
30
+ }
31
+ ]
32
+ }
33
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ module.exports = async function (fastify, opts) {
4
+ fastify.get('/', async function (request, reply) {
5
+ return { hello: 'from root' }
6
+ })
7
+ }
package/lib/api-client.js CHANGED
@@ -29,7 +29,9 @@ class RuntimeApiClient extends EventEmitter {
29
29
  }
30
30
 
31
31
  async start () {
32
- return this.#sendCommand('plt:start-services')
32
+ const address = await this.#sendCommand('plt:start-services')
33
+ this.emit('start', address)
34
+ return address
33
35
  }
34
36
 
35
37
  async close () {
@@ -134,6 +136,7 @@ class RuntimeApiClient extends EventEmitter {
134
136
  (metric) => metric.name === 'nodejs_eventloop_utilization'
135
137
  )
136
138
 
139
+ let p50Value = 0
137
140
  let p90Value = 0
138
141
  let p95Value = 0
139
142
  let p99Value = 0
@@ -142,6 +145,9 @@ class RuntimeApiClient extends EventEmitter {
142
145
  const metricName = entrypointMetricsPrefix + 'http_request_all_summary_seconds'
143
146
  const httpLatencyMetrics = metrics.find((metric) => metric.name === metricName)
144
147
 
148
+ p50Value = httpLatencyMetrics.values.find(
149
+ (value) => value.labels.quantile === 0.5
150
+ ).value || 0
145
151
  p90Value = httpLatencyMetrics.values.find(
146
152
  (value) => value.labels.quantile === 0.9
147
153
  ).value || 0
@@ -152,6 +158,7 @@ class RuntimeApiClient extends EventEmitter {
152
158
  (value) => value.labels.quantile === 0.99
153
159
  ).value || 0
154
160
 
161
+ p50Value = Math.round(p50Value * 1000)
155
162
  p90Value = Math.round(p90Value * 1000)
156
163
  p95Value = Math.round(p95Value * 1000)
157
164
  p99Value = Math.round(p99Value * 1000)
@@ -177,6 +184,7 @@ class RuntimeApiClient extends EventEmitter {
177
184
  oldSpaceSize,
178
185
  entrypoint: {
179
186
  latency: {
187
+ p50: p50Value,
180
188
  p90: p90Value,
181
189
  p95: p95Value,
182
190
  p99: p99Value
@@ -232,6 +240,7 @@ class RuntimeApiClient extends EventEmitter {
232
240
  return once(this.worker, 'exit').then((msg) => {
233
241
  clearInterval(this.#metricsTimeout)
234
242
  this.#exitCode = msg[0]
243
+ this.emit('close')
235
244
  return msg
236
245
  })
237
246
  }
package/lib/api.js CHANGED
@@ -33,7 +33,7 @@ class RuntimeApi {
33
33
  }
34
34
  }
35
35
 
36
- const app = new PlatformaticApp(service, loaderPort, logger, serviceTelemetryConfig, serverConfig)
36
+ const app = new PlatformaticApp(service, loaderPort, logger, serviceTelemetryConfig, serverConfig, !!config.managementApi)
37
37
 
38
38
  this.#services.set(service.id, app)
39
39
  }
@@ -211,7 +211,8 @@ class RuntimeApi {
211
211
  const service = this.#services.get(id)
212
212
 
213
213
  if (!service) {
214
- throw new errors.ServiceNotFoundError(id)
214
+ const listOfServices = this.#getServices().services.map(svc => svc.id).join(', ')
215
+ throw new errors.ServiceNotFoundError(listOfServices)
215
216
  }
216
217
 
217
218
  return service
@@ -221,7 +222,7 @@ class RuntimeApi {
221
222
  const service = this.#getServiceById(id)
222
223
  const status = service.getStatus()
223
224
 
224
- const type = service.config.configType
225
+ const type = service.config?.configType
225
226
  const { entrypoint, dependencies, localUrl } = service.appConfig
226
227
  const serviceDetails = { id, type, status, localUrl, entrypoint, dependencies }
227
228
 
package/lib/app.js CHANGED
@@ -20,8 +20,9 @@ class PlatformaticApp {
20
20
  #telemetryConfig
21
21
  #serverConfig
22
22
  #debouncedRestart
23
+ #hasManagementApi
23
24
 
24
- constructor (appConfig, loaderPort, logger, telemetryConfig, serverConfig) {
25
+ constructor (appConfig, loaderPort, logger, telemetryConfig, serverConfig, hasManagementApi) {
25
26
  this.appConfig = appConfig
26
27
  this.config = null
27
28
  this.#hotReload = false
@@ -31,6 +32,7 @@ class PlatformaticApp {
31
32
  this.#started = false
32
33
  this.#originalWatch = null
33
34
  this.#fileWatcher = null
35
+ this.#hasManagementApi = !!hasManagementApi
34
36
  this.#logger = logger.child({
35
37
  name: this.appConfig.id
36
38
  })
@@ -107,14 +109,17 @@ class PlatformaticApp {
107
109
  })
108
110
  }
109
111
 
110
- configManager.update({
111
- ...configManager.current,
112
- metrics: {
113
- ...configManager.current.metrics,
114
- defaultMetrics: { enabled: this.appConfig.entrypoint },
115
- prefix: snakeCase(this.appConfig.id) + '_'
116
- }
117
- })
112
+ if (this.#hasManagementApi || configManager.current.metrics) {
113
+ configManager.update({
114
+ ...configManager.current,
115
+ metrics: {
116
+ server: 'parent',
117
+ defaultMetrics: { enabled: this.appConfig.entrypoint },
118
+ prefix: snakeCase(this.appConfig.id) + '_',
119
+ ...configManager.current.metrics
120
+ }
121
+ })
122
+ }
118
123
 
119
124
  if (!this.appConfig.entrypoint) {
120
125
  configManager.update({
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
+
2
3
  const ConfigManager = require('@platformatic/config')
3
4
  const { platformaticRuntime } = require('./config')
4
- const { startWithConfig } = require('./start')
5
+ const { buildRuntime } = require('./start')
5
6
  const { buildServer: buildServerService } = require('@platformatic/service')
6
7
  const { loadConfig } = require('./load-config')
7
8
 
@@ -35,7 +36,7 @@ async function buildServerRuntime (options = {}) {
35
36
  }
36
37
  }
37
38
 
38
- return startWithConfig(options.configManager)
39
+ return buildRuntime(options.configManager)
39
40
  }
40
41
 
41
42
  async function buildServer (options) {
package/lib/errors.js CHANGED
@@ -7,7 +7,7 @@ const ERROR_PREFIX = 'PLT_RUNTIME'
7
7
  module.exports = {
8
8
  RuntimeExitedError: createError(`${ERROR_PREFIX}_RUNTIME_EXIT`, 'The runtime exited before the operation completed'),
9
9
  UnknownRuntimeAPICommandError: createError(`${ERROR_PREFIX}_UNKNOWN_RUNTIME_API_COMMAND`, 'Unknown Runtime API command "%s"'),
10
- ServiceNotFoundError: createError(`${ERROR_PREFIX}_SERVICE_NOT_FOUND`, "Service with id '%s' not found"),
10
+ ServiceNotFoundError: createError(`${ERROR_PREFIX}_SERVICE_NOT_FOUND`, 'Service not found. Available services are: %s'),
11
11
  ServiceNotStartedError: createError(`${ERROR_PREFIX}_SERVICE_NOT_STARTED`, "Service with id '%s' is not started"),
12
12
  FailedToRetrieveOpenAPISchemaError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_OPENAPI_SCHEMA`, 'Failed to retrieve OpenAPI schema for service with id "%s": %s'),
13
13
  FailedToRetrieveGraphQLSchemaError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_GRAPHQL_SCHEMA`, 'Failed to retrieve GraphQL schema for service with id "%s": %s'),
@@ -23,5 +23,7 @@ module.exports = {
23
23
  CannotMapSpecifierToAbsolutePathError: createError(`${ERROR_PREFIX}_CANNOT_MAP_SPECIFIER_TO_ABSOLUTE_PATH`, 'Cannot map "%s" to an absolute path'),
24
24
  NodeInspectorFlagsNotSupportedError: createError(`${ERROR_PREFIX}_NODE_INSPECTOR_FLAGS_NOT_SUPPORTED`, 'The Node.js inspector flags are not supported. Please use \'platformatic start --inspect\' instead.'),
25
25
  FailedToUnlinkManagementApiSocket: createError(`${ERROR_PREFIX}_FAILED_TO_UNLINK_MANAGEMENT_API_SOCKET`, 'Failed to unlink management API socket "%s"'),
26
- LogFileNotFound: createError(`${ERROR_PREFIX}_LOG_FILE_NOT_FOUND`, 'Log file with index %s not found', 404)
26
+ LogFileNotFound: createError(`${ERROR_PREFIX}_LOG_FILE_NOT_FOUND`, 'Log file with index %s not found', 404),
27
+ CannotFindGeneratorForTemplateError: createError(`${ERROR_PREFIX}_CANNOT_FIND_GENERATOR_FOR_TEMPLATE`, 'Cannot find a generator for template "%s"'),
28
+ CannotRemoveServiceOnUpdateError: createError(`${ERROR_PREFIX}_CANNOT_REMOVE_SERVICE_ON_UPDATE`, 'Cannot remove service "%s" when updating a Runtime')
27
29
  }
@@ -5,9 +5,15 @@ const { NoEntryPointError, NoServiceNamedError } = require('./errors')
5
5
  const generateName = require('boring-name-generator')
6
6
  const { join } = require('node:path')
7
7
  const { envObjectToString } = require('@platformatic/generators/lib/utils')
8
- const { readFile } = require('node:fs/promises')
8
+ const { readFile, readdir, stat } = require('node:fs/promises')
9
9
  const { ConfigManager } = require('@platformatic/config')
10
10
  const { platformaticRuntime } = require('../config')
11
+ const ServiceGenerator = require('@platformatic/service/lib/generator/service-generator')
12
+ const DBGenerator = require('@platformatic/db/lib/generator/db-generator')
13
+ const ComposerGenerator = require('@platformatic/composer/lib/generator/composer-generator')
14
+ const { CannotFindGeneratorForTemplateError, CannotRemoveServiceOnUpdateError } = require('../errors')
15
+ const { getServiceTemplateFromSchemaUrl } = require('@platformatic/generators/lib/utils')
16
+ const { DotEnvTool } = require('dotenv-tool')
11
17
 
12
18
  class RuntimeGenerator extends BaseGenerator {
13
19
  constructor (opts) {
@@ -200,8 +206,10 @@ class RuntimeGenerator extends BaseGenerator {
200
206
 
201
207
  async writeFiles () {
202
208
  await super.writeFiles()
203
- for (const { service } of this.services) {
204
- await service.writeFiles()
209
+ if (!this.config.isUpdating) {
210
+ for (const { service } of this.services) {
211
+ await service.writeFiles()
212
+ }
205
213
  }
206
214
  }
207
215
 
@@ -286,6 +294,121 @@ class RuntimeGenerator extends BaseGenerator {
286
294
  await service.postInstallActions()
287
295
  }
288
296
  }
297
+
298
+ getGeneratorForTemplate (templateName) {
299
+ switch (templateName) {
300
+ case '@platformatic/service':
301
+ return ServiceGenerator
302
+ case '@platformatic/db':
303
+ return DBGenerator
304
+ case '@platformatic/composer':
305
+ return ComposerGenerator
306
+ default:
307
+ throw new CannotFindGeneratorForTemplateError(templateName)
308
+ }
309
+ }
310
+
311
+ async loadFromDir () {
312
+ const output = {
313
+ services: []
314
+ }
315
+ const runtimePkgConfigFileData = JSON.parse(await readFile(join(this.targetDirectory, 'platformatic.json'), 'utf-8'))
316
+ const servicesPath = join(this.targetDirectory, runtimePkgConfigFileData.autoload.path)
317
+
318
+ // load all services
319
+ const allServices = await readdir(servicesPath)
320
+ for (const s of allServices) {
321
+ // check is a directory
322
+ const currentServicePath = join(servicesPath, s)
323
+ const dirStat = await stat(currentServicePath)
324
+ if (dirStat.isDirectory()) {
325
+ // load the package json file
326
+ const servicePkgJson = JSON.parse(await readFile(join(currentServicePath, 'platformatic.json'), 'utf-8'))
327
+ // get generator for this module
328
+ const template = getServiceTemplateFromSchemaUrl(servicePkgJson.$schema)
329
+ const Generator = this.getGeneratorForTemplate(template)
330
+ const instance = new Generator()
331
+ this.addService(instance, s)
332
+ output.services.push(await instance.loadFromDir(s, this.targetDirectory))
333
+ }
334
+ }
335
+ return output
336
+ }
337
+
338
+ async update (newConfig) {
339
+ let allServicesDependencies = {}
340
+ function getDifference (a, b) {
341
+ return a.filter(element => {
342
+ return !b.includes(element)
343
+ })
344
+ }
345
+ this.config.isUpdating = true
346
+
347
+ // check all services are present with the same template
348
+ const allCurrentServicesNames = this.services.map((s) => s.name)
349
+ const allNewServicesNames = newConfig.services.map((s) => s.name)
350
+ // load dotenv tool
351
+ const envTool = new DotEnvTool({
352
+ path: join(this.targetDirectory, '.env')
353
+ })
354
+
355
+ await envTool.load()
356
+
357
+ const removedServices = getDifference(allCurrentServicesNames, allNewServicesNames)
358
+ if (removedServices.length > 0) {
359
+ throw new CannotRemoveServiceOnUpdateError(removedServices.join(', '))
360
+ }
361
+
362
+ // handle new services
363
+ for (const newService of newConfig.services) {
364
+ // create generator for the service
365
+ const ServiceGenerator = this.getGeneratorForTemplate(newService.template)
366
+ const serviceInstance = new ServiceGenerator()
367
+ const baseConfig = {
368
+ isRuntimeContext: true,
369
+ targetDirectory: join(this.targetDirectory, 'services', newService.name),
370
+ serviceName: newService.name
371
+ }
372
+ if (allCurrentServicesNames.includes(newService.name)) {
373
+ // update existing services env values
374
+ // otherwise, is a new service
375
+ baseConfig.isUpdating = true
376
+ }
377
+ serviceInstance.setConfig(baseConfig)
378
+ for (const plug of newService.plugins) {
379
+ await serviceInstance.addPackage(plug)
380
+ for (const opt of plug.options) {
381
+ const key = `PLT_${serviceInstance.config.envPrefix}_${opt.name}`
382
+ const value = opt.value
383
+ if (envTool.hasKey(key)) {
384
+ envTool.updateKey(key, value)
385
+ } else {
386
+ envTool.addKey(key, value)
387
+ }
388
+ }
389
+ }
390
+ allServicesDependencies = { ...allServicesDependencies, ...serviceInstance.config.dependencies }
391
+ await serviceInstance.prepare()
392
+ await serviceInstance.writeFiles()
393
+ }
394
+
395
+ // update runtime package.json dependencies
396
+ // read current package.json file
397
+ const currrentPackageJson = JSON.parse(await readFile(join(this.targetDirectory, 'package.json'), 'utf-8'))
398
+ currrentPackageJson.dependencies = {
399
+ ...currrentPackageJson.dependencies,
400
+ ...allServicesDependencies
401
+ }
402
+ this.addFile({
403
+ path: '',
404
+ file: 'package.json',
405
+ contents: JSON.stringify(currrentPackageJson)
406
+ })
407
+
408
+ await this.writeFiles()
409
+ // save new env
410
+ await envTool.save()
411
+ }
289
412
  }
290
413
 
291
414
  module.exports = RuntimeGenerator
@@ -4,6 +4,7 @@ const { tmpdir, platform } = require('node:os')
4
4
  const { join } = require('node:path')
5
5
  const { readFile, mkdir, unlink } = require('node:fs/promises')
6
6
  const fastify = require('fastify')
7
+ const ws = require('ws')
7
8
  const errors = require('./errors')
8
9
  const { pipeLiveLogs, getLogFileStream, getLogIndexes } = require('./logs')
9
10
  const platformaticVersion = require('../package.json').version
@@ -115,30 +116,30 @@ async function createManagementApi (configManager, runtimeApiClient) {
115
116
  .send(res.body)
116
117
  })
117
118
 
118
- app.get('/metrics/live', { websocket: true }, async (connection) => {
119
+ app.get('/metrics/live', { websocket: true }, async (socket) => {
119
120
  const cachedMetrics = runtimeApiClient.getCachedMetrics()
120
121
  const serializedMetrics = cachedMetrics
121
122
  .map((metric) => JSON.stringify(metric))
122
123
  .join('\n')
123
- connection.socket.send(serializedMetrics)
124
+ socket.send(serializedMetrics)
124
125
 
125
126
  const eventHandler = (metrics) => {
126
127
  const serializedMetrics = JSON.stringify(metrics)
127
- connection.socket.send(serializedMetrics)
128
+ socket.send(serializedMetrics)
128
129
  }
129
130
 
130
131
  runtimeApiClient.on('metrics', eventHandler)
131
132
 
132
- connection.on('error', () => {
133
+ socket.on('error', () => {
133
134
  runtimeApiClient.off('metrics', eventHandler)
134
135
  })
135
136
 
136
- connection.on('close', () => {
137
+ socket.on('close', () => {
137
138
  runtimeApiClient.off('metrics', eventHandler)
138
139
  })
139
140
  })
140
141
 
141
- app.get('/logs/live', { websocket: true }, async (connection, req) => {
142
+ app.get('/logs/live', { websocket: true }, async (socket, req) => {
142
143
  const startLogIndex = req.query.start ? parseInt(req.query.start) : null
143
144
 
144
145
  if (startLogIndex) {
@@ -148,7 +149,9 @@ async function createManagementApi (configManager, runtimeApiClient) {
148
149
  }
149
150
  }
150
151
 
151
- pipeLiveLogs(connection, req.log, startLogIndex)
152
+ const stream = ws.createWebSocketStream(socket)
153
+
154
+ pipeLiveLogs(stream, req.log, startLogIndex)
152
155
  })
153
156
 
154
157
  app.get('/logs/indexes', async () => {
@@ -0,0 +1,50 @@
1
+ 'use strict'
2
+
3
+ const fastify = require('fastify')
4
+
5
+ async function startPrometheusServer (runtimeApiClient, opts) {
6
+ const host = opts.hostname ?? '0.0.0.0'
7
+ const port = opts.port ?? 9090
8
+ const metricsEndpoint = opts.endpoint ?? '/metrics'
9
+ const auth = opts.auth ?? null
10
+
11
+ const promServer = fastify({ name: 'Prometheus server' })
12
+
13
+ runtimeApiClient.on('close', async () => {
14
+ await promServer.close()
15
+ })
16
+
17
+ let onRequestHook
18
+ if (auth) {
19
+ const { username, password } = auth
20
+
21
+ await promServer.register(require('@fastify/basic-auth'), {
22
+ validate: function (user, pass, req, reply, done) {
23
+ if (username !== user || password !== pass) {
24
+ return reply.code(401).send({ message: 'Unauthorized' })
25
+ }
26
+ return done()
27
+ }
28
+ })
29
+ onRequestHook = promServer.basicAuth
30
+ }
31
+
32
+ promServer.route({
33
+ url: metricsEndpoint,
34
+ method: 'GET',
35
+ logLevel: 'warn',
36
+ onRequest: onRequestHook,
37
+ handler: async (req, reply) => {
38
+ reply.type('text/plain')
39
+ const { metrics } = await runtimeApiClient.getMetrics('text')
40
+ return metrics
41
+ }
42
+ })
43
+
44
+ await promServer.listen({ port, host })
45
+ return promServer
46
+ }
47
+
48
+ module.exports = {
49
+ startPrometheusServer
50
+ }
package/lib/schema.js CHANGED
@@ -152,7 +152,44 @@ const platformaticRuntimeSchema = {
152
152
  { type: 'boolean' },
153
153
  {
154
154
  type: 'object',
155
- properties: {}
155
+ properties: {
156
+ logs: {
157
+ maxSize: {
158
+ type: 'number',
159
+ minimum: 5,
160
+ default: 200
161
+ }
162
+ }
163
+ },
164
+ additionalProperties: false
165
+ }
166
+ ]
167
+ },
168
+ metrics: {
169
+ anyOf: [
170
+ { type: 'boolean' },
171
+ {
172
+ type: 'object',
173
+ properties: {
174
+ port: {
175
+ anyOf: [
176
+ { type: 'integer' },
177
+ { type: 'string' }
178
+ ]
179
+ },
180
+ hostname: { type: 'string' },
181
+ endpoint: { type: 'string' },
182
+ auth: {
183
+ type: 'object',
184
+ properties: {
185
+ username: { type: 'string' },
186
+ password: { type: 'string' }
187
+ },
188
+ additionalProperties: false,
189
+ required: ['username', 'password']
190
+ }
191
+ },
192
+ additionalProperties: false
156
193
  }
157
194
  ]
158
195
  }
package/lib/start.js CHANGED
@@ -11,6 +11,7 @@ const { printConfigValidationErrors } = require('@platformatic/config')
11
11
  const closeWithGrace = require('close-with-grace')
12
12
  const { loadConfig } = require('./load-config')
13
13
  const { startManagementApi } = require('./management-api')
14
+ const { startPrometheusServer } = require('./prom-server.js')
14
15
  const { parseInspectorOptions, wrapConfigInRuntimeConfig } = require('./config')
15
16
  const RuntimeApiClient = require('./api-client.js')
16
17
  const errors = require('./errors')
@@ -24,7 +25,7 @@ const kWorkerExecArgv = [
24
25
  kLoaderFile
25
26
  ]
26
27
 
27
- async function startWithConfig (configManager, env = process.env) {
28
+ async function buildRuntime (configManager, env = process.env) {
28
29
  const config = configManager.current
29
30
 
30
31
  if (inspector.url()) {
@@ -108,7 +109,14 @@ async function startWithConfig (configManager, env = process.env) {
108
109
  runtimeApiClient
109
110
  )
110
111
  runtimeApiClient.managementApi = managementApi
111
- runtimeApiClient.startCollectingMetrics()
112
+ runtimeApiClient.on('start', () => {
113
+ runtimeApiClient.startCollectingMetrics()
114
+ })
115
+ }
116
+ if (config.metrics) {
117
+ runtimeApiClient.on('start', async () => {
118
+ await startPrometheusServer(runtimeApiClient, config.metrics)
119
+ })
112
120
  }
113
121
 
114
122
  return runtimeApiClient
@@ -119,7 +127,7 @@ async function start (args) {
119
127
 
120
128
  if (config.configType === 'runtime') {
121
129
  config.configManager.args = config.args
122
- const app = await startWithConfig(config.configManager)
130
+ const app = await buildRuntime(config.configManager)
123
131
  await app.start()
124
132
  return app
125
133
  }
@@ -134,11 +142,11 @@ async function startCommand (args) {
134
142
 
135
143
  if (config.configType === 'runtime') {
136
144
  config.configManager.args = config.args
137
- runtime = await startWithConfig(config.configManager)
145
+ runtime = await buildRuntime(config.configManager)
138
146
  } else {
139
147
  const wrappedConfig = await wrapConfigInRuntimeConfig(config)
140
148
  wrappedConfig.args = config.args
141
- runtime = await startWithConfig(wrappedConfig)
149
+ runtime = await buildRuntime(wrappedConfig)
142
150
  }
143
151
 
144
152
  return await runtime.start()
@@ -194,4 +202,4 @@ In alternative run "npm create platformatic@latest" to generate a basic plt serv
194
202
  process.exit(1)
195
203
  }
196
204
 
197
- module.exports = { start, startWithConfig, startCommand }
205
+ module.exports = { buildRuntime, start, startCommand }
package/lib/worker.js CHANGED
@@ -61,10 +61,27 @@ function createLogger (config) {
61
61
  multiStream.add({ level: 'trace', stream: portStream })
62
62
  }
63
63
  if (config.managementApi) {
64
+ const logsFileMb = 5
65
+ const logsLimitMb = config.managementApi?.logs?.maxSize || 200
66
+
67
+ let logsLimitCount = Math.ceil(logsLimitMb / logsFileMb) - 1
68
+ if (logsLimitCount < 1) {
69
+ logsLimitCount = 1
70
+ }
71
+
64
72
  const logsPath = join(PLATFORMATIC_TMP_DIR, process.pid.toString(), 'logs')
65
73
  const pinoRoll = pino.transport({
66
74
  target: 'pino-roll',
67
- options: { file: logsPath, mode: 0o600, size: '5m', mkdir: true }
75
+ options: {
76
+ file: logsPath,
77
+ mode: 0o600,
78
+ size: logsFileMb + 'm',
79
+ mkdir: true,
80
+ fsync: true,
81
+ limit: {
82
+ count: logsLimitCount
83
+ }
84
+ }
68
85
  })
69
86
  multiStream.add({ level: 'trace', stream: pinoRoll })
70
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -33,13 +33,13 @@
33
33
  "typescript": "^5.4.2",
34
34
  "undici-oidc-interceptor": "^0.5.0",
35
35
  "why-is-node-running": "^2.2.2",
36
- "ws": "^8.16.0",
37
- "@platformatic/sql-graphql": "1.28.0",
38
- "@platformatic/sql-mapper": "1.28.0"
36
+ "@platformatic/sql-graphql": "1.29.0",
37
+ "@platformatic/sql-mapper": "1.29.0"
39
38
  },
40
39
  "dependencies": {
40
+ "ws": "^8.16.0",
41
41
  "@fastify/error": "^3.4.1",
42
- "@fastify/websocket": "^9.0.0",
42
+ "@fastify/websocket": "^10.0.0",
43
43
  "@hapi/topo": "^6.0.2",
44
44
  "boring-name-generator": "^1.0.3",
45
45
  "change-case-all": "^2.1.0",
@@ -47,6 +47,7 @@
47
47
  "commist": "^3.2.0",
48
48
  "debounce": "^2.0.0",
49
49
  "desm": "^1.3.1",
50
+ "dotenv-tool": "^0.0.2",
50
51
  "es-main": "^1.3.0",
51
52
  "fastest-levenshtein": "^1.0.16",
52
53
  "fastify": "^4.26.2",
@@ -57,17 +58,17 @@
57
58
  "pino": "^8.19.0",
58
59
  "pino-pretty": "^10.3.1",
59
60
  "semgrator": "^0.3.0",
60
- "pino-roll": "1.0.0-rc.1",
61
+ "pino-roll": "^1.0.0",
61
62
  "tail-file-stream": "^0.1.0",
62
63
  "undici": "^6.9.0",
63
64
  "why-is-node-running": "^2.2.2",
64
- "@platformatic/db": "1.28.0",
65
- "@platformatic/config": "1.28.0",
66
- "@platformatic/generators": "1.28.0",
67
- "@platformatic/service": "1.28.0",
68
- "@platformatic/composer": "1.28.0",
69
- "@platformatic/utils": "1.28.0",
70
- "@platformatic/telemetry": "1.28.0"
65
+ "@platformatic/composer": "1.29.0",
66
+ "@platformatic/config": "1.29.0",
67
+ "@platformatic/db": "1.29.0",
68
+ "@platformatic/generators": "1.29.0",
69
+ "@platformatic/telemetry": "1.29.0",
70
+ "@platformatic/service": "1.29.0",
71
+ "@platformatic/utils": "1.29.0"
71
72
  },
72
73
  "standard": {
73
74
  "ignore": [