@platformatic/runtime 3.58.1 → 3.60.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
@@ -18,6 +18,12 @@ export type PlatformaticRuntimeConfig = {
18
18
  mappings?: {
19
19
  [k: string]: {
20
20
  id: string;
21
+ enabled?:
22
+ | boolean
23
+ | string
24
+ | {
25
+ [k: string]: boolean;
26
+ };
21
27
  config?: string;
22
28
  useHttp?: boolean;
23
29
  reuseTcpPorts?: boolean;
@@ -180,6 +186,11 @@ export type PlatformaticRuntimeConfig = {
180
186
  url: string;
181
187
  };
182
188
  captureStdio?: boolean;
189
+ pino?: {
190
+ level?: string;
191
+ time?: string;
192
+ message?: string;
193
+ };
183
194
  [k: string]: unknown;
184
195
  };
185
196
  server?: {
@@ -243,7 +254,40 @@ export type PlatformaticRuntimeConfig = {
243
254
  bufferPoolSize?: number | string;
244
255
  defaultHighWaterMark?: number | string;
245
256
  };
246
- 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
+ };
247
291
  undici?: {
248
292
  agentOptions?: {
249
293
  [k: string]: unknown;
@@ -402,6 +446,10 @@ export type PlatformaticRuntimeConfig = {
402
446
  body?: string;
403
447
  };
404
448
  };
449
+ /**
450
+ * @deprecated
451
+ * Deprecated. Health probe timeout configuration is no longer used.
452
+ */
405
453
  healthChecksTimeouts?: number | string;
406
454
  plugins?: string[];
407
455
  timeout?: number | string;
@@ -436,6 +484,29 @@ export type PlatformaticRuntimeConfig = {
436
484
  */
437
485
  serviceVersion?: string;
438
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
+ };
439
510
  /**
440
511
  * Custom labels to add to HTTP metrics (http_request_all_duration_seconds). Each label extracts its value from an HTTP request header.
441
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/config.js CHANGED
@@ -295,8 +295,27 @@ export async function prepareApplication (config, application, defaultWorkers) {
295
295
  return application
296
296
  }
297
297
 
298
+ function isApplicationEnabled (application, environment) {
299
+ const { enabled } = application
300
+
301
+ if (typeof enabled === 'undefined') {
302
+ return true
303
+ }
304
+
305
+ if (typeof enabled === 'string') {
306
+ return enabled !== 'false'
307
+ }
308
+
309
+ if (typeof enabled === 'object' && enabled !== null) {
310
+ return enabled[environment] ?? true
311
+ }
312
+
313
+ return enabled
314
+ }
315
+
298
316
  export async function transform (config, _, context) {
299
317
  const production = context?.isProduction ?? context?.production
318
+ const environment = production ? 'production' : 'development'
300
319
  const applications = [...(config.applications ?? []), ...(config.services ?? []), ...(config.web ?? [])]
301
320
 
302
321
  const watchType = typeof config.watch
@@ -391,6 +410,12 @@ export async function transform (config, _, context) {
391
410
  }
392
411
  }
393
412
 
413
+ for (let i = applications.length - 1; i >= 0; --i) {
414
+ if (!isApplicationEnabled(applications[i], environment)) {
415
+ applications.splice(i, 1)
416
+ }
417
+ }
418
+
394
419
  config.inspectorOptions = undefined
395
420
  parseInspectorOptions(config, context?.inspect, context?.inspectBreak)
396
421
 
package/lib/logger.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  buildPinoFormatters,
3
3
  buildPinoTimestamp,
4
+ kMetadata,
4
5
  } from '@platformatic/foundation'
5
6
  import { isatty } from 'node:tty'
6
7
  import pino from 'pino'
@@ -63,7 +64,8 @@ const colors = [
63
64
  226 // bright yellow
64
65
  ]
65
66
 
66
- function createLoggerContext () {
67
+ function createLoggerContext (config) {
68
+ const env = config[kMetadata].env
67
69
  const context = {
68
70
  colors: {},
69
71
  closeables: [],
@@ -76,7 +78,7 @@ function createLoggerContext () {
76
78
  context.maxLength = Math.max(context.maxLength, id.length)
77
79
  let hash = 0
78
80
 
79
- if (!pretty.isColorSupported && process.env.FORCE_COLOR !== 'true') {
81
+ if (!pretty.isColorSupported && env.FORCE_COLOR !== 'true') {
80
82
  context.colors[id] = ''
81
83
  continue
82
84
  }
@@ -151,7 +153,8 @@ function createPrettifier (context) {
151
153
 
152
154
  // Create the runtime logger
153
155
  export async function createLogger (config) {
154
- const context = createLoggerContext()
156
+ const env = config[kMetadata].env
157
+ const context = createLoggerContext(config)
155
158
 
156
159
  const loggerConfig = { ...config.logger, transport: undefined }
157
160
  if (config.logger.base === null) {
@@ -168,7 +171,7 @@ export async function createLogger (config) {
168
171
  }
169
172
 
170
173
  cliStream = pino.transport(config.logger.transport)
171
- } else if ((process.env.FORCE_TTY || isatty(1)) && process.env.PLT_PRETTY_PRINT !== 'false') {
174
+ } else if ((env.FORCE_TTY || isatty(1)) && env.PLT_PRETTY_PRINT !== 'false') {
172
175
  cliStream = createPrettifier(context)
173
176
  } else {
174
177
  cliStream = pino.destination(1)
@@ -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'
@@ -116,6 +121,10 @@ export class Runtime extends EventEmitter {
116
121
  #root
117
122
  #config
118
123
  #env
124
+ #pinoLevelKey
125
+ #pinoTimeKey
126
+ #pinoMessageKey
127
+ #pinoCustomizedKeys
119
128
  #context
120
129
  #sharedContext
121
130
  #isProduction
@@ -131,6 +140,8 @@ export class Runtime extends EventEmitter {
131
140
 
132
141
  #managementApi
133
142
  #prometheusServer
143
+ #healthProbesServer
144
+ #opentelemetryMetricsForwarder
134
145
  #inspectorServer
135
146
  #metricsLabelName
136
147
 
@@ -159,6 +170,11 @@ export class Runtime extends EventEmitter {
159
170
  this.#config = config
160
171
  this.#root = config[kMetadata].root
161
172
  this.#env = config[kMetadata].env
173
+ this.#pinoLevelKey = config.logger.pino?.level ?? 'level'
174
+ this.#pinoTimeKey = config.logger.pino?.time ?? 'time'
175
+ this.#pinoMessageKey = config.logger.pino?.message ?? 'msg'
176
+ this.#pinoCustomizedKeys =
177
+ this.#pinoLevelKey !== 'level' || this.#pinoTimeKey !== 'time' || this.#pinoMessageKey !== 'msg'
162
178
  this.#context = context ?? {}
163
179
  this.#isProduction = this.#context.isProduction ?? this.#context.production ?? false
164
180
  this.#concurrency = Math.max(1, config.startupConcurrency ?? this.#context.concurrency ?? DEFAULT_CONCURRENCY)
@@ -229,12 +245,17 @@ export class Runtime extends EventEmitter {
229
245
  if (config.metrics) {
230
246
  // Use the configured application label name for metrics (defaults to 'applicationId')
231
247
  this.#metricsLabelName = config.metrics.applicationLabel || 'applicationId'
232
- this.#prometheusServer = await startPrometheusServer(this, config.metrics, config.healthProbes)
233
248
  } else {
234
249
  // Default to applicationId if metrics are not configured
235
250
  this.#metricsLabelName = 'applicationId'
236
251
  }
237
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
+
238
259
  // Initialize process-level metrics registry in the main thread if metrics or management API is enabled
239
260
  // These metrics are the same across all workers and only need to be collected once
240
261
  // We need this for management API as it can request metrics even without explicit metrics config
@@ -249,6 +270,8 @@ export class Runtime extends EventEmitter {
249
270
  this.#loggerDestination = destination
250
271
  this.#loggerContext = context
251
272
 
273
+ await this.#startOpenTelemetryMetricsForwarder(config.metrics?.opentelemetry)
274
+
252
275
  this.#createWorkersBroadcastChannel()
253
276
 
254
277
  if (this.#config.workers.dynamic) {
@@ -398,6 +421,15 @@ export class Runtime extends EventEmitter {
398
421
  await this.#prometheusServer.close()
399
422
  }
400
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
+
401
433
  // Clean up process metrics registry
402
434
  if (this.#processMetricsRegistry) {
403
435
  this.#processMetricsRegistry.clear()
@@ -855,12 +887,24 @@ export class Runtime extends EventEmitter {
855
887
  this.#prometheusServer = null
856
888
  }
857
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
+
858
900
  this.#config.metrics = metricsConfig
859
901
  this.#metricsLabelName = metricsConfig?.applicationLabel || 'applicationId'
860
902
 
861
- if (metricsConfig.enabled !== false || this.#config.healthProbes !== false) {
862
- this.#prometheusServer = await startPrometheusServer(this, metricsConfig, this.#config.healthProbes)
863
- }
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)
864
908
 
865
909
  const promises = []
866
910
  for (const worker of this.#workers.values()) {
@@ -880,6 +924,17 @@ export class Runtime extends EventEmitter {
880
924
  return { success: true, config: metricsConfig }
881
925
  }
882
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
+
883
938
  // TODO: Remove in next major version
884
939
  startCollectingMetrics () {
885
940
  this.logger.warn(
@@ -2017,6 +2072,10 @@ export class Runtime extends EventEmitter {
2017
2072
  worker[kIsSubprocessHost] = true
2018
2073
  })
2019
2074
 
2075
+ worker[kITC].on(openTelemetryITCMessage, resourceMetrics => {
2076
+ this.#opentelemetryMetricsForwarder?.collect(resourceMetrics)
2077
+ })
2078
+
2020
2079
  worker[kITC].on('request:restart', async () => {
2021
2080
  // Do not restart applications that are not fully started yet or when the runtime is still starting.
2022
2081
  // The gateway sends request:restart when it receives application:added events,
@@ -2884,13 +2943,16 @@ export class Runtime extends EventEmitter {
2884
2943
  }
2885
2944
 
2886
2945
  let pinoLog
2946
+ let pinoLevel
2887
2947
 
2888
2948
  if (message !== null && typeof message === 'object') {
2949
+ pinoLevel = message[this.#pinoLevelKey]
2950
+
2889
2951
  pinoLog =
2890
- typeof message.level === 'number' &&
2952
+ (typeof pinoLevel === 'number' || (this.#pinoCustomizedKeys && typeof pinoLevel === 'string')) &&
2891
2953
  // We want to accept both pino raw time (number) and time as formatted string
2892
- (typeof message.time === 'number' || typeof message.time === 'string') &&
2893
- typeof message.msg === 'string'
2954
+ (typeof message[this.#pinoTimeKey] === 'number' || typeof message[this.#pinoTimeKey] === 'string') &&
2955
+ typeof message[this.#pinoMessageKey] === 'string'
2894
2956
  }
2895
2957
 
2896
2958
  // Directly write to the Pino destination
@@ -2899,9 +2961,14 @@ export class Runtime extends EventEmitter {
2899
2961
  continue
2900
2962
  }
2901
2963
 
2902
- this.#loggerDestination.lastLevel = message.level
2903
- this.#loggerDestination.lastTime = message.time
2904
- this.#loggerDestination.lastMsg = message.msg
2964
+ if (typeof pinoLevel === 'string') {
2965
+ pinoLevel =
2966
+ logger.levels.values[pinoLevel] ?? logger.levels.values[pinoLevel.toLowerCase()] ?? logger.levels.values[level]
2967
+ }
2968
+
2969
+ this.#loggerDestination.lastLevel = pinoLevel
2970
+ this.#loggerDestination.lastTime = message[this.#pinoTimeKey]
2971
+ this.#loggerDestination.lastMsg = message[this.#pinoMessageKey]
2905
2972
  this.#loggerDestination.lastObj = message
2906
2973
  this.#loggerDestination.lastLogger = logger
2907
2974
  this.#loggerDestination.write(raw + '\n')
package/lib/schema.js CHANGED
@@ -10,6 +10,25 @@ const runtimeLogger = {
10
10
  captureStdio: {
11
11
  type: 'boolean',
12
12
  default: true
13
+ },
14
+ pino: {
15
+ type: 'object',
16
+ default: {},
17
+ properties: {
18
+ level: {
19
+ type: 'string',
20
+ default: 'level'
21
+ },
22
+ time: {
23
+ type: 'string',
24
+ default: 'time'
25
+ },
26
+ message: {
27
+ type: 'string',
28
+ default: 'msg'
29
+ }
30
+ },
31
+ additionalProperties: false
13
32
  }
14
33
  }
15
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.58.1",
3
+ "version": "3.60.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.58.1",
42
- "@platformatic/gateway": "3.58.1",
43
- "@platformatic/node": "3.58.1",
44
- "@platformatic/sql-graphql": "3.58.1",
45
- "@platformatic/sql-mapper": "3.58.1",
46
- "@platformatic/db": "3.58.1",
47
- "@platformatic/service": "3.58.1",
48
- "@platformatic/wattpm-pprof-capture": "3.58.1"
42
+ "@platformatic/composer": "3.60.0",
43
+ "@platformatic/db": "3.60.0",
44
+ "@platformatic/gateway": "3.60.0",
45
+ "@platformatic/node": "3.60.0",
46
+ "@platformatic/service": "3.60.0",
47
+ "@platformatic/sql-graphql": "3.60.0",
48
+ "@platformatic/sql-mapper": "3.60.0",
49
+ "@platformatic/wattpm-pprof-capture": "3.60.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.58.1",
79
- "@platformatic/foundation": "3.58.1",
80
- "@platformatic/globals": "3.58.1",
81
- "@platformatic/generators": "3.58.1",
82
- "@platformatic/metrics": "3.58.1",
83
- "@platformatic/itc": "3.58.1",
84
- "@platformatic/telemetry": "3.58.1"
80
+ "@platformatic/basic": "3.60.0",
81
+ "@platformatic/foundation": "3.60.0",
82
+ "@platformatic/generators": "3.60.0",
83
+ "@platformatic/globals": "3.60.0",
84
+ "@platformatic/itc": "3.60.0",
85
+ "@platformatic/telemetry": "3.60.0",
86
+ "@platformatic/metrics": "3.60.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.58.1.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.60.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",
@@ -58,6 +58,23 @@
58
58
  "id": {
59
59
  "type": "string"
60
60
  },
61
+ "enabled": {
62
+ "anyOf": [
63
+ {
64
+ "type": "boolean"
65
+ },
66
+ {
67
+ "type": "string"
68
+ },
69
+ {
70
+ "type": "object",
71
+ "additionalProperties": {
72
+ "type": "boolean"
73
+ }
74
+ }
75
+ ],
76
+ "default": true
77
+ },
61
78
  "config": {
62
79
  "type": "string"
63
80
  },
@@ -427,6 +444,23 @@
427
444
  "id": {
428
445
  "type": "string"
429
446
  },
447
+ "enabled": {
448
+ "anyOf": [
449
+ {
450
+ "type": "boolean"
451
+ },
452
+ {
453
+ "type": "string"
454
+ },
455
+ {
456
+ "type": "object",
457
+ "additionalProperties": {
458
+ "type": "boolean"
459
+ }
460
+ }
461
+ ],
462
+ "default": true
463
+ },
430
464
  "path": {
431
465
  "type": "string",
432
466
  "allowEmptyPaths": true,
@@ -806,6 +840,23 @@
806
840
  "id": {
807
841
  "type": "string"
808
842
  },
843
+ "enabled": {
844
+ "anyOf": [
845
+ {
846
+ "type": "boolean"
847
+ },
848
+ {
849
+ "type": "string"
850
+ },
851
+ {
852
+ "type": "object",
853
+ "additionalProperties": {
854
+ "type": "boolean"
855
+ }
856
+ }
857
+ ],
858
+ "default": true
859
+ },
809
860
  "path": {
810
861
  "type": "string",
811
862
  "allowEmptyPaths": true,
@@ -1185,6 +1236,23 @@
1185
1236
  "id": {
1186
1237
  "type": "string"
1187
1238
  },
1239
+ "enabled": {
1240
+ "anyOf": [
1241
+ {
1242
+ "type": "boolean"
1243
+ },
1244
+ {
1245
+ "type": "string"
1246
+ },
1247
+ {
1248
+ "type": "object",
1249
+ "additionalProperties": {
1250
+ "type": "boolean"
1251
+ }
1252
+ }
1253
+ ],
1254
+ "default": true
1255
+ },
1188
1256
  "path": {
1189
1257
  "type": "string",
1190
1258
  "allowEmptyPaths": true,
@@ -1781,6 +1849,25 @@
1781
1849
  "captureStdio": {
1782
1850
  "type": "boolean",
1783
1851
  "default": true
1852
+ },
1853
+ "pino": {
1854
+ "type": "object",
1855
+ "default": {},
1856
+ "properties": {
1857
+ "level": {
1858
+ "type": "string",
1859
+ "default": "level"
1860
+ },
1861
+ "time": {
1862
+ "type": "string",
1863
+ "default": "time"
1864
+ },
1865
+ "message": {
1866
+ "type": "string",
1867
+ "default": "msg"
1868
+ }
1869
+ },
1870
+ "additionalProperties": false
1784
1871
  }
1785
1872
  },
1786
1873
  "default": {},
@@ -2124,6 +2211,115 @@
2124
2211
  },
2125
2212
  {
2126
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
2127
2323
  }
2128
2324
  ],
2129
2325
  "default": true
@@ -2600,7 +2796,9 @@
2600
2796
  "type": "string"
2601
2797
  }
2602
2798
  ],
2603
- "default": 5000
2799
+ "default": 5000,
2800
+ "deprecated": true,
2801
+ "description": "Deprecated. Health probe timeout configuration is no longer used."
2604
2802
  },
2605
2803
  "plugins": {
2606
2804
  "type": "array",
@@ -2676,6 +2874,50 @@
2676
2874
  ],
2677
2875
  "additionalProperties": false
2678
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
+ },
2679
2921
  "httpCustomLabels": {
2680
2922
  "type": "array",
2681
2923
  "description": "Custom labels to add to HTTP metrics (http_request_all_duration_seconds). Each label extracts its value from an HTTP request header.",