@platformatic/runtime 2.0.0-alpha.1 → 2.0.0-alpha.10

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 +284 -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-autoload-with-services.json +1 -1
  7. package/fixtures/configs/invalid-entrypoint.json +1 -1
  8. package/fixtures/configs/invalid-schema-type.config.json +1 -1
  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 +26 -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 +1 -1
  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 +1 -1
  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 +1 -1
  89. package/fixtures/sample-runtime/package.json +1 -1
  90. package/fixtures/sample-runtime/platformatic.json +2 -2
  91. package/fixtures/sample-runtime/services/rival/package.json +1 -1
  92. package/fixtures/sample-runtime/services/rival/platformatic.json +1 -1
  93. package/fixtures/sample-runtime-with-2-services/package.json +1 -1
  94. package/fixtures/sample-runtime-with-2-services/platformatic.json +2 -2
  95. package/fixtures/sample-runtime-with-2-services/services/foobar/package.json +1 -1
  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 +1 -1
  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 +15 -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 +21 -14
  145. package/lib/dependencies.js +2 -1
  146. package/lib/errors.js +5 -2
  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 +93 -64
  150. package/lib/logger.js +55 -0
  151. package/lib/management-api.js +29 -44
  152. package/lib/prom-server.js +5 -9
  153. package/lib/runtime.js +1005 -0
  154. package/lib/schema.js +46 -41
  155. package/lib/start.js +42 -109
  156. package/lib/upgrade.js +4 -3
  157. package/lib/utils.js +49 -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 +17 -0
  161. package/lib/worker/app.js +266 -0
  162. package/lib/worker/default-stackable.js +32 -0
  163. package/lib/worker/itc.js +149 -0
  164. package/lib/worker/main.js +129 -0
  165. package/lib/worker/metrics.js +106 -0
  166. package/lib/worker/symbols.js +7 -0
  167. package/package.json +34 -31
  168. package/runtime.mjs +5 -5
  169. package/schema.json +840 -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, serverConfig, hasManagementApi, watch, metricsConfig) {
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
+ serverConfig,
42
+ hasManagementApi: !!hasManagementApi,
43
+ localServiceEnvVars: this.appConfig.localServiceEnvVars
44
+ }
45
+ }
46
+
47
+ getStatus () {
48
+ if (this.#starting) return 'starting'
49
+ if (this.#started) return 'started'
50
+ return 'stopped'
51
+ }
52
+
53
+ async updateContext (context) {
54
+ this.#context = { ...this.#context, ...context }
55
+ if (this.stackable) {
56
+ this.stackable.updateContext(context)
57
+ }
58
+ }
59
+
60
+ async getBootstrapDependencies () {
61
+ return this.stackable.getBootstrapDependencies()
62
+ }
63
+
64
+ async init () {
65
+ try {
66
+ const appConfig = this.appConfig
67
+ let loadedConfig
68
+
69
+ if (!appConfig.config) {
70
+ loadedConfig = await loadEmptyConfig(
71
+ appConfig.path,
72
+ {
73
+ onMissingEnv: this.#fetchServiceUrl,
74
+ context: appConfig
75
+ },
76
+ true
77
+ )
78
+ } else {
79
+ loadedConfig = await loadConfig(
80
+ {},
81
+ ['-c', appConfig.config],
82
+ {
83
+ onMissingEnv: this.#fetchServiceUrl,
84
+ context: appConfig
85
+ },
86
+ true
87
+ )
88
+ }
89
+
90
+ const app = loadedConfig.app
91
+
92
+ if (appConfig.isProduction && !process.env.NODE_ENV) {
93
+ process.env.NODE_ENV = 'production'
94
+ }
95
+
96
+ const stackable = await app.buildStackable({
97
+ onMissingEnv: this.#fetchServiceUrl,
98
+ config: this.appConfig.config,
99
+ context: this.#context
100
+ })
101
+ this.stackable = this.#wrapStackable(stackable)
102
+
103
+ const metricsConfig = this.#context.metricsConfig
104
+ if (metricsConfig !== false) {
105
+ this.#metricsRegistry = await collectMetrics(
106
+ this.stackable,
107
+ this.appConfig.id,
108
+ metricsConfig
109
+ )
110
+ }
111
+
112
+ this.#updateDispatcher()
113
+ } catch (err) {
114
+ this.#logAndExit(err)
115
+ }
116
+ }
117
+
118
+ async start () {
119
+ if (this.#starting || this.#started) {
120
+ throw new errors.ApplicationAlreadyStartedError()
121
+ }
122
+
123
+ this.#starting = true
124
+
125
+ try {
126
+ await this.stackable.init()
127
+ } catch (err) {
128
+ this.#logAndExit(err)
129
+ }
130
+
131
+ if (this.#watch) {
132
+ const watchConfig = await this.stackable.getWatchConfig()
133
+ if (watchConfig.enabled !== false) {
134
+ /* c8 ignore next 4 */
135
+ this.#debouncedRestart = debounce(() => {
136
+ this.stackable.log({ message: 'files changed', level: 'debug' })
137
+ this.emit('changed')
138
+ }, 100) // debounce restart for 100ms
139
+
140
+ this.#startFileWatching(watchConfig)
141
+ }
142
+ }
143
+
144
+ const listen = !!this.appConfig.useHttp
145
+ try {
146
+ await this.stackable.start({ listen })
147
+ this.#listening = listen
148
+ /* c8 ignore next 5 */
149
+ } catch (err) {
150
+ this.stackable.log({ message: err.message, level: 'debug' })
151
+ this.#starting = false
152
+ throw err
153
+ }
154
+
155
+ this.#started = true
156
+ this.#starting = false
157
+ this.emit('start')
158
+ }
159
+
160
+ async stop () {
161
+ if (!this.#started || this.#starting) {
162
+ throw new errors.ApplicationNotStartedError()
163
+ }
164
+
165
+ await this.#stopFileWatching()
166
+ await this.stackable.stop()
167
+
168
+ this.#started = false
169
+ this.#starting = false
170
+ this.#listening = false
171
+ this.emit('stop')
172
+ }
173
+
174
+ async listen () {
175
+ // This server is not an entrypoint or already listened in start. Behave as no-op.
176
+ if (!this.appConfig.entrypoint || this.appConfig.useHttp || this.#listening) {
177
+ return
178
+ }
179
+
180
+ await this.stackable.start({ listen: true })
181
+ }
182
+
183
+ async getMetrics ({ format }) {
184
+ if (!this.#metricsRegistry) return null
185
+
186
+ return format === 'json'
187
+ ? this.#metricsRegistry.getMetricsAsJSON()
188
+ : 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,32 @@
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
+ getInfo: () => null,
14
+ getDispatchFunc: () => null,
15
+ getOpenapiSchema: () => null,
16
+ getGraphqlSchema: () => null,
17
+ getMeta: () => ({}),
18
+ collectMetrics: () => ({
19
+ defaultMetrics: true,
20
+ httpMetrics: true,
21
+ }),
22
+ inject: () => {
23
+ throw new Error('Stackable inject not implemented')
24
+ },
25
+ log: ({ message }) => {
26
+ console.log(message)
27
+ },
28
+ getBootstrapDependencies: () => [],
29
+ getWatchConfig: () => ({ enabled: false }),
30
+ }
31
+
32
+ module.exports = defaultStackable
@@ -0,0 +1,149 @@
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 sendViaITC (worker, name, message) {
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
+ worker[kITC].send(name, message),
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
+ function setupITC (app, service, dispatcher) {
46
+ const itc = new ITC({
47
+ name: app.appConfig.id + '-worker',
48
+ port: parentPort,
49
+ handlers: {
50
+ async start () {
51
+ const status = app.getStatus()
52
+
53
+ if (status === 'starting') {
54
+ await once(app, 'start')
55
+ } else {
56
+ await app.start()
57
+ }
58
+
59
+ if (service.entrypoint) {
60
+ await app.listen()
61
+ }
62
+
63
+ const url = app.stackable.getUrl()
64
+
65
+ const dispatchFunc = await app.stackable.getDispatchFunc()
66
+ dispatcher.replaceServer(url ?? dispatchFunc)
67
+
68
+ return service.entrypoint ? url : null
69
+ },
70
+
71
+ async stop () {
72
+ const status = app.getStatus()
73
+
74
+ if (status === 'starting') {
75
+ await once(app, 'start')
76
+ }
77
+
78
+ if (status !== 'stopped') {
79
+ await app.stop()
80
+ }
81
+
82
+ dispatcher.interceptor.close()
83
+ itc.close()
84
+ },
85
+
86
+ async build () {
87
+ return app.stackable.build()
88
+ },
89
+
90
+ getStatus () {
91
+ return app.getStatus()
92
+ },
93
+
94
+ getServiceInfo () {
95
+ return app.stackable.getInfo()
96
+ },
97
+
98
+ async getServiceConfig () {
99
+ const current = await app.stackable.getConfig()
100
+ // Remove all undefined keys from the config
101
+ return JSON.parse(JSON.stringify(current))
102
+ },
103
+
104
+ async getServiceOpenAPISchema () {
105
+ try {
106
+ return await app.stackable.getOpenapiSchema()
107
+ } catch (err) {
108
+ throw new errors.FailedToRetrieveOpenAPISchemaError(service.id, err.message)
109
+ }
110
+ },
111
+
112
+ async getServiceGraphQLSchema () {
113
+ try {
114
+ return await app.stackable.getGraphqlSchema()
115
+ } catch (err) {
116
+ throw new errors.FailedToRetrieveGraphQLSchemaError(service.id, err.message)
117
+ }
118
+ },
119
+
120
+ async getServiceMeta () {
121
+ try {
122
+ return await app.stackable.getMeta()
123
+ } catch (err) {
124
+ throw new errors.FailedToRetrieveMetaError(service.id, err.message)
125
+ }
126
+ },
127
+
128
+ async getMetrics (format) {
129
+ try {
130
+ return await app.getMetrics({ format })
131
+ } catch (err) {
132
+ throw new errors.FailedToRetrieveMetricsError(service.id, err.message)
133
+ }
134
+ },
135
+
136
+ inject (injectParams) {
137
+ return app.stackable.inject(injectParams)
138
+ }
139
+ }
140
+ })
141
+
142
+ app.on('changed', () => {
143
+ itc.notify('changed')
144
+ })
145
+
146
+ return itc
147
+ }
148
+
149
+ module.exports = { sendViaITC, setupITC }
@@ -0,0 +1,129 @@
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 { MessagePortWritable, createPinoWritable, executeWithTimeout, errors } = require('@platformatic/utils')
16
+ const { kId, kITC } = require('./symbols')
17
+
18
+ process.on('uncaughtException', handleUnhandled.bind(null, 'uncaught exception'))
19
+ process.on('unhandledRejection', handleUnhandled.bind(null, 'unhandled rejection'))
20
+
21
+ globalThis.fetch = fetch
22
+ globalThis[kId] = threadId
23
+
24
+ let app
25
+ const config = workerData.config
26
+ globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, { logger: createLogger() })
27
+
28
+ function handleUnhandled (type, err) {
29
+ globalThis.platformatic.logger.error(
30
+ { err: errors.ensureLoggableError(err) },
31
+ `Service ${workerData.serviceConfig.id} threw an ${type}.`
32
+ )
33
+
34
+ executeWithTimeout(app?.stop(), 1000)
35
+ .catch()
36
+ .finally(() => {
37
+ process.exit(1)
38
+ })
39
+ }
40
+
41
+ function createLogger () {
42
+ const destination = new MessagePortWritable({ port: workerData.loggingPort })
43
+ const loggerInstance = pino({ level: 'trace', name: workerData.serviceConfig.id }, destination)
44
+
45
+ Reflect.defineProperty(process, 'stdout', { value: createPinoWritable(loggerInstance, 'info') })
46
+ Reflect.defineProperty(process, 'stderr', { value: createPinoWritable(loggerInstance, 'error') })
47
+
48
+ return loggerInstance
49
+ }
50
+
51
+ async function main () {
52
+ if (config.preload) {
53
+ await import(pathToFileURL(config.preload))
54
+ }
55
+
56
+ const service = workerData.serviceConfig
57
+
58
+ // Setup undici
59
+ const interceptors = {}
60
+ const composedInterceptors = []
61
+
62
+ if (config.undici?.interceptors) {
63
+ const _require = createRequire(join(workerData.dirname, 'package.json'))
64
+ for (const key of ['Agent', 'Pool', 'Client']) {
65
+ if (config.undici.interceptors[key]) {
66
+ interceptors[key] = await loadInterceptors(_require, config.undici.interceptors[key])
67
+ }
68
+ }
69
+
70
+ if (Array.isArray(config.undici.interceptors)) {
71
+ composedInterceptors.push(...(await loadInterceptors(_require, config.undici.interceptors)))
72
+ }
73
+ }
74
+
75
+ const globalDispatcher = new Agent({
76
+ ...config.undici,
77
+ interceptors
78
+ }).compose(composedInterceptors)
79
+
80
+ setGlobalDispatcher(globalDispatcher)
81
+
82
+ // Setup mesh networker
83
+ const threadDispatcher = wire({ port: parentPort, useNetwork: service.useHttp, timeout: true })
84
+
85
+ // If the service is an entrypoint and runtime server config is defined, use it.
86
+ let serverConfig = null
87
+ if (config.server && service.entrypoint) {
88
+ serverConfig = config.server
89
+ } else if (service.useHttp) {
90
+ serverConfig = {
91
+ port: 0,
92
+ hostname: '127.0.0.1',
93
+ keepAliveTimeout: 5000
94
+ }
95
+ }
96
+
97
+ let telemetryConfig = config.telemetry
98
+ if (telemetryConfig) {
99
+ telemetryConfig = {
100
+ ...telemetryConfig,
101
+ serviceName: `${telemetryConfig.serviceName}-${service.id}`
102
+ }
103
+ }
104
+
105
+ // Create the application
106
+ app = new PlatformaticApp(
107
+ service,
108
+ telemetryConfig,
109
+ serverConfig,
110
+ !!config.managementApi,
111
+ !!config.watch,
112
+ config.metrics
113
+ )
114
+
115
+ await app.init()
116
+
117
+ // Setup interaction with parent port
118
+ const itc = setupITC(app, service, threadDispatcher)
119
+
120
+ // Get the dependencies
121
+ const dependencies = config.autoload ? await app.getBootstrapDependencies() : []
122
+ itc.notify('init', { dependencies })
123
+ itc.listen()
124
+
125
+ globalThis[kITC] = itc
126
+ }
127
+
128
+ // No need to catch this because there is the unhadledRejection handler on top.
129
+ 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 }