@platformatic/runtime 2.0.0-alpha.3 → 2.0.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.d.ts +1 -5
- package/fixtures/crash-on-bootstrap/platformatic.runtime.json +15 -0
- package/fixtures/crash-on-bootstrap/services/service-1/platformatic.service.json +14 -0
- package/fixtures/crash-on-bootstrap/services/service-1/plugin.js +5 -0
- package/fixtures/crash-on-bootstrap/services/service-2/platformatic.service.json +14 -0
- package/fixtures/crash-on-bootstrap/services/service-2/plugin.js +5 -0
- package/fixtures/telemetry/services/echo/routes/span.js +16 -2
- package/fixtures/telemetry/services/service-1/platformatic.service.json +19 -0
- package/fixtures/telemetry/services/service-1/routes/echo.js +7 -0
- package/lib/errors.js +1 -0
- package/lib/generator/runtime-generator.js +2 -2
- package/lib/management-api.js +14 -0
- package/lib/runtime.js +116 -43
- package/lib/schema.js +7 -1
- package/lib/worker/app.js +28 -10
- package/lib/worker/default-stackable.js +1 -0
- package/lib/worker/itc.js +8 -0
- package/lib/worker/main.js +12 -5
- package/package.json +19 -17
- package/runtime.mjs +1 -1
- package/schema.json +17 -4
- package/lib/streams/message-port-writable.js +0 -44
- package/lib/streams/pino-writable.js +0 -30
package/config.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and run json-schema-to-typescript to regenerate this file.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export type
|
|
8
|
+
export type HttpsSchemasPlatformaticDevPlatformaticRuntime200Alpha5Json = {
|
|
9
9
|
[k: string]: unknown;
|
|
10
10
|
} & {
|
|
11
11
|
$schema?: string;
|
|
@@ -201,10 +201,6 @@ export type HttpsSchemasPlatformaticDevPlatformaticRuntime200Alpha2Json = {
|
|
|
201
201
|
};
|
|
202
202
|
restartOnError?: boolean | number;
|
|
203
203
|
services?: {
|
|
204
|
-
id: string;
|
|
205
|
-
path: string;
|
|
206
|
-
config?: string;
|
|
207
|
-
useHttp?: boolean;
|
|
208
204
|
[k: string]: unknown;
|
|
209
205
|
}[];
|
|
210
206
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://schemas.platformatic.dev/@platformatic/runtime/1.52.0.json",
|
|
3
|
+
"entrypoint": "service-1",
|
|
4
|
+
"autoload": {
|
|
5
|
+
"path": "./services"
|
|
6
|
+
},
|
|
7
|
+
"restartOnError": 100,
|
|
8
|
+
"server": {
|
|
9
|
+
"hostname": "127.0.0.1",
|
|
10
|
+
"port": "0",
|
|
11
|
+
"logger": {
|
|
12
|
+
"level": "info"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
'use strict'
|
|
2
|
+
|
|
3
|
+
const { request } = require('undici')
|
|
4
|
+
|
|
2
5
|
module.exports = async function (fastify, opts) {
|
|
3
6
|
// This returns the traceId set on the span by the service
|
|
4
|
-
fastify.get('/', async (
|
|
5
|
-
const traceId =
|
|
7
|
+
fastify.get('/', async (req, reply) => {
|
|
8
|
+
const traceId = req.span.spanContext().traceId
|
|
6
9
|
return { traceId }
|
|
7
10
|
})
|
|
11
|
+
|
|
12
|
+
fastify.get('/service-1/echo-headers', async (req, reply) => {
|
|
13
|
+
const res = await request('http://service-1.plt.local/echo-headers', {
|
|
14
|
+
method: 'GET',
|
|
15
|
+
headers: {
|
|
16
|
+
'content-type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
const body = await res.body.json()
|
|
20
|
+
return body
|
|
21
|
+
})
|
|
8
22
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://schemas.platformatic.dev/@platformatic/service/1.52.0.json",
|
|
3
|
+
"server": {
|
|
4
|
+
"hostname": "127.0.0.1",
|
|
5
|
+
"port": "0",
|
|
6
|
+
"logger": {
|
|
7
|
+
"level": "info"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"service": {
|
|
11
|
+
"openapi": true
|
|
12
|
+
},
|
|
13
|
+
"plugins": {
|
|
14
|
+
"paths": [
|
|
15
|
+
"./routes"
|
|
16
|
+
],
|
|
17
|
+
"typescript": false
|
|
18
|
+
}
|
|
19
|
+
}
|
package/lib/errors.js
CHANGED
|
@@ -13,6 +13,7 @@ module.exports = {
|
|
|
13
13
|
ServiceNotStartedError: createError(`${ERROR_PREFIX}_SERVICE_NOT_STARTED`, "Service with id '%s' is not started"),
|
|
14
14
|
FailedToRetrieveOpenAPISchemaError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_OPENAPI_SCHEMA`, 'Failed to retrieve OpenAPI schema for service with id "%s": %s'),
|
|
15
15
|
FailedToRetrieveGraphQLSchemaError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_GRAPHQL_SCHEMA`, 'Failed to retrieve GraphQL schema for service with id "%s": %s'),
|
|
16
|
+
FailedToRetrieveMetaError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_META`, 'Failed to retrieve metadata for service with id "%s": %s'),
|
|
16
17
|
ApplicationAlreadyStartedError: createError(`${ERROR_PREFIX}_APPLICATION_ALREADY_STARTED`, 'Application is already started'),
|
|
17
18
|
ApplicationNotStartedError: createError(`${ERROR_PREFIX}_APPLICATION_NOT_STARTED`, 'Application has not been started'),
|
|
18
19
|
ConfigPathMustBeStringError: createError(`${ERROR_PREFIX}_CONFIG_PATH_MUST_BE_STRING`, 'Config path must be a string'),
|
|
@@ -202,7 +202,7 @@ class RuntimeGenerator extends BaseGenerator {
|
|
|
202
202
|
})
|
|
203
203
|
|
|
204
204
|
if (!this.existingConfig) {
|
|
205
|
-
this.addFile({ path: '', file: 'README.md', contents: await readFile(join(__dirname, 'README.md')) })
|
|
205
|
+
this.addFile({ path: '', file: 'README.md', contents: await readFile(join(__dirname, 'README.md'), 'utf-8') })
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
return {
|
|
@@ -375,7 +375,7 @@ class RuntimeGenerator extends BaseGenerator {
|
|
|
375
375
|
|
|
376
376
|
// delete dependencies
|
|
377
377
|
const servicePackageJson = JSON.parse(
|
|
378
|
-
await readFile(join(this.targetDirectory, 'services', s.name, 'platformatic.json'))
|
|
378
|
+
await readFile(join(this.targetDirectory, 'services', s.name, 'platformatic.json'), 'utf-8')
|
|
379
379
|
)
|
|
380
380
|
if (servicePackageJson.plugins && servicePackageJson.plugins.packages) {
|
|
381
381
|
servicePackageJson.plugins.packages.forEach(p => {
|
package/lib/management-api.js
CHANGED
|
@@ -72,6 +72,20 @@ async function managementApiPlugin (app, opts) {
|
|
|
72
72
|
return runtime.getServiceGraphqlSchema(id)
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
+
app.get('/services/:id/connection-strings', async request => {
|
|
76
|
+
const { id } = request.params
|
|
77
|
+
try {
|
|
78
|
+
app.log.debug('get connection strings', { id })
|
|
79
|
+
const meta = await runtime.getServiceMeta(id)
|
|
80
|
+
if (meta.db) {
|
|
81
|
+
return meta.db
|
|
82
|
+
}
|
|
83
|
+
return null
|
|
84
|
+
} catch (err) {
|
|
85
|
+
throw new errors.FailedToRetrieveMetaError(id, err.message)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
75
89
|
app.post('/services/:id/start', async request => {
|
|
76
90
|
const { id } = request.params
|
|
77
91
|
app.log.debug('start service', { id })
|
package/lib/runtime.js
CHANGED
|
@@ -28,6 +28,8 @@ const MAX_LISTENERS_COUNT = 100
|
|
|
28
28
|
const MAX_METRICS_QUEUE_LENGTH = 5 * 60 // 5 minutes in seconds
|
|
29
29
|
const COLLECT_METRICS_TIMEOUT = 1000
|
|
30
30
|
|
|
31
|
+
const MAX_BOOTSTRAP_ATTEMPTS = 5
|
|
32
|
+
|
|
31
33
|
class Runtime extends EventEmitter {
|
|
32
34
|
#configManager
|
|
33
35
|
#runtimeTmpDir
|
|
@@ -46,7 +48,8 @@ class Runtime extends EventEmitter {
|
|
|
46
48
|
#managementApi
|
|
47
49
|
#prometheusServer
|
|
48
50
|
#startedServices
|
|
49
|
-
#
|
|
51
|
+
#restartPromises
|
|
52
|
+
#bootstrapAttempts
|
|
50
53
|
|
|
51
54
|
constructor (configManager, runtimeLogsDir, env) {
|
|
52
55
|
super()
|
|
@@ -63,7 +66,8 @@ class Runtime extends EventEmitter {
|
|
|
63
66
|
this.#interceptor = createThreadInterceptor({ domain: '.plt.local' })
|
|
64
67
|
this.#status = undefined
|
|
65
68
|
this.#startedServices = new Map()
|
|
66
|
-
this.#
|
|
69
|
+
this.#restartPromises = new Map()
|
|
70
|
+
this.#bootstrapAttempts = new Map()
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
async init () {
|
|
@@ -126,8 +130,15 @@ class Runtime extends EventEmitter {
|
|
|
126
130
|
this.#updateStatus('starting')
|
|
127
131
|
|
|
128
132
|
// Important: do not use Promise.all here since it won't properly manage dependencies
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
try {
|
|
134
|
+
for (const service of this.#servicesIds) {
|
|
135
|
+
await this.startService(service)
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// Wait for the next tick so that the error is logged first
|
|
139
|
+
await sleep(1)
|
|
140
|
+
await this.close()
|
|
141
|
+
throw error
|
|
131
142
|
}
|
|
132
143
|
|
|
133
144
|
this.#updateStatus('started')
|
|
@@ -145,13 +156,7 @@ class Runtime extends EventEmitter {
|
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
this.#updateStatus('stopping')
|
|
148
|
-
|
|
149
|
-
for (const timer of this.#crashedServicesTimers.values()) {
|
|
150
|
-
clearTimeout(timer)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
159
|
this.#startedServices.clear()
|
|
154
|
-
this.#crashedServicesTimers.clear()
|
|
155
160
|
|
|
156
161
|
await Promise.all(this.#servicesIds.map(service => this._stopService(service)))
|
|
157
162
|
|
|
@@ -209,12 +214,6 @@ class Runtime extends EventEmitter {
|
|
|
209
214
|
// This is set here so that if the service fails while starting we track the status
|
|
210
215
|
this.#startedServices.set(id, true)
|
|
211
216
|
|
|
212
|
-
// Make sure we don't restart twice
|
|
213
|
-
const crashedTimer = this.#crashedServicesTimers.get(id)
|
|
214
|
-
if (crashedTimer) {
|
|
215
|
-
clearTimeout(crashedTimer)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
217
|
let service = await this.#getServiceById(id, false, false)
|
|
219
218
|
|
|
220
219
|
// The service was stopped, recreate the thread
|
|
@@ -226,10 +225,39 @@ class Runtime extends EventEmitter {
|
|
|
226
225
|
service = await this.#getServiceById(id)
|
|
227
226
|
}
|
|
228
227
|
|
|
229
|
-
|
|
228
|
+
try {
|
|
229
|
+
const serviceUrl = await sendViaITC(service, 'start')
|
|
230
|
+
if (serviceUrl) {
|
|
231
|
+
this.#url = serviceUrl
|
|
232
|
+
}
|
|
233
|
+
this.#bootstrapAttempts.set(id, 0)
|
|
234
|
+
} catch (error) {
|
|
235
|
+
// TODO: handle port allocation error here
|
|
236
|
+
if (error.code === 'EADDRINUSE') throw error
|
|
237
|
+
|
|
238
|
+
this.logger.error({ error }, `Failed to start service "${id}".`)
|
|
239
|
+
|
|
240
|
+
const config = this.#configManager.current
|
|
241
|
+
const restartOnError = config.restartOnError
|
|
242
|
+
|
|
243
|
+
if (!restartOnError) {
|
|
244
|
+
this.logger.error(`Failed to start service "${id}".`)
|
|
245
|
+
throw error
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let bootstrapAttempt = this.#bootstrapAttempts.get(id)
|
|
249
|
+
if (bootstrapAttempt++ >= MAX_BOOTSTRAP_ATTEMPTS || restartOnError === 0) {
|
|
250
|
+
this.logger.error(`Failed to start service "${id}" after ${MAX_BOOTSTRAP_ATTEMPTS} attempts.`)
|
|
251
|
+
throw error
|
|
252
|
+
}
|
|
230
253
|
|
|
231
|
-
|
|
232
|
-
|
|
254
|
+
this.logger.warn(
|
|
255
|
+
`Starting a service "${id}" in ${restartOnError}ms. ` +
|
|
256
|
+
`Attempt ${bootstrapAttempt} of ${MAX_BOOTSTRAP_ATTEMPTS}...`
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
this.#bootstrapAttempts.set(id, bootstrapAttempt)
|
|
260
|
+
await this.#restartCrashedService(id)
|
|
233
261
|
}
|
|
234
262
|
}
|
|
235
263
|
|
|
@@ -243,11 +271,13 @@ class Runtime extends EventEmitter {
|
|
|
243
271
|
|
|
244
272
|
this.#startedServices.set(id, false)
|
|
245
273
|
|
|
274
|
+
this.logger?.info(`Stopping service "${id}"...`)
|
|
275
|
+
|
|
246
276
|
// Always send the stop message, it will shut down workers that only had ITC and interceptors setup
|
|
247
277
|
try {
|
|
248
278
|
await Promise.race([sendViaITC(service, 'stop'), sleep(10000, 'timeout', { ref: false })])
|
|
249
279
|
} catch (error) {
|
|
250
|
-
this.logger
|
|
280
|
+
this.logger?.info(`Failed to stop service "${id}". Killing a worker thread.`, error)
|
|
251
281
|
}
|
|
252
282
|
|
|
253
283
|
// Wait for the worker thread to finish, we're going to create a new one if the service is ever restarted
|
|
@@ -638,6 +668,8 @@ class Runtime extends EventEmitter {
|
|
|
638
668
|
}
|
|
639
669
|
|
|
640
670
|
async #setupService (serviceConfig) {
|
|
671
|
+
if (this.#status === 'stopping' || this.#status === 'closed') return
|
|
672
|
+
|
|
641
673
|
const config = this.#configManager.current
|
|
642
674
|
const { autoload, restartOnError } = config
|
|
643
675
|
|
|
@@ -645,6 +677,10 @@ class Runtime extends EventEmitter {
|
|
|
645
677
|
const { port1: loggerDestination, port2: loggingPort } = new MessageChannel()
|
|
646
678
|
loggerDestination.on('message', this.#forwardThreadLog.bind(this))
|
|
647
679
|
|
|
680
|
+
if (!this.#bootstrapAttempts.has(id)) {
|
|
681
|
+
this.#bootstrapAttempts.set(id, 0)
|
|
682
|
+
}
|
|
683
|
+
|
|
648
684
|
const service = new Worker(kWorkerFile, {
|
|
649
685
|
workerData: {
|
|
650
686
|
config,
|
|
@@ -677,16 +713,21 @@ class Runtime extends EventEmitter {
|
|
|
677
713
|
loggerDestination.close()
|
|
678
714
|
loggingPort.close()
|
|
679
715
|
|
|
716
|
+
if (this.#status === 'stopping') return
|
|
717
|
+
|
|
680
718
|
// Wait for the next tick so that crashed from the thread are logged first
|
|
681
719
|
setImmediate(() => {
|
|
682
|
-
|
|
683
|
-
|
|
720
|
+
this.logger.warn(`Service "${id}" unexpectedly exited with code ${code}.`)
|
|
721
|
+
|
|
722
|
+
// Restart the service if it was started
|
|
723
|
+
if (started && this.#status === 'started') {
|
|
684
724
|
if (restartOnError > 0) {
|
|
685
|
-
this
|
|
725
|
+
this.logger.warn(`Restarting a service "${id}" in ${restartOnError}ms...`)
|
|
726
|
+
this.#restartCrashedService(id).catch(err => {
|
|
727
|
+
this.logger.error({ err }, `Failed to restart service "${id}".`)
|
|
728
|
+
})
|
|
686
729
|
} else {
|
|
687
|
-
this.logger.warn(
|
|
688
|
-
`Service ${id} unexpectedly exited with code ${code}. The service is no longer available ...`
|
|
689
|
-
)
|
|
730
|
+
this.logger.warn(`The "${id}" service is no longer available.`)
|
|
690
731
|
}
|
|
691
732
|
}
|
|
692
733
|
})
|
|
@@ -698,6 +739,7 @@ class Runtime extends EventEmitter {
|
|
|
698
739
|
// Setup ITC
|
|
699
740
|
service[kITC] = new ITC({ port: service })
|
|
700
741
|
service[kITC].listen()
|
|
742
|
+
service[kITC].handle('getServiceMeta', this.getServiceMeta.bind(this))
|
|
701
743
|
|
|
702
744
|
// Handle services changes
|
|
703
745
|
// This is not purposely activated on when this.#configManager.current.watch === true
|
|
@@ -713,9 +755,9 @@ class Runtime extends EventEmitter {
|
|
|
713
755
|
await this.startService(id)
|
|
714
756
|
}
|
|
715
757
|
|
|
716
|
-
this.logger
|
|
758
|
+
this.logger?.info(`Service ${id} has been successfully reloaded ...`)
|
|
717
759
|
} catch (e) {
|
|
718
|
-
this.logger
|
|
760
|
+
this.logger?.error(e)
|
|
719
761
|
}
|
|
720
762
|
})
|
|
721
763
|
|
|
@@ -743,26 +785,38 @@ class Runtime extends EventEmitter {
|
|
|
743
785
|
}
|
|
744
786
|
}
|
|
745
787
|
|
|
746
|
-
async #restartCrashedService (
|
|
747
|
-
const
|
|
748
|
-
const
|
|
788
|
+
async #restartCrashedService (id) {
|
|
789
|
+
const config = this.#configManager.current
|
|
790
|
+
const serviceConfig = config.services.find(s => s.id === id)
|
|
749
791
|
|
|
750
|
-
this.
|
|
792
|
+
let restartPromise = this.#restartPromises.get(id)
|
|
793
|
+
if (restartPromise) {
|
|
794
|
+
await restartPromise
|
|
795
|
+
return
|
|
796
|
+
}
|
|
751
797
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
798
|
+
restartPromise = new Promise((resolve, reject) => {
|
|
799
|
+
setTimeout(async () => {
|
|
800
|
+
this.#restartPromises.delete(id)
|
|
755
801
|
|
|
756
|
-
|
|
757
|
-
this.#
|
|
758
|
-
|
|
802
|
+
try {
|
|
803
|
+
await this.#setupService(serviceConfig)
|
|
804
|
+
|
|
805
|
+
const started = this.#startedServices.get(id)
|
|
806
|
+
if (started) {
|
|
807
|
+
this.#startedServices.set(id, false)
|
|
808
|
+
await this.startService(id)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
resolve()
|
|
812
|
+
} catch (err) {
|
|
813
|
+
reject(err)
|
|
759
814
|
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
}, restartTimeout).unref()
|
|
815
|
+
}, config.restartOnError)
|
|
816
|
+
})
|
|
764
817
|
|
|
765
|
-
this.#
|
|
818
|
+
this.#restartPromises.set(id, restartPromise)
|
|
819
|
+
await restartPromise
|
|
766
820
|
}
|
|
767
821
|
|
|
768
822
|
async #getServiceById (id, ensureStarted = false, mustExist = true) {
|
|
@@ -787,6 +841,25 @@ class Runtime extends EventEmitter {
|
|
|
787
841
|
return service
|
|
788
842
|
}
|
|
789
843
|
|
|
844
|
+
async getServiceMeta (id) {
|
|
845
|
+
const service = this.#services.get(id)
|
|
846
|
+
|
|
847
|
+
if (!service) {
|
|
848
|
+
throw new errors.ServiceNotFoundError(id, Array.from(this.#services.keys()).join(', '))
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
try {
|
|
852
|
+
return await service[kITC].send('getServiceMeta')
|
|
853
|
+
} catch (e) {
|
|
854
|
+
// The service exports no meta, return an empty object
|
|
855
|
+
if (e.code === 'PLT_ITC_HANDLER_NOT_FOUND') {
|
|
856
|
+
return {}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
throw e
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
790
863
|
async #getRuntimePackageJson () {
|
|
791
864
|
const runtimeDir = this.#configManager.dirname
|
|
792
865
|
const packageJsonPath = join(runtimeDir, 'package.json')
|
package/lib/schema.js
CHANGED
|
@@ -191,7 +191,10 @@ const platformaticRuntimeSchema = {
|
|
|
191
191
|
type: 'array',
|
|
192
192
|
items: {
|
|
193
193
|
type: 'object',
|
|
194
|
-
|
|
194
|
+
anyOf: [
|
|
195
|
+
{ required: ['id', 'path'] },
|
|
196
|
+
{ required: ['id', 'url'] },
|
|
197
|
+
],
|
|
195
198
|
properties: {
|
|
196
199
|
id: {
|
|
197
200
|
type: 'string',
|
|
@@ -203,6 +206,9 @@ const platformaticRuntimeSchema = {
|
|
|
203
206
|
config: {
|
|
204
207
|
type: 'string',
|
|
205
208
|
},
|
|
209
|
+
url: {
|
|
210
|
+
type: 'string',
|
|
211
|
+
},
|
|
206
212
|
useHttp: {
|
|
207
213
|
type: 'boolean',
|
|
208
214
|
},
|
package/lib/worker/app.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { EventEmitter } = require('node:events')
|
|
4
4
|
const { FileWatcher } = require('@platformatic/utils')
|
|
5
|
+
const { getGlobalDispatcher, setGlobalDispatcher } = require('undici')
|
|
5
6
|
const debounce = require('debounce')
|
|
6
7
|
|
|
7
8
|
const errors = require('../errors')
|
|
@@ -31,6 +32,7 @@ class PlatformaticApp extends EventEmitter {
|
|
|
31
32
|
serviceId: this.appConfig.id,
|
|
32
33
|
directory: this.appConfig.path,
|
|
33
34
|
isEntrypoint: this.appConfig.entrypoint,
|
|
35
|
+
isProduction: false,
|
|
34
36
|
telemetryConfig,
|
|
35
37
|
metricsConfig,
|
|
36
38
|
serverConfig,
|
|
@@ -90,6 +92,8 @@ class PlatformaticApp extends EventEmitter {
|
|
|
90
92
|
context: this.#context,
|
|
91
93
|
})
|
|
92
94
|
this.stackable = this.#wrapStackable(stackable)
|
|
95
|
+
|
|
96
|
+
this.#updateDispatcher()
|
|
93
97
|
} catch (err) {
|
|
94
98
|
this.#logAndExit(err)
|
|
95
99
|
}
|
|
@@ -103,7 +107,7 @@ class PlatformaticApp extends EventEmitter {
|
|
|
103
107
|
this.#starting = true
|
|
104
108
|
|
|
105
109
|
try {
|
|
106
|
-
await this.stackable.init
|
|
110
|
+
await this.stackable.init()
|
|
107
111
|
} catch (err) {
|
|
108
112
|
this.#logAndExit(err)
|
|
109
113
|
}
|
|
@@ -201,24 +205,38 @@ class PlatformaticApp extends EventEmitter {
|
|
|
201
205
|
|
|
202
206
|
#logAndExit (err) {
|
|
203
207
|
// Runtime logs here with console.error because stackable is not initialized
|
|
204
|
-
console.error(
|
|
205
|
-
JSON.stringify({
|
|
206
|
-
msg: err.message,
|
|
207
|
-
name: this.appConfig.id,
|
|
208
|
-
})
|
|
209
|
-
)
|
|
208
|
+
console.error(err.message)
|
|
210
209
|
process.exit(1)
|
|
211
210
|
}
|
|
212
211
|
|
|
213
212
|
#wrapStackable (stackable) {
|
|
214
213
|
const newStackable = {}
|
|
215
214
|
for (const method of Object.keys(defaultStackable)) {
|
|
216
|
-
newStackable[method] = stackable[method]
|
|
217
|
-
? stackable[method].bind(stackable)
|
|
218
|
-
: defaultStackable[method]
|
|
215
|
+
newStackable[method] = stackable[method] ? stackable[method].bind(stackable) : defaultStackable[method]
|
|
219
216
|
}
|
|
220
217
|
return newStackable
|
|
221
218
|
}
|
|
219
|
+
|
|
220
|
+
#updateDispatcher () {
|
|
221
|
+
const telemetryConfig = this.#context.telemetryConfig
|
|
222
|
+
const telemetryId = telemetryConfig?.serviceName
|
|
223
|
+
|
|
224
|
+
const interceptor = dispatch => {
|
|
225
|
+
return function InterceptedDispatch (opts, handler) {
|
|
226
|
+
if (telemetryId) {
|
|
227
|
+
opts.headers = {
|
|
228
|
+
...opts.headers,
|
|
229
|
+
'x-plt-telemetry-id': telemetryId,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return dispatch(opts, handler)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const dispatcher = getGlobalDispatcher().compose(interceptor)
|
|
237
|
+
|
|
238
|
+
setGlobalDispatcher(dispatcher)
|
|
239
|
+
}
|
|
222
240
|
}
|
|
223
241
|
|
|
224
242
|
module.exports = { PlatformaticApp }
|
package/lib/worker/itc.js
CHANGED
|
@@ -110,6 +110,14 @@ function setupITC (app, service, dispatcher) {
|
|
|
110
110
|
}
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
+
itc.handle('getServiceMeta', async () => {
|
|
114
|
+
try {
|
|
115
|
+
return app.stackable.getMeta()
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw new errors.FailedToRetrieveMetaError(service.id, err.message)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
113
121
|
itc.handle('getMetrics', async format => {
|
|
114
122
|
return app.stackable.getMetrics({ format })
|
|
115
123
|
})
|
package/lib/worker/main.js
CHANGED
|
@@ -13,8 +13,7 @@ const { wire } = require('undici-thread-interceptor')
|
|
|
13
13
|
const { PlatformaticApp } = require('./app')
|
|
14
14
|
const { setupITC } = require('./itc')
|
|
15
15
|
const loadInterceptors = require('./interceptors')
|
|
16
|
-
const { MessagePortWritable } = require('
|
|
17
|
-
const { createPinoWritable } = require('../streams/pino-writable')
|
|
16
|
+
const { MessagePortWritable, createPinoWritable } = require('@platformatic/utils')
|
|
18
17
|
const { kId, kITC } = require('./symbols')
|
|
19
18
|
|
|
20
19
|
process.on('uncaughtException', handleUnhandled.bind(null, 'uncaught exception'))
|
|
@@ -39,7 +38,7 @@ function handleUnhandled (type, err) {
|
|
|
39
38
|
|
|
40
39
|
function createLogger () {
|
|
41
40
|
const destination = new MessagePortWritable({ port: workerData.loggingPort })
|
|
42
|
-
const loggerInstance = pino({ level: 'trace' }, destination)
|
|
41
|
+
const loggerInstance = pino({ level: 'trace', name: workerData.serviceConfig.id }, destination)
|
|
43
42
|
|
|
44
43
|
Reflect.defineProperty(process, 'stdout', { value: createPinoWritable(loggerInstance, 'info') })
|
|
45
44
|
Reflect.defineProperty(process, 'stderr', { value: createPinoWritable(loggerInstance, 'error') })
|
|
@@ -88,15 +87,23 @@ async function main () {
|
|
|
88
87
|
} else if (service.useHttp) {
|
|
89
88
|
serverConfig = {
|
|
90
89
|
port: 0,
|
|
91
|
-
|
|
90
|
+
hostname: '127.0.0.1',
|
|
92
91
|
keepAliveTimeout: 5000,
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
94
|
|
|
95
|
+
let telemetryConfig = config.telemetry
|
|
96
|
+
if (telemetryConfig) {
|
|
97
|
+
telemetryConfig = {
|
|
98
|
+
...telemetryConfig,
|
|
99
|
+
serviceName: `${telemetryConfig.serviceName}-${service.id}`,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
96
103
|
// Create the application
|
|
97
104
|
app = new PlatformaticApp(
|
|
98
105
|
service,
|
|
99
|
-
|
|
106
|
+
telemetryConfig,
|
|
100
107
|
serverConfig,
|
|
101
108
|
!!config.managementApi,
|
|
102
109
|
!!config.watch,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.5",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"express": "^4.18.3",
|
|
27
27
|
"fast-jwt": "^4.0.0",
|
|
28
28
|
"get-port": "^7.1.0",
|
|
29
|
+
"json-schema-to-typescript": "^15.0.0",
|
|
29
30
|
"neostandard": "^0.11.1",
|
|
30
31
|
"pino-abstract-transport": "^1.1.0",
|
|
31
32
|
"split2": "^4.2.0",
|
|
@@ -33,11 +34,11 @@
|
|
|
33
34
|
"typescript": "^5.5.4",
|
|
34
35
|
"undici-oidc-interceptor": "^0.5.0",
|
|
35
36
|
"why-is-node-running": "^2.2.2",
|
|
36
|
-
"@platformatic/composer": "2.0.0-alpha.
|
|
37
|
-
"@platformatic/
|
|
38
|
-
"@platformatic/sql-graphql": "2.0.0-alpha.
|
|
39
|
-
"@platformatic/
|
|
40
|
-
"@platformatic/
|
|
37
|
+
"@platformatic/composer": "2.0.0-alpha.5",
|
|
38
|
+
"@platformatic/db": "2.0.0-alpha.5",
|
|
39
|
+
"@platformatic/sql-graphql": "2.0.0-alpha.5",
|
|
40
|
+
"@platformatic/sql-mapper": "2.0.0-alpha.5",
|
|
41
|
+
"@platformatic/service": "2.0.0-alpha.5"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"@fastify/error": "^3.4.1",
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"@hapi/topo": "^6.0.2",
|
|
46
47
|
"boring-name-generator": "^1.0.3",
|
|
47
48
|
"change-case-all": "^2.1.0",
|
|
48
|
-
"close-with-grace": "^
|
|
49
|
+
"close-with-grace": "^2.0.0",
|
|
49
50
|
"commist": "^3.2.0",
|
|
50
51
|
"debounce": "^2.0.0",
|
|
51
52
|
"desm": "^1.3.1",
|
|
@@ -63,21 +64,22 @@
|
|
|
63
64
|
"semgrator": "^0.3.0",
|
|
64
65
|
"tail-file-stream": "^0.2.0",
|
|
65
66
|
"undici": "^6.9.0",
|
|
66
|
-
"undici-thread-interceptor": "^0.5.
|
|
67
|
+
"undici-thread-interceptor": "^0.5.1",
|
|
67
68
|
"ws": "^8.16.0",
|
|
68
|
-
"@platformatic/basic": "2.0.0-alpha.
|
|
69
|
-
"@platformatic/itc": "2.0.0-alpha.
|
|
70
|
-
"@platformatic/
|
|
71
|
-
"@platformatic/
|
|
72
|
-
"@platformatic/
|
|
73
|
-
"@platformatic/utils": "2.0.0-alpha.
|
|
74
|
-
"@platformatic/
|
|
69
|
+
"@platformatic/basic": "2.0.0-alpha.5",
|
|
70
|
+
"@platformatic/itc": "2.0.0-alpha.5",
|
|
71
|
+
"@platformatic/generators": "2.0.0-alpha.5",
|
|
72
|
+
"@platformatic/telemetry": "2.0.0-alpha.5",
|
|
73
|
+
"@platformatic/ts-compiler": "2.0.0-alpha.5",
|
|
74
|
+
"@platformatic/utils": "2.0.0-alpha.5",
|
|
75
|
+
"@platformatic/config": "2.0.0-alpha.5"
|
|
75
76
|
},
|
|
76
77
|
"scripts": {
|
|
77
78
|
"test": "npm run lint && borp --concurrency=1 --timeout=180000 && tsd",
|
|
78
79
|
"coverage": "npm run lint && borp -X=fixtures -X=test -C --concurrency=1 --timeout=180000 && tsd",
|
|
79
|
-
"lint": "eslint",
|
|
80
80
|
"gen-schema": "node lib/schema.js > schema.json",
|
|
81
|
-
"gen-types": "json2ts > config.d.ts < schema.json"
|
|
81
|
+
"gen-types": "json2ts > config.d.ts < schema.json",
|
|
82
|
+
"build": "pnpm run gen-schema && pnpm run gen-types",
|
|
83
|
+
"lint": "eslint"
|
|
82
84
|
}
|
|
83
85
|
}
|
package/runtime.mjs
CHANGED
|
@@ -33,7 +33,7 @@ export async function run (argv) {
|
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
if (args.version) {
|
|
36
|
-
console.log('v' + JSON.parse(await readFile(join(import.meta.url, 'package.json'))).version)
|
|
36
|
+
console.log('v' + JSON.parse(await readFile(join(import.meta.url, 'package.json'), 'utf-8')).version)
|
|
37
37
|
process.exit(0)
|
|
38
38
|
}
|
|
39
39
|
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.0.0-alpha.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.0.0-alpha.5.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -766,9 +766,19 @@
|
|
|
766
766
|
"type": "array",
|
|
767
767
|
"items": {
|
|
768
768
|
"type": "object",
|
|
769
|
-
"
|
|
770
|
-
|
|
771
|
-
|
|
769
|
+
"anyOf": [
|
|
770
|
+
{
|
|
771
|
+
"required": [
|
|
772
|
+
"id",
|
|
773
|
+
"path"
|
|
774
|
+
]
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
"required": [
|
|
778
|
+
"id",
|
|
779
|
+
"url"
|
|
780
|
+
]
|
|
781
|
+
}
|
|
772
782
|
],
|
|
773
783
|
"properties": {
|
|
774
784
|
"id": {
|
|
@@ -781,6 +791,9 @@
|
|
|
781
791
|
"config": {
|
|
782
792
|
"type": "string"
|
|
783
793
|
},
|
|
794
|
+
"url": {
|
|
795
|
+
"type": "string"
|
|
796
|
+
},
|
|
784
797
|
"useHttp": {
|
|
785
798
|
"type": "boolean"
|
|
786
799
|
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { Writable } = require('node:stream')
|
|
4
|
-
|
|
5
|
-
class MessagePortWritable extends Writable {
|
|
6
|
-
#port
|
|
7
|
-
#metadata
|
|
8
|
-
|
|
9
|
-
constructor (options) {
|
|
10
|
-
const { port, metadata, ...opts } = options
|
|
11
|
-
|
|
12
|
-
super({ ...opts, decodeStrings: false })
|
|
13
|
-
this.#port = port
|
|
14
|
-
this.#metadata = metadata
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Since this is only invoked by pino, we only receive strings
|
|
18
|
-
_write (chunk, encoding, callback) {
|
|
19
|
-
this.#port.postMessage({ metadata: this.#metadata, logs: [chunk.toString(encoding ?? 'utf-8')] })
|
|
20
|
-
|
|
21
|
-
// Important: do not remove nextTick otherwise _writev will never be used
|
|
22
|
-
process.nextTick(callback)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Since this is only invoked by pino, we only receive strings
|
|
26
|
-
_writev (chunks, callback) {
|
|
27
|
-
this.#port.postMessage({ metadata: this.#metadata, logs: chunks.map(c => c.chunk.toString(c.encoding ?? 'utf-8')) })
|
|
28
|
-
|
|
29
|
-
// Important: do not remove nextTick otherwise _writev will never be used
|
|
30
|
-
process.nextTick(callback)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
_final (callback) {
|
|
34
|
-
this.#port.close()
|
|
35
|
-
callback()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
_destroy (err, callback) {
|
|
39
|
-
this.#port.close()
|
|
40
|
-
callback(err)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
module.exports = { MessagePortWritable }
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { Writable } = require('node:stream')
|
|
4
|
-
const inspect = require('node:util')
|
|
5
|
-
|
|
6
|
-
class PinoWritable extends Writable {
|
|
7
|
-
#write
|
|
8
|
-
|
|
9
|
-
constructor (options) {
|
|
10
|
-
const { pino, level, ...opts } = options
|
|
11
|
-
|
|
12
|
-
super({ ...opts, decodeStrings: false })
|
|
13
|
-
this.#write = pino[level].bind(pino)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
_write (chunk, encoding, callback) {
|
|
17
|
-
this.#write({ raw: encoding === 'buffer' ? inspect(chunk) : chunk.toString(encoding ?? 'utf-8') })
|
|
18
|
-
callback()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// We don't define _writev as we have to serialize messages one by one so batching wouldn't make any sense.
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function createPinoWritable (pino, level) {
|
|
25
|
-
const writable = new PinoWritable({ pino, level })
|
|
26
|
-
writable.write = writable.write.bind(writable)
|
|
27
|
-
return writable
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
module.exports = { PinoWritable, createPinoWritable }
|