@platformatic/runtime 2.0.0-alpha.2 → 2.0.0-alpha.20

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.
Files changed (177) hide show
  1. package/config.d.ts +218 -0
  2. package/eslint.config.js +8 -0
  3. package/fixtures/botched-start/platformatic.runtime.json +1 -1
  4. package/fixtures/botched-start/services/a/platformatic.service.json +1 -1
  5. package/fixtures/composerApp/platformatic.composer.json +1 -1
  6. package/fixtures/configs/invalid-entrypoint.json +1 -1
  7. package/fixtures/configs/invalid-schema-type.config.json +1 -1
  8. package/fixtures/configs/{invalid-autoload-with-services.json → invalid-web-with-services.json} +9 -5
  9. package/fixtures/configs/missing-property.config.json +1 -1
  10. package/fixtures/configs/missing-service-config.json +1 -1
  11. package/fixtures/configs/monorepo-composer-no-autoload.json +2 -2
  12. package/fixtures/configs/monorepo-composer.json +2 -2
  13. package/fixtures/configs/monorepo-create-cycle.json +2 -2
  14. package/fixtures/configs/monorepo-missing-dependencies.json +2 -2
  15. package/fixtures/configs/monorepo-no-cycles.json +2 -2
  16. package/fixtures/configs/monorepo-openapi.json +2 -2
  17. package/fixtures/configs/{monorepo-hotreload-env.json → monorepo-watch-env.json} +2 -2
  18. package/fixtures/configs/monorepo-watch-single.json +12 -0
  19. package/fixtures/configs/monorepo-watch.json +24 -9
  20. package/fixtures/configs/monorepo-with-dependencies.json +2 -2
  21. package/fixtures/configs/monorepo-with-management-api-without-metrics.json +21 -0
  22. package/fixtures/configs/monorepo-with-management-api.json +2 -2
  23. package/fixtures/configs/{monorepo-hotreload.json → monorepo-with-metrics.json} +5 -4
  24. package/fixtures/configs/monorepo.json +2 -2
  25. package/fixtures/configs/no-services.config.json +1 -1
  26. package/fixtures/configs/no-sources.config.json +1 -1
  27. package/fixtures/configs/service-throws-on-start.json +1 -1
  28. package/fixtures/configs/service-with-env-port.json +2 -2
  29. package/fixtures/configs/service-with-stdio.json +12 -0
  30. package/fixtures/configs/{hotreload.json → watch.json} +2 -2
  31. package/fixtures/crash-on-bootstrap/platformatic.runtime.json +15 -0
  32. package/fixtures/crash-on-bootstrap/services/service-1/platformatic.service.json +14 -0
  33. package/fixtures/crash-on-bootstrap/services/service-1/plugin.js +5 -0
  34. package/fixtures/crash-on-bootstrap/services/service-2/platformatic.service.json +14 -0
  35. package/fixtures/crash-on-bootstrap/services/service-2/plugin.js +5 -0
  36. package/fixtures/dbApp/platformatic.db.json +1 -1
  37. package/fixtures/dbAppNoName/platformatic.db.json +1 -1
  38. package/fixtures/dbAppNoPackageJson/platformatic.db.json +1 -1
  39. package/fixtures/dbAppWithMigrationError/platformatic.db.json +1 -1
  40. package/fixtures/do-not-reload-dependencies/platformatic.service.json +1 -1
  41. package/fixtures/do-not-restart-on-crash/platformatic.runtime.json +3 -2
  42. package/fixtures/do-not-restart-on-crash/services/a/platformatic.service.json +8 -6
  43. package/fixtures/express/platformatic.runtime.json +1 -1
  44. package/fixtures/express/services/a/platformatic.service.json +1 -1
  45. package/fixtures/express/services/b/platformatic.service.json +1 -1
  46. package/fixtures/external-client/platformatic.service.json +1 -1
  47. package/fixtures/interceptors/idp.js +2 -2
  48. package/fixtures/interceptors/platformatic.runtime.json +1 -1
  49. package/fixtures/interceptors/services/a/platformatic.service.json +1 -1
  50. package/fixtures/interceptors-2/platformatic.runtime.json +1 -1
  51. package/fixtures/interceptors-2/services/a/platformatic.service.json +1 -1
  52. package/fixtures/leven/platformatic.runtime.json +2 -2
  53. package/fixtures/leven/services/deeply-spittle/platformatic.service.json +1 -1
  54. package/fixtures/leven/services/rainy-empire/platformatic.composer.json +1 -1
  55. package/fixtures/management-api/platformatic.json +8 -3
  56. package/fixtures/management-api/services/service-1/platformatic.json +8 -5
  57. package/fixtures/management-api/services/service-1/plugin.js +4 -3
  58. package/fixtures/management-api/services/service-2/platformatic.json +1 -1
  59. package/fixtures/management-api/services/service-db/platformatic.db.json +1 -1
  60. package/fixtures/management-api-custom-labels/platformatic.json +2 -2
  61. package/fixtures/management-api-custom-labels/services/service-1/platformatic.json +1 -1
  62. package/fixtures/management-api-custom-labels/services/service-1/plugin.js +4 -3
  63. package/fixtures/management-api-custom-labels/services/service-2/platformatic.json +1 -1
  64. package/fixtures/management-api-custom-labels/services/service-db/platformatic.db.json +1 -1
  65. package/fixtures/management-api-without-metrics/platformatic.json +3 -2
  66. package/fixtures/management-api-without-metrics/services/service-1/platformatic.json +1 -1
  67. package/fixtures/monorepo/composerApp/platformatic.composer.json +1 -1
  68. package/fixtures/monorepo/dbApp/platformatic.db.json +1 -1
  69. package/fixtures/monorepo/serviceApp/platformatic.service.json +3 -2
  70. package/fixtures/monorepo/serviceApp/with-logger/with-logger.cjs +2 -2
  71. package/fixtures/monorepo/serviceApp/with-logger/with-logger.d.ts +7 -7
  72. package/fixtures/monorepo/serviceAppWithLogger/platformatic.service.json +1 -1
  73. package/fixtures/monorepo/serviceAppWithLogger/plugin.js +12 -0
  74. package/fixtures/monorepo/serviceAppWithMultiplePlugins/platformatic.service.json +3 -2
  75. package/fixtures/monorepo-missing-dependencies/composer/platformatic.json +1 -1
  76. package/fixtures/monorepo-openapi/serviceAppWithoutOpenapi/platformatic.service.json +1 -1
  77. package/fixtures/monorepo-watch/service1/platformatic.service.json +1 -1
  78. package/fixtures/monorepo-with-dependencies/main/platformatic.json +1 -1
  79. package/fixtures/monorepo-with-dependencies/service-1/platformatic.json +1 -1
  80. package/fixtures/monorepo-with-dependencies/service-2/platformatic.json +1 -1
  81. package/fixtures/no-env.service.json +1 -1
  82. package/fixtures/preload/platformatic.runtime.json +1 -1
  83. package/fixtures/preload/services/a/platformatic.service.json +1 -1
  84. package/fixtures/prom-server/platformatic.json +2 -2
  85. package/fixtures/prom-server/services/service-1/platformatic.json +1 -1
  86. package/fixtures/prom-server/services/service-2/platformatic.json +1 -1
  87. package/fixtures/restart-on-crash/platformatic.runtime.json +1 -1
  88. package/fixtures/restart-on-crash/services/a/platformatic.service.json +8 -6
  89. package/fixtures/sample-runtime/package.json +3 -3
  90. package/fixtures/sample-runtime/platformatic.json +2 -2
  91. package/fixtures/sample-runtime/services/rival/package.json +2 -2
  92. package/fixtures/sample-runtime/services/rival/platformatic.json +1 -1
  93. package/fixtures/sample-runtime-with-2-services/package.json +3 -3
  94. package/fixtures/sample-runtime-with-2-services/platformatic.json +2 -2
  95. package/fixtures/sample-runtime-with-2-services/services/foobar/package.json +2 -2
  96. package/fixtures/sample-runtime-with-2-services/services/foobar/platformatic.json +1 -1
  97. package/fixtures/sample-runtime-with-2-services/services/rival/package.json +2 -2
  98. package/fixtures/sample-runtime-with-2-services/services/rival/platformatic.json +1 -1
  99. package/fixtures/server/logger-transport/platformatic.runtime.json +2 -2
  100. package/fixtures/server/logger-transport/services/echo/platformatic.service.json +1 -1
  101. package/fixtures/server/overrides-service/platformatic.runtime.json +2 -2
  102. package/fixtures/server/overrides-service/services/echo/platformatic.service.json +1 -1
  103. package/fixtures/server/runtime-server/platformatic.runtime.json +2 -2
  104. package/fixtures/server/runtime-server/services/echo/platformatic.service.json +1 -1
  105. package/fixtures/serviceAppThrowsOnStart/platformatic.service.json +1 -1
  106. package/fixtures/stackables/node_modules/foo/foo.js +2 -1
  107. package/fixtures/start-command-in-runtime.js +1 -1
  108. package/fixtures/stdio/platformatic.service.json +6 -0
  109. package/fixtures/stdio/plugin.js +24 -0
  110. package/fixtures/telemetry/platformatic.runtime.json +2 -2
  111. package/fixtures/telemetry/services/echo/platformatic.service.json +1 -1
  112. package/fixtures/telemetry/services/echo/routes/span.js +16 -2
  113. package/fixtures/telemetry/services/service-1/platformatic.service.json +19 -0
  114. package/fixtures/telemetry/services/service-1/routes/echo.js +7 -0
  115. package/fixtures/typescript/platformatic.runtime.json +2 -2
  116. package/fixtures/typescript/services/composer/platformatic.composer.json +1 -1
  117. package/fixtures/typescript/services/movies/global.d.ts +2 -3
  118. package/fixtures/typescript/services/movies/platformatic.db.json +1 -1
  119. package/fixtures/typescript/services/movies/types/Movie.d.ts +3 -3
  120. package/fixtures/typescript/services/movies/types/index.d.ts +6 -6
  121. package/fixtures/typescript/services/titles/client/client.d.ts +35 -35
  122. package/fixtures/typescript/services/titles/platformatic.service.json +1 -1
  123. package/fixtures/typescript-custom-flags/platformatic.runtime.json +2 -2
  124. package/fixtures/typescript-custom-flags/services/composer/platformatic.composer.json +1 -1
  125. package/fixtures/typescript-custom-flags/services/movies/global.d.ts +2 -3
  126. package/fixtures/typescript-custom-flags/services/movies/platformatic.db.json +1 -1
  127. package/fixtures/typescript-custom-flags/services/movies/types/Movie.d.ts +3 -3
  128. package/fixtures/typescript-custom-flags/services/movies/types/index.d.ts +6 -6
  129. package/fixtures/typescript-custom-flags/services/titles/client/client.d.ts +35 -35
  130. package/fixtures/typescript-custom-flags/services/titles/platformatic.service.json +1 -1
  131. package/fixtures/typescript-no-env/platformatic.runtime.json +2 -2
  132. package/fixtures/typescript-no-env/services/composer/platformatic.composer.json +1 -1
  133. package/fixtures/typescript-no-env/services/movies/global.d.ts +2 -3
  134. package/fixtures/typescript-no-env/services/movies/platformatic.db.json +1 -1
  135. package/fixtures/typescript-no-env/services/movies/types/Movie.d.ts +3 -3
  136. package/fixtures/typescript-no-env/services/movies/types/index.d.ts +6 -6
  137. package/fixtures/typescript-no-env/services/titles/client/client.d.ts +35 -35
  138. package/fixtures/typescript-no-env/services/titles/platformatic.service.json +1 -1
  139. package/index.d.ts +7 -8
  140. package/index.js +18 -10
  141. package/index.test-d.ts +10 -12
  142. package/lib/build-server.js +11 -13
  143. package/lib/compile.js +11 -10
  144. package/lib/config.js +32 -19
  145. package/lib/dependencies.js +2 -1
  146. package/lib/errors.js +6 -3
  147. package/lib/generator/errors.js +1 -1
  148. package/lib/generator/runtime-generator.d.ts +15 -15
  149. package/lib/generator/runtime-generator.js +95 -66
  150. package/lib/logger.js +55 -0
  151. package/lib/management-api.js +40 -46
  152. package/lib/prom-server.js +5 -9
  153. package/lib/runtime.js +1012 -0
  154. package/lib/schema.js +109 -91
  155. package/lib/start.js +51 -108
  156. package/lib/upgrade.js +2 -5
  157. package/lib/utils.js +65 -1
  158. package/lib/versions/v1.36.0.js +1 -1
  159. package/lib/versions/v1.5.0.js +1 -1
  160. package/lib/versions/v2.0.0.js +26 -0
  161. package/lib/worker/app.js +266 -0
  162. package/lib/worker/default-stackable.js +33 -0
  163. package/lib/worker/itc.js +162 -0
  164. package/lib/worker/main.js +135 -0
  165. package/lib/worker/metrics.js +106 -0
  166. package/lib/worker/symbols.js +7 -0
  167. package/package.json +36 -34
  168. package/runtime.mjs +5 -5
  169. package/schema.json +639 -0
  170. package/lib/api-client.js +0 -500
  171. package/lib/api.js +0 -420
  172. package/lib/app.js +0 -397
  173. package/lib/load-config.js +0 -12
  174. package/lib/loader.mjs +0 -103
  175. package/lib/message-port-writable.js +0 -50
  176. package/lib/worker.js +0 -182
  177. /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 }