@platformatic/runtime 1.28.1 → 1.30.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.
@@ -3,6 +3,7 @@
3
3
  "entrypoint": "serviceApp",
4
4
  "allowCycles": true,
5
5
  "hotReload": false,
6
+ "managementApi": false,
6
7
  "autoload": {
7
8
  "path": "../monorepo",
8
9
  "exclude": [
@@ -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(100)
11
+ for (let i = 0; i < 500000; 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
@@ -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
@@ -109,13 +109,14 @@ class PlatformaticApp {
109
109
  })
110
110
  }
111
111
 
112
- if (this.#hasManagementApi) {
112
+ if (this.#hasManagementApi || configManager.current.metrics) {
113
113
  configManager.update({
114
114
  ...configManager.current,
115
115
  metrics: {
116
- ...configManager.current.metrics,
116
+ server: 'parent',
117
117
  defaultMetrics: { enabled: this.appConfig.entrypoint },
118
- prefix: snakeCase(this.appConfig.id) + '_'
118
+ prefix: snakeCase(this.appConfig.id) + '_',
119
+ ...configManager.current.metrics
119
120
  }
120
121
  })
121
122
  }
@@ -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,16 @@ 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')
17
+ const { getArrayDifference } = require('../utils')
11
18
 
12
19
  class RuntimeGenerator extends BaseGenerator {
13
20
  constructor (opts) {
@@ -85,7 +92,8 @@ class RuntimeGenerator extends BaseGenerator {
85
92
  this.addEnvVars({
86
93
  PLT_SERVER_HOSTNAME: '0.0.0.0',
87
94
  PORT: this.config.port || 3042,
88
- PLT_SERVER_LOGGER_LEVEL: this.config.logLevel || 'info'
95
+ PLT_SERVER_LOGGER_LEVEL: this.config.logLevel || 'info',
96
+ PLT_MANAGEMENT_API: true
89
97
  }, { overwrite: false })
90
98
  }
91
99
 
@@ -159,7 +167,8 @@ class RuntimeGenerator extends BaseGenerator {
159
167
  logger: {
160
168
  level: '{PLT_SERVER_LOGGER_LEVEL}'
161
169
  }
162
- }
170
+ },
171
+ managementApi: '{PLT_MANAGEMENT_API}'
163
172
  }
164
173
 
165
174
  return config
@@ -200,8 +209,10 @@ class RuntimeGenerator extends BaseGenerator {
200
209
 
201
210
  async writeFiles () {
202
211
  await super.writeFiles()
203
- for (const { service } of this.services) {
204
- await service.writeFiles()
212
+ if (!this.config.isUpdating) {
213
+ for (const { service } of this.services) {
214
+ await service.writeFiles()
215
+ }
205
216
  }
206
217
  }
207
218
 
@@ -286,6 +297,134 @@ class RuntimeGenerator extends BaseGenerator {
286
297
  await service.postInstallActions()
287
298
  }
288
299
  }
300
+
301
+ getGeneratorForTemplate (templateName) {
302
+ switch (templateName) {
303
+ case '@platformatic/service':
304
+ return ServiceGenerator
305
+ case '@platformatic/db':
306
+ return DBGenerator
307
+ case '@platformatic/composer':
308
+ return ComposerGenerator
309
+ default:
310
+ throw new CannotFindGeneratorForTemplateError(templateName)
311
+ }
312
+ }
313
+
314
+ async loadFromDir () {
315
+ const output = {
316
+ services: []
317
+ }
318
+ const runtimePkgConfigFileData = JSON.parse(await readFile(join(this.targetDirectory, 'platformatic.json'), 'utf-8'))
319
+ const servicesPath = join(this.targetDirectory, runtimePkgConfigFileData.autoload.path)
320
+
321
+ // load all services
322
+ const allServices = await readdir(servicesPath)
323
+ for (const s of allServices) {
324
+ // check is a directory
325
+ const currentServicePath = join(servicesPath, s)
326
+ const dirStat = await stat(currentServicePath)
327
+ if (dirStat.isDirectory()) {
328
+ // load the package json file
329
+ const servicePkgJson = JSON.parse(await readFile(join(currentServicePath, 'platformatic.json'), 'utf-8'))
330
+ // get generator for this module
331
+ const template = getServiceTemplateFromSchemaUrl(servicePkgJson.$schema)
332
+ const Generator = this.getGeneratorForTemplate(template)
333
+ const instance = new Generator()
334
+ this.addService(instance, s)
335
+ output.services.push(await instance.loadFromDir(s, this.targetDirectory))
336
+ }
337
+ }
338
+ return output
339
+ }
340
+
341
+ async update (newConfig) {
342
+ let allServicesDependencies = {}
343
+ const runtimeAddedEnvKeys = []
344
+
345
+ this.config.isUpdating = true
346
+ const currrentPackageJson = JSON.parse(await readFile(join(this.targetDirectory, 'package.json'), 'utf-8'))
347
+ const currentRuntimeDependencies = currrentPackageJson.dependencies
348
+ // check all services are present with the same template
349
+ const allCurrentServicesNames = this.services.map((s) => s.name)
350
+ const allNewServicesNames = newConfig.services.map((s) => s.name)
351
+ // load dotenv tool
352
+ const envTool = new DotEnvTool({
353
+ path: join(this.targetDirectory, '.env')
354
+ })
355
+
356
+ await envTool.load()
357
+
358
+ const removedServices = getArrayDifference(allCurrentServicesNames, allNewServicesNames)
359
+ if (removedServices.length > 0) {
360
+ throw new CannotRemoveServiceOnUpdateError(removedServices.join(', '))
361
+ }
362
+
363
+ // handle new services
364
+ for (const newService of newConfig.services) {
365
+ // create generator for the service
366
+ const ServiceGenerator = this.getGeneratorForTemplate(newService.template)
367
+ const serviceInstance = new ServiceGenerator()
368
+ const baseConfig = {
369
+ isRuntimeContext: true,
370
+ targetDirectory: join(this.targetDirectory, 'services', newService.name),
371
+ serviceName: newService.name
372
+ }
373
+ if (allCurrentServicesNames.includes(newService.name)) {
374
+ // update existing services env values
375
+ // otherwise, is a new service
376
+ baseConfig.isUpdating = true
377
+
378
+ // handle service's plugin differences
379
+ const oldServiceMetadata = await serviceInstance.loadFromDir(newService.name, this.targetDirectory)
380
+ const oldServicePackages = oldServiceMetadata.plugins.map((meta) => meta.name)
381
+ const newServicePackages = newService.plugins.map((meta) => meta.name)
382
+ const pluginsToRemove = getArrayDifference(oldServicePackages, newServicePackages)
383
+ pluginsToRemove.forEach((p) => delete currentRuntimeDependencies[p])
384
+ }
385
+ serviceInstance.setConfig(baseConfig)
386
+
387
+ const serviceEnvPrefix = `PLT_${serviceInstance.config.envPrefix}`
388
+ for (const plug of newService.plugins) {
389
+ await serviceInstance.addPackage(plug)
390
+ for (const opt of plug.options) {
391
+ const key = `${serviceEnvPrefix}_${opt.name}`
392
+ runtimeAddedEnvKeys.push(key)
393
+ const value = opt.value
394
+ if (envTool.hasKey(key)) {
395
+ envTool.updateKey(key, value)
396
+ } else {
397
+ envTool.addKey(key, value)
398
+ }
399
+ }
400
+ }
401
+ allServicesDependencies = { ...allServicesDependencies, ...serviceInstance.config.dependencies }
402
+ await serviceInstance.prepare()
403
+ await serviceInstance.writeFiles()
404
+ // cleanup runtime env removing keys not present anymore in service plugins
405
+ const allKeys = envTool.getKeys()
406
+ allKeys.forEach((k) => {
407
+ if (k.startsWith('PLT_') && !runtimeAddedEnvKeys.includes(k)) {
408
+ envTool.deleteKey(k)
409
+ }
410
+ })
411
+ }
412
+
413
+ // update runtime package.json dependencies
414
+ currrentPackageJson.dependencies = {
415
+ ...currrentPackageJson.dependencies,
416
+ ...allServicesDependencies
417
+ }
418
+ this.addFile({
419
+ path: '',
420
+ file: 'package.json',
421
+ contents: JSON.stringify(currrentPackageJson, null, 2)
422
+ })
423
+
424
+ await this.writeFiles()
425
+ // save new env
426
+ await envTool.save()
427
+ }
289
428
  }
290
429
 
291
430
  module.exports = RuntimeGenerator
package/lib/logs.js CHANGED
@@ -21,7 +21,9 @@ async function getLogFiles () {
21
21
  return runtimeLogFiles
22
22
  }
23
23
 
24
- async function pipeLiveLogs (writableStream, logger, startLogIndex) {
24
+ async function pipeLogsStream (writableStream, logger, startLogIndex, endLogIndex) {
25
+ endLogIndex = endLogIndex || Infinity
26
+
25
27
  const runtimeLogFiles = await getLogFiles()
26
28
  if (runtimeLogFiles.length === 0) {
27
29
  writableStream.end()
@@ -47,6 +49,11 @@ async function pipeLiveLogs (writableStream, logger, startLogIndex) {
47
49
  }).unref()
48
50
 
49
51
  const streamLogFile = () => {
52
+ if (fileIndex > endLogIndex) {
53
+ writableStream.end()
54
+ return
55
+ }
56
+
50
57
  const fileName = 'logs.' + fileIndex
51
58
  const filePath = join(runtimeTmpDir, fileName)
52
59
 
@@ -61,7 +68,7 @@ async function pipeLiveLogs (writableStream, logger, startLogIndex) {
61
68
  }
62
69
 
63
70
  fileStream.on('error', (err) => {
64
- logger.log.error(err, 'Error streaming log file')
71
+ logger.error(err, 'Error streaming log file')
65
72
  fileStream.destroy()
66
73
  watcher.close()
67
74
  writableStream.end()
@@ -72,6 +79,10 @@ async function pipeLiveLogs (writableStream, logger, startLogIndex) {
72
79
  })
73
80
 
74
81
  fileStream.on('eof', () => {
82
+ if (fileIndex >= endLogIndex) {
83
+ writableStream.end()
84
+ return
85
+ }
75
86
  if (latestFileIndex > fileIndex) {
76
87
  streamLogFile(++fileIndex)
77
88
  } else {
@@ -106,7 +117,7 @@ async function getLogFileStream (logFileIndex) {
106
117
  }
107
118
 
108
119
  module.exports = {
109
- pipeLiveLogs,
120
+ pipeLogsStream,
110
121
  getLogFileStream,
111
122
  getLogIndexes
112
123
  }
@@ -4,8 +4,9 @@ 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
- const { pipeLiveLogs, getLogFileStream, getLogIndexes } = require('./logs')
9
+ const { pipeLogsStream, getLogFileStream, getLogIndexes } = require('./logs')
9
10
  const platformaticVersion = require('../package.json').version
10
11
 
11
12
  const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
@@ -115,30 +116,32 @@ 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
- const serializedMetrics = cachedMetrics
121
- .map((metric) => JSON.stringify(metric))
122
- .join('\n')
123
- connection.socket.send(serializedMetrics)
121
+ if (cachedMetrics.length > 0) {
122
+ const serializedMetrics = cachedMetrics
123
+ .map((metric) => JSON.stringify(metric))
124
+ .join('\n')
125
+ socket.send(serializedMetrics + '\n')
126
+ }
124
127
 
125
128
  const eventHandler = (metrics) => {
126
129
  const serializedMetrics = JSON.stringify(metrics)
127
- connection.socket.send(serializedMetrics)
130
+ socket.send(serializedMetrics + '\n')
128
131
  }
129
132
 
130
133
  runtimeApiClient.on('metrics', eventHandler)
131
134
 
132
- connection.on('error', () => {
135
+ socket.on('error', () => {
133
136
  runtimeApiClient.off('metrics', eventHandler)
134
137
  })
135
138
 
136
- connection.on('close', () => {
139
+ socket.on('close', () => {
137
140
  runtimeApiClient.off('metrics', eventHandler)
138
141
  })
139
142
  })
140
143
 
141
- app.get('/logs/live', { websocket: true }, async (connection, req) => {
144
+ app.get('/logs/live', { websocket: true }, async (socket, req) => {
142
145
  const startLogIndex = req.query.start ? parseInt(req.query.start) : null
143
146
 
144
147
  if (startLogIndex) {
@@ -148,7 +151,8 @@ async function createManagementApi (configManager, runtimeApiClient) {
148
151
  }
149
152
  }
150
153
 
151
- pipeLiveLogs(connection, req.log, startLogIndex)
154
+ const stream = ws.createWebSocketStream(socket)
155
+ pipeLogsStream(stream, req.log, startLogIndex)
152
156
  })
153
157
 
154
158
  app.get('/logs/indexes', async () => {
@@ -156,6 +160,15 @@ async function createManagementApi (configManager, runtimeApiClient) {
156
160
  return { indexes: logIndexes }
157
161
  })
158
162
 
163
+ app.get('/logs/all', async (req, reply) => {
164
+ const logIndexes = await getLogIndexes()
165
+ const startLogIndex = logIndexes.at(0)
166
+ const endLogIndex = logIndexes.at(-1)
167
+
168
+ reply.hijack()
169
+ pipeLogsStream(reply.raw, req.log, startLogIndex, endLogIndex)
170
+ })
171
+
159
172
  app.get('/logs/:id', async (req) => {
160
173
  const { id } = req.params
161
174
 
@@ -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
@@ -150,9 +150,48 @@ const platformaticRuntimeSchema = {
150
150
  managementApi: {
151
151
  anyOf: [
152
152
  { type: 'boolean' },
153
+ { type: 'string' },
153
154
  {
154
155
  type: 'object',
155
- properties: {}
156
+ properties: {
157
+ logs: {
158
+ maxSize: {
159
+ type: 'number',
160
+ minimum: 5,
161
+ default: 200
162
+ }
163
+ }
164
+ },
165
+ additionalProperties: false
166
+ }
167
+ ],
168
+ default: true
169
+ },
170
+ metrics: {
171
+ anyOf: [
172
+ { type: 'boolean' },
173
+ {
174
+ type: 'object',
175
+ properties: {
176
+ port: {
177
+ anyOf: [
178
+ { type: 'integer' },
179
+ { type: 'string' }
180
+ ]
181
+ },
182
+ hostname: { type: 'string' },
183
+ endpoint: { type: 'string' },
184
+ auth: {
185
+ type: 'object',
186
+ properties: {
187
+ username: { type: 'string' },
188
+ password: { type: 'string' }
189
+ },
190
+ additionalProperties: false,
191
+ required: ['username', 'password']
192
+ }
193
+ },
194
+ additionalProperties: false
156
195
  }
157
196
  ]
158
197
  }
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/utils.js ADDED
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+ function getArrayDifference (a, b) {
3
+ return a.filter(element => {
4
+ return !b.includes(element)
5
+ })
6
+ }
7
+
8
+ module.exports = {
9
+ getArrayDifference
10
+ }
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.1",
3
+ "version": "1.30.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.1",
38
- "@platformatic/sql-mapper": "1.28.1"
36
+ "@platformatic/sql-mapper": "1.30.0",
37
+ "@platformatic/sql-graphql": "1.30.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.1.1",
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/composer": "1.28.1",
65
- "@platformatic/config": "1.28.1",
66
- "@platformatic/generators": "1.28.1",
67
- "@platformatic/db": "1.28.1",
68
- "@platformatic/service": "1.28.1",
69
- "@platformatic/telemetry": "1.28.1",
70
- "@platformatic/utils": "1.28.1"
65
+ "@platformatic/composer": "1.30.0",
66
+ "@platformatic/db": "1.30.0",
67
+ "@platformatic/config": "1.30.0",
68
+ "@platformatic/generators": "1.30.0",
69
+ "@platformatic/service": "1.30.0",
70
+ "@platformatic/telemetry": "1.30.0",
71
+ "@platformatic/utils": "1.30.0"
71
72
  },
72
73
  "standard": {
73
74
  "ignore": [