@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.
- package/fixtures/configs/monorepo.json +1 -0
- package/fixtures/management-api/platformatic.json +5 -1
- package/fixtures/management-api/services/service-1/plugin.js +7 -0
- package/fixtures/monorepo/serviceAppWithLogger/platformatic.service.json +1 -4
- package/fixtures/prom-server/platformatic.json +17 -0
- package/fixtures/prom-server/services/service-1/platformatic.json +13 -0
- package/fixtures/prom-server/services/service-1/plugin.js +8 -0
- package/fixtures/prom-server/services/service-2/platformatic.json +12 -0
- package/fixtures/prom-server/services/service-2/plugin.js +8 -0
- package/fixtures/sample-runtime/.env +9 -0
- package/fixtures/sample-runtime/package.json +16 -0
- package/fixtures/sample-runtime/platformatic.json +19 -0
- package/fixtures/sample-runtime/services/rival/package.json +16 -0
- package/fixtures/sample-runtime/services/rival/platformatic.json +33 -0
- package/fixtures/sample-runtime/services/rival/routes/root.js +7 -0
- package/lib/api-client.js +10 -1
- package/lib/api.js +3 -2
- package/lib/app.js +4 -3
- package/lib/build-server.js +3 -2
- package/lib/errors.js +4 -2
- package/lib/generator/runtime-generator.js +144 -5
- package/lib/logs.js +14 -3
- package/lib/management-api.js +24 -11
- package/lib/prom-server.js +50 -0
- package/lib/schema.js +40 -1
- package/lib/start.js +14 -6
- package/lib/utils.js +10 -0
- package/lib/worker.js +18 -1
- package/package.json +14 -13
|
@@ -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
|
}
|
|
@@ -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,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
|
+
}
|
package/lib/api-client.js
CHANGED
|
@@ -29,7 +29,9 @@ class RuntimeApiClient extends EventEmitter {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
async start () {
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/lib/build-server.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
|
+
|
|
2
3
|
const ConfigManager = require('@platformatic/config')
|
|
3
4
|
const { platformaticRuntime } = require('./config')
|
|
4
|
-
const {
|
|
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
|
|
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`,
|
|
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
|
-
|
|
204
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
120
|
+
pipeLogsStream,
|
|
110
121
|
getLogFileStream,
|
|
111
122
|
getLogIndexes
|
|
112
123
|
}
|
package/lib/management-api.js
CHANGED
|
@@ -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 {
|
|
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 (
|
|
119
|
+
app.get('/metrics/live', { websocket: true }, async (socket) => {
|
|
119
120
|
const cachedMetrics = runtimeApiClient.getCachedMetrics()
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
130
|
+
socket.send(serializedMetrics + '\n')
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
runtimeApiClient.on('metrics', eventHandler)
|
|
131
134
|
|
|
132
|
-
|
|
135
|
+
socket.on('error', () => {
|
|
133
136
|
runtimeApiClient.off('metrics', eventHandler)
|
|
134
137
|
})
|
|
135
138
|
|
|
136
|
-
|
|
139
|
+
socket.on('close', () => {
|
|
137
140
|
runtimeApiClient.off('metrics', eventHandler)
|
|
138
141
|
})
|
|
139
142
|
})
|
|
140
143
|
|
|
141
|
-
app.get('/logs/live', { websocket: true }, async (
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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 = {
|
|
205
|
+
module.exports = { buildRuntime, start, startCommand }
|
package/lib/utils.js
ADDED
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: {
|
|
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.
|
|
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
|
-
"
|
|
37
|
-
"@platformatic/sql-graphql": "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": "^
|
|
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
|
|
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.
|
|
65
|
-
"@platformatic/
|
|
66
|
-
"@platformatic/
|
|
67
|
-
"@platformatic/
|
|
68
|
-
"@platformatic/service": "1.
|
|
69
|
-
"@platformatic/telemetry": "1.
|
|
70
|
-
"@platformatic/utils": "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": [
|