@platformatic/watt-extra 0.1.0
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/README.md +87 -0
- package/app.js +124 -0
- package/cli.js +141 -0
- package/clients/compliance/compliance-types.d.ts +887 -0
- package/clients/compliance/compliance.mjs +1049 -0
- package/clients/compliance/compliance.openapi.json +6127 -0
- package/clients/control-plane/control-plane-types.d.ts +2696 -0
- package/clients/control-plane/control-plane.mjs +3051 -0
- package/clients/control-plane/control-plane.openapi.json +13693 -0
- package/clients/cron/cron-types.d.ts +1479 -0
- package/clients/cron/cron.mjs +872 -0
- package/clients/cron/cron.openapi.json +9330 -0
- package/compliance/index.js +21 -0
- package/compliance/rules/dependencies.js +76 -0
- package/compliance/rules/utils.js +12 -0
- package/eslint.config.js +11 -0
- package/help/start.txt +12 -0
- package/help/watt-extra.txt +12 -0
- package/index.js +45 -0
- package/lib/banner.js +22 -0
- package/lib/errors.js +34 -0
- package/lib/utils.js +34 -0
- package/lib/wattpro.js +580 -0
- package/package.json +50 -0
- package/plugins/alerts.js +115 -0
- package/plugins/auth.js +89 -0
- package/plugins/compliancy.js +70 -0
- package/plugins/env.js +58 -0
- package/plugins/flamegraphs.js +100 -0
- package/plugins/init.js +70 -0
- package/plugins/metadata.js +84 -0
- package/plugins/scheduler.js +48 -0
- package/plugins/update.js +128 -0
- package/renovate.json +6 -0
- package/test/alerts.test.js +607 -0
- package/test/auth.test.js +128 -0
- package/test/auto-cache.test.js +401 -0
- package/test/cli.test.js +75 -0
- package/test/compliancy.test.js +87 -0
- package/test/fixtures/runtime-domains/alpha/package.json +5 -0
- package/test/fixtures/runtime-domains/alpha/platformatic.json +6 -0
- package/test/fixtures/runtime-domains/alpha/plugin.js +16 -0
- package/test/fixtures/runtime-domains/beta/package.json +5 -0
- package/test/fixtures/runtime-domains/beta/platformatic.json +6 -0
- package/test/fixtures/runtime-domains/beta/plugin.js +7 -0
- package/test/fixtures/runtime-domains/composer/package.json +5 -0
- package/test/fixtures/runtime-domains/composer/platformatic.json +19 -0
- package/test/fixtures/runtime-domains/package.json +1 -0
- package/test/fixtures/runtime-domains/platformatic.json +27 -0
- package/test/fixtures/runtime-health/package.json +20 -0
- package/test/fixtures/runtime-health/platformatic.json +16 -0
- package/test/fixtures/runtime-health/services/service-1/package.json +17 -0
- package/test/fixtures/runtime-health/services/service-1/platformatic.json +16 -0
- package/test/fixtures/runtime-health/services/service-1/plugins/example.js +6 -0
- package/test/fixtures/runtime-health/services/service-1/routes/root.cjs +8 -0
- package/test/fixtures/runtime-health/services/service-2/package.json +17 -0
- package/test/fixtures/runtime-health/services/service-2/platformatic.json +16 -0
- package/test/fixtures/runtime-health/services/service-2/plugins/example.js +6 -0
- package/test/fixtures/runtime-health/services/service-2/routes/root.cjs +8 -0
- package/test/fixtures/runtime-next/package.json +5 -0
- package/test/fixtures/runtime-next/platformatic.json +9 -0
- package/test/fixtures/runtime-next/web/next/next.config.js +2 -0
- package/test/fixtures/runtime-next/web/next/package.json +7 -0
- package/test/fixtures/runtime-next/web/next/platformatic.json +9 -0
- package/test/fixtures/runtime-next/web/next/src/app/direct/route.js +3 -0
- package/test/fixtures/runtime-next/web/next/src/app/layout.jsx +7 -0
- package/test/fixtures/runtime-next/web/next/src/app/page.jsx +3 -0
- package/test/fixtures/runtime-scheduler/main/package.json +5 -0
- package/test/fixtures/runtime-scheduler/main/platformatic.json +9 -0
- package/test/fixtures/runtime-scheduler/main/routes/root.cjs +11 -0
- package/test/fixtures/runtime-scheduler/package.json +1 -0
- package/test/fixtures/runtime-scheduler/platformatic.json +27 -0
- package/test/fixtures/runtime-service/main/package.json +5 -0
- package/test/fixtures/runtime-service/main/platformatic.json +12 -0
- package/test/fixtures/runtime-service/main/routes/root.cjs +11 -0
- package/test/fixtures/runtime-service/package.json +1 -0
- package/test/fixtures/runtime-service/platformatic.json +19 -0
- package/test/fixtures/service-1/package.json +7 -0
- package/test/fixtures/service-1/platformatic.json +18 -0
- package/test/fixtures/service-1/routes/root.cjs +48 -0
- package/test/fixtures/service-2/platformatic.json +21 -0
- package/test/fixtures/service-2/routes/root.cjs +5 -0
- package/test/fixtures/service-3/package.json +5 -0
- package/test/fixtures/service-3/platformatic.json +21 -0
- package/test/fixtures/service-3/routes/root.cjs +8 -0
- package/test/health.test.js +44 -0
- package/test/helper.js +274 -0
- package/test/init.test.js +243 -0
- package/test/patch-config.test.js +434 -0
- package/test/scheduler.test.js +71 -0
- package/test/send-to-icc-retry.test.js +138 -0
- package/test/shared-context.test.js +82 -0
- package/test/spawn.test.js +110 -0
- package/test/trigger-flamegraphs.test.js +226 -0
- package/test/update.test.js +519 -0
package/lib/wattpro.js
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { loadConfiguration } from '@platformatic/runtime'
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url)
|
|
7
|
+
|
|
8
|
+
// Simple replacement for ensureLoggableError
|
|
9
|
+
function ensureLoggableError (err) {
|
|
10
|
+
if (!err) return err
|
|
11
|
+
if (typeof err === 'string') return new Error(err)
|
|
12
|
+
if (err instanceof Error) return err
|
|
13
|
+
return new Error(String(err))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function restartRuntime (runtime) {
|
|
17
|
+
runtime.logger.info('Received SIGUSR2, restarting all services ...')
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await runtime.restart()
|
|
21
|
+
} catch (err) {
|
|
22
|
+
runtime.logger.error(
|
|
23
|
+
{ err: ensureLoggableError(err) },
|
|
24
|
+
'Failed to restart services.'
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class WattPro {
|
|
30
|
+
#env
|
|
31
|
+
#logger
|
|
32
|
+
#require
|
|
33
|
+
#appDir
|
|
34
|
+
#applicationName
|
|
35
|
+
#instanceId
|
|
36
|
+
#instanceConfig
|
|
37
|
+
#originalConfig
|
|
38
|
+
#config
|
|
39
|
+
#sharedContext
|
|
40
|
+
|
|
41
|
+
constructor (app) {
|
|
42
|
+
this.#env = app.env
|
|
43
|
+
this.#logger = app.log
|
|
44
|
+
this.#appDir = app.env.PLT_APP_DIR
|
|
45
|
+
this.#applicationName = app.applicationName || app.env.PLT_APP_NAME
|
|
46
|
+
this.#require = createRequire(join(this.#appDir, 'package.json'))
|
|
47
|
+
this.#instanceId = app.instanceId
|
|
48
|
+
this.runtime = null
|
|
49
|
+
this.#sharedContext = {}
|
|
50
|
+
this.#instanceConfig = app.instanceConfig
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async spawn () {
|
|
54
|
+
try {
|
|
55
|
+
this.runtime = await this.#createRuntime()
|
|
56
|
+
this.#logger.info('Starting runtime -WATT')
|
|
57
|
+
await this.runtime.start()
|
|
58
|
+
await this.updateSharedContext(this.#sharedContext)
|
|
59
|
+
this.#logger.info('Runtime started')
|
|
60
|
+
} catch (err) {
|
|
61
|
+
this.#logger.error(
|
|
62
|
+
{ err: ensureLoggableError(err) },
|
|
63
|
+
'Failed to start runtime'
|
|
64
|
+
)
|
|
65
|
+
throw err
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async close () {
|
|
70
|
+
if (this.runtime) {
|
|
71
|
+
const runtime = this.runtime
|
|
72
|
+
this.runtime = null
|
|
73
|
+
|
|
74
|
+
this.#logger.info('Closing runtime')
|
|
75
|
+
await runtime.close()
|
|
76
|
+
this.#logger.info('Runtime closed')
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async applyIccConfigUpdates (config) {
|
|
81
|
+
this.#logger.info({ config }, 'Applying ICC config updates')
|
|
82
|
+
|
|
83
|
+
if (this.#instanceConfig) {
|
|
84
|
+
this.#instanceConfig.config = config
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (config.httpCacheConfig) {
|
|
88
|
+
try {
|
|
89
|
+
const undiciConfig = await this.#getUndiciConfig()
|
|
90
|
+
await this.runtime.updateUndiciInterceptors?.(undiciConfig)
|
|
91
|
+
} catch (err) {
|
|
92
|
+
this.#logger.error({ err }, 'Failed to update undici interceptors')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (config.resources?.services && config.resources.services.length > 0) {
|
|
97
|
+
const resourceUpdates = config.resources.services.map((service) => ({
|
|
98
|
+
service: service.name,
|
|
99
|
+
workers: service.threads,
|
|
100
|
+
health: {
|
|
101
|
+
maxHeapTotal: `${service.heap}MB`,
|
|
102
|
+
},
|
|
103
|
+
}))
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await this.runtime.updateServicesResources(resourceUpdates)
|
|
107
|
+
this.#logger.info(
|
|
108
|
+
{ resourceUpdates },
|
|
109
|
+
'Successfully updated service resources'
|
|
110
|
+
)
|
|
111
|
+
} catch (err) {
|
|
112
|
+
this.#logger.error(
|
|
113
|
+
{ err, resourceUpdates },
|
|
114
|
+
'Failed to update service resources'
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async updateSharedContext (context) {
|
|
121
|
+
this.#sharedContext = context
|
|
122
|
+
await this.runtime?.updateSharedContext?.({ context })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async #loadAppConfig () {
|
|
126
|
+
this.#logger.info('Loading app config')
|
|
127
|
+
try {
|
|
128
|
+
const config = await loadConfiguration(this.#appDir)
|
|
129
|
+
return config
|
|
130
|
+
} catch (err) {
|
|
131
|
+
this.#logger.error(err, 'Failed to load app config')
|
|
132
|
+
throw new Error('Failed to load app config.', { cause: err })
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async #createRuntime () {
|
|
137
|
+
this.#logger.info('Creating runtime')
|
|
138
|
+
const { Runtime } = this.#require('@platformatic/runtime')
|
|
139
|
+
|
|
140
|
+
this.#config = await this.#loadAppConfig()
|
|
141
|
+
|
|
142
|
+
this.#logger.info('Patching runtime config')
|
|
143
|
+
|
|
144
|
+
this.#originalConfig = structuredClone(this.#config)
|
|
145
|
+
|
|
146
|
+
if (this.#config) {
|
|
147
|
+
this.#patchRuntimeConfig(this.#config)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.#logger.info('Building runtime')
|
|
151
|
+
|
|
152
|
+
const runtime = new Runtime(this.#config, { isProduction: true })
|
|
153
|
+
|
|
154
|
+
/* c8 ignore next 3 */
|
|
155
|
+
const restartListener = restartRuntime.bind(null, runtime)
|
|
156
|
+
process.on('SIGUSR2', restartListener)
|
|
157
|
+
runtime.on('closed', () => {
|
|
158
|
+
process.removeListener('SIGUSR2', restartListener)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
await this.#configureServices(runtime)
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await runtime.init()
|
|
165
|
+
} catch (e) {
|
|
166
|
+
await runtime.close()
|
|
167
|
+
throw e
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return runtime
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#patchRuntimeConfig (config) {
|
|
174
|
+
this.#configureRuntime(config)
|
|
175
|
+
this.#configureTelemetry(config)
|
|
176
|
+
this.#configureHttpCaching(config)
|
|
177
|
+
this.#configureHealth(config)
|
|
178
|
+
this.#configureSystemResources(config)
|
|
179
|
+
this.#configureScheduler(config)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#configureRuntime (config) {
|
|
183
|
+
const { https, ...serverConfig } = config.server ?? {}
|
|
184
|
+
config.server = {
|
|
185
|
+
...serverConfig,
|
|
186
|
+
hostname: this.#env.PLT_APP_HOSTNAME || serverConfig.hostname,
|
|
187
|
+
port: this.#env.PLT_APP_PORT || serverConfig.port,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
config.hotReload = false
|
|
191
|
+
config.restartOnError = 1000
|
|
192
|
+
config.metrics = {
|
|
193
|
+
server: 'hide',
|
|
194
|
+
defaultMetrics: {
|
|
195
|
+
enabled: true,
|
|
196
|
+
},
|
|
197
|
+
hostname: this.#env.PLT_APP_HOSTNAME || '127.0.0.1',
|
|
198
|
+
port: this.#env.PLT_METRICS_PORT || 9090,
|
|
199
|
+
labels: {
|
|
200
|
+
serviceId: 'main',
|
|
201
|
+
applicationId: this.#instanceConfig.applicationId,
|
|
202
|
+
instanceId: this.#instanceId,
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (this.#env.PLT_DISABLE_FLAMEGRAPHS !== true) {
|
|
207
|
+
if (config.preload === undefined) {
|
|
208
|
+
config.preload = []
|
|
209
|
+
}
|
|
210
|
+
const pprofPath = require.resolve('@platformatic/wattpm-pprof-capture')
|
|
211
|
+
config.preload.push(pprofPath)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.#configureUndici(config)
|
|
215
|
+
config.managementApi = true
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#getUndiciConfig () {
|
|
219
|
+
const config = this.#config
|
|
220
|
+
|
|
221
|
+
const undiciConfig = structuredClone(this.#originalConfig.undici ?? {})
|
|
222
|
+
|
|
223
|
+
if (undiciConfig.interceptors === undefined) {
|
|
224
|
+
undiciConfig.interceptors = []
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const enableSlicerInterceptor =
|
|
228
|
+
this.#instanceConfig?.enableSlicerInterceptor ?? false
|
|
229
|
+
if (enableSlicerInterceptor) {
|
|
230
|
+
const slicerInterceptorConfig = this.#getSlicerInterceptorConfig(config)
|
|
231
|
+
if (slicerInterceptorConfig) {
|
|
232
|
+
undiciConfig.interceptors.push(slicerInterceptorConfig)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const enableTrafficInterceptor =
|
|
237
|
+
this.#instanceConfig?.enableTrafficInterceptor ?? false
|
|
238
|
+
if (enableTrafficInterceptor) {
|
|
239
|
+
const trafficInterceptorConfig =
|
|
240
|
+
this.#getTrafficInterceptorConfig()
|
|
241
|
+
if (trafficInterceptorConfig) {
|
|
242
|
+
undiciConfig.interceptors.push(trafficInterceptorConfig)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return undiciConfig
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#configureUndici (config) {
|
|
250
|
+
config.undici = this.#getUndiciConfig(config)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#getTrafficInterceptorConfig () {
|
|
254
|
+
if (!this.#instanceConfig?.iccServices?.trafficante?.url) {
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
const { origin: trafficanteOrigin, pathname: trafficantePath } = new URL(
|
|
258
|
+
this.#instanceConfig.iccServices.trafficante.url
|
|
259
|
+
)
|
|
260
|
+
return {
|
|
261
|
+
module: this.#require.resolve(
|
|
262
|
+
'undici-traffic-interceptor'
|
|
263
|
+
),
|
|
264
|
+
options: {
|
|
265
|
+
labels: {
|
|
266
|
+
applicationId: this.#instanceConfig.applicationId,
|
|
267
|
+
},
|
|
268
|
+
bloomFilter: {
|
|
269
|
+
size: 100000,
|
|
270
|
+
errorRate: 0.01,
|
|
271
|
+
},
|
|
272
|
+
maxResponseSize: 5 * 1024 * 1024, // 5MB
|
|
273
|
+
trafficInspectorOptions: {
|
|
274
|
+
url: trafficanteOrigin,
|
|
275
|
+
pathSendBody: join(trafficantePath, '/requests'),
|
|
276
|
+
pathSendMeta: join(trafficantePath, '/requests/hash'),
|
|
277
|
+
},
|
|
278
|
+
matchingDomains: [this.#env.PLT_APP_INTERNAL_SUB_DOMAIN],
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#getSlicerInterceptorConfig (config) {
|
|
284
|
+
// We need to initialize the slicer interceptor even if there is no cache config
|
|
285
|
+
// to be able to update the onfiguration at runtime
|
|
286
|
+
const defaultCacheConfig = {
|
|
287
|
+
rules: [
|
|
288
|
+
{
|
|
289
|
+
routeToMatch: 'http://plt.slicer.default/',
|
|
290
|
+
headers: {},
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// This is the cache config from ICC
|
|
296
|
+
const httpCacheConfig =
|
|
297
|
+
this.#instanceConfig?.config?.httpCacheConfig ?? defaultCacheConfig
|
|
298
|
+
let autoGeneratedConfig = null
|
|
299
|
+
if (httpCacheConfig) {
|
|
300
|
+
try {
|
|
301
|
+
autoGeneratedConfig = httpCacheConfig
|
|
302
|
+
} catch (e) {
|
|
303
|
+
this.#logger.error(
|
|
304
|
+
{ err: ensureLoggableError(e) },
|
|
305
|
+
'Failed to parse auto generated cache config'
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let userConfig = null
|
|
311
|
+
// This is the user config from the environment variable
|
|
312
|
+
if (this.#env.PLT_CACHE_CONFIG) {
|
|
313
|
+
try {
|
|
314
|
+
userConfig = JSON.parse(this.#env.PLT_CACHE_CONFIG)
|
|
315
|
+
} catch (e) {
|
|
316
|
+
this.#logger.error(
|
|
317
|
+
{ err: ensureLoggableError(e) },
|
|
318
|
+
'Failed to parse user cache config'
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!userConfig && !autoGeneratedConfig) return null
|
|
324
|
+
|
|
325
|
+
let cacheConfig = userConfig ?? autoGeneratedConfig
|
|
326
|
+
if (autoGeneratedConfig && userConfig) {
|
|
327
|
+
cacheConfig = this.#mergeCacheConfigs(autoGeneratedConfig, userConfig)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const cacheTagsHeader = this.#getCacheTagsHeader(config)
|
|
331
|
+
|
|
332
|
+
for (const rule of cacheConfig.rules ?? []) {
|
|
333
|
+
if (rule.cacheTags) {
|
|
334
|
+
if (!rule.headers) {
|
|
335
|
+
rule.headers = {}
|
|
336
|
+
}
|
|
337
|
+
rule.headers[cacheTagsHeader] = rule.cacheTags
|
|
338
|
+
delete rule.cacheTags
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
module: this.#require.resolve('@platformatic/slicer-interceptor'),
|
|
344
|
+
options: cacheConfig,
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#mergeCacheConfigs (autoGeneratedConfig, userConfig) {
|
|
349
|
+
const mergedConfig = { ...userConfig }
|
|
350
|
+
|
|
351
|
+
for (const rule of autoGeneratedConfig.rules ?? []) {
|
|
352
|
+
const ruleIndex = mergedConfig.rules.findIndex(
|
|
353
|
+
(r) => r.routeToMatch === rule.routeToMatch
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
if (ruleIndex === -1) {
|
|
357
|
+
mergedConfig.rules.push(rule)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return mergedConfig
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
#configureTelemetry (config) {
|
|
365
|
+
const enableOpenTelemetry =
|
|
366
|
+
!!this.#instanceConfig?.enableOpenTelemetry &&
|
|
367
|
+
!!this.#instanceConfig?.iccServices?.riskEngine?.url
|
|
368
|
+
|
|
369
|
+
// We need to always set an opentelemetry config to pass a telemetry
|
|
370
|
+
// applicationName to render a taxonomy diagram
|
|
371
|
+
config.telemetry = config.telemetry ?? {
|
|
372
|
+
enabled: enableOpenTelemetry,
|
|
373
|
+
applicationName: `${this.#applicationName}`,
|
|
374
|
+
skip: [
|
|
375
|
+
{ method: 'GET', path: '/documentation' },
|
|
376
|
+
{ method: 'GET', path: '/documentation/json' },
|
|
377
|
+
],
|
|
378
|
+
exporter: {
|
|
379
|
+
type: 'otlp',
|
|
380
|
+
options: {
|
|
381
|
+
url:
|
|
382
|
+
this.#instanceConfig?.iccServices?.riskEngine?.url + '/v1/traces',
|
|
383
|
+
headers: {
|
|
384
|
+
'x-platformatic-application-id': this.#instanceConfig.applicationId,
|
|
385
|
+
},
|
|
386
|
+
keepAlive: true,
|
|
387
|
+
httpAgentOptions: {
|
|
388
|
+
rejectUnauthorized: false,
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
#configureHttpCaching (config) {
|
|
396
|
+
const cacheTagsHeader = this.#getCacheTagsHeader(config)
|
|
397
|
+
const httpCache = this.#instanceConfig?.httpCache?.clientOpts
|
|
398
|
+
|
|
399
|
+
if (!httpCache?.host) {
|
|
400
|
+
this.#logger.warn(
|
|
401
|
+
'Missing required environment variables for Redis cache, not setting up HTTP cache'
|
|
402
|
+
)
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
config.httpCache = {
|
|
407
|
+
...config.httpCache,
|
|
408
|
+
cacheTagsHeader,
|
|
409
|
+
store: this.#require.resolve('@platformatic/undici-cache-redis'),
|
|
410
|
+
clientOpts: httpCache,
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#configureHealth (config) {
|
|
415
|
+
config.health = {
|
|
416
|
+
...config.health,
|
|
417
|
+
enabled: true,
|
|
418
|
+
interval: 1000,
|
|
419
|
+
maxUnhealthyChecks: 30,
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#configureScheduler (config) {
|
|
424
|
+
// Disable all watt schedules. We do that because
|
|
425
|
+
// we will create/update them in ICC, not on watt in memory
|
|
426
|
+
if (config.scheduler) {
|
|
427
|
+
config.scheduler = config.scheduler.map((scheduler) => ({
|
|
428
|
+
...scheduler,
|
|
429
|
+
enabled: false,
|
|
430
|
+
}))
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
#configureSystemResources (config) {
|
|
435
|
+
if (!this.#instanceConfig) {
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
// Set system wide resources
|
|
439
|
+
const resources = this.#instanceConfig?.config?.resources
|
|
440
|
+
if (!resources) {
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const { threads, heap } = resources
|
|
445
|
+
|
|
446
|
+
if (threads > 0) {
|
|
447
|
+
config.workers = threads
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (heap > 0) {
|
|
451
|
+
config.health ??= {}
|
|
452
|
+
config.health.maxHeapTotal = heap * 1024 * 1024
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// In v3 we renamed services to applications, so we support both (note that this is coming from icc)
|
|
456
|
+
const res = resources.services || resources.applications
|
|
457
|
+
|
|
458
|
+
// Set services resources
|
|
459
|
+
for (const application of config.applications ?? []) {
|
|
460
|
+
let applicationResources = res?.find((s) => s.name === application.id)
|
|
461
|
+
|
|
462
|
+
if (!applicationResources) {
|
|
463
|
+
applicationResources = {
|
|
464
|
+
threads,
|
|
465
|
+
heap,
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
application.workers = applicationResources.threads
|
|
469
|
+
application.health ??= {}
|
|
470
|
+
application.health.maxHeapTotal = applicationResources.heap * 1024 * 1024
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async #configureServices (runtime) {
|
|
475
|
+
if (typeof runtime.setApplicationConfigPatch !== 'function') {
|
|
476
|
+
return
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const config = runtime.getRuntimeConfig(true)
|
|
480
|
+
|
|
481
|
+
for (const app of config.applications ?? []) {
|
|
482
|
+
if (app.type === 'next') {
|
|
483
|
+
await this.#configureNextService(runtime, app)
|
|
484
|
+
} else if (
|
|
485
|
+
[
|
|
486
|
+
'@platformatic/service',
|
|
487
|
+
'@platformatic/composer',
|
|
488
|
+
'@platformatic/db',
|
|
489
|
+
].includes(app.type)
|
|
490
|
+
) {
|
|
491
|
+
await this.#configurePlatformaticServices(runtime, app)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async #configureNextService (runtime, service) {
|
|
497
|
+
let nextSchema
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
const nextPackage = createRequire(
|
|
501
|
+
resolve(service.path, 'index.js')
|
|
502
|
+
).resolve('@platformatic/next')
|
|
503
|
+
nextSchema = JSON.parse(
|
|
504
|
+
await readFile(resolve(nextPackage, '../schema.json'), 'utf8')
|
|
505
|
+
)
|
|
506
|
+
} catch (e) {
|
|
507
|
+
this.#logger.error(
|
|
508
|
+
{ err: ensureLoggableError(e) },
|
|
509
|
+
`Failed to load @platformatic/next schema for service ${service.id}`
|
|
510
|
+
)
|
|
511
|
+
throw e
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const patches = []
|
|
515
|
+
|
|
516
|
+
if ('cache' in nextSchema.properties) {
|
|
517
|
+
const httpCache = this.#instanceConfig?.httpCache?.clientOpts || {}
|
|
518
|
+
const { keyPrefix, host, port, username, password } = httpCache
|
|
519
|
+
|
|
520
|
+
if (!keyPrefix || !host || !port) {
|
|
521
|
+
this.#logger.warn(
|
|
522
|
+
'Missing required environment variables for Redis cache, not setting up HTTP next cache'
|
|
523
|
+
)
|
|
524
|
+
} else {
|
|
525
|
+
patches.push({
|
|
526
|
+
op: 'add',
|
|
527
|
+
path: '/cache',
|
|
528
|
+
value: {
|
|
529
|
+
adapter: 'valkey',
|
|
530
|
+
url: `valkey://${username}:${password}@${host}:${port}`,
|
|
531
|
+
prefix: keyPrefix,
|
|
532
|
+
maxTTL: 604800, // 86400 * 7
|
|
533
|
+
},
|
|
534
|
+
})
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Add trailingSlash true to Next entrypoints that support it
|
|
539
|
+
// This is technically useless as Next.js will manage it at build time, but we keep it
|
|
540
|
+
// in case in the future they compare build and production next.config.js
|
|
541
|
+
if (
|
|
542
|
+
service.entrypoint &&
|
|
543
|
+
nextSchema.properties.next?.properties.trailingSlash?.type === 'boolean'
|
|
544
|
+
) {
|
|
545
|
+
patches.push({ op: 'add', path: '/next/trailingSlash', value: true })
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (patches.length) {
|
|
549
|
+
this.#patchService(runtime, service.id, patches)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async #configurePlatformaticServices (runtime, app) {
|
|
554
|
+
if (app.entrypoint) {
|
|
555
|
+
const config = app
|
|
556
|
+
const patches = [{ op: 'add', path: '/server/trustProxy', value: true }]
|
|
557
|
+
|
|
558
|
+
if (!config.server) {
|
|
559
|
+
patches.unshift({ op: 'add', path: '/server', value: {} })
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
patches.push({ op: 'remove', path: '/server/https' })
|
|
563
|
+
|
|
564
|
+
this.#patchService(runtime, app.id, patches)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async #patchService (runtime, id, patches) {
|
|
569
|
+
this.#logger.info({ patches }, `Applying patches to service ${id} ...`)
|
|
570
|
+
runtime.setApplicationConfigPatch(id, patches)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#getCacheTagsHeader (config) {
|
|
574
|
+
const customCacheTagsHeader = config.httpCache?.cacheTagsHeader
|
|
575
|
+
const defaultCacheTagsHeader = this.#env.PLT_DEFAULT_CACHE_TAGS_HEADER
|
|
576
|
+
return customCacheTagsHeader ?? defaultCacheTagsHeader
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export default WattPro
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@platformatic/watt-extra",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Platformatic runtime manager",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"lint": "eslint",
|
|
8
|
+
"test": "pnpm run lint && borp -c 1 --timeout=180000 ./test/*.test.js",
|
|
9
|
+
"start": "node index.js"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"watt-extra": "./cli.js"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@fastify/websocket": "^11.1.0",
|
|
16
|
+
"@platformatic/composer": "^3.0.1",
|
|
17
|
+
"@platformatic/next": "^3.0.1",
|
|
18
|
+
"@platformatic/node": "^3.0.1",
|
|
19
|
+
"@platformatic/service": "^3.0.1",
|
|
20
|
+
"borp": "^0.20.0",
|
|
21
|
+
"eslint": "9",
|
|
22
|
+
"fastify": "^5.4.0",
|
|
23
|
+
"fastify-plugin": "^5.0.1",
|
|
24
|
+
"neostandard": "^0.12.0",
|
|
25
|
+
"next": "^15.3.4",
|
|
26
|
+
"platformatic": "^3.0.1",
|
|
27
|
+
"pprof-format": "^2.1.0",
|
|
28
|
+
"why-is-node-running": "^2.3.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@datadog/pprof": "^5.9.0",
|
|
32
|
+
"@fastify/error": "^4.2.0",
|
|
33
|
+
"@platformatic/runtime": "^3.0.1",
|
|
34
|
+
"@platformatic/slicer-interceptor": "^0.3.0",
|
|
35
|
+
"@platformatic/undici-cache-redis": "^0.7.1",
|
|
36
|
+
"@platformatic/wattpm-pprof-capture": "^3.0.1",
|
|
37
|
+
"undici-traffic-interceptor": "^0.1.4",
|
|
38
|
+
"avvio": "^9.1.0",
|
|
39
|
+
"chalk": "^4.1.2",
|
|
40
|
+
"commist": "^3.2.0",
|
|
41
|
+
"env-schema": "^6.0.1",
|
|
42
|
+
"execa": "^9.6.0",
|
|
43
|
+
"help-me": "^5.0.0",
|
|
44
|
+
"minimist": "^1.2.8",
|
|
45
|
+
"pino": "^9.7.0",
|
|
46
|
+
"pino-pretty": "^13.0.0",
|
|
47
|
+
"undici": "^7.11.0",
|
|
48
|
+
"ws": "^8.18.3"
|
|
49
|
+
}
|
|
50
|
+
}
|