@platformatic/runtime 3.63.0-alpha.3 → 3.63.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/config.d.ts +4 -0
- package/index.d.ts +138 -45
- package/lib/errors.js +30 -11
- package/lib/management-api.js +0 -17
- package/lib/management-handlers.js +0 -20
- package/lib/prom-server.js +81 -8
- package/lib/runtime.js +538 -134
- package/lib/scheduler.js +60 -192
- package/lib/worker/controller.js +4 -2
- package/lib/worker/itc.js +38 -44
- package/lib/worker/main.js +14 -4
- package/lib/worker/management.js +0 -16
- package/lib/worker/messaging.js +20 -7
- package/lib/worker-scaler.js +20 -9
- package/package.json +17 -17
- package/schema.json +13 -2
package/config.d.ts
CHANGED
|
@@ -696,6 +696,10 @@ export type PlatformaticRuntimeConfig = {
|
|
|
696
696
|
[k: string]: string;
|
|
697
697
|
};
|
|
698
698
|
envfile?: string;
|
|
699
|
+
/**
|
|
700
|
+
* When set to true, the configuration loading fails if a {PLT_*} placeholder references an environment variable which is not set. When set to "warn", a warning listing the missing variables is logged but the placeholders are still replaced with an empty string. Defaults to false.
|
|
701
|
+
*/
|
|
702
|
+
strictEnv?: boolean | string;
|
|
699
703
|
sourceMaps?: boolean;
|
|
700
704
|
nodeModulesSourceMaps?: string[];
|
|
701
705
|
scheduler?: {
|
package/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { FastifyError } from '@fastify/error'
|
|
|
2
2
|
import { Configuration, ConfigurationOptions, logFatalError, parseArgs } from '@platformatic/foundation'
|
|
3
3
|
import { BaseGenerator } from '@platformatic/generators'
|
|
4
4
|
import { PlatformaticGlobal } from '@platformatic/globals'
|
|
5
|
+
import * as PromClient from '@platformatic/prom-client'
|
|
5
6
|
import { JSONSchemaType } from 'ajv'
|
|
6
7
|
import * as colorette from 'colorette'
|
|
7
8
|
import { EventEmitter } from 'node:events'
|
|
@@ -57,9 +58,20 @@ export namespace errors {
|
|
|
57
58
|
export const InvalidExtensionError: (path: string) => FastifyError
|
|
58
59
|
export const ReservedITCHandlerNameError: (name: string) => FastifyError
|
|
59
60
|
export const DuplicateITCHandlerNameError: (name: string) => FastifyError
|
|
61
|
+
export const MetricFamilyCollisionError: (
|
|
62
|
+
extension: string,
|
|
63
|
+
metricFamily: string,
|
|
64
|
+
otherSource: string
|
|
65
|
+
) => FastifyError
|
|
66
|
+
export const DuplicateExtensionHealthCheckError: (kind: string, name: string, extension: string) => FastifyError
|
|
67
|
+
export const DuplicateExtensionHealthRouteError: (
|
|
68
|
+
extension: string,
|
|
69
|
+
method: string,
|
|
70
|
+
url: string,
|
|
71
|
+
error: string
|
|
72
|
+
) => FastifyError
|
|
73
|
+
export const ExtensionHealthRoutesUnavailableError: () => FastifyError
|
|
60
74
|
export const LastProfileTimeoutError: (id: string) => FastifyError
|
|
61
|
-
export const SchedulerJobNotFoundError: (name: string) => FastifyError
|
|
62
|
-
export const DuplicateSchedulerJobError: (name: string) => FastifyError
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
export namespace symbols {
|
|
@@ -111,43 +123,26 @@ export interface ApplicationDetails {
|
|
|
111
123
|
url?: string | null
|
|
112
124
|
}
|
|
113
125
|
|
|
114
|
-
export interface
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
maxRetries: number
|
|
119
|
-
lastExecutedAt?: string | null
|
|
120
|
-
lastStatus?: 'success' | 'failed' | null
|
|
121
|
-
nextRunAt?: string | null
|
|
126
|
+
export interface ApplicationsTopology {
|
|
127
|
+
entrypoint: string
|
|
128
|
+
production: boolean
|
|
129
|
+
applications: ApplicationDetails[]
|
|
122
130
|
}
|
|
123
131
|
|
|
124
|
-
export interface
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
export interface WorkerDetails {
|
|
133
|
+
application: string
|
|
134
|
+
worker: string
|
|
135
|
+
status: string
|
|
136
|
+
thread: number
|
|
137
|
+
raw?: unknown
|
|
130
138
|
}
|
|
131
139
|
|
|
132
|
-
export interface
|
|
133
|
-
|
|
134
|
-
applicationId: string
|
|
135
|
-
scheduleId: string
|
|
136
|
-
tasks: string[]
|
|
140
|
+
export interface SharedContextUpdateOptions {
|
|
141
|
+
overwrite?: boolean
|
|
137
142
|
}
|
|
138
143
|
|
|
139
|
-
export
|
|
140
|
-
|
|
141
|
-
export interface SchedulerRunResult {
|
|
142
|
-
name: string
|
|
143
|
-
success: boolean
|
|
144
|
-
executedAt: string
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export interface ApplicationSchedule {
|
|
148
|
-
id: string
|
|
149
|
-
cron: string
|
|
150
|
-
tasks: string[]
|
|
144
|
+
export interface RuntimeSharedContextUpdateOptions extends SharedContextUpdateOptions {
|
|
145
|
+
context?: object
|
|
151
146
|
}
|
|
152
147
|
|
|
153
148
|
export interface RuntimeMetadata {
|
|
@@ -172,18 +167,14 @@ export declare class ManagementClient {
|
|
|
172
167
|
getRuntimeConfig (): Promise<Record<string, unknown>>
|
|
173
168
|
getRuntimeEnv (): Promise<Record<string, string>>
|
|
174
169
|
getApplicationsIds (): Promise<string[]>
|
|
175
|
-
getApplications (): Promise<
|
|
176
|
-
getWorkers (): Promise<Record<string,
|
|
170
|
+
getApplications (): Promise<ApplicationsTopology>
|
|
171
|
+
getWorkers (): Promise<Record<string, WorkerDetails>>
|
|
177
172
|
getApplicationDetails (id: string): Promise<ApplicationDetails>
|
|
178
173
|
getApplicationConfig (id: string): Promise<Record<string, unknown>>
|
|
179
174
|
getApplicationEnv (id: string): Promise<Record<string, string>>
|
|
180
175
|
getApplicationOpenapiSchema (id: string): Promise<unknown>
|
|
181
176
|
getApplicationGraphqlSchema (id: string): Promise<unknown>
|
|
182
177
|
getMetrics (format?: string): Promise<{ metrics: unknown }>
|
|
183
|
-
getScheduler (): Promise<SchedulerJob[]>
|
|
184
|
-
pauseSchedulerJob (name: string): Promise<SchedulerJob>
|
|
185
|
-
resumeSchedulerJob (name: string): Promise<SchedulerJob>
|
|
186
|
-
runSchedulerJob (name: string): Promise<SchedulerRunResult>
|
|
187
178
|
startApplication (id: string): Promise<void>
|
|
188
179
|
stopApplication (id: string): Promise<void>
|
|
189
180
|
restartApplication (id: string): Promise<void>
|
|
@@ -205,15 +196,78 @@ export interface RuntimeExtensionITC {
|
|
|
205
196
|
notify (target: string, name: string, payload?: unknown): Promise<void>
|
|
206
197
|
}
|
|
207
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Shared context facade exposed to main-thread runtime extensions.
|
|
201
|
+
*
|
|
202
|
+
* Mirrors the worker-side `sharedContext` API from `@platformatic/globals`:
|
|
203
|
+
* - `get()` returns the current snapshot (a plain object). On the main thread
|
|
204
|
+
* this is always synchronous; the `Promise` variant exists for parity with
|
|
205
|
+
* workers where the first read may be asynchronous.
|
|
206
|
+
* - `update(update, options?)` merges `update` into the current context by
|
|
207
|
+
* default. Pass `{ overwrite: true }` to replace it. Updates are broadcast
|
|
208
|
+
* to all running workers exactly like worker-originated updates.
|
|
209
|
+
*/
|
|
210
|
+
export interface RuntimeExtensionSharedContext {
|
|
211
|
+
get (): object | Promise<object>
|
|
212
|
+
update (update: object, options?: SharedContextUpdateOptions): Promise<void>
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface RuntimeExtensionMetrics {
|
|
216
|
+
client: typeof PromClient
|
|
217
|
+
registry: PromClient.Registry
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export type ExtensionHealthCheckResult =
|
|
221
|
+
| boolean
|
|
222
|
+
| {
|
|
223
|
+
status: boolean
|
|
224
|
+
statusCode?: number
|
|
225
|
+
body?: string | object
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export type ExtensionHealthCheck = () => ExtensionHealthCheckResult | Promise<ExtensionHealthCheckResult>
|
|
229
|
+
|
|
230
|
+
export interface RuntimeExtensionHealth {
|
|
231
|
+
/**
|
|
232
|
+
* Registers a readiness check that participates in `/ready`.
|
|
233
|
+
* Readiness-only failures do not fail `/status` (liveness).
|
|
234
|
+
* Returns an unregister function.
|
|
235
|
+
*/
|
|
236
|
+
registerReadinessCheck (name: string, check: ExtensionHealthCheck): () => void
|
|
237
|
+
/**
|
|
238
|
+
* Registers a liveness check that participates in `/status`.
|
|
239
|
+
* Returns an unregister function.
|
|
240
|
+
*/
|
|
241
|
+
registerLivenessCheck (name: string, check: ExtensionHealthCheck): () => void
|
|
242
|
+
/**
|
|
243
|
+
* Registers a Fastify plugin on the health probes server (shared with metrics
|
|
244
|
+
* when they use the same address). Routes are registered before the server
|
|
245
|
+
* starts listening. Returns an unregister function that disables the routes.
|
|
246
|
+
*/
|
|
247
|
+
registerRoutes (plugin: (instance: any, opts: any) => unknown | Promise<unknown>): () => void
|
|
248
|
+
}
|
|
249
|
+
|
|
208
250
|
export interface RuntimeExtensionContext {
|
|
209
251
|
runtime: Runtime
|
|
210
252
|
itc: RuntimeExtensionITC
|
|
211
253
|
logger: Logger
|
|
212
254
|
options: Record<string, unknown>
|
|
213
255
|
root: string
|
|
256
|
+
sharedContext: RuntimeExtensionSharedContext
|
|
257
|
+
/**
|
|
258
|
+
* Per-extension Prometheus client and registry. Metrics registered here are
|
|
259
|
+
* collected once by `Runtime.getMetrics()`, the management metrics API, and
|
|
260
|
+
* the existing `/metrics` endpoint. Runtime does not invent a worker ID or
|
|
261
|
+
* application ID for these main-thread metrics; only configured static
|
|
262
|
+
* `metrics.labels` (excluding the application label name) are applied.
|
|
263
|
+
*/
|
|
264
|
+
metrics: RuntimeExtensionMetrics
|
|
265
|
+
health: RuntimeExtensionHealth
|
|
214
266
|
}
|
|
215
267
|
|
|
216
268
|
export interface RuntimeExtensionInstance {
|
|
269
|
+
start?: () => void | Promise<void>
|
|
270
|
+
stop?: () => void | Promise<void>
|
|
217
271
|
close?: () => void | Promise<void>
|
|
218
272
|
}
|
|
219
273
|
|
|
@@ -234,7 +288,52 @@ export declare class Runtime extends EventEmitter {
|
|
|
234
288
|
getRuntimeEnv (): Record<string, string>
|
|
235
289
|
getRuntimeConfig (includeMeta?: boolean): Record<string, unknown>
|
|
236
290
|
getApplicationsIds (): string[]
|
|
291
|
+
/**
|
|
292
|
+
* Returns topology for every configured application.
|
|
293
|
+
* When `allowUnloaded` is `true`, applications without running workers are
|
|
294
|
+
* reported as `{ id, status: 'stopped' }` instead of throwing.
|
|
295
|
+
*/
|
|
296
|
+
getApplications (allowUnloaded?: boolean): Promise<ApplicationsTopology>
|
|
297
|
+
getWorkers (includeRaw?: boolean): Promise<Record<string, WorkerDetails>>
|
|
298
|
+
/**
|
|
299
|
+
* Returns details for a single application.
|
|
300
|
+
* When `allowUnloaded` is `true` and the application has no running workers,
|
|
301
|
+
* returns `{ id, status: 'stopped' }` instead of throwing.
|
|
302
|
+
*/
|
|
237
303
|
getApplicationDetails (id: string, allowUnloaded?: boolean): Promise<ApplicationDetails>
|
|
304
|
+
/**
|
|
305
|
+
* Returns the resolved configuration of an application worker.
|
|
306
|
+
* When `ensureStarted` is `true` (default), throws `PLT_RUNTIME_APPLICATION_NOT_STARTED`
|
|
307
|
+
* if the application is not running. When no worker exists for a known application,
|
|
308
|
+
* throws `PLT_RUNTIME_WORKER_NOT_FOUND`. Unknown applications throw
|
|
309
|
+
* `PLT_RUNTIME_APPLICATION_NOT_FOUND`.
|
|
310
|
+
*/
|
|
311
|
+
getApplicationConfig (id: string, ensureStarted?: boolean): Promise<Record<string, unknown>>
|
|
312
|
+
/**
|
|
313
|
+
* Returns the effective environment of an application worker (`process.env`
|
|
314
|
+
* merged with the capability env).
|
|
315
|
+
* When `ensureStarted` is `true` (default), throws `PLT_RUNTIME_APPLICATION_NOT_STARTED`
|
|
316
|
+
* if the application is not running. When no worker exists for a known application
|
|
317
|
+
* (stopped/unloaded), throws `PLT_RUNTIME_WORKER_NOT_FOUND`. Unknown applications
|
|
318
|
+
* throw `PLT_RUNTIME_APPLICATION_NOT_FOUND`.
|
|
319
|
+
*/
|
|
320
|
+
getApplicationEnv (id: string, ensureStarted?: boolean): Promise<Record<string, string>>
|
|
321
|
+
getApplicationOpenapiSchema (id: string): Promise<unknown>
|
|
322
|
+
getApplicationGraphqlSchema (id: string): Promise<unknown>
|
|
323
|
+
getMetrics (format?: string): Promise<{ metrics: unknown }>
|
|
324
|
+
/**
|
|
325
|
+
* Returns the current shared context. Synchronous on the main thread.
|
|
326
|
+
* Do not mutate it in place — use `updateSharedContext()` so changes are
|
|
327
|
+
* broadcast to workers.
|
|
328
|
+
*/
|
|
329
|
+
getSharedContext (): object
|
|
330
|
+
/**
|
|
331
|
+
* Merges `options.context` into the shared context (or replaces it when
|
|
332
|
+
* `options.overwrite` is `true`) and broadcasts the result to every running
|
|
333
|
+
* worker. Returns the updated snapshot. Broadcast failures are logged and do
|
|
334
|
+
* not reject the promise.
|
|
335
|
+
*/
|
|
336
|
+
updateSharedContext (options?: RuntimeSharedContextUpdateOptions): Promise<object>
|
|
238
337
|
startApplication (id: string, silent?: boolean): Promise<void>
|
|
239
338
|
stopApplication (id: string, silent?: boolean): Promise<void>
|
|
240
339
|
restartApplication (id: string): Promise<void>
|
|
@@ -243,12 +342,6 @@ export declare class Runtime extends EventEmitter {
|
|
|
243
342
|
startApplicationProfiling (id: string, options?: Record<string, unknown>, ensureStarted?: boolean): Promise<void>
|
|
244
343
|
stopApplicationProfiling (id: string, options?: Record<string, unknown>, ensureStarted?: boolean): Promise<Buffer>
|
|
245
344
|
getApplicationLastProfile (id: string, options?: Record<string, unknown>, ensureStarted?: boolean): Promise<{ profile: Buffer, timestamp: number | null, preserved: boolean }>
|
|
246
|
-
getScheduler (): SchedulerJob[]
|
|
247
|
-
pauseSchedulerJob (name: string): Promise<SchedulerJob>
|
|
248
|
-
resumeSchedulerJob (name: string): Promise<SchedulerJob>
|
|
249
|
-
runSchedulerJob (name: string): Promise<SchedulerRunResult>
|
|
250
|
-
getApplicationScheduledTasks (id: string): Promise<ApplicationSchedule[]>
|
|
251
|
-
runApplicationScheduledTasks (id: string, scheduleId: string, scheduledTime: number): Promise<unknown>
|
|
252
345
|
}
|
|
253
346
|
|
|
254
347
|
export function wrapInRuntimeConfig (
|
package/lib/errors.js
CHANGED
|
@@ -140,17 +140,6 @@ export const MessagingError = createError(
|
|
|
140
140
|
'Cannot send a message to application "%s": %s'
|
|
141
141
|
)
|
|
142
142
|
|
|
143
|
-
export const SchedulerJobNotFoundError = createError(
|
|
144
|
-
`${ERROR_PREFIX}_SCHEDULER_JOB_NOT_FOUND`,
|
|
145
|
-
'Scheduler "%s" not found',
|
|
146
|
-
404
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
export const DuplicateSchedulerJobError = createError(
|
|
150
|
-
`${ERROR_PREFIX}_DUPLICATE_SCHEDULER_JOB`,
|
|
151
|
-
'Scheduler "%s" is already registered'
|
|
152
|
-
)
|
|
153
|
-
|
|
154
143
|
export const MissingPprofCapture = createError(
|
|
155
144
|
`${ERROR_PREFIX}_MISSING_PPROF_CAPTURE`,
|
|
156
145
|
'Please install @platformatic/wattpm-pprof-capture'
|
|
@@ -166,6 +155,16 @@ export const FailedToLoadExtensionError = createError(
|
|
|
166
155
|
'Failed to load the extension "%s": %s'
|
|
167
156
|
)
|
|
168
157
|
|
|
158
|
+
export const FailedToStartExtensionError = createError(
|
|
159
|
+
`${ERROR_PREFIX}_FAILED_TO_START_EXTENSION`,
|
|
160
|
+
'Failed to start the extension "%s": %s'
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
export const FailedToStopExtensionError = createError(
|
|
164
|
+
`${ERROR_PREFIX}_FAILED_TO_STOP_EXTENSION`,
|
|
165
|
+
'Failed to stop the extension "%s": %s'
|
|
166
|
+
)
|
|
167
|
+
|
|
169
168
|
export const InvalidExtensionError = createError(
|
|
170
169
|
`${ERROR_PREFIX}_INVALID_EXTENSION`,
|
|
171
170
|
'The extension "%s" must export a setup function as its default export'
|
|
@@ -181,6 +180,26 @@ export const DuplicateITCHandlerNameError = createError(
|
|
|
181
180
|
'The ITC command "%s" has already been registered'
|
|
182
181
|
)
|
|
183
182
|
|
|
183
|
+
export const MetricFamilyCollisionError = createError(
|
|
184
|
+
`${ERROR_PREFIX}_METRIC_FAMILY_COLLISION`,
|
|
185
|
+
'Extension "%s" registered metric family "%s" which collides with %s'
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
export const DuplicateExtensionHealthCheckError = createError(
|
|
189
|
+
`${ERROR_PREFIX}_DUPLICATE_EXTENSION_HEALTH_CHECK`,
|
|
190
|
+
'The extension health %s check "%s" has already been registered by "%s"'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
export const DuplicateExtensionHealthRouteError = createError(
|
|
194
|
+
`${ERROR_PREFIX}_DUPLICATE_EXTENSION_HEALTH_ROUTE`,
|
|
195
|
+
'The extension "%s" failed to register health route %s %s: %s'
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
export const ExtensionHealthRoutesUnavailableError = createError(
|
|
199
|
+
`${ERROR_PREFIX}_EXTENSION_HEALTH_ROUTES_UNAVAILABLE`,
|
|
200
|
+
'Extensions registered health routes but no health probes server is available'
|
|
201
|
+
)
|
|
202
|
+
|
|
184
203
|
export const FailedToSendHealthSignalsError = createError(
|
|
185
204
|
`${ERROR_PREFIX}_FAILED_TO_SEND_HEALTH_SIGNALS`,
|
|
186
205
|
'Cannot send health signals from application "%s": %s'
|
package/lib/management-api.js
CHANGED
|
@@ -79,23 +79,6 @@ export async function managementApiPlugin (app, opts) {
|
|
|
79
79
|
await runtime.restart(applications)
|
|
80
80
|
})
|
|
81
81
|
|
|
82
|
-
app.get('/scheduler', async () => {
|
|
83
|
-
const jobs = await runtime.getScheduler()
|
|
84
|
-
return { jobs }
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
app.post('/scheduler/:name/pause', async request => {
|
|
88
|
-
return runtime.pauseSchedulerJob(request.params.name)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
app.post('/scheduler/:name/resume', async request => {
|
|
92
|
-
return runtime.resumeSchedulerJob(request.params.name)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
app.post('/scheduler/:name/run', async request => {
|
|
96
|
-
return runtime.runSchedulerJob(request.params.name)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
82
|
app.get('/applications', async () => {
|
|
100
83
|
return runtime.getApplications()
|
|
101
84
|
})
|
|
@@ -19,10 +19,6 @@ const allOperations = [
|
|
|
19
19
|
'getApplicationOpenapiSchema',
|
|
20
20
|
'getApplicationGraphqlSchema',
|
|
21
21
|
'getMetrics',
|
|
22
|
-
'getScheduler',
|
|
23
|
-
'pauseSchedulerJob',
|
|
24
|
-
'resumeSchedulerJob',
|
|
25
|
-
'runSchedulerJob',
|
|
26
22
|
'startApplication',
|
|
27
23
|
'stopApplication',
|
|
28
24
|
'restartApplication',
|
|
@@ -100,10 +96,6 @@ export function createManagementHandlers (runtime, allowedOperations) {
|
|
|
100
96
|
return runtime.getMetrics(format)
|
|
101
97
|
})
|
|
102
98
|
|
|
103
|
-
register('getScheduler', async () => {
|
|
104
|
-
return runtime.getScheduler()
|
|
105
|
-
})
|
|
106
|
-
|
|
107
99
|
// Write operations
|
|
108
100
|
register('startApplication', async ({ id }) => {
|
|
109
101
|
await runtime.startApplication(id)
|
|
@@ -121,18 +113,6 @@ export function createManagementHandlers (runtime, allowedOperations) {
|
|
|
121
113
|
return runtime.restart(applications)
|
|
122
114
|
})
|
|
123
115
|
|
|
124
|
-
register('pauseSchedulerJob', async ({ name }) => {
|
|
125
|
-
return runtime.pauseSchedulerJob(name)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
register('resumeSchedulerJob', async ({ name }) => {
|
|
129
|
-
return runtime.resumeSchedulerJob(name)
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
register('runSchedulerJob', async ({ name }) => {
|
|
133
|
-
return runtime.runSchedulerJob(name)
|
|
134
|
-
})
|
|
135
|
-
|
|
136
116
|
register('addApplications', async ({ applications, start }) => {
|
|
137
117
|
const config = runtime.getRuntimeConfig(true)
|
|
138
118
|
|
package/lib/prom-server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { loadModule, sanitizeHTTPSOptions } from '@platformatic/foundation'
|
|
|
4
4
|
import fastify from 'fastify'
|
|
5
5
|
import { createRequire } from 'node:module'
|
|
6
6
|
import { resolve } from 'node:path'
|
|
7
|
+
import { DuplicateExtensionHealthRouteError } from './errors.js'
|
|
7
8
|
|
|
8
9
|
const DEFAULT_HOSTNAME = '0.0.0.0'
|
|
9
10
|
const DEFAULT_PORT = 9090
|
|
@@ -19,7 +20,7 @@ const DEFAULT_LIVENESS_SUCCESS_BODY = 'OK'
|
|
|
19
20
|
const DEFAULT_LIVENESS_FAIL_STATUS_CODE = 500
|
|
20
21
|
const DEFAULT_LIVENESS_FAIL_BODY = 'ERR'
|
|
21
22
|
|
|
22
|
-
async function
|
|
23
|
+
async function checkWorkerReadiness (runtime) {
|
|
23
24
|
const workers = await runtime.getWorkers()
|
|
24
25
|
const applications = await runtime.getApplicationsIds()
|
|
25
26
|
|
|
@@ -52,18 +53,39 @@ async function checkReadiness (runtime) {
|
|
|
52
53
|
return { response, status }
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
async function checkReadiness (runtime) {
|
|
57
|
+
const workerResult = await checkWorkerReadiness(runtime)
|
|
58
|
+
if (!workerResult.status) {
|
|
59
|
+
return workerResult
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Extension readiness checks participate in /ready only.
|
|
63
|
+
const extensionResult = await runtime.runExtensionReadinessChecks()
|
|
64
|
+
if (!extensionResult.status) {
|
|
65
|
+
return {
|
|
66
|
+
status: false,
|
|
67
|
+
response: extensionResult.response ?? workerResult.response
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
status: true,
|
|
73
|
+
response: extensionResult.response ?? workerResult.response
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
async function checkLiveness (runtime) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
// Worker readiness still gates liveness (existing semantics), but extension
|
|
79
|
+
// readiness-only failures must not fail /status and cause restart loops.
|
|
80
|
+
const workerReadiness = await checkWorkerReadiness(runtime)
|
|
81
|
+
if (!workerReadiness.status) {
|
|
82
|
+
return { status: false, readiness: workerReadiness.response }
|
|
59
83
|
}
|
|
60
|
-
// TODO test, doc
|
|
61
|
-
// in case of readiness check failure, if custom readiness response is set, we return the readiness check response on health check endpoint
|
|
62
84
|
|
|
63
85
|
const checks = await runtime.getCustomHealthChecks()
|
|
64
86
|
|
|
65
87
|
let response
|
|
66
|
-
const
|
|
88
|
+
const workerStatus = Object.values(checks).every(check => {
|
|
67
89
|
if (typeof check === 'boolean') {
|
|
68
90
|
return check
|
|
69
91
|
} else if (typeof check === 'object') {
|
|
@@ -73,7 +95,22 @@ async function checkLiveness (runtime) {
|
|
|
73
95
|
return false
|
|
74
96
|
})
|
|
75
97
|
|
|
76
|
-
|
|
98
|
+
if (!workerStatus) {
|
|
99
|
+
return { response, status: false }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const extensionResult = await runtime.runExtensionLivenessChecks()
|
|
103
|
+
if (!extensionResult.status) {
|
|
104
|
+
return {
|
|
105
|
+
status: false,
|
|
106
|
+
response: extensionResult.response ?? response
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
status: true,
|
|
112
|
+
response: extensionResult.response ?? response
|
|
113
|
+
}
|
|
77
114
|
}
|
|
78
115
|
|
|
79
116
|
function isHealthProbesServerEnabled (healthProbes) {
|
|
@@ -122,6 +159,37 @@ function resolveHealthProbesOptions (metricsOpts, healthProbes) {
|
|
|
122
159
|
}
|
|
123
160
|
}
|
|
124
161
|
|
|
162
|
+
async function registerExtensionHealthRoutes (promServer, runtime) {
|
|
163
|
+
const routes = runtime.getExtensionHealthRoutes?.() ?? []
|
|
164
|
+
|
|
165
|
+
for (const entry of routes) {
|
|
166
|
+
if (!entry.active) {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await promServer.register(async function extensionHealthRoutes (app) {
|
|
172
|
+
app.addHook('onRequest', async (_req, reply) => {
|
|
173
|
+
if (!entry.active) {
|
|
174
|
+
return reply.code(404).send({ message: 'Not Found' })
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
await app.register(entry.plugin)
|
|
179
|
+
})
|
|
180
|
+
entry.applied = true
|
|
181
|
+
} catch (err) {
|
|
182
|
+
const routeMatch = typeof err.message === 'string'
|
|
183
|
+
? err.message.match(/Method '([^']+)' already declared for route '([^']+)'/i)
|
|
184
|
+
: null
|
|
185
|
+
const method = routeMatch?.[1] ?? 'UNKNOWN'
|
|
186
|
+
const url = routeMatch?.[2] ?? 'unknown'
|
|
187
|
+
|
|
188
|
+
throw new DuplicateExtensionHealthRouteError(entry.extensionPath, method, url, err.message, { cause: err })
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
125
193
|
async function startServer (runtime, opts, metricsEnabled, healthProbesEnabled) {
|
|
126
194
|
opts = typeof opts === 'object' && opts !== null ? opts : {}
|
|
127
195
|
|
|
@@ -281,6 +349,11 @@ async function startServer (runtime, opts, metricsEnabled, healthProbesEnabled)
|
|
|
281
349
|
await promServer.register(plugin)
|
|
282
350
|
}
|
|
283
351
|
|
|
352
|
+
// Extension routes must be registered before the server starts listening.
|
|
353
|
+
if (healthProbesEnabled) {
|
|
354
|
+
await registerExtensionHealthRoutes(promServer, runtime)
|
|
355
|
+
}
|
|
356
|
+
|
|
284
357
|
await promServer.listen({ port, host })
|
|
285
358
|
return promServer
|
|
286
359
|
}
|