@platformatic/runtime 3.59.0 → 3.61.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 CHANGED
@@ -254,7 +254,40 @@ export type PlatformaticRuntimeConfig = {
254
254
  bufferPoolSize?: number | string;
255
255
  defaultHighWaterMark?: number | string;
256
256
  };
257
- healthProbes?: boolean | string;
257
+ healthProbes?:
258
+ | boolean
259
+ | string
260
+ | {
261
+ enabled?: boolean | string;
262
+ hostname?: string;
263
+ port?: number | string;
264
+ readiness?:
265
+ | boolean
266
+ | {
267
+ endpoint?: string;
268
+ success?: {
269
+ statusCode?: number;
270
+ body?: string;
271
+ };
272
+ fail?: {
273
+ statusCode?: number;
274
+ body?: string;
275
+ };
276
+ };
277
+ liveness?:
278
+ | boolean
279
+ | {
280
+ endpoint?: string;
281
+ success?: {
282
+ statusCode?: number;
283
+ body?: string;
284
+ };
285
+ fail?: {
286
+ statusCode?: number;
287
+ body?: string;
288
+ };
289
+ };
290
+ };
258
291
  undici?: {
259
292
  agentOptions?: {
260
293
  [k: string]: unknown;
@@ -413,6 +446,10 @@ export type PlatformaticRuntimeConfig = {
413
446
  body?: string;
414
447
  };
415
448
  };
449
+ /**
450
+ * @deprecated
451
+ * Deprecated. Health probe timeout configuration is no longer used.
452
+ */
416
453
  healthChecksTimeouts?: number | string;
417
454
  plugins?: string[];
418
455
  timeout?: number | string;
@@ -447,6 +484,29 @@ export type PlatformaticRuntimeConfig = {
447
484
  */
448
485
  serviceVersion?: string;
449
486
  };
487
+ /**
488
+ * Configuration for forwarding user OpenTelemetry metrics to an OTLP endpoint
489
+ */
490
+ opentelemetry?: {
491
+ /**
492
+ * Enable or disable OpenTelemetry metrics forwarding
493
+ */
494
+ enabled?: boolean | string;
495
+ /**
496
+ * OTLP metrics endpoint URL (e.g., http://collector:4318/v1/metrics)
497
+ */
498
+ endpoint: string;
499
+ /**
500
+ * Interval in milliseconds between metric forwards
501
+ */
502
+ interval?: number | string;
503
+ /**
504
+ * Additional HTTP headers for authentication
505
+ */
506
+ headers?: {
507
+ [k: string]: string;
508
+ };
509
+ };
450
510
  /**
451
511
  * Custom labels to add to HTTP metrics (http_request_all_duration_seconds). Each label extracts its value from an HTTP request header.
452
512
  */
package/index.js CHANGED
@@ -87,7 +87,7 @@ export async function loadConfiguration (configOrRoot, sourceOrConfig, context)
87
87
  })
88
88
  }
89
89
 
90
- export async function loadApplicationsCommands (executableName = '') {
90
+ export async function loadApplicationsCommands (executableName = '', configurationFile = null) {
91
91
  const applications = {}
92
92
  const commands = {}
93
93
  const help = {}
@@ -97,7 +97,7 @@ export async function loadApplicationsCommands (executableName = '') {
97
97
  const file = await findRuntimeConfigurationFile(
98
98
  abstractLogger,
99
99
  process.cwd(),
100
- null,
100
+ configurationFile,
101
101
  false,
102
102
  false,
103
103
  true,
package/lib/errors.js CHANGED
@@ -39,7 +39,7 @@ export const ApplicationNotFoundError = createError(
39
39
  )
40
40
  export const WorkerNotFoundError = createError(
41
41
  `${ERROR_PREFIX}_WORKER_NOT_FOUND`,
42
- 'Worker %s of application %s not found. Available applications are: %s'
42
+ 'Worker %s of application %s not found. Available workers are: %s'
43
43
  )
44
44
  export const ApplicationNotStartedError = createError(
45
45
  `${ERROR_PREFIX}_APPLICATION_NOT_STARTED`,
@@ -3,11 +3,13 @@ import fastifyWebsocket from '@fastify/websocket'
3
3
  import {
4
4
  applications as applicationSchema,
5
5
  createDirectory,
6
+ createSharedTemporaryDirectory,
6
7
  kMetadata,
7
8
  safeRemove,
8
9
  validate
9
10
  } from '@platformatic/foundation'
10
11
  import fastify from 'fastify'
12
+ import { chmod } from 'node:fs/promises'
11
13
  import { platform, tmpdir } from 'node:os'
12
14
  import { dirname, join } from 'node:path'
13
15
  import { setTimeout as sleep } from 'node:timers/promises'
@@ -343,7 +345,14 @@ export async function startManagementApi (runtime, config) {
343
345
 
344
346
  const runtimePIDDir = join(PLATFORMATIC_TMP_DIR, runtimePID.toString())
345
347
  if (platform() !== 'win32') {
346
- await createDirectory(customSocket ? dirname(customSocket) : runtimePIDDir, typeof customSocket !== 'string')
348
+ if (customSocket) {
349
+ await createDirectory(dirname(customSocket))
350
+ } else {
351
+ await createSharedTemporaryDirectory('platformatic', 'runtimes')
352
+ await createDirectory(runtimePIDDir, true)
353
+ // The socket must only be accessible by the current user
354
+ await chmod(runtimePIDDir, 0o700)
355
+ }
347
356
  }
348
357
 
349
358
  let socketPath = null
@@ -0,0 +1,93 @@
1
+ export class OpenTelemetryMetricsForwarder {
2
+ #config
3
+ #exporter
4
+ #logger
5
+ #queue
6
+ #timer
7
+ #flushing
8
+
9
+ constructor (config, logger) {
10
+ this.#config = config
11
+ this.#logger = logger
12
+ this.#queue = []
13
+ this.#timer = null
14
+ this.#flushing = false
15
+ }
16
+
17
+ async start () {
18
+ if (!this.#config || this.#config.enabled === false || this.#config.enabled === 'false' || !this.#config.endpoint) {
19
+ return false
20
+ }
21
+
22
+ const { OTLPMetricExporter } = await import('@opentelemetry/exporter-metrics-otlp-proto')
23
+ this.#exporter = new OTLPMetricExporter({
24
+ url: this.#config.endpoint,
25
+ headers: this.#config.headers
26
+ })
27
+
28
+ const interval = this.#config.interval ?? 60000
29
+ this.#timer = setInterval(() => {
30
+ this.flush().catch(error => {
31
+ this.#logger.error({ err: error }, 'OpenTelemetry metrics export failed.')
32
+ })
33
+ }, interval)
34
+ this.#timer.unref()
35
+
36
+ return true
37
+ }
38
+
39
+ collect (resourceMetrics) {
40
+ if (!this.#exporter) {
41
+ return
42
+ }
43
+
44
+ this.#queue.push(resourceMetrics)
45
+ }
46
+
47
+ async flush () {
48
+ if (!this.#exporter || this.#flushing || this.#queue.length === 0) {
49
+ return
50
+ }
51
+
52
+ this.#flushing = true
53
+ const batch = this.#queue.splice(0)
54
+
55
+ try {
56
+ await Promise.all(batch.map(resourceMetrics => this.#export(resourceMetrics)))
57
+ } finally {
58
+ this.#flushing = false
59
+ }
60
+ }
61
+
62
+ async close () {
63
+ if (this.#timer) {
64
+ clearInterval(this.#timer)
65
+ this.#timer = null
66
+ }
67
+
68
+ try {
69
+ await this.flush()
70
+ } catch (error) {
71
+ this.#logger.error({ err: error }, 'OpenTelemetry metrics export failed.')
72
+ }
73
+
74
+ if (this.#exporter) {
75
+ await this.#exporter.shutdown()
76
+ this.#exporter = null
77
+ }
78
+ }
79
+
80
+ #export (resourceMetrics) {
81
+ const { promise, resolve, reject } = Promise.withResolvers()
82
+
83
+ this.#exporter.export(resourceMetrics, result => {
84
+ if (result.code === 0) {
85
+ resolve()
86
+ } else {
87
+ reject(result.error ?? new Error('OpenTelemetry metrics export failed.'))
88
+ }
89
+ })
90
+
91
+ return promise
92
+ }
93
+ }
@@ -76,13 +76,59 @@ async function checkLiveness (runtime) {
76
76
  return { response, status }
77
77
  }
78
78
 
79
- export async function startPrometheusServer (runtime, opts, healthProbes) {
80
- const metricsEnabled = opts.enabled !== false
81
- const healthProbesEnabled = healthProbes !== false
79
+ function isHealthProbesServerEnabled (healthProbes) {
80
+ return typeof healthProbes === 'object' && healthProbes !== null
81
+ }
82
+
83
+ function isHealthProbesEnabled (healthProbes) {
84
+ return healthProbes !== false && healthProbes?.enabled !== false
85
+ }
86
+
87
+ function resolveServerAddress (opts, fallback) {
88
+ opts = typeof opts === 'object' && opts !== null ? opts : {}
89
+ fallback = typeof fallback === 'object' && fallback !== null ? fallback : {}
90
+
91
+ return {
92
+ host: opts.hostname ?? fallback.hostname ?? DEFAULT_HOSTNAME,
93
+ port: opts.port ?? fallback.port ?? DEFAULT_PORT
94
+ }
95
+ }
96
+
97
+ function useSharedServer (metricsOpts, healthProbes) {
98
+ if (!isHealthProbesServerEnabled(healthProbes)) {
99
+ return true
100
+ }
101
+
102
+ const metricsAddress = resolveServerAddress(metricsOpts)
103
+ const healthProbesAddress = resolveServerAddress(healthProbes, metricsOpts)
104
+
105
+ return metricsAddress.host === healthProbesAddress.host && String(metricsAddress.port) === String(healthProbesAddress.port)
106
+ }
107
+
108
+ function resolveHealthProbesOptions (metricsOpts, healthProbes) {
109
+ metricsOpts = typeof metricsOpts === 'object' && metricsOpts !== null ? metricsOpts : {}
110
+
111
+ if (!isHealthProbesServerEnabled(healthProbes)) {
112
+ return metricsOpts
113
+ }
114
+
115
+ const { host, port } = resolveServerAddress(healthProbes, metricsOpts)
116
+ return {
117
+ ...healthProbes,
118
+ hostname: host,
119
+ port,
120
+ readiness: healthProbes.readiness ?? metricsOpts.readiness,
121
+ liveness: healthProbes.liveness ?? metricsOpts.liveness
122
+ }
123
+ }
124
+
125
+ async function startServer (runtime, opts, metricsEnabled, healthProbesEnabled) {
126
+ opts = typeof opts === 'object' && opts !== null ? opts : {}
82
127
 
83
128
  if (!metricsEnabled && !healthProbesEnabled) {
84
129
  return
85
130
  }
131
+
86
132
  const host = opts.hostname ?? DEFAULT_HOSTNAME
87
133
  const port = opts.port ?? DEFAULT_PORT
88
134
  const metricsEndpoint = opts.endpoint ?? DEFAULT_METRICS_ENDPOINT
@@ -238,3 +284,19 @@ export async function startPrometheusServer (runtime, opts, healthProbes) {
238
284
  await promServer.listen({ port, host })
239
285
  return promServer
240
286
  }
287
+
288
+ export async function startPrometheusServer (runtime, opts, healthProbes) {
289
+ const metricsEnabled = opts !== false && opts?.enabled !== false
290
+ const healthProbesEnabled = isHealthProbesEnabled(healthProbes) && useSharedServer(opts, healthProbes)
291
+ const serverOpts = healthProbesEnabled && isHealthProbesServerEnabled(healthProbes) ? resolveHealthProbesOptions(opts, healthProbes) : opts
292
+
293
+ return startServer(runtime, serverOpts, metricsEnabled, healthProbesEnabled)
294
+ }
295
+
296
+ export async function startHealthProbesServer (runtime, metricsOpts, healthProbes) {
297
+ if (!isHealthProbesEnabled(healthProbes) || useSharedServer(metricsOpts, healthProbes)) {
298
+ return
299
+ }
300
+
301
+ return startServer(runtime, resolveHealthProbesOptions(metricsOpts, healthProbes), false, true)
302
+ }
package/lib/runtime.js CHANGED
@@ -11,7 +11,11 @@ import {
11
11
  } from '@platformatic/foundation'
12
12
  import { getExecutable } from '@platformatic/globals'
13
13
  import { ITC } from '@platformatic/itc'
14
- import { collectProcessMetrics, client as metricsClient } from '@platformatic/metrics'
14
+ import {
15
+ collectProcessMetrics,
16
+ client as metricsClient,
17
+ openTelemetryITCMessage
18
+ } from '@platformatic/metrics'
15
19
  import fastify from 'fastify'
16
20
  import { EventEmitter, once } from 'node:events'
17
21
  import { existsSync } from 'node:fs'
@@ -45,8 +49,9 @@ import {
45
49
  import { abstractLogger, createLogger } from './logger.js'
46
50
  import { startManagementApi } from './management-api.js'
47
51
  import { createManagementHandlers } from './management-handlers.js'
52
+ import { OpenTelemetryMetricsForwarder } from './opentelemetry-metrics.js'
48
53
  import { createChannelCreationHook } from './policies.js'
49
- import { startPrometheusServer } from './prom-server.js'
54
+ import { startHealthProbesServer, startPrometheusServer } from './prom-server.js'
50
55
  import { startScheduler } from './scheduler.js'
51
56
  import { createSharedStore } from './shared-http-cache.js'
52
57
  import { topologicalLevels, topologicalSort } from './utils.js'
@@ -135,6 +140,8 @@ export class Runtime extends EventEmitter {
135
140
 
136
141
  #managementApi
137
142
  #prometheusServer
143
+ #healthProbesServer
144
+ #opentelemetryMetricsForwarder
138
145
  #inspectorServer
139
146
  #metricsLabelName
140
147
 
@@ -238,12 +245,17 @@ export class Runtime extends EventEmitter {
238
245
  if (config.metrics) {
239
246
  // Use the configured application label name for metrics (defaults to 'applicationId')
240
247
  this.#metricsLabelName = config.metrics.applicationLabel || 'applicationId'
241
- this.#prometheusServer = await startPrometheusServer(this, config.metrics, config.healthProbes)
242
248
  } else {
243
249
  // Default to applicationId if metrics are not configured
244
250
  this.#metricsLabelName = 'applicationId'
245
251
  }
246
252
 
253
+ if (config.metrics || (typeof config.healthProbes === 'object' && config.healthProbes !== null)) {
254
+ this.#prometheusServer = await startPrometheusServer(this, config.metrics ?? false, config.healthProbes)
255
+ }
256
+
257
+ this.#healthProbesServer = await startHealthProbesServer(this, config.metrics, config.healthProbes)
258
+
247
259
  // Initialize process-level metrics registry in the main thread if metrics or management API is enabled
248
260
  // These metrics are the same across all workers and only need to be collected once
249
261
  // We need this for management API as it can request metrics even without explicit metrics config
@@ -258,6 +270,8 @@ export class Runtime extends EventEmitter {
258
270
  this.#loggerDestination = destination
259
271
  this.#loggerContext = context
260
272
 
273
+ await this.#startOpenTelemetryMetricsForwarder(config.metrics?.opentelemetry)
274
+
261
275
  this.#createWorkersBroadcastChannel()
262
276
 
263
277
  if (this.#config.workers.dynamic) {
@@ -407,6 +421,15 @@ export class Runtime extends EventEmitter {
407
421
  await this.#prometheusServer.close()
408
422
  }
409
423
 
424
+ if (this.#healthProbesServer) {
425
+ await this.#healthProbesServer.close()
426
+ }
427
+
428
+ if (this.#opentelemetryMetricsForwarder) {
429
+ await this.#opentelemetryMetricsForwarder.close()
430
+ this.#opentelemetryMetricsForwarder = null
431
+ }
432
+
410
433
  // Clean up process metrics registry
411
434
  if (this.#processMetricsRegistry) {
412
435
  this.#processMetricsRegistry.clear()
@@ -864,12 +887,24 @@ export class Runtime extends EventEmitter {
864
887
  this.#prometheusServer = null
865
888
  }
866
889
 
890
+ if (this.#healthProbesServer) {
891
+ await this.#healthProbesServer.close()
892
+ this.#healthProbesServer = null
893
+ }
894
+
895
+ if (this.#opentelemetryMetricsForwarder) {
896
+ await this.#opentelemetryMetricsForwarder.close()
897
+ this.#opentelemetryMetricsForwarder = null
898
+ }
899
+
867
900
  this.#config.metrics = metricsConfig
868
901
  this.#metricsLabelName = metricsConfig?.applicationLabel || 'applicationId'
869
902
 
870
- if (metricsConfig.enabled !== false || this.#config.healthProbes !== false) {
871
- this.#prometheusServer = await startPrometheusServer(this, metricsConfig, this.#config.healthProbes)
872
- }
903
+ this.#prometheusServer = await startPrometheusServer(this, metricsConfig, this.#config.healthProbes)
904
+
905
+ this.#healthProbesServer = await startHealthProbesServer(this, metricsConfig, this.#config.healthProbes)
906
+
907
+ await this.#startOpenTelemetryMetricsForwarder(metricsConfig?.opentelemetry)
873
908
 
874
909
  const promises = []
875
910
  for (const worker of this.#workers.values()) {
@@ -889,6 +924,17 @@ export class Runtime extends EventEmitter {
889
924
  return { success: true, config: metricsConfig }
890
925
  }
891
926
 
927
+ async #startOpenTelemetryMetricsForwarder (config) {
928
+ if (!config?.endpoint || config.enabled === false || config.enabled === 'false') {
929
+ return
930
+ }
931
+
932
+ const forwarder = new OpenTelemetryMetricsForwarder(config, this.logger)
933
+ if (await forwarder.start()) {
934
+ this.#opentelemetryMetricsForwarder = forwarder
935
+ }
936
+ }
937
+
892
938
  // TODO: Remove in next major version
893
939
  startCollectingMetrics () {
894
940
  this.logger.warn(
@@ -2026,6 +2072,10 @@ export class Runtime extends EventEmitter {
2026
2072
  worker[kIsSubprocessHost] = true
2027
2073
  })
2028
2074
 
2075
+ worker[kITC].on(openTelemetryITCMessage, resourceMetrics => {
2076
+ this.#opentelemetryMetricsForwarder?.collect(resourceMetrics)
2077
+ })
2078
+
2029
2079
  worker[kITC].on('request:restart', async () => {
2030
2080
  // Do not restart applications that are not fully started yet or when the runtime is still starting.
2031
2081
  // The gateway sends request:restart when it receives application:added events,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.59.0",
3
+ "version": "3.61.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -18,6 +18,7 @@
18
18
  "@fastify/compress": "^8.0.0",
19
19
  "@fastify/express": "^4.0.0",
20
20
  "@fastify/formbody": "^8.0.0",
21
+ "@opentelemetry/sdk-metrics": "^2.8.0",
21
22
  "@sentry/node": "^10.58.0",
22
23
  "atomic-sleep": "^1.0.0",
23
24
  "autocannon": "^8.0.0",
@@ -38,14 +39,14 @@
38
39
  "typescript": "^5.5.4",
39
40
  "undici-oidc-interceptor": "^0.9.0",
40
41
  "why-is-node-running": "^2.2.2",
41
- "@platformatic/composer": "3.59.0",
42
- "@platformatic/db": "3.59.0",
43
- "@platformatic/gateway": "3.59.0",
44
- "@platformatic/node": "3.59.0",
45
- "@platformatic/service": "3.59.0",
46
- "@platformatic/sql-mapper": "3.59.0",
47
- "@platformatic/sql-graphql": "3.59.0",
48
- "@platformatic/wattpm-pprof-capture": "3.59.0"
42
+ "@platformatic/composer": "3.61.0",
43
+ "@platformatic/db": "3.61.0",
44
+ "@platformatic/gateway": "3.61.0",
45
+ "@platformatic/node": "3.61.0",
46
+ "@platformatic/service": "3.61.0",
47
+ "@platformatic/sql-graphql": "3.61.0",
48
+ "@platformatic/wattpm-pprof-capture": "3.61.0",
49
+ "@platformatic/sql-mapper": "3.61.0"
49
50
  },
50
51
  "dependencies": {
51
52
  "@fastify/accepts": "^5.0.0",
@@ -53,6 +54,7 @@
53
54
  "@fastify/error": "^4.0.0",
54
55
  "@fastify/websocket": "^11.0.0",
55
56
  "@opentelemetry/api": "^1.9.0",
57
+ "@opentelemetry/exporter-metrics-otlp-proto": "^0.219.0",
56
58
  "@platformatic/prom-client": "^1.0.0",
57
59
  "@platformatic/undici-cache-memory": "^0.9.0",
58
60
  "@watchable/unpromise": "^1.0.2",
@@ -75,13 +77,13 @@
75
77
  "undici": "^7.27.2",
76
78
  "undici-thread-interceptor": "^1.3.1",
77
79
  "ws": "^8.16.0",
78
- "@platformatic/basic": "3.59.0",
79
- "@platformatic/generators": "3.59.0",
80
- "@platformatic/globals": "3.59.0",
81
- "@platformatic/metrics": "3.59.0",
82
- "@platformatic/foundation": "3.59.0",
83
- "@platformatic/telemetry": "3.59.0",
84
- "@platformatic/itc": "3.59.0"
80
+ "@platformatic/basic": "3.61.0",
81
+ "@platformatic/generators": "3.61.0",
82
+ "@platformatic/foundation": "3.61.0",
83
+ "@platformatic/globals": "3.61.0",
84
+ "@platformatic/itc": "3.61.0",
85
+ "@platformatic/telemetry": "3.61.0",
86
+ "@platformatic/metrics": "3.61.0"
85
87
  },
86
88
  "engines": {
87
89
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.59.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.61.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",
@@ -2211,6 +2211,115 @@
2211
2211
  },
2212
2212
  {
2213
2213
  "type": "string"
2214
+ },
2215
+ {
2216
+ "type": "object",
2217
+ "properties": {
2218
+ "enabled": {
2219
+ "anyOf": [
2220
+ {
2221
+ "type": "boolean"
2222
+ },
2223
+ {
2224
+ "type": "string"
2225
+ }
2226
+ ]
2227
+ },
2228
+ "hostname": {
2229
+ "type": "string"
2230
+ },
2231
+ "port": {
2232
+ "anyOf": [
2233
+ {
2234
+ "type": "integer"
2235
+ },
2236
+ {
2237
+ "type": "string"
2238
+ }
2239
+ ]
2240
+ },
2241
+ "readiness": {
2242
+ "anyOf": [
2243
+ {
2244
+ "type": "boolean"
2245
+ },
2246
+ {
2247
+ "type": "object",
2248
+ "properties": {
2249
+ "endpoint": {
2250
+ "type": "string"
2251
+ },
2252
+ "success": {
2253
+ "type": "object",
2254
+ "properties": {
2255
+ "statusCode": {
2256
+ "type": "number"
2257
+ },
2258
+ "body": {
2259
+ "type": "string"
2260
+ }
2261
+ },
2262
+ "additionalProperties": false
2263
+ },
2264
+ "fail": {
2265
+ "type": "object",
2266
+ "properties": {
2267
+ "statusCode": {
2268
+ "type": "number"
2269
+ },
2270
+ "body": {
2271
+ "type": "string"
2272
+ }
2273
+ },
2274
+ "additionalProperties": false
2275
+ }
2276
+ },
2277
+ "additionalProperties": false
2278
+ }
2279
+ ]
2280
+ },
2281
+ "liveness": {
2282
+ "anyOf": [
2283
+ {
2284
+ "type": "boolean"
2285
+ },
2286
+ {
2287
+ "type": "object",
2288
+ "properties": {
2289
+ "endpoint": {
2290
+ "type": "string"
2291
+ },
2292
+ "success": {
2293
+ "type": "object",
2294
+ "properties": {
2295
+ "statusCode": {
2296
+ "type": "number"
2297
+ },
2298
+ "body": {
2299
+ "type": "string"
2300
+ }
2301
+ },
2302
+ "additionalProperties": false
2303
+ },
2304
+ "fail": {
2305
+ "type": "object",
2306
+ "properties": {
2307
+ "statusCode": {
2308
+ "type": "number"
2309
+ },
2310
+ "body": {
2311
+ "type": "string"
2312
+ }
2313
+ },
2314
+ "additionalProperties": false
2315
+ }
2316
+ },
2317
+ "additionalProperties": false
2318
+ }
2319
+ ]
2320
+ }
2321
+ },
2322
+ "additionalProperties": false
2214
2323
  }
2215
2324
  ],
2216
2325
  "default": true
@@ -2687,7 +2796,9 @@
2687
2796
  "type": "string"
2688
2797
  }
2689
2798
  ],
2690
- "default": 5000
2799
+ "default": 5000,
2800
+ "deprecated": true,
2801
+ "description": "Deprecated. Health probe timeout configuration is no longer used."
2691
2802
  },
2692
2803
  "plugins": {
2693
2804
  "type": "array",
@@ -2763,6 +2874,50 @@
2763
2874
  ],
2764
2875
  "additionalProperties": false
2765
2876
  },
2877
+ "opentelemetry": {
2878
+ "type": "object",
2879
+ "description": "Configuration for forwarding user OpenTelemetry metrics to an OTLP endpoint",
2880
+ "properties": {
2881
+ "enabled": {
2882
+ "anyOf": [
2883
+ {
2884
+ "type": "boolean"
2885
+ },
2886
+ {
2887
+ "type": "string"
2888
+ }
2889
+ ],
2890
+ "description": "Enable or disable OpenTelemetry metrics forwarding"
2891
+ },
2892
+ "endpoint": {
2893
+ "type": "string",
2894
+ "description": "OTLP metrics endpoint URL (e.g., http://collector:4318/v1/metrics)"
2895
+ },
2896
+ "interval": {
2897
+ "anyOf": [
2898
+ {
2899
+ "type": "integer"
2900
+ },
2901
+ {
2902
+ "type": "string"
2903
+ }
2904
+ ],
2905
+ "default": 60000,
2906
+ "description": "Interval in milliseconds between metric forwards"
2907
+ },
2908
+ "headers": {
2909
+ "type": "object",
2910
+ "additionalProperties": {
2911
+ "type": "string"
2912
+ },
2913
+ "description": "Additional HTTP headers for authentication"
2914
+ }
2915
+ },
2916
+ "required": [
2917
+ "endpoint"
2918
+ ],
2919
+ "additionalProperties": false
2920
+ },
2766
2921
  "httpCustomLabels": {
2767
2922
  "type": "array",
2768
2923
  "description": "Custom labels to add to HTTP metrics (http_request_all_duration_seconds). Each label extracts its value from an HTTP request header.",