@platformatic/runtime 2.0.0-alpha.1 → 2.0.0-alpha.3
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 +285 -0
- package/eslint.config.js +8 -0
- package/fixtures/botched-start/platformatic.runtime.json +1 -1
- package/fixtures/botched-start/services/a/platformatic.service.json +1 -1
- package/fixtures/composerApp/platformatic.composer.json +1 -1
- package/fixtures/configs/invalid-autoload-with-services.json +1 -1
- package/fixtures/configs/invalid-entrypoint.json +1 -1
- package/fixtures/configs/invalid-schema-type.config.json +1 -1
- package/fixtures/configs/missing-property.config.json +1 -1
- package/fixtures/configs/missing-service-config.json +1 -1
- package/fixtures/configs/monorepo-composer-no-autoload.json +2 -2
- package/fixtures/configs/monorepo-composer.json +2 -2
- package/fixtures/configs/monorepo-create-cycle.json +2 -2
- package/fixtures/configs/monorepo-missing-dependencies.json +2 -2
- package/fixtures/configs/monorepo-no-cycles.json +2 -2
- package/fixtures/configs/monorepo-openapi.json +2 -2
- package/fixtures/configs/{monorepo-hotreload-env.json → monorepo-watch-env.json} +2 -2
- package/fixtures/configs/monorepo-watch-single.json +12 -0
- package/fixtures/configs/monorepo-watch.json +26 -9
- package/fixtures/configs/monorepo-with-dependencies.json +2 -2
- package/fixtures/configs/monorepo-with-management-api-without-metrics.json +21 -0
- package/fixtures/configs/monorepo-with-management-api.json +2 -2
- package/fixtures/configs/{monorepo-hotreload.json → monorepo-with-metrics.json} +5 -4
- package/fixtures/configs/monorepo.json +2 -2
- package/fixtures/configs/no-services.config.json +1 -1
- package/fixtures/configs/no-sources.config.json +1 -1
- package/fixtures/configs/service-throws-on-start.json +1 -1
- package/fixtures/configs/service-with-env-port.json +2 -2
- package/fixtures/configs/service-with-stdio.json +12 -0
- package/fixtures/configs/{hotreload.json → watch.json} +2 -2
- package/fixtures/dbApp/platformatic.db.json +1 -1
- package/fixtures/dbAppNoName/platformatic.db.json +1 -1
- package/fixtures/dbAppNoPackageJson/platformatic.db.json +1 -1
- package/fixtures/dbAppWithMigrationError/platformatic.db.json +1 -1
- package/fixtures/do-not-reload-dependencies/platformatic.service.json +1 -1
- package/fixtures/do-not-restart-on-crash/platformatic.runtime.json +3 -2
- package/fixtures/do-not-restart-on-crash/services/a/platformatic.service.json +1 -1
- package/fixtures/express/platformatic.runtime.json +1 -1
- package/fixtures/express/services/a/platformatic.service.json +1 -1
- package/fixtures/express/services/b/platformatic.service.json +1 -1
- package/fixtures/external-client/platformatic.service.json +1 -1
- package/fixtures/interceptors/idp.js +2 -2
- package/fixtures/interceptors/platformatic.runtime.json +1 -1
- package/fixtures/interceptors/services/a/platformatic.service.json +1 -1
- package/fixtures/interceptors-2/platformatic.runtime.json +1 -1
- package/fixtures/interceptors-2/services/a/platformatic.service.json +1 -1
- package/fixtures/leven/platformatic.runtime.json +2 -2
- package/fixtures/leven/services/deeply-spittle/platformatic.service.json +1 -1
- package/fixtures/leven/services/rainy-empire/platformatic.composer.json +1 -1
- package/fixtures/management-api/platformatic.json +3 -3
- package/fixtures/management-api/services/service-1/platformatic.json +1 -1
- package/fixtures/management-api/services/service-1/plugin.js +4 -3
- package/fixtures/management-api/services/service-2/platformatic.json +1 -1
- package/fixtures/management-api/services/service-db/platformatic.db.json +1 -1
- package/fixtures/management-api-custom-labels/platformatic.json +2 -2
- package/fixtures/management-api-custom-labels/services/service-1/platformatic.json +1 -1
- package/fixtures/management-api-custom-labels/services/service-1/plugin.js +4 -3
- package/fixtures/management-api-custom-labels/services/service-2/platformatic.json +1 -1
- package/fixtures/management-api-custom-labels/services/service-db/platformatic.db.json +1 -1
- package/fixtures/management-api-without-metrics/platformatic.json +3 -2
- package/fixtures/management-api-without-metrics/services/service-1/platformatic.json +1 -1
- package/fixtures/monorepo/composerApp/platformatic.composer.json +1 -1
- package/fixtures/monorepo/dbApp/platformatic.db.json +1 -1
- package/fixtures/monorepo/serviceApp/platformatic.service.json +3 -2
- package/fixtures/monorepo/serviceApp/with-logger/with-logger.cjs +2 -2
- package/fixtures/monorepo/serviceApp/with-logger/with-logger.d.ts +7 -7
- package/fixtures/monorepo/serviceAppWithLogger/platformatic.service.json +1 -1
- package/fixtures/monorepo/serviceAppWithLogger/plugin.js +12 -0
- package/fixtures/monorepo/serviceAppWithMultiplePlugins/platformatic.service.json +3 -2
- package/fixtures/monorepo-missing-dependencies/composer/platformatic.json +1 -1
- package/fixtures/monorepo-openapi/serviceAppWithoutOpenapi/platformatic.service.json +1 -1
- package/fixtures/monorepo-watch/service1/platformatic.service.json +1 -1
- package/fixtures/monorepo-with-dependencies/main/platformatic.json +1 -1
- package/fixtures/monorepo-with-dependencies/service-1/platformatic.json +1 -1
- package/fixtures/monorepo-with-dependencies/service-2/platformatic.json +1 -1
- package/fixtures/no-env.service.json +1 -1
- package/fixtures/preload/platformatic.runtime.json +1 -1
- package/fixtures/preload/services/a/platformatic.service.json +1 -1
- package/fixtures/prom-server/platformatic.json +2 -2
- package/fixtures/prom-server/services/service-1/platformatic.json +1 -1
- package/fixtures/prom-server/services/service-2/platformatic.json +1 -1
- package/fixtures/restart-on-crash/platformatic.runtime.json +1 -1
- package/fixtures/restart-on-crash/services/a/platformatic.service.json +1 -1
- package/fixtures/sample-runtime/package.json +1 -1
- package/fixtures/sample-runtime/platformatic.json +2 -2
- package/fixtures/sample-runtime/services/rival/package.json +1 -1
- package/fixtures/sample-runtime/services/rival/platformatic.json +1 -1
- package/fixtures/sample-runtime-with-2-services/package.json +1 -1
- package/fixtures/sample-runtime-with-2-services/platformatic.json +2 -2
- package/fixtures/sample-runtime-with-2-services/services/foobar/package.json +1 -1
- package/fixtures/sample-runtime-with-2-services/services/foobar/platformatic.json +1 -1
- package/fixtures/sample-runtime-with-2-services/services/rival/package.json +1 -1
- package/fixtures/sample-runtime-with-2-services/services/rival/platformatic.json +1 -1
- package/fixtures/server/logger-transport/platformatic.runtime.json +2 -2
- package/fixtures/server/logger-transport/services/echo/platformatic.service.json +1 -1
- package/fixtures/server/overrides-service/platformatic.runtime.json +2 -2
- package/fixtures/server/overrides-service/services/echo/platformatic.service.json +1 -1
- package/fixtures/server/runtime-server/platformatic.runtime.json +2 -2
- package/fixtures/server/runtime-server/services/echo/platformatic.service.json +1 -1
- package/fixtures/serviceAppThrowsOnStart/platformatic.service.json +1 -1
- package/fixtures/stackables/node_modules/foo/foo.js +2 -1
- package/fixtures/start-command-in-runtime.js +1 -1
- package/fixtures/stdio/platformatic.service.json +6 -0
- package/fixtures/stdio/plugin.js +24 -0
- package/fixtures/telemetry/platformatic.runtime.json +2 -2
- package/fixtures/telemetry/services/echo/platformatic.service.json +1 -1
- package/fixtures/typescript/platformatic.runtime.json +2 -2
- package/fixtures/typescript/services/composer/platformatic.composer.json +1 -1
- package/fixtures/typescript/services/movies/global.d.ts +2 -3
- package/fixtures/typescript/services/movies/platformatic.db.json +1 -1
- package/fixtures/typescript/services/movies/types/Movie.d.ts +3 -3
- package/fixtures/typescript/services/movies/types/index.d.ts +6 -6
- package/fixtures/typescript/services/titles/client/client.d.ts +35 -35
- package/fixtures/typescript/services/titles/platformatic.service.json +1 -1
- package/fixtures/typescript-custom-flags/platformatic.runtime.json +2 -2
- package/fixtures/typescript-custom-flags/services/composer/platformatic.composer.json +1 -1
- package/fixtures/typescript-custom-flags/services/movies/global.d.ts +2 -3
- package/fixtures/typescript-custom-flags/services/movies/platformatic.db.json +1 -1
- package/fixtures/typescript-custom-flags/services/movies/types/Movie.d.ts +3 -3
- package/fixtures/typescript-custom-flags/services/movies/types/index.d.ts +6 -6
- package/fixtures/typescript-custom-flags/services/titles/client/client.d.ts +35 -35
- package/fixtures/typescript-custom-flags/services/titles/platformatic.service.json +1 -1
- package/fixtures/typescript-no-env/platformatic.runtime.json +2 -2
- package/fixtures/typescript-no-env/services/composer/platformatic.composer.json +1 -1
- package/fixtures/typescript-no-env/services/movies/global.d.ts +2 -3
- package/fixtures/typescript-no-env/services/movies/platformatic.db.json +1 -1
- package/fixtures/typescript-no-env/services/movies/types/Movie.d.ts +3 -3
- package/fixtures/typescript-no-env/services/movies/types/index.d.ts +6 -6
- package/fixtures/typescript-no-env/services/titles/client/client.d.ts +35 -35
- package/fixtures/typescript-no-env/services/titles/platformatic.service.json +1 -1
- package/index.d.ts +7 -8
- package/index.js +14 -10
- package/index.test-d.ts +10 -12
- package/lib/build-server.js +5 -11
- package/lib/compile.js +11 -10
- package/lib/config.js +21 -14
- package/lib/dependencies.js +2 -1
- package/lib/errors.js +3 -2
- package/lib/generator/errors.js +1 -1
- package/lib/generator/runtime-generator.d.ts +15 -15
- package/lib/generator/runtime-generator.js +92 -63
- package/lib/logger.js +55 -0
- package/lib/management-api.js +29 -44
- package/lib/prom-server.js +5 -9
- package/lib/runtime.js +885 -0
- package/lib/schema.js +79 -76
- package/lib/start.js +35 -113
- package/lib/streams/message-port-writable.js +44 -0
- package/lib/streams/pino-writable.js +30 -0
- package/lib/upgrade.js +4 -3
- package/lib/utils.js +49 -1
- package/lib/versions/v1.36.0.js +1 -1
- package/lib/versions/v1.5.0.js +1 -1
- package/lib/versions/v2.0.0.js +17 -0
- package/lib/worker/app.js +224 -0
- package/lib/worker/default-stackable.js +27 -0
- package/lib/worker/itc.js +128 -0
- package/lib/worker/main.js +120 -0
- package/lib/worker/symbols.js +7 -0
- package/package.json +23 -25
- package/runtime.mjs +4 -4
- package/schema.json +824 -0
- package/lib/api-client.js +0 -500
- package/lib/api.js +0 -420
- package/lib/app.js +0 -397
- package/lib/load-config.js +0 -12
- package/lib/loader.mjs +0 -103
- package/lib/message-port-writable.js +0 -50
- package/lib/worker.js +0 -182
- /package/lib/{interceptors.js → worker/interceptors.js} +0 -0
package/lib/runtime.js
ADDED
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { once, EventEmitter } = require('node:events')
|
|
4
|
+
const { createReadStream, watch } = require('node:fs')
|
|
5
|
+
const { readdir, readFile, stat, access } = require('node:fs/promises')
|
|
6
|
+
const inspector = require('node:inspector')
|
|
7
|
+
const { join } = require('node:path')
|
|
8
|
+
const { setTimeout: sleep } = require('node:timers/promises')
|
|
9
|
+
const { Worker } = require('node:worker_threads')
|
|
10
|
+
|
|
11
|
+
const { ITC } = require('@platformatic/itc')
|
|
12
|
+
const ts = require('tail-file-stream')
|
|
13
|
+
const { createThreadInterceptor } = require('undici-thread-interceptor')
|
|
14
|
+
|
|
15
|
+
const { checkDependencies, topologicalSort } = require('./dependencies')
|
|
16
|
+
const errors = require('./errors')
|
|
17
|
+
const { createLogger } = require('./logger')
|
|
18
|
+
const { startManagementApi } = require('./management-api')
|
|
19
|
+
const { startPrometheusServer } = require('./prom-server')
|
|
20
|
+
const { getRuntimeTmpDir } = require('./utils')
|
|
21
|
+
const { sendViaITC } = require('./worker/itc')
|
|
22
|
+
const { kId, kITC, kConfig } = require('./worker/symbols')
|
|
23
|
+
|
|
24
|
+
const platformaticVersion = require('../package.json').version
|
|
25
|
+
const kWorkerFile = join(__dirname, 'worker/main.js')
|
|
26
|
+
|
|
27
|
+
const MAX_LISTENERS_COUNT = 100
|
|
28
|
+
const MAX_METRICS_QUEUE_LENGTH = 5 * 60 // 5 minutes in seconds
|
|
29
|
+
const COLLECT_METRICS_TIMEOUT = 1000
|
|
30
|
+
|
|
31
|
+
class Runtime extends EventEmitter {
|
|
32
|
+
#configManager
|
|
33
|
+
#runtimeTmpDir
|
|
34
|
+
#runtimeLogsDir
|
|
35
|
+
#env
|
|
36
|
+
#services
|
|
37
|
+
#servicesIds
|
|
38
|
+
#entrypoint
|
|
39
|
+
#entrypointId
|
|
40
|
+
#url
|
|
41
|
+
#loggerDestination
|
|
42
|
+
#metrics
|
|
43
|
+
#metricsTimeout
|
|
44
|
+
#status
|
|
45
|
+
#interceptor
|
|
46
|
+
#managementApi
|
|
47
|
+
#prometheusServer
|
|
48
|
+
#startedServices
|
|
49
|
+
#crashedServicesTimers
|
|
50
|
+
|
|
51
|
+
constructor (configManager, runtimeLogsDir, env) {
|
|
52
|
+
super()
|
|
53
|
+
this.setMaxListeners(MAX_LISTENERS_COUNT)
|
|
54
|
+
|
|
55
|
+
this.#configManager = configManager
|
|
56
|
+
this.#runtimeTmpDir = getRuntimeTmpDir(configManager.dirname)
|
|
57
|
+
this.#runtimeLogsDir = runtimeLogsDir
|
|
58
|
+
this.#env = env
|
|
59
|
+
this.#services = new Map()
|
|
60
|
+
this.#servicesIds = []
|
|
61
|
+
this.#url = undefined
|
|
62
|
+
// Note: nothing hits the main thread so there is no reason to set the globalDispatcher here
|
|
63
|
+
this.#interceptor = createThreadInterceptor({ domain: '.plt.local' })
|
|
64
|
+
this.#status = undefined
|
|
65
|
+
this.#startedServices = new Map()
|
|
66
|
+
this.#crashedServicesTimers = new Map()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async init () {
|
|
70
|
+
const config = this.#configManager.current
|
|
71
|
+
const autoloadEnabled = config.autoload
|
|
72
|
+
|
|
73
|
+
// This cannot be transferred to worker threads
|
|
74
|
+
delete config.configManager
|
|
75
|
+
|
|
76
|
+
if (config.managementApi) {
|
|
77
|
+
this.#managementApi = await startManagementApi(this, this.#configManager)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (config.metrics) {
|
|
81
|
+
this.#prometheusServer = await startPrometheusServer(this, config.metrics)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Create the logger
|
|
85
|
+
const [logger, destination] = createLogger(config, this.#runtimeLogsDir)
|
|
86
|
+
this.logger = logger
|
|
87
|
+
this.#loggerDestination = destination
|
|
88
|
+
|
|
89
|
+
// Handle inspector
|
|
90
|
+
const inspectorOptions = config.inspectorOptions
|
|
91
|
+
if (inspectorOptions) {
|
|
92
|
+
/* c8 ignore next 6 */
|
|
93
|
+
if (inspectorOptions.watchDisabled) {
|
|
94
|
+
logger.info('debugging flags were detected. hot reloading has been disabled')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
inspector.open(inspectorOptions.port, inspectorOptions.host, inspectorOptions.breakFirstLine)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Create all services, each in is own worker thread
|
|
101
|
+
for (const serviceConfig of config.services) {
|
|
102
|
+
// Setup forwarding of logs from the worker threads to the main thread
|
|
103
|
+
await this.#setupService(serviceConfig)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Make sure the list exists before computing the dependencies, otherwise some services might not be stopped
|
|
108
|
+
this.#servicesIds = config.services.map(service => service.id)
|
|
109
|
+
|
|
110
|
+
if (autoloadEnabled) {
|
|
111
|
+
checkDependencies(config.services)
|
|
112
|
+
this.#services = topologicalSort(this.#services, config)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Recompute the list of services after sorting
|
|
116
|
+
this.#servicesIds = config.services.map(service => service.id)
|
|
117
|
+
} catch (e) {
|
|
118
|
+
await this.close()
|
|
119
|
+
throw e
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.#updateStatus('init')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async start () {
|
|
126
|
+
this.#updateStatus('starting')
|
|
127
|
+
|
|
128
|
+
// Important: do not use Promise.all here since it won't properly manage dependencies
|
|
129
|
+
for (const service of this.#servicesIds) {
|
|
130
|
+
await this.startService(service)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.#updateStatus('started')
|
|
134
|
+
|
|
135
|
+
if (this.#managementApi && typeof this.#metrics === 'undefined') {
|
|
136
|
+
this.startCollectingMetrics()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return this.#url
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async stop () {
|
|
143
|
+
if (this.#status === 'starting') {
|
|
144
|
+
await once(this, 'started')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.#updateStatus('stopping')
|
|
148
|
+
|
|
149
|
+
for (const timer of this.#crashedServicesTimers.values()) {
|
|
150
|
+
clearTimeout(timer)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.#startedServices.clear()
|
|
154
|
+
this.#crashedServicesTimers.clear()
|
|
155
|
+
|
|
156
|
+
await Promise.all(this.#servicesIds.map(service => this._stopService(service)))
|
|
157
|
+
|
|
158
|
+
this.#updateStatus('stopped')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async restart () {
|
|
162
|
+
this.emit('restarting')
|
|
163
|
+
|
|
164
|
+
await this.stop()
|
|
165
|
+
await this.start()
|
|
166
|
+
|
|
167
|
+
this.emit('restarted')
|
|
168
|
+
|
|
169
|
+
return this.#url
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async close (fromManagementApi) {
|
|
173
|
+
this.#updateStatus('closing')
|
|
174
|
+
|
|
175
|
+
clearInterval(this.#metricsTimeout)
|
|
176
|
+
|
|
177
|
+
await this.stop()
|
|
178
|
+
|
|
179
|
+
if (this.#managementApi) {
|
|
180
|
+
if (fromManagementApi) {
|
|
181
|
+
// This allow a close request coming from the management API to correctly be handled
|
|
182
|
+
setImmediate(() => {
|
|
183
|
+
this.#managementApi.close()
|
|
184
|
+
})
|
|
185
|
+
} else {
|
|
186
|
+
await this.#managementApi.close()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (this.#prometheusServer) {
|
|
191
|
+
await this.#prometheusServer.close()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (this.logger) {
|
|
195
|
+
this.#loggerDestination.end()
|
|
196
|
+
|
|
197
|
+
this.logger = null
|
|
198
|
+
this.#loggerDestination = null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.#updateStatus('closed')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async startService (id) {
|
|
205
|
+
if (this.#startedServices.get(id)) {
|
|
206
|
+
throw new errors.ApplicationAlreadyStartedError()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// This is set here so that if the service fails while starting we track the status
|
|
210
|
+
this.#startedServices.set(id, true)
|
|
211
|
+
|
|
212
|
+
// Make sure we don't restart twice
|
|
213
|
+
const crashedTimer = this.#crashedServicesTimers.get(id)
|
|
214
|
+
if (crashedTimer) {
|
|
215
|
+
clearTimeout(crashedTimer)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let service = await this.#getServiceById(id, false, false)
|
|
219
|
+
|
|
220
|
+
// The service was stopped, recreate the thread
|
|
221
|
+
if (!service) {
|
|
222
|
+
const config = this.#configManager.current
|
|
223
|
+
const serviceConfig = config.services.find(s => s.id === id)
|
|
224
|
+
|
|
225
|
+
await this.#setupService(serviceConfig)
|
|
226
|
+
service = await this.#getServiceById(id)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const serviceUrl = await sendViaITC(service, 'start')
|
|
230
|
+
|
|
231
|
+
if (serviceUrl) {
|
|
232
|
+
this.#url = serviceUrl
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Do not rename to #stopService as this is used in tests
|
|
237
|
+
async _stopService (id) {
|
|
238
|
+
const service = await this.#getServiceById(id, false, false)
|
|
239
|
+
|
|
240
|
+
if (!service) {
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this.#startedServices.set(id, false)
|
|
245
|
+
|
|
246
|
+
// Always send the stop message, it will shut down workers that only had ITC and interceptors setup
|
|
247
|
+
try {
|
|
248
|
+
await Promise.race([sendViaITC(service, 'stop'), sleep(10000, 'timeout', { ref: false })])
|
|
249
|
+
} catch (error) {
|
|
250
|
+
this.logger.info(`Failed to stop service "${id}". Killing a worker thread.`, error)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Wait for the worker thread to finish, we're going to create a new one if the service is ever restarted
|
|
254
|
+
const res = await Promise.race([once(service, 'exit'), sleep(10000, 'timeout', { ref: false })])
|
|
255
|
+
|
|
256
|
+
// If the worker didn't exit in time, kill it
|
|
257
|
+
if (res === 'timeout') {
|
|
258
|
+
await service.terminate()
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async inject (id, injectParams) {
|
|
263
|
+
const service = await this.#getServiceById(id, true)
|
|
264
|
+
return sendViaITC(service, 'inject', injectParams)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
startCollectingMetrics () {
|
|
268
|
+
this.#metrics = []
|
|
269
|
+
this.#metricsTimeout = setInterval(async () => {
|
|
270
|
+
if (this.#status !== 'started') {
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let metrics = null
|
|
275
|
+
try {
|
|
276
|
+
metrics = await this.getFormattedMetrics()
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (!(error instanceof errors.RuntimeExitedError)) {
|
|
279
|
+
// TODO(mcollina): use the logger
|
|
280
|
+
console.error('Error collecting metrics', error)
|
|
281
|
+
}
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
this.emit('metrics', metrics)
|
|
286
|
+
this.#metrics.push(metrics)
|
|
287
|
+
if (this.#metrics.length > MAX_METRICS_QUEUE_LENGTH) {
|
|
288
|
+
this.#metrics.shift()
|
|
289
|
+
}
|
|
290
|
+
}, COLLECT_METRICS_TIMEOUT).unref()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async pipeLogsStream (writableStream, logger, startLogId, endLogId, runtimePID) {
|
|
294
|
+
endLogId = endLogId || Infinity
|
|
295
|
+
runtimePID = runtimePID ?? process.pid
|
|
296
|
+
|
|
297
|
+
const runtimeLogFiles = await this.#getRuntimeLogFiles(runtimePID)
|
|
298
|
+
if (runtimeLogFiles.length === 0) {
|
|
299
|
+
writableStream.end()
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let latestFileId = parseInt(runtimeLogFiles.at(-1).slice('logs.'.length))
|
|
304
|
+
|
|
305
|
+
let fileStream = null
|
|
306
|
+
let fileId = startLogId ?? latestFileId
|
|
307
|
+
let isClosed = false
|
|
308
|
+
|
|
309
|
+
const runtimeLogsDir = this.#getRuntimeLogsDir(runtimePID)
|
|
310
|
+
|
|
311
|
+
const watcher = watch(runtimeLogsDir, async (event, filename) => {
|
|
312
|
+
if (event === 'rename' && filename.startsWith('logs')) {
|
|
313
|
+
const logFileId = parseInt(filename.slice('logs.'.length))
|
|
314
|
+
if (logFileId > latestFileId) {
|
|
315
|
+
latestFileId = logFileId
|
|
316
|
+
fileStream.unwatch()
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}).unref()
|
|
320
|
+
|
|
321
|
+
const streamLogFile = () => {
|
|
322
|
+
if (fileId > endLogId) {
|
|
323
|
+
writableStream.end()
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const fileName = 'logs.' + fileId
|
|
328
|
+
const filePath = join(runtimeLogsDir, fileName)
|
|
329
|
+
|
|
330
|
+
const prevFileStream = fileStream
|
|
331
|
+
|
|
332
|
+
fileStream = ts.createReadStream(filePath)
|
|
333
|
+
fileStream.pipe(writableStream, { end: false, persistent: false })
|
|
334
|
+
|
|
335
|
+
if (prevFileStream) {
|
|
336
|
+
prevFileStream.unpipe(writableStream)
|
|
337
|
+
prevFileStream.destroy()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fileStream.on('close', () => {
|
|
341
|
+
if (latestFileId > fileId && !isClosed) {
|
|
342
|
+
streamLogFile(++fileId)
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
fileStream.on('error', err => {
|
|
347
|
+
isClosed = true
|
|
348
|
+
logger.error(err, 'Error streaming log file')
|
|
349
|
+
fileStream.destroy()
|
|
350
|
+
watcher.close()
|
|
351
|
+
writableStream.end()
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
fileStream.on('eof', () => {
|
|
355
|
+
if (fileId >= endLogId) {
|
|
356
|
+
writableStream.end()
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
if (latestFileId > fileId) {
|
|
360
|
+
fileStream.unwatch()
|
|
361
|
+
}
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
return fileStream
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
streamLogFile(fileId)
|
|
368
|
+
|
|
369
|
+
const onClose = () => {
|
|
370
|
+
isClosed = true
|
|
371
|
+
watcher.close()
|
|
372
|
+
fileStream.destroy()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
writableStream.on('close', onClose)
|
|
376
|
+
writableStream.on('error', onClose)
|
|
377
|
+
this.on('closed', onClose)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async getRuntimeMetadata () {
|
|
381
|
+
const packageJson = await this.#getRuntimePackageJson()
|
|
382
|
+
const entrypointDetails = await this.getEntrypointDetails()
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
pid: process.pid,
|
|
386
|
+
cwd: process.cwd(),
|
|
387
|
+
argv: process.argv,
|
|
388
|
+
uptimeSeconds: Math.floor(process.uptime()),
|
|
389
|
+
execPath: process.execPath,
|
|
390
|
+
nodeVersion: process.version,
|
|
391
|
+
projectDir: this.#configManager.dirname,
|
|
392
|
+
packageName: packageJson.name ?? null,
|
|
393
|
+
packageVersion: packageJson.version ?? null,
|
|
394
|
+
url: entrypointDetails?.url ?? null,
|
|
395
|
+
platformaticVersion,
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
getRuntimeConfig () {
|
|
400
|
+
return this.#configManager.current
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
getInterceptor () {
|
|
404
|
+
return this.#interceptor
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
getManagementApi () {
|
|
408
|
+
return this.#managementApi
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
getManagementApiUrl () {
|
|
412
|
+
return this.#managementApi?.server.address()
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async getEntrypointDetails () {
|
|
416
|
+
return this.getServiceDetails(this.#entrypointId)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async getServices () {
|
|
420
|
+
return {
|
|
421
|
+
entrypoint: this.#entrypointId,
|
|
422
|
+
services: await Promise.all(this.#servicesIds.map(id => this.getServiceDetails(id))),
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async getServiceDetails (id, allowUnloaded = false) {
|
|
427
|
+
let service
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
service = await this.#getServiceById(id)
|
|
431
|
+
} catch (e) {
|
|
432
|
+
if (allowUnloaded) {
|
|
433
|
+
return { id, status: 'stopped' }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
throw e
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const { entrypoint, dependencies, localUrl } = service[kConfig]
|
|
440
|
+
|
|
441
|
+
const status = await sendViaITC(service, 'getStatus')
|
|
442
|
+
const { type, version } = await sendViaITC(service, 'getServiceInfo')
|
|
443
|
+
|
|
444
|
+
const serviceDetails = {
|
|
445
|
+
id,
|
|
446
|
+
type,
|
|
447
|
+
status,
|
|
448
|
+
version,
|
|
449
|
+
localUrl,
|
|
450
|
+
entrypoint,
|
|
451
|
+
dependencies,
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (entrypoint) {
|
|
455
|
+
serviceDetails.url = status === 'started' ? this.#url : null
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return serviceDetails
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async getService (id) {
|
|
462
|
+
return this.#getServiceById(id, true)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async getServiceConfig (id) {
|
|
466
|
+
const service = await this.#getServiceById(id, true)
|
|
467
|
+
|
|
468
|
+
return sendViaITC(service, 'getServiceConfig')
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async getServiceOpenapiSchema (id) {
|
|
472
|
+
const service = await this.#getServiceById(id, true)
|
|
473
|
+
|
|
474
|
+
return sendViaITC(service, 'getServiceOpenAPISchema')
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async getServiceGraphqlSchema (id) {
|
|
478
|
+
const service = await this.#getServiceById(id, true)
|
|
479
|
+
|
|
480
|
+
return sendViaITC(service, 'getServiceGraphQLSchema')
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async getMetrics (format = 'json') {
|
|
484
|
+
let metrics = null
|
|
485
|
+
|
|
486
|
+
for (const id of this.#servicesIds) {
|
|
487
|
+
try {
|
|
488
|
+
const service = await this.#getServiceById(id, true, false)
|
|
489
|
+
|
|
490
|
+
// The service might be temporarily unavailable
|
|
491
|
+
if (!service) {
|
|
492
|
+
continue
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const serviceMetrics = await sendViaITC(service, 'getMetrics', format)
|
|
496
|
+
|
|
497
|
+
if (serviceMetrics) {
|
|
498
|
+
if (metrics === null) {
|
|
499
|
+
metrics = format === 'json' ? [] : ''
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (format === 'json') {
|
|
503
|
+
metrics.push(...serviceMetrics)
|
|
504
|
+
} else {
|
|
505
|
+
metrics += serviceMetrics
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
} catch (e) {
|
|
509
|
+
// The service exited while we were sending the ITC, skip it
|
|
510
|
+
if (e.code === 'PLT_RUNTIME_SERVICE_NOT_STARTED' || e.code === 'PLT_RUNTIME_SERVICE_EXIT') {
|
|
511
|
+
continue
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
throw e
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return { metrics }
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
getCachedMetrics () {
|
|
522
|
+
return this.#metrics
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async getFormattedMetrics () {
|
|
526
|
+
try {
|
|
527
|
+
const { metrics } = await this.getMetrics()
|
|
528
|
+
|
|
529
|
+
if (metrics === null) {
|
|
530
|
+
return null
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const cpuMetric = metrics.find(metric => metric.name === 'process_cpu_percent_usage')
|
|
534
|
+
const rssMetric = metrics.find(metric => metric.name === 'process_resident_memory_bytes')
|
|
535
|
+
const totalHeapSizeMetric = metrics.find(metric => metric.name === 'nodejs_heap_size_total_bytes')
|
|
536
|
+
const usedHeapSizeMetric = metrics.find(metric => metric.name === 'nodejs_heap_size_used_bytes')
|
|
537
|
+
const heapSpaceSizeTotalMetric = metrics.find(metric => metric.name === 'nodejs_heap_space_size_total_bytes')
|
|
538
|
+
const newSpaceSizeTotalMetric = heapSpaceSizeTotalMetric.values.find(value => value.labels.space === 'new')
|
|
539
|
+
const oldSpaceSizeTotalMetric = heapSpaceSizeTotalMetric.values.find(value => value.labels.space === 'old')
|
|
540
|
+
const eventLoopUtilizationMetric = metrics.find(metric => metric.name === 'nodejs_eventloop_utilization')
|
|
541
|
+
|
|
542
|
+
let p50Value = 0
|
|
543
|
+
let p90Value = 0
|
|
544
|
+
let p95Value = 0
|
|
545
|
+
let p99Value = 0
|
|
546
|
+
|
|
547
|
+
const metricName = 'http_request_all_summary_seconds'
|
|
548
|
+
const httpLatencyMetrics = metrics.find(metric => metric.name === metricName)
|
|
549
|
+
|
|
550
|
+
p50Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.5).value || 0
|
|
551
|
+
p90Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.9).value || 0
|
|
552
|
+
p95Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.95).value || 0
|
|
553
|
+
p99Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.99).value || 0
|
|
554
|
+
|
|
555
|
+
p50Value = Math.round(p50Value * 1000)
|
|
556
|
+
p90Value = Math.round(p90Value * 1000)
|
|
557
|
+
p95Value = Math.round(p95Value * 1000)
|
|
558
|
+
p99Value = Math.round(p99Value * 1000)
|
|
559
|
+
|
|
560
|
+
const cpu = cpuMetric.values[0].value
|
|
561
|
+
const rss = rssMetric.values[0].value
|
|
562
|
+
const elu = eventLoopUtilizationMetric.values[0].value
|
|
563
|
+
const totalHeapSize = totalHeapSizeMetric.values[0].value
|
|
564
|
+
const usedHeapSize = usedHeapSizeMetric.values[0].value
|
|
565
|
+
const newSpaceSize = newSpaceSizeTotalMetric.value
|
|
566
|
+
const oldSpaceSize = oldSpaceSizeTotalMetric.value
|
|
567
|
+
|
|
568
|
+
const formattedMetrics = {
|
|
569
|
+
version: 1,
|
|
570
|
+
date: new Date().toISOString(),
|
|
571
|
+
cpu,
|
|
572
|
+
elu,
|
|
573
|
+
rss,
|
|
574
|
+
totalHeapSize,
|
|
575
|
+
usedHeapSize,
|
|
576
|
+
newSpaceSize,
|
|
577
|
+
oldSpaceSize,
|
|
578
|
+
entrypoint: {
|
|
579
|
+
latency: {
|
|
580
|
+
p50: p50Value,
|
|
581
|
+
p90: p90Value,
|
|
582
|
+
p95: p95Value,
|
|
583
|
+
p99: p99Value,
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return formattedMetrics
|
|
589
|
+
} catch (err) {
|
|
590
|
+
// If any metric is missing, return nothing
|
|
591
|
+
this.logger.warn({ err }, 'Cannot fetch metrics')
|
|
592
|
+
|
|
593
|
+
return null
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async getLogIds (runtimePID) {
|
|
598
|
+
runtimePID = runtimePID ?? process.pid
|
|
599
|
+
|
|
600
|
+
const runtimeLogFiles = await this.#getRuntimeLogFiles(runtimePID)
|
|
601
|
+
const runtimeLogIds = []
|
|
602
|
+
|
|
603
|
+
for (const logFile of runtimeLogFiles) {
|
|
604
|
+
const logId = parseInt(logFile.slice('logs.'.length))
|
|
605
|
+
runtimeLogIds.push(logId)
|
|
606
|
+
}
|
|
607
|
+
return runtimeLogIds
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async getAllLogIds () {
|
|
611
|
+
const runtimesLogFiles = await this.#getAllLogsFiles()
|
|
612
|
+
const runtimesLogsIds = []
|
|
613
|
+
|
|
614
|
+
for (const runtime of runtimesLogFiles) {
|
|
615
|
+
const runtimeLogIds = []
|
|
616
|
+
for (const logFile of runtime.runtimeLogFiles) {
|
|
617
|
+
const logId = parseInt(logFile.slice('logs.'.length))
|
|
618
|
+
runtimeLogIds.push(logId)
|
|
619
|
+
}
|
|
620
|
+
runtimesLogsIds.push({
|
|
621
|
+
pid: runtime.runtimePID,
|
|
622
|
+
indexes: runtimeLogIds,
|
|
623
|
+
})
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return runtimesLogsIds
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async getLogFileStream (logFileId, runtimePID) {
|
|
630
|
+
const runtimeLogsDir = this.#getRuntimeLogsDir(runtimePID)
|
|
631
|
+
const filePath = join(runtimeLogsDir, `logs.${logFileId}`)
|
|
632
|
+
return createReadStream(filePath)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
#updateStatus (status) {
|
|
636
|
+
this.#status = status
|
|
637
|
+
this.emit(status)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async #setupService (serviceConfig) {
|
|
641
|
+
const config = this.#configManager.current
|
|
642
|
+
const { autoload, restartOnError } = config
|
|
643
|
+
|
|
644
|
+
const id = serviceConfig.id
|
|
645
|
+
const { port1: loggerDestination, port2: loggingPort } = new MessageChannel()
|
|
646
|
+
loggerDestination.on('message', this.#forwardThreadLog.bind(this))
|
|
647
|
+
|
|
648
|
+
const service = new Worker(kWorkerFile, {
|
|
649
|
+
workerData: {
|
|
650
|
+
config,
|
|
651
|
+
serviceConfig,
|
|
652
|
+
dirname: this.#configManager.dirname,
|
|
653
|
+
runtimeLogsDir: this.#runtimeLogsDir,
|
|
654
|
+
loggingPort,
|
|
655
|
+
},
|
|
656
|
+
execArgv: [], // Avoid side effects
|
|
657
|
+
env: this.#env,
|
|
658
|
+
transferList: [loggingPort],
|
|
659
|
+
/*
|
|
660
|
+
Important: always set stdout and stderr to true, so that worker's output is not automatically
|
|
661
|
+
piped to the parent thread. We actually never output the thread output since we replace it
|
|
662
|
+
with PinoWritable, and disabling the piping avoids us to redeclare some internal Node.js methods.
|
|
663
|
+
|
|
664
|
+
The author of this (Paolo and Matteo) are not proud of the solution. Forgive us.
|
|
665
|
+
*/
|
|
666
|
+
stdout: true,
|
|
667
|
+
stderr: true,
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
// Make sure the listener can handle a lot of API requests at once before raising a warning
|
|
671
|
+
service.setMaxListeners(1e3)
|
|
672
|
+
|
|
673
|
+
// Track service exiting
|
|
674
|
+
service.once('exit', code => {
|
|
675
|
+
const started = this.#startedServices.get(id)
|
|
676
|
+
this.#services.delete(id)
|
|
677
|
+
loggerDestination.close()
|
|
678
|
+
loggingPort.close()
|
|
679
|
+
|
|
680
|
+
// Wait for the next tick so that crashed from the thread are logged first
|
|
681
|
+
setImmediate(() => {
|
|
682
|
+
// If this was started, it means it crashed
|
|
683
|
+
if (started && (this.#status === 'started' || this.#status === 'starting')) {
|
|
684
|
+
if (restartOnError > 0) {
|
|
685
|
+
this.#restartCrashedService(serviceConfig, code, started)
|
|
686
|
+
} else {
|
|
687
|
+
this.logger.warn(
|
|
688
|
+
`Service ${id} unexpectedly exited with code ${code}. The service is no longer available ...`
|
|
689
|
+
)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
})
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
service[kId] = id
|
|
696
|
+
service[kConfig] = serviceConfig
|
|
697
|
+
|
|
698
|
+
// Setup ITC
|
|
699
|
+
service[kITC] = new ITC({ port: service })
|
|
700
|
+
service[kITC].listen()
|
|
701
|
+
|
|
702
|
+
// Handle services changes
|
|
703
|
+
// This is not purposely activated on when this.#configManager.current.watch === true
|
|
704
|
+
// so that services can eventually manually trigger a restart. This mechanism is current
|
|
705
|
+
// used by the composer
|
|
706
|
+
service[kITC].on('changed', async () => {
|
|
707
|
+
try {
|
|
708
|
+
const wasStarted = this.#startedServices.get(id)
|
|
709
|
+
|
|
710
|
+
await this._stopService(id)
|
|
711
|
+
|
|
712
|
+
if (wasStarted) {
|
|
713
|
+
await this.startService(id)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
this.logger.info(`Service ${id} has been successfully reloaded ...`)
|
|
717
|
+
} catch (e) {
|
|
718
|
+
this.logger.error(e)
|
|
719
|
+
}
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
// Store locally
|
|
723
|
+
this.#services.set(id, service)
|
|
724
|
+
|
|
725
|
+
if (serviceConfig.entrypoint) {
|
|
726
|
+
this.#entrypoint = service
|
|
727
|
+
this.#entrypointId = id
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Setup the interceptor
|
|
731
|
+
this.#interceptor.route(id, service)
|
|
732
|
+
|
|
733
|
+
// Store dependencies
|
|
734
|
+
const [{ dependencies }] = await once(service[kITC], 'init')
|
|
735
|
+
|
|
736
|
+
if (autoload) {
|
|
737
|
+
serviceConfig.dependencies = dependencies
|
|
738
|
+
for (const { envVar, url } of dependencies) {
|
|
739
|
+
if (envVar) {
|
|
740
|
+
serviceConfig.localServiceEnvVars.set(envVar, url)
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async #restartCrashedService (serviceConfig, code, started) {
|
|
747
|
+
const restartTimeout = this.#configManager.current.restartOnError
|
|
748
|
+
const id = serviceConfig.id
|
|
749
|
+
|
|
750
|
+
this.logger.warn(`Service ${id} unexpectedly exited with code ${code}. Restarting in ${restartTimeout}ms ...`)
|
|
751
|
+
|
|
752
|
+
const timer = setTimeout(async () => {
|
|
753
|
+
try {
|
|
754
|
+
await this.#setupService(serviceConfig)
|
|
755
|
+
|
|
756
|
+
if (started) {
|
|
757
|
+
this.#startedServices.set(id, false)
|
|
758
|
+
await this.startService(id)
|
|
759
|
+
}
|
|
760
|
+
} catch (err) {
|
|
761
|
+
this.logger.error({ err }, `Failed to restart service ${id}.`)
|
|
762
|
+
}
|
|
763
|
+
}, restartTimeout).unref()
|
|
764
|
+
|
|
765
|
+
this.#crashedServicesTimers.set(id, timer)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async #getServiceById (id, ensureStarted = false, mustExist = true) {
|
|
769
|
+
const service = this.#services.get(id)
|
|
770
|
+
|
|
771
|
+
if (!service) {
|
|
772
|
+
if (!mustExist && this.#servicesIds.includes(id)) {
|
|
773
|
+
return null
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
throw new errors.ServiceNotFoundError(id, Array.from(this.#services.keys()).join(', '))
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (ensureStarted) {
|
|
780
|
+
const serviceStatus = await sendViaITC(service, 'getStatus')
|
|
781
|
+
|
|
782
|
+
if (serviceStatus !== 'started') {
|
|
783
|
+
throw new errors.ServiceNotStartedError(id)
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return service
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async #getRuntimePackageJson () {
|
|
791
|
+
const runtimeDir = this.#configManager.dirname
|
|
792
|
+
const packageJsonPath = join(runtimeDir, 'package.json')
|
|
793
|
+
const packageJsonFile = await readFile(packageJsonPath, 'utf8')
|
|
794
|
+
const packageJson = JSON.parse(packageJsonFile)
|
|
795
|
+
return packageJson
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
#getRuntimeLogsDir (runtimePID) {
|
|
799
|
+
return join(this.#runtimeTmpDir, runtimePID.toString(), 'logs')
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async #getRuntimeLogFiles (runtimePID) {
|
|
803
|
+
const runtimeLogsDir = this.#getRuntimeLogsDir(runtimePID)
|
|
804
|
+
const runtimeLogsFiles = await readdir(runtimeLogsDir)
|
|
805
|
+
return runtimeLogsFiles
|
|
806
|
+
.filter(file => file.startsWith('logs'))
|
|
807
|
+
.sort((log1, log2) => {
|
|
808
|
+
const index1 = parseInt(log1.slice('logs.'.length))
|
|
809
|
+
const index2 = parseInt(log2.slice('logs.'.length))
|
|
810
|
+
return index1 - index2
|
|
811
|
+
})
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async #getAllLogsFiles () {
|
|
815
|
+
try {
|
|
816
|
+
await access(this.#runtimeTmpDir)
|
|
817
|
+
} catch (err) {
|
|
818
|
+
this.logger.error({ err }, 'Cannot access temporary folder.')
|
|
819
|
+
return []
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const runtimePIDs = await readdir(this.#runtimeTmpDir)
|
|
823
|
+
const runtimesLogFiles = []
|
|
824
|
+
|
|
825
|
+
for (const runtimePID of runtimePIDs) {
|
|
826
|
+
const runtimeLogsDir = this.#getRuntimeLogsDir(runtimePID)
|
|
827
|
+
const runtimeLogsDirStat = await stat(runtimeLogsDir)
|
|
828
|
+
const runtimeLogFiles = await this.#getRuntimeLogFiles(runtimePID)
|
|
829
|
+
const lastModified = runtimeLogsDirStat.mtime
|
|
830
|
+
|
|
831
|
+
runtimesLogFiles.push({
|
|
832
|
+
runtimePID: parseInt(runtimePID),
|
|
833
|
+
runtimeLogFiles,
|
|
834
|
+
lastModified,
|
|
835
|
+
})
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return runtimesLogFiles.sort((runtime1, runtime2) => runtime1.lastModified - runtime2.lastModified)
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
#forwardThreadLog (message) {
|
|
842
|
+
if (!this.#loggerDestination) {
|
|
843
|
+
return
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
for (const log of message.logs) {
|
|
847
|
+
// In order to being able to forward messages serialized in the
|
|
848
|
+
// worker threads by directly writing to the destinations using multistream
|
|
849
|
+
// we unfortunately need to reparse the message to set some internal flags
|
|
850
|
+
// of the destination which are never set since we bypass pino.
|
|
851
|
+
let message = JSON.parse(log)
|
|
852
|
+
let { level, time, msg, raw } = message
|
|
853
|
+
|
|
854
|
+
try {
|
|
855
|
+
const parsed = JSON.parse(raw.trimEnd())
|
|
856
|
+
|
|
857
|
+
if (typeof parsed.level === 'number' && typeof parsed.time === 'number') {
|
|
858
|
+
level = parsed.level
|
|
859
|
+
time = parsed.time
|
|
860
|
+
message = parsed
|
|
861
|
+
} else {
|
|
862
|
+
message.raw = undefined
|
|
863
|
+
message.payload = parsed
|
|
864
|
+
}
|
|
865
|
+
} catch {
|
|
866
|
+
if (typeof message.raw === 'string') {
|
|
867
|
+
message.msg = message.raw.replace(/\n$/, '')
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
message.raw = undefined
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
this.#loggerDestination.lastLevel = level
|
|
874
|
+
this.#loggerDestination.lastTime = time
|
|
875
|
+
this.#loggerDestination.lastMsg = msg
|
|
876
|
+
this.#loggerDestination.lastObj = message
|
|
877
|
+
this.#loggerDestination.lastLogger = this.logger
|
|
878
|
+
|
|
879
|
+
// Never drop the `\n` as the worker thread trimmed the message
|
|
880
|
+
this.#loggerDestination.write(JSON.stringify(message) + '\n')
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
module.exports = { Runtime }
|