@platformatic/runtime 2.0.0-alpha.2 → 2.0.0-alpha.21
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 +218 -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-entrypoint.json +1 -1
- package/fixtures/configs/invalid-schema-type.config.json +1 -1
- package/fixtures/configs/{invalid-autoload-with-services.json → invalid-web-with-services.json} +9 -5
- 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 +24 -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/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/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 +8 -6
- 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 +8 -3
- package/fixtures/management-api/services/service-1/platformatic.json +8 -5
- 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 +8 -6
- package/fixtures/sample-runtime/package.json +3 -3
- package/fixtures/sample-runtime/platformatic.json +2 -2
- package/fixtures/sample-runtime/services/rival/package.json +2 -2
- package/fixtures/sample-runtime/services/rival/platformatic.json +1 -1
- package/fixtures/sample-runtime-with-2-services/package.json +3 -3
- package/fixtures/sample-runtime-with-2-services/platformatic.json +2 -2
- package/fixtures/sample-runtime-with-2-services/services/foobar/package.json +2 -2
- package/fixtures/sample-runtime-with-2-services/services/foobar/platformatic.json +1 -1
- package/fixtures/sample-runtime-with-2-services/services/rival/package.json +2 -2
- 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/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/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 +18 -10
- package/index.test-d.ts +10 -12
- package/lib/build-server.js +11 -13
- package/lib/compile.js +11 -10
- package/lib/config.js +32 -19
- package/lib/dependencies.js +2 -1
- package/lib/errors.js +6 -3
- package/lib/generator/errors.js +1 -1
- package/lib/generator/runtime-generator.d.ts +15 -15
- package/lib/generator/runtime-generator.js +95 -66
- package/lib/logger.js +55 -0
- package/lib/management-api.js +40 -46
- package/lib/prom-server.js +5 -9
- package/lib/runtime.js +1012 -0
- package/lib/schema.js +109 -91
- package/lib/start.js +51 -108
- package/lib/upgrade.js +2 -5
- package/lib/utils.js +65 -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 +26 -0
- package/lib/worker/app.js +266 -0
- package/lib/worker/default-stackable.js +33 -0
- package/lib/worker/itc.js +162 -0
- package/lib/worker/main.js +135 -0
- package/lib/worker/metrics.js +106 -0
- package/lib/worker/symbols.js +7 -0
- package/package.json +36 -34
- package/runtime.mjs +5 -5
- package/schema.json +639 -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
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('node:events')
|
|
4
|
+
const { FileWatcher } = require('@platformatic/utils')
|
|
5
|
+
const { getGlobalDispatcher, setGlobalDispatcher } = require('undici')
|
|
6
|
+
const debounce = require('debounce')
|
|
7
|
+
|
|
8
|
+
const errors = require('../errors')
|
|
9
|
+
const defaultStackable = require('./default-stackable')
|
|
10
|
+
const { collectMetrics } = require('./metrics')
|
|
11
|
+
const { getServiceUrl, loadConfig, loadEmptyConfig } = require('../utils')
|
|
12
|
+
|
|
13
|
+
class PlatformaticApp extends EventEmitter {
|
|
14
|
+
#starting
|
|
15
|
+
#started
|
|
16
|
+
#listening
|
|
17
|
+
#watch
|
|
18
|
+
#fileWatcher
|
|
19
|
+
#metricsRegistry
|
|
20
|
+
#debouncedRestart
|
|
21
|
+
#context
|
|
22
|
+
|
|
23
|
+
constructor (appConfig, telemetryConfig, loggerConfig, serverConfig, metricsConfig, hasManagementApi, watch) {
|
|
24
|
+
super()
|
|
25
|
+
this.appConfig = appConfig
|
|
26
|
+
this.#watch = watch
|
|
27
|
+
this.#starting = false
|
|
28
|
+
this.#started = false
|
|
29
|
+
this.#listening = false
|
|
30
|
+
this.stackable = null
|
|
31
|
+
this.#fileWatcher = null
|
|
32
|
+
this.#metricsRegistry = null
|
|
33
|
+
|
|
34
|
+
this.#context = {
|
|
35
|
+
serviceId: this.appConfig.id,
|
|
36
|
+
directory: this.appConfig.path,
|
|
37
|
+
isEntrypoint: this.appConfig.entrypoint,
|
|
38
|
+
isProduction: this.appConfig.isProduction,
|
|
39
|
+
telemetryConfig,
|
|
40
|
+
metricsConfig,
|
|
41
|
+
loggerConfig,
|
|
42
|
+
serverConfig,
|
|
43
|
+
hasManagementApi: !!hasManagementApi,
|
|
44
|
+
localServiceEnvVars: this.appConfig.localServiceEnvVars
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getStatus () {
|
|
49
|
+
if (this.#starting) return 'starting'
|
|
50
|
+
if (this.#started) return 'started'
|
|
51
|
+
return 'stopped'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async updateContext (context) {
|
|
55
|
+
this.#context = { ...this.#context, ...context }
|
|
56
|
+
if (this.stackable) {
|
|
57
|
+
this.stackable.updateContext(context)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async getBootstrapDependencies () {
|
|
62
|
+
return this.stackable.getBootstrapDependencies()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async init () {
|
|
66
|
+
try {
|
|
67
|
+
const appConfig = this.appConfig
|
|
68
|
+
let loadedConfig
|
|
69
|
+
|
|
70
|
+
if (!appConfig.config) {
|
|
71
|
+
loadedConfig = await loadEmptyConfig(
|
|
72
|
+
appConfig.path,
|
|
73
|
+
{
|
|
74
|
+
onMissingEnv: this.#fetchServiceUrl,
|
|
75
|
+
context: appConfig
|
|
76
|
+
},
|
|
77
|
+
true
|
|
78
|
+
)
|
|
79
|
+
} else {
|
|
80
|
+
loadedConfig = await loadConfig(
|
|
81
|
+
{},
|
|
82
|
+
['-c', appConfig.config],
|
|
83
|
+
{
|
|
84
|
+
onMissingEnv: this.#fetchServiceUrl,
|
|
85
|
+
context: appConfig
|
|
86
|
+
},
|
|
87
|
+
true
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const app = loadedConfig.app
|
|
92
|
+
|
|
93
|
+
if (appConfig.isProduction && !process.env.NODE_ENV) {
|
|
94
|
+
process.env.NODE_ENV = 'production'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const stackable = await app.buildStackable({
|
|
98
|
+
onMissingEnv: this.#fetchServiceUrl,
|
|
99
|
+
config: this.appConfig.config,
|
|
100
|
+
context: this.#context
|
|
101
|
+
})
|
|
102
|
+
this.stackable = this.#wrapStackable(stackable)
|
|
103
|
+
|
|
104
|
+
const metricsConfig = this.#context.metricsConfig
|
|
105
|
+
if (metricsConfig !== false) {
|
|
106
|
+
this.#metricsRegistry = await collectMetrics(this.stackable, this.appConfig.id, metricsConfig)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.#updateDispatcher()
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err.validationErrors) {
|
|
112
|
+
console.error('Validation errors:', err.validationErrors)
|
|
113
|
+
process.exit(1)
|
|
114
|
+
} else {
|
|
115
|
+
this.#logAndExit(err)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async start () {
|
|
121
|
+
if (this.#starting || this.#started) {
|
|
122
|
+
throw new errors.ApplicationAlreadyStartedError()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.#starting = true
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await this.stackable.init()
|
|
129
|
+
} catch (err) {
|
|
130
|
+
this.#logAndExit(err)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (this.#watch) {
|
|
134
|
+
const watchConfig = await this.stackable.getWatchConfig()
|
|
135
|
+
if (watchConfig.enabled !== false) {
|
|
136
|
+
/* c8 ignore next 4 */
|
|
137
|
+
this.#debouncedRestart = debounce(() => {
|
|
138
|
+
this.stackable.log({ message: 'files changed', level: 'debug' })
|
|
139
|
+
this.emit('changed')
|
|
140
|
+
}, 100) // debounce restart for 100ms
|
|
141
|
+
|
|
142
|
+
this.#startFileWatching(watchConfig)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const listen = !!this.appConfig.useHttp
|
|
147
|
+
try {
|
|
148
|
+
await this.stackable.start({ listen })
|
|
149
|
+
this.#listening = listen
|
|
150
|
+
/* c8 ignore next 5 */
|
|
151
|
+
} catch (err) {
|
|
152
|
+
this.stackable.log({ message: err.message, level: 'debug' })
|
|
153
|
+
this.#starting = false
|
|
154
|
+
throw err
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.#started = true
|
|
158
|
+
this.#starting = false
|
|
159
|
+
this.emit('start')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async stop () {
|
|
163
|
+
if (!this.#started || this.#starting) {
|
|
164
|
+
throw new errors.ApplicationNotStartedError()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await this.#stopFileWatching()
|
|
168
|
+
await this.stackable.stop()
|
|
169
|
+
|
|
170
|
+
this.#started = false
|
|
171
|
+
this.#starting = false
|
|
172
|
+
this.#listening = false
|
|
173
|
+
this.emit('stop')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async listen () {
|
|
177
|
+
// This server is not an entrypoint or already listened in start. Behave as no-op.
|
|
178
|
+
if (!this.appConfig.entrypoint || this.appConfig.useHttp || this.#listening) {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await this.stackable.start({ listen: true })
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async getMetrics ({ format }) {
|
|
186
|
+
if (!this.#metricsRegistry) return null
|
|
187
|
+
|
|
188
|
+
return format === 'json' ? this.#metricsRegistry.getMetricsAsJSON() : this.#metricsRegistry.metrics()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#fetchServiceUrl (key, { parent, context: service }) {
|
|
192
|
+
if (service.localServiceEnvVars.has(key)) {
|
|
193
|
+
return service.localServiceEnvVars.get(key)
|
|
194
|
+
} else if (!key.endsWith('_URL') || !parent.serviceId) {
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return getServiceUrl(parent.serviceId)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#startFileWatching (watch) {
|
|
202
|
+
if (this.#fileWatcher) {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const fileWatcher = new FileWatcher({
|
|
207
|
+
path: watch.path,
|
|
208
|
+
/* c8 ignore next 2 */
|
|
209
|
+
allowToWatch: watch?.allow,
|
|
210
|
+
watchIgnore: watch?.ignore || []
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
fileWatcher.on('update', this.#debouncedRestart)
|
|
214
|
+
|
|
215
|
+
fileWatcher.startWatching()
|
|
216
|
+
this.stackable.log({ message: 'start watching files', level: 'debug' })
|
|
217
|
+
this.#fileWatcher = fileWatcher
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async #stopFileWatching () {
|
|
221
|
+
const watcher = this.#fileWatcher
|
|
222
|
+
|
|
223
|
+
if (watcher) {
|
|
224
|
+
this.stackable.log({ message: 'stop watching files', level: 'debug' })
|
|
225
|
+
await watcher.stopWatching()
|
|
226
|
+
this.#fileWatcher = null
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#logAndExit (err) {
|
|
231
|
+
// Runtime logs here with console.error because stackable is not initialized
|
|
232
|
+
console.error(err.message)
|
|
233
|
+
process.exit(1)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#wrapStackable (stackable) {
|
|
237
|
+
const newStackable = {}
|
|
238
|
+
for (const method of Object.keys(defaultStackable)) {
|
|
239
|
+
newStackable[method] = stackable[method] ? stackable[method].bind(stackable) : defaultStackable[method]
|
|
240
|
+
}
|
|
241
|
+
return newStackable
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#updateDispatcher () {
|
|
245
|
+
const telemetryConfig = this.#context.telemetryConfig
|
|
246
|
+
const telemetryId = telemetryConfig?.serviceName
|
|
247
|
+
|
|
248
|
+
const interceptor = dispatch => {
|
|
249
|
+
return function InterceptedDispatch (opts, handler) {
|
|
250
|
+
if (telemetryId) {
|
|
251
|
+
opts.headers = {
|
|
252
|
+
...opts.headers,
|
|
253
|
+
'x-plt-telemetry-id': telemetryId
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return dispatch(opts, handler)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const dispatcher = getGlobalDispatcher().compose(interceptor)
|
|
261
|
+
|
|
262
|
+
setGlobalDispatcher(dispatcher)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = { PlatformaticApp }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const defaultStackable = {
|
|
4
|
+
init: () => {},
|
|
5
|
+
start: () => {
|
|
6
|
+
throw new Error('Stackable start not implemented')
|
|
7
|
+
},
|
|
8
|
+
stop: () => {},
|
|
9
|
+
build: () => {},
|
|
10
|
+
getUrl: () => null,
|
|
11
|
+
updateContext: () => {},
|
|
12
|
+
getConfig: () => null,
|
|
13
|
+
getEnv: () => null,
|
|
14
|
+
getInfo: () => null,
|
|
15
|
+
getDispatchFunc: () => null,
|
|
16
|
+
getOpenapiSchema: () => null,
|
|
17
|
+
getGraphqlSchema: () => null,
|
|
18
|
+
getMeta: () => ({}),
|
|
19
|
+
collectMetrics: () => ({
|
|
20
|
+
defaultMetrics: true,
|
|
21
|
+
httpMetrics: true
|
|
22
|
+
}),
|
|
23
|
+
inject: () => {
|
|
24
|
+
throw new Error('Stackable inject not implemented')
|
|
25
|
+
},
|
|
26
|
+
log: ({ message }) => {
|
|
27
|
+
console.log(message)
|
|
28
|
+
},
|
|
29
|
+
getBootstrapDependencies: () => [],
|
|
30
|
+
getWatchConfig: () => ({ enabled: false })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = defaultStackable
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { once } = require('node:events')
|
|
4
|
+
const { parentPort } = require('node:worker_threads')
|
|
5
|
+
|
|
6
|
+
const { ITC } = require('@platformatic/itc')
|
|
7
|
+
const { Unpromise } = require('@watchable/unpromise')
|
|
8
|
+
|
|
9
|
+
const errors = require('../errors')
|
|
10
|
+
const { kITC, kId } = require('./symbols')
|
|
11
|
+
|
|
12
|
+
async function safeHandleInITC (worker, fn) {
|
|
13
|
+
try {
|
|
14
|
+
// Make sure to catch when the worker exits, otherwise we're stuck forever
|
|
15
|
+
const ac = new AbortController()
|
|
16
|
+
let exitCode
|
|
17
|
+
|
|
18
|
+
const response = await Unpromise.race([
|
|
19
|
+
fn(),
|
|
20
|
+
once(worker, 'exit', { signal: ac.signal }).then(([code]) => {
|
|
21
|
+
exitCode = code
|
|
22
|
+
})
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
if (typeof exitCode === 'number') {
|
|
26
|
+
throw new errors.ServiceExitedError(worker[kId], exitCode)
|
|
27
|
+
} else {
|
|
28
|
+
ac.abort()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return response
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (!error.handlerError) {
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (error.handlerErrorCode && !error.handlerError.code) {
|
|
38
|
+
error.handlerError.code = error.handlerErrorCode
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw error.handlerError
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function sendViaITC (worker, name, message) {
|
|
46
|
+
return safeHandleInITC(worker, () => worker[kITC].send(name, message))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function waitEventFromITC (worker, event) {
|
|
50
|
+
return safeHandleInITC(worker, () => once(worker[kITC], event))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function setupITC (app, service, dispatcher) {
|
|
54
|
+
const itc = new ITC({
|
|
55
|
+
name: app.appConfig.id + '-worker',
|
|
56
|
+
port: parentPort,
|
|
57
|
+
handlers: {
|
|
58
|
+
async start () {
|
|
59
|
+
const status = app.getStatus()
|
|
60
|
+
|
|
61
|
+
if (status === 'starting') {
|
|
62
|
+
await once(app, 'start')
|
|
63
|
+
} else {
|
|
64
|
+
await app.start()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (service.entrypoint) {
|
|
68
|
+
await app.listen()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const url = app.stackable.getUrl()
|
|
72
|
+
|
|
73
|
+
const dispatchFunc = await app.stackable.getDispatchFunc()
|
|
74
|
+
dispatcher.replaceServer(url ?? dispatchFunc)
|
|
75
|
+
|
|
76
|
+
return service.entrypoint ? url : null
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async stop () {
|
|
80
|
+
const status = app.getStatus()
|
|
81
|
+
|
|
82
|
+
if (status === 'starting') {
|
|
83
|
+
await once(app, 'start')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (status !== 'stopped') {
|
|
87
|
+
await app.stop()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
dispatcher.interceptor.close()
|
|
91
|
+
itc.close()
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async build () {
|
|
95
|
+
return app.stackable.build()
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
getStatus () {
|
|
99
|
+
return app.getStatus()
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
getServiceInfo () {
|
|
103
|
+
return app.stackable.getInfo()
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async getServiceConfig () {
|
|
107
|
+
const current = await app.stackable.getConfig()
|
|
108
|
+
// Remove all undefined keys from the config
|
|
109
|
+
return JSON.parse(JSON.stringify(current))
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
async getServiceEnv () {
|
|
113
|
+
// Remove all undefined keys from the config
|
|
114
|
+
return JSON.parse(JSON.stringify({ ...process.env, ...(await app.stackable.getEnv()) }))
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async getServiceOpenAPISchema () {
|
|
118
|
+
try {
|
|
119
|
+
return await app.stackable.getOpenapiSchema()
|
|
120
|
+
} catch (err) {
|
|
121
|
+
throw new errors.FailedToRetrieveOpenAPISchemaError(service.id, err.message)
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async getServiceGraphQLSchema () {
|
|
126
|
+
try {
|
|
127
|
+
return await app.stackable.getGraphqlSchema()
|
|
128
|
+
} catch (err) {
|
|
129
|
+
throw new errors.FailedToRetrieveGraphQLSchemaError(service.id, err.message)
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async getServiceMeta () {
|
|
134
|
+
try {
|
|
135
|
+
return await app.stackable.getMeta()
|
|
136
|
+
} catch (err) {
|
|
137
|
+
throw new errors.FailedToRetrieveMetaError(service.id, err.message)
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
async getMetrics (format) {
|
|
142
|
+
try {
|
|
143
|
+
return await app.getMetrics({ format })
|
|
144
|
+
} catch (err) {
|
|
145
|
+
throw new errors.FailedToRetrieveMetricsError(service.id, err.message)
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
inject (injectParams) {
|
|
150
|
+
return app.stackable.inject(injectParams)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
app.on('changed', () => {
|
|
156
|
+
itc.notify('changed')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
return itc
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { sendViaITC, setupITC, waitEventFromITC }
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { createRequire } = require('node:module')
|
|
4
|
+
const { join } = require('node:path')
|
|
5
|
+
const { parentPort, workerData, threadId } = require('node:worker_threads')
|
|
6
|
+
const { pathToFileURL } = require('node:url')
|
|
7
|
+
|
|
8
|
+
const pino = require('pino')
|
|
9
|
+
const { fetch, setGlobalDispatcher, Agent } = require('undici')
|
|
10
|
+
const { wire } = require('undici-thread-interceptor')
|
|
11
|
+
|
|
12
|
+
const { PlatformaticApp } = require('./app')
|
|
13
|
+
const { setupITC } = require('./itc')
|
|
14
|
+
const loadInterceptors = require('./interceptors')
|
|
15
|
+
const {
|
|
16
|
+
MessagePortWritable,
|
|
17
|
+
createPinoWritable,
|
|
18
|
+
executeWithTimeout,
|
|
19
|
+
ensureLoggableError
|
|
20
|
+
} = require('@platformatic/utils')
|
|
21
|
+
const { kId, kITC } = require('./symbols')
|
|
22
|
+
|
|
23
|
+
process.on('uncaughtException', handleUnhandled.bind(null, 'uncaught exception'))
|
|
24
|
+
process.on('unhandledRejection', handleUnhandled.bind(null, 'unhandled rejection'))
|
|
25
|
+
|
|
26
|
+
globalThis.fetch = fetch
|
|
27
|
+
globalThis[kId] = threadId
|
|
28
|
+
|
|
29
|
+
let app
|
|
30
|
+
const config = workerData.config
|
|
31
|
+
globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, { logger: createLogger() })
|
|
32
|
+
|
|
33
|
+
function handleUnhandled (type, err) {
|
|
34
|
+
globalThis.platformatic.logger.error(
|
|
35
|
+
{ err: ensureLoggableError(err) },
|
|
36
|
+
`Service ${workerData.serviceConfig.id} threw an ${type}.`
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
executeWithTimeout(app?.stop(), 1000)
|
|
40
|
+
.catch()
|
|
41
|
+
.finally(() => {
|
|
42
|
+
process.exit(1)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createLogger () {
|
|
47
|
+
const destination = new MessagePortWritable({ port: workerData.loggingPort })
|
|
48
|
+
const loggerInstance = pino({ level: 'trace', name: workerData.serviceConfig.id }, destination)
|
|
49
|
+
|
|
50
|
+
Reflect.defineProperty(process, 'stdout', { value: createPinoWritable(loggerInstance, 'info') })
|
|
51
|
+
Reflect.defineProperty(process, 'stderr', { value: createPinoWritable(loggerInstance, 'error') })
|
|
52
|
+
|
|
53
|
+
return loggerInstance
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function main () {
|
|
57
|
+
if (config.preload) {
|
|
58
|
+
await import(pathToFileURL(config.preload))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const service = workerData.serviceConfig
|
|
62
|
+
|
|
63
|
+
// Setup undici
|
|
64
|
+
const interceptors = {}
|
|
65
|
+
const composedInterceptors = []
|
|
66
|
+
|
|
67
|
+
if (config.undici?.interceptors) {
|
|
68
|
+
const _require = createRequire(join(workerData.dirname, 'package.json'))
|
|
69
|
+
for (const key of ['Agent', 'Pool', 'Client']) {
|
|
70
|
+
if (config.undici.interceptors[key]) {
|
|
71
|
+
interceptors[key] = await loadInterceptors(_require, config.undici.interceptors[key])
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(config.undici.interceptors)) {
|
|
76
|
+
composedInterceptors.push(...(await loadInterceptors(_require, config.undici.interceptors)))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const globalDispatcher = new Agent({
|
|
81
|
+
...config.undici,
|
|
82
|
+
interceptors
|
|
83
|
+
}).compose(composedInterceptors)
|
|
84
|
+
|
|
85
|
+
setGlobalDispatcher(globalDispatcher)
|
|
86
|
+
|
|
87
|
+
// Setup mesh networker
|
|
88
|
+
const threadDispatcher = wire({ port: parentPort, useNetwork: service.useHttp, timeout: true })
|
|
89
|
+
|
|
90
|
+
// If the service is an entrypoint and runtime server config is defined, use it.
|
|
91
|
+
let serverConfig = null
|
|
92
|
+
if (config.server && service.entrypoint) {
|
|
93
|
+
serverConfig = config.server
|
|
94
|
+
} else if (service.useHttp) {
|
|
95
|
+
serverConfig = {
|
|
96
|
+
port: 0,
|
|
97
|
+
hostname: '127.0.0.1',
|
|
98
|
+
keepAliveTimeout: 5000
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let telemetryConfig = config.telemetry
|
|
103
|
+
if (telemetryConfig) {
|
|
104
|
+
telemetryConfig = {
|
|
105
|
+
...telemetryConfig,
|
|
106
|
+
serviceName: `${telemetryConfig.serviceName}-${service.id}`
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Create the application
|
|
111
|
+
app = new PlatformaticApp(
|
|
112
|
+
service,
|
|
113
|
+
telemetryConfig,
|
|
114
|
+
config.logger,
|
|
115
|
+
serverConfig,
|
|
116
|
+
config.metrics,
|
|
117
|
+
!!config.managementApi,
|
|
118
|
+
!!config.watch
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
await app.init()
|
|
122
|
+
|
|
123
|
+
// Setup interaction with parent port
|
|
124
|
+
const itc = setupITC(app, service, threadDispatcher)
|
|
125
|
+
|
|
126
|
+
// Get the dependencies
|
|
127
|
+
const dependencies = config.autoload ? await app.getBootstrapDependencies() : []
|
|
128
|
+
itc.notify('init', { dependencies })
|
|
129
|
+
itc.listen()
|
|
130
|
+
|
|
131
|
+
globalThis[kITC] = itc
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// No need to catch this because there is the unhadledRejection handler on top.
|
|
135
|
+
main()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const os = require('node:os')
|
|
4
|
+
const { eventLoopUtilization } = require('node:perf_hooks').performance
|
|
5
|
+
const { Registry, Gauge, collectDefaultMetrics } = require('prom-client')
|
|
6
|
+
const collectHttpMetrics = require('@platformatic/http-metrics')
|
|
7
|
+
|
|
8
|
+
async function collectMetrics (stackable, serviceId, opts = {}) {
|
|
9
|
+
const registry = new Registry()
|
|
10
|
+
const metricsConfig = await stackable.collectMetrics({ registry })
|
|
11
|
+
|
|
12
|
+
const labels = opts.labels ?? {}
|
|
13
|
+
registry.setDefaultLabels({ ...labels, serviceId })
|
|
14
|
+
|
|
15
|
+
if (metricsConfig.defaultMetrics) {
|
|
16
|
+
collectDefaultMetrics({ register: registry })
|
|
17
|
+
collectEluMetric(registry)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (metricsConfig.httpMetrics) {
|
|
21
|
+
collectHttpMetrics(registry, {
|
|
22
|
+
customLabels: ['telemetry_id'],
|
|
23
|
+
getCustomLabels: (req) => {
|
|
24
|
+
const telemetryId = req.headers['x-plt-telemetry-id'] ?? 'unknown'
|
|
25
|
+
return { telemetry_id: telemetryId }
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// TODO: check if it's a nodejs environment
|
|
30
|
+
// Needed for the Meraki metrics
|
|
31
|
+
collectHttpMetrics(registry, {
|
|
32
|
+
customLabels: ['telemetry_id'],
|
|
33
|
+
getCustomLabels: (req) => {
|
|
34
|
+
const telemetryId = req.headers['x-plt-telemetry-id'] ?? 'unknown'
|
|
35
|
+
return { telemetry_id: telemetryId }
|
|
36
|
+
},
|
|
37
|
+
histogram: {
|
|
38
|
+
name: 'http_request_all_duration_seconds',
|
|
39
|
+
help: 'request duration in seconds summary for all requests',
|
|
40
|
+
collect: function () {
|
|
41
|
+
process.nextTick(() => this.reset())
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
summary: {
|
|
45
|
+
name: 'http_request_all_summary_seconds',
|
|
46
|
+
help: 'request duration in seconds histogram for all requests',
|
|
47
|
+
collect: function () {
|
|
48
|
+
process.nextTick(() => this.reset())
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return registry
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function collectEluMetric (register) {
|
|
58
|
+
let startELU = eventLoopUtilization()
|
|
59
|
+
const eluMetric = new Gauge({
|
|
60
|
+
name: 'nodejs_eventloop_utilization',
|
|
61
|
+
help: 'The event loop utilization as a fraction of the loop time. 1 is fully utilized, 0 is fully idle.',
|
|
62
|
+
collect: () => {
|
|
63
|
+
const endELU = eventLoopUtilization()
|
|
64
|
+
const result = eventLoopUtilization(endELU, startELU).utilization
|
|
65
|
+
eluMetric.set(result)
|
|
66
|
+
startELU = endELU
|
|
67
|
+
},
|
|
68
|
+
registers: [register],
|
|
69
|
+
})
|
|
70
|
+
register.registerMetric(eluMetric)
|
|
71
|
+
|
|
72
|
+
let previousIdleTime = 0
|
|
73
|
+
let previousTotalTime = 0
|
|
74
|
+
const cpuMetric = new Gauge({
|
|
75
|
+
name: 'process_cpu_percent_usage',
|
|
76
|
+
help: 'The process CPU percent usage.',
|
|
77
|
+
collect: () => {
|
|
78
|
+
const cpus = os.cpus()
|
|
79
|
+
let idleTime = 0
|
|
80
|
+
let totalTime = 0
|
|
81
|
+
|
|
82
|
+
cpus.forEach(cpu => {
|
|
83
|
+
for (const type in cpu.times) {
|
|
84
|
+
totalTime += cpu.times[type]
|
|
85
|
+
if (type === 'idle') {
|
|
86
|
+
idleTime += cpu.times[type]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const idleDiff = idleTime - previousIdleTime
|
|
92
|
+
const totalDiff = totalTime - previousTotalTime
|
|
93
|
+
|
|
94
|
+
const usagePercent = 100 - ((100 * idleDiff) / totalDiff)
|
|
95
|
+
const roundedUsage = Math.round(usagePercent * 100) / 100
|
|
96
|
+
cpuMetric.set(roundedUsage)
|
|
97
|
+
|
|
98
|
+
previousIdleTime = idleTime
|
|
99
|
+
previousTotalTime = totalTime
|
|
100
|
+
},
|
|
101
|
+
registers: [register],
|
|
102
|
+
})
|
|
103
|
+
register.registerMetric(cpuMetric)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { collectMetrics }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const kConfig = Symbol.for('plt.runtime.config')
|
|
4
|
+
const kId = Symbol.for('plt.runtime.id') // This is also used to detect if we are running in a Platformatic runtime thread
|
|
5
|
+
const kITC = Symbol.for('plt.runtime.itc')
|
|
6
|
+
|
|
7
|
+
module.exports = { kConfig, kId, kITC }
|