@platformatic/runtime 3.53.0-alpha.0 → 3.54.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
@@ -493,6 +493,10 @@ export type PlatformaticRuntimeConfig = {
493
493
  additionalProperties?: never;
494
494
  [k: string]: unknown;
495
495
  };
496
+ /**
497
+ * Enable the OpenTelemetry diagnostic logger. Diagnostic messages are forwarded to the Platformatic global logger using the current logger level.
498
+ */
499
+ diagLogger?: boolean | string;
496
500
  };
497
501
  verticalScaler?: {
498
502
  enabled?: boolean;
package/lib/runtime.js CHANGED
@@ -77,6 +77,8 @@ const kInspectorOptions = Symbol('plt.runtime.worker.inspectorOptions')
77
77
  const kHeapCheckCounter = Symbol('plt.runtime.worker.heapCheckCounter')
78
78
  const kLastHeapStats = Symbol('plt.runtime.worker.lastHeapStats')
79
79
  const kHealthITCTimeoutMs = 5000
80
+ const kApplicationRestartsMetricName = 'platformatic_application_restarts_total'
81
+ const kApplicationRestartsMetricHelp = 'Total number of restarts triggered by the runtime for an application.'
80
82
 
81
83
  const MAX_LISTENERS_COUNT = 100
82
84
 
@@ -133,6 +135,7 @@ export class Runtime extends EventEmitter {
133
135
 
134
136
  #applicationsConfigsPatches
135
137
  #applications
138
+ #applicationRestartCounts
136
139
  #workers
137
140
  #workersBroadcastChannel
138
141
  #workerITCHandlers
@@ -158,6 +161,7 @@ export class Runtime extends EventEmitter {
158
161
  this.#isProduction = this.#context.isProduction ?? this.#context.production ?? false
159
162
  this.#concurrency = Math.max(1, config.startupConcurrency ?? this.#context.concurrency ?? DEFAULT_CONCURRENCY)
160
163
  this.#applications = new Map()
164
+ this.#applicationRestartCounts = new Map()
161
165
  this.#workers = new RoundRobinMap()
162
166
  this.#url = undefined
163
167
  this.#entrypointPort = undefined
@@ -501,6 +505,7 @@ export class Runtime extends EventEmitter {
501
505
  }
502
506
 
503
507
  this.#applications.set(application.id, application)
508
+ this.#applicationRestartCounts.set(application.id, this.#applicationRestartCounts.get(application.id) ?? 0)
504
509
  setupInvocations.push([application])
505
510
  toStart.push(application.id)
506
511
  }
@@ -542,6 +547,7 @@ export class Runtime extends EventEmitter {
542
547
  for (const application of applications) {
543
548
  this.#dynamicWorkersScaler?.remove(application)
544
549
  this.#applications.delete(application)
550
+ this.#applicationRestartCounts.delete(application)
545
551
  }
546
552
 
547
553
  for (const application of applications) {
@@ -712,6 +718,7 @@ export class Runtime extends EventEmitter {
712
718
  await this.#replaceWorker(config, applicationConfig, workersCount, id, i, worker, true)
713
719
  }
714
720
 
721
+ this.#incrementApplicationRestartCount(id)
715
722
  this.emitAndNotify('application:restarted', id)
716
723
  } finally {
717
724
  this.#restartingApplications.delete(id)
@@ -1151,6 +1158,10 @@ export class Runtime extends EventEmitter {
1151
1158
  async getMetrics (format = 'json') {
1152
1159
  let metrics = null
1153
1160
 
1161
+ const applicationRestartMetrics = format === 'json'
1162
+ ? this.#getApplicationRestartMetricsJson()
1163
+ : this.#formatApplicationRestartMetricsText()
1164
+
1154
1165
  // Get process-level metrics once from main thread registry (if available)
1155
1166
  let processMetricsJson = null
1156
1167
  if (this.#processMetricsRegistry) {
@@ -1216,9 +1227,68 @@ export class Runtime extends EventEmitter {
1216
1227
  }
1217
1228
  }
1218
1229
 
1230
+ if (metrics !== null && applicationRestartMetrics.length > 0) {
1231
+ metrics = format === 'json'
1232
+ ? [...applicationRestartMetrics, ...metrics]
1233
+ : applicationRestartMetrics + metrics
1234
+ }
1235
+
1219
1236
  return { metrics }
1220
1237
  }
1221
1238
 
1239
+ #incrementApplicationRestartCount (applicationId) {
1240
+ this.#applicationRestartCounts.set(applicationId, (this.#applicationRestartCounts.get(applicationId) ?? 0) + 1)
1241
+ }
1242
+
1243
+ #getApplicationRestartMetricLabels (applicationId) {
1244
+ return {
1245
+ ...this.#config.metrics?.labels,
1246
+ [this.#metricsLabelName]: applicationId
1247
+ }
1248
+ }
1249
+
1250
+ #getApplicationRestartMetricsJson () {
1251
+ const metrics = []
1252
+
1253
+ for (const applicationId of this.#applications.keys()) {
1254
+ metrics.push({
1255
+ name: kApplicationRestartsMetricName,
1256
+ help: kApplicationRestartsMetricHelp,
1257
+ type: 'counter',
1258
+ aggregator: 'sum',
1259
+ values: [{
1260
+ value: this.#applicationRestartCounts.get(applicationId) ?? 0,
1261
+ labels: this.#getApplicationRestartMetricLabels(applicationId),
1262
+ metricName: kApplicationRestartsMetricName
1263
+ }]
1264
+ })
1265
+ }
1266
+
1267
+ return metrics
1268
+ }
1269
+
1270
+ #formatApplicationRestartMetricsText () {
1271
+ let output = ''
1272
+
1273
+ for (const applicationId of this.#applications.keys()) {
1274
+ const labelParts = []
1275
+ const labels = this.#getApplicationRestartMetricLabels(applicationId)
1276
+
1277
+ for (const [key, val] of Object.entries(labels)) {
1278
+ const escapedVal = String(val).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')
1279
+ labelParts.push(`${key}="${escapedVal}"`)
1280
+ }
1281
+
1282
+ const labelStr = labelParts.length > 0 ? `{${labelParts.join(',')}}` : ''
1283
+
1284
+ output += `# HELP ${kApplicationRestartsMetricName} ${kApplicationRestartsMetricHelp}\n`
1285
+ output += `# TYPE ${kApplicationRestartsMetricName} counter\n`
1286
+ output += `${kApplicationRestartsMetricName}${labelStr} ${this.#applicationRestartCounts.get(applicationId) ?? 0}\n`
1287
+ }
1288
+
1289
+ return output
1290
+ }
1291
+
1222
1292
  // Apply labels to process metrics and push to output array (for JSON format)
1223
1293
  #applyLabelsToMetrics (processMetrics, labels, outputArray) {
1224
1294
  for (let i = 0; i < processMetrics.length; i++) {
@@ -2152,6 +2222,7 @@ export class Runtime extends EventEmitter {
2152
2222
  )
2153
2223
 
2154
2224
  await this.#replaceWorker(config, applicationConfig, workersCount, id, index, worker)
2225
+ this.#incrementApplicationRestartCount(id)
2155
2226
  } catch (e) {
2156
2227
  this.logger.error(
2157
2228
  `Cannot replace the ${errorLabel}. Forcefully terminating it ...`
@@ -2477,6 +2548,7 @@ export class Runtime extends EventEmitter {
2477
2548
  try {
2478
2549
  await this.#setupWorker(config, applicationConfig, workersCount, id, index)
2479
2550
  await this.#startWorker(config, applicationConfig, workersCount, id, index, silent, bootstrapAttempt)
2551
+ this.#incrementApplicationRestartCount(id)
2480
2552
 
2481
2553
  this.logger.info(
2482
2554
  `The ${this.#workerExtendedLabel(id, index, workersCount)} has been successfully restarted ...`
package/lib/worker/itc.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ensureLoggableError, executeInParallel, executeWithTimeout, kTimeout } from '@platformatic/foundation'
2
- import { ITC } from '@platformatic/itc'
2
+ import { initializeITCTelemetry, ITC } from '@platformatic/itc'
3
3
  import { Unpromise } from '@watchable/unpromise'
4
4
  import { once } from 'node:events'
5
5
  import { createRequire } from 'node:module'
@@ -147,7 +147,9 @@ export async function waitEventFromITC (worker, event) {
147
147
  return safeHandleInITC(worker, () => once(worker[kITC], event))
148
148
  }
149
149
 
150
- export function setupITC (controller, application, dispatcher, sharedContext) {
150
+ export async function setupITC (controller, application, dispatcher, sharedContext) {
151
+ await initializeITCTelemetry()
152
+
151
153
  const logger = globalThis.platformatic.logger
152
154
  const messaging = new MessagingITC(controller.applicationConfig.id, workerData.config, logger)
153
155
 
@@ -271,7 +271,7 @@ async function main () {
271
271
  }
272
272
 
273
273
  // Setup interaction with parent port
274
- const itc = setupITC(controller, applicationConfig, threadDispatcher, sharedContext)
274
+ const itc = await setupITC(controller, applicationConfig, threadDispatcher, sharedContext)
275
275
  globalThis[kITC] = itc
276
276
  globalThis.platformatic.itc = itc
277
277
 
@@ -1,5 +1,15 @@
1
1
  import { ensureLoggableError, executeWithTimeout, kTimeout } from '@platformatic/foundation'
2
- import { errors, generateRequest, generateResponse, ITC, parseRequest, sanitize } from '@platformatic/itc'
2
+ import {
3
+ errors,
4
+ generateRequest,
5
+ generateResponse,
6
+ ITC,
7
+ parseRequest,
8
+ sanitize,
9
+ startOutgoingMessagingSpan,
10
+ startOutgoingMessagingSpanSync,
11
+ traceIncomingMessagingHandler
12
+ } from '@platformatic/itc'
3
13
  import { MessagingError } from '../errors.js'
4
14
  import { RoundRobinMap } from './round-robin-map.js'
5
15
  import { kITC, kWorkersBroadcast } from './symbols.js'
@@ -50,69 +60,106 @@ export class MessagingITC extends ITC {
50
60
  handle (message, handler) {
51
61
  if (typeof message === 'object') {
52
62
  for (const [name, fn] of Object.entries(message)) {
53
- super.handle(name, fn)
63
+ super.handle(name, this.#wrapHandler(name, fn))
54
64
  }
55
65
  } else {
56
- super.handle(message, handler)
66
+ super.handle(message, this.#wrapHandler(message, handler))
57
67
  }
58
68
  }
59
69
 
60
70
  async send (application, name, message, options) {
61
- // Get the next worker for the application
62
- const worker = this.#workers.next(application)
71
+ const telemetry = startOutgoingMessagingSpan('send', this.#id, application, name, {
72
+ telemetryContext: options?.telemetryContext,
73
+ telemetryMetadata: options?.telemetryMetadata
74
+ })
75
+ let error = null
63
76
 
64
- if (!worker) {
65
- throw new MessagingError(application, 'No workers available')
66
- }
77
+ try {
78
+ // Get the next worker for the application
79
+ const worker = this.#workers.next(application)
67
80
 
68
- if (!worker.channel) {
69
- // Use twice the value here as a fallback measure. The target handler in the main thread is forwarding
70
- // the request to the worker, using executeWithTimeout with the user set timeout value.
71
- const channel = await executeWithTimeout(
72
- globalThis[kITC].send('getWorkerMessagingChannel', {
73
- id: this.#id,
74
- application: worker.application,
75
- worker: worker.worker
76
- }),
77
- this.#timeout * 2
78
- )
81
+ if (!worker) {
82
+ throw new MessagingError(application, 'No workers available')
83
+ }
84
+
85
+ if (!worker.channel) {
86
+ // Use twice the value here as a fallback measure. The target handler in the main thread is forwarding
87
+ // the request to the worker, using executeWithTimeout with the user set timeout value.
88
+ const channel = await executeWithTimeout(
89
+ globalThis[kITC].send('getWorkerMessagingChannel', {
90
+ id: this.#id,
91
+ application: worker.application,
92
+ worker: worker.worker
93
+ }),
94
+ this.#timeout * 2
95
+ )
96
+
97
+ /* c8 ignore next 3 - Hard to test */
98
+ if (channel === kTimeout) {
99
+ throw new MessagingError(application, 'Timeout while waiting for a communication channel.')
100
+ }
101
+
102
+ worker.channel = channel
103
+ this.#setupChannel(channel)
79
104
 
80
- /* c8 ignore next 3 - Hard to test */
81
- if (channel === kTimeout) {
82
- throw new MessagingError(application, 'Timeout while waiting for a communication channel.')
105
+ channel[kPendingResponses] = new Map()
106
+ channel.on('close', this.#handlePendingResponse.bind(this, channel))
83
107
  }
84
108
 
85
- worker.channel = channel
86
- this.#setupChannel(channel)
109
+ const context = { ...options }
110
+ context.channel = worker.channel
111
+ context.application = worker.application
112
+ context.trackResponse = true
87
113
 
88
- channel[kPendingResponses] = new Map()
89
- channel.on('close', this.#handlePendingResponse.bind(this, channel))
90
- }
114
+ if (options?.meta || telemetry?.meta) {
115
+ context.meta = { ...options?.meta, ...telemetry?.meta }
116
+ }
91
117
 
92
- const context = { ...options }
93
- context.channel = worker.channel
94
- context.application = worker.application
95
- context.trackResponse = true
118
+ const send = () => executeWithTimeout(super.send(name, message, context), this.#timeout)
119
+ const response = telemetry ? await telemetry.run(send) : await send()
96
120
 
97
- const response = await executeWithTimeout(super.send(name, message, context), this.#timeout)
121
+ if (response === kTimeout) {
122
+ throw new MessagingError(application, 'Timeout while waiting for a response.')
123
+ }
98
124
 
99
- if (response === kTimeout) {
100
- throw new MessagingError(application, 'Timeout while waiting for a response.')
125
+ return response
126
+ } catch (err) {
127
+ error = err
128
+ throw err
129
+ } finally {
130
+ telemetry?.end(error)
101
131
  }
102
-
103
- return response
104
132
  }
105
133
 
106
- notify (application, name, message) {
107
- const request = generateRequest(name, message)
134
+ notify (application, name, message, options) {
135
+ const telemetry = startOutgoingMessagingSpanSync('notify', this.#id, application, name, {
136
+ telemetryContext: options?.telemetryContext,
137
+ telemetryMetadata: options?.telemetryMetadata
138
+ })
139
+ const metadata = options?.meta || telemetry?.meta ? { ...options?.meta, ...telemetry?.meta } : undefined
140
+ const request = generateRequest(name, message, metadata)
141
+
142
+ let error = null
108
143
 
109
- let channel = this.#notificationsChannels.get(application)
110
- if (!channel) {
111
- channel = new BroadcastChannel(`plt.messaging.notifications-${application}`)
112
- this.#notificationsChannels.set(application, channel)
113
- }
144
+ try {
145
+ let channel = this.#notificationsChannels.get(application)
146
+ if (!channel) {
147
+ channel = new BroadcastChannel(`plt.messaging.notifications-${application}`)
148
+ this.#notificationsChannels.set(application, channel)
149
+ }
114
150
 
115
- channel.postMessage(sanitize(request))
151
+ const postMessage = () => channel.postMessage(sanitize(request))
152
+ if (telemetry) {
153
+ telemetry.run(postMessage)
154
+ } else {
155
+ postMessage()
156
+ }
157
+ } catch (err) {
158
+ error = err
159
+ throw err
160
+ } finally {
161
+ telemetry?.end(error)
162
+ }
116
163
  }
117
164
 
118
165
  async addSource (channel) {
@@ -165,7 +212,7 @@ export class MessagingITC extends ITC {
165
212
  #setupChannel (channel) {
166
213
  // Setup the message for processing
167
214
  channel.on('message', event => {
168
- this.#listener(event, { channel })
215
+ this.#listener(event, { channel, message: event })
169
216
  })
170
217
  }
171
218
 
@@ -207,12 +254,16 @@ export class MessagingITC extends ITC {
207
254
  throw new errors.HandlerNotFoundError(request.name)
208
255
  }
209
256
 
210
- await handler(request.data)
257
+ await handler(request.data, { application: this.#id, message: request, notification: true })
211
258
  } catch (error) {
212
259
  this.#logger.error({ error }, `"Handler for the "${request.name}" message failed.`)
213
260
  }
214
261
  }
215
262
 
263
+ #wrapHandler (messageName, handler) {
264
+ return (data, context) => traceIncomingMessagingHandler(this.#id, messageName, handler, data, context)
265
+ }
266
+
216
267
  #handlePendingResponse (channel) {
217
268
  for (const { application, request } of channel[kPendingResponses].values()) {
218
269
  this._emitResponse(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.53.0-alpha.0",
3
+ "version": "3.54.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -36,14 +36,14 @@
36
36
  "typescript": "^5.5.4",
37
37
  "undici-oidc-interceptor": "^0.5.0",
38
38
  "why-is-node-running": "^2.2.2",
39
- "@platformatic/composer": "3.53.0-alpha.0",
40
- "@platformatic/gateway": "3.53.0-alpha.0",
41
- "@platformatic/db": "3.53.0-alpha.0",
42
- "@platformatic/service": "3.53.0-alpha.0",
43
- "@platformatic/sql-graphql": "3.53.0-alpha.0",
44
- "@platformatic/node": "3.53.0-alpha.0",
45
- "@platformatic/sql-mapper": "3.53.0-alpha.0",
46
- "@platformatic/wattpm-pprof-capture": "3.53.0-alpha.0"
39
+ "@platformatic/db": "3.54.0",
40
+ "@platformatic/gateway": "3.54.0",
41
+ "@platformatic/node": "3.54.0",
42
+ "@platformatic/service": "3.54.0",
43
+ "@platformatic/composer": "3.54.0",
44
+ "@platformatic/sql-mapper": "3.54.0",
45
+ "@platformatic/sql-graphql": "3.54.0",
46
+ "@platformatic/wattpm-pprof-capture": "3.54.0"
47
47
  },
48
48
  "dependencies": {
49
49
  "@fastify/accepts": "^5.0.0",
@@ -65,7 +65,7 @@
65
65
  "help-me": "^5.0.0",
66
66
  "minimist": "^1.2.8",
67
67
  "pino": "^10.1.0",
68
- "pino-opentelemetry-transport": "^2.0.0",
68
+ "pino-opentelemetry-transport": "^3.0.0",
69
69
  "pino-pretty": "^13.0.0",
70
70
  "semgrator": "^0.3.0",
71
71
  "sonic-boom": "^4.2.0",
@@ -73,12 +73,12 @@
73
73
  "undici": "^7.0.0",
74
74
  "undici-thread-interceptor": "^1.3.1",
75
75
  "ws": "^8.16.0",
76
- "@platformatic/foundation": "3.53.0-alpha.0",
77
- "@platformatic/basic": "3.53.0-alpha.0",
78
- "@platformatic/generators": "3.53.0-alpha.0",
79
- "@platformatic/metrics": "3.53.0-alpha.0",
80
- "@platformatic/itc": "3.53.0-alpha.0",
81
- "@platformatic/telemetry": "3.53.0-alpha.0"
76
+ "@platformatic/basic": "3.54.0",
77
+ "@platformatic/foundation": "3.54.0",
78
+ "@platformatic/itc": "3.54.0",
79
+ "@platformatic/metrics": "3.54.0",
80
+ "@platformatic/generators": "3.54.0",
81
+ "@platformatic/telemetry": "3.54.0"
82
82
  },
83
83
  "engines": {
84
84
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.53.0-alpha.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.54.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",
@@ -2595,6 +2595,17 @@
2595
2595
  }
2596
2596
  }
2597
2597
  ]
2598
+ },
2599
+ "diagLogger": {
2600
+ "anyOf": [
2601
+ {
2602
+ "type": "boolean"
2603
+ },
2604
+ {
2605
+ "type": "string"
2606
+ }
2607
+ ],
2608
+ "description": "Enable the OpenTelemetry diagnostic logger. Diagnostic messages are forwarded to the Platformatic global logger using the current logger level."
2598
2609
  }
2599
2610
  },
2600
2611
  "required": [