@platformatic/runtime 3.30.0 → 3.31.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
@@ -78,6 +78,18 @@ export type PlatformaticRuntimeConfig = {
78
78
  )[];
79
79
  [k: string]: unknown;
80
80
  };
81
+ compileCache?:
82
+ | boolean
83
+ | {
84
+ /**
85
+ * Enable Node.js module compile cache for faster startup
86
+ */
87
+ enabled?: boolean;
88
+ /**
89
+ * Directory to store compile cache. Defaults to .plt/compile-cache in app root
90
+ */
91
+ directory?: string;
92
+ };
81
93
  };
82
94
  };
83
95
  };
@@ -108,7 +120,7 @@ export type PlatformaticRuntimeConfig = {
108
120
  };
109
121
  workersRestartDelay?: number | string;
110
122
  logger?: {
111
- level: (
123
+ level?: (
112
124
  | ("fatal" | "error" | "warn" | "info" | "debug" | "trace" | "silent")
113
125
  | {
114
126
  [k: string]: unknown;
@@ -501,4 +513,16 @@ export type PlatformaticRuntimeConfig = {
501
513
  [k: string]: string | [string, ...string[]];
502
514
  };
503
515
  };
516
+ compileCache?:
517
+ | boolean
518
+ | {
519
+ /**
520
+ * Enable Node.js module compile cache for faster startup
521
+ */
522
+ enabled?: boolean;
523
+ /**
524
+ * Directory to store compile cache. Defaults to .plt/compile-cache in app root
525
+ */
526
+ directory?: string;
527
+ };
504
528
  };
package/lib/runtime.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  parseMemorySize
11
11
  } from '@platformatic/foundation'
12
12
  import { ITC } from '@platformatic/itc'
13
+ import { client as metricsClient, collectProcessMetrics } from '@platformatic/metrics'
13
14
  import fastify from 'fastify'
14
15
  import { EventEmitter, once } from 'node:events'
15
16
  import { existsSync } from 'node:fs'
@@ -119,6 +120,8 @@ export class Runtime extends EventEmitter {
119
120
 
120
121
  #channelCreationHook
121
122
 
123
+ #processMetricsRegistry
124
+
122
125
  constructor (config, context) {
123
126
  super()
124
127
  this.setMaxListeners(MAX_LISTENERS_COUNT)
@@ -197,6 +200,14 @@ export class Runtime extends EventEmitter {
197
200
  this.#metricsLabelName = 'applicationId'
198
201
  }
199
202
 
203
+ // Initialize process-level metrics registry in the main thread if metrics or management API is enabled
204
+ // These metrics are the same across all workers and only need to be collected once
205
+ // We need this for management API as it can request metrics even without explicit metrics config
206
+ if (config.metrics || config.managementApi) {
207
+ this.#processMetricsRegistry = new metricsClient.Registry()
208
+ collectProcessMetrics(this.#processMetricsRegistry)
209
+ }
210
+
200
211
  // Create the logger
201
212
  const [logger, destination, context] = await createLogger(config)
202
213
  this.logger = logger
@@ -346,6 +357,12 @@ export class Runtime extends EventEmitter {
346
357
  await this.#prometheusServer.close()
347
358
  }
348
359
 
360
+ // Clean up process metrics registry
361
+ if (this.#processMetricsRegistry) {
362
+ this.#processMetricsRegistry.clear()
363
+ this.#processMetricsRegistry = null
364
+ }
365
+
349
366
  if (this.#sharedHttpCache?.close) {
350
367
  await this.#sharedHttpCache.close()
351
368
  }
@@ -1059,6 +1076,12 @@ export class Runtime extends EventEmitter {
1059
1076
  async getMetrics (format = 'json') {
1060
1077
  let metrics = null
1061
1078
 
1079
+ // Get process-level metrics once from main thread registry (if available)
1080
+ let processMetricsJson = null
1081
+ if (this.#processMetricsRegistry) {
1082
+ processMetricsJson = await this.#processMetricsRegistry.getMetricsAsJSON()
1083
+ }
1084
+
1062
1085
  for (const worker of this.#workers.values()) {
1063
1086
  try {
1064
1087
  // The application might be temporarily unavailable
@@ -1066,6 +1089,7 @@ export class Runtime extends EventEmitter {
1066
1089
  continue
1067
1090
  }
1068
1091
 
1092
+ // Get thread-specific metrics from worker
1069
1093
  const applicationMetrics = await executeWithTimeout(
1070
1094
  sendViaITC(worker, 'getMetrics', format),
1071
1095
  this.#config.metrics?.timeout ?? 10000
@@ -1076,9 +1100,30 @@ export class Runtime extends EventEmitter {
1076
1100
  metrics = format === 'json' ? [] : ''
1077
1101
  }
1078
1102
 
1103
+ // Build worker labels including custom labels from metrics config
1104
+ const workerLabels = {
1105
+ ...this.#config.metrics?.labels,
1106
+ [this.#metricsLabelName]: worker[kApplicationId]
1107
+ }
1108
+ const workerId = worker[kWorkerId]
1109
+ if (workerId >= 0) {
1110
+ workerLabels.workerId = workerId
1111
+ }
1112
+
1079
1113
  if (format === 'json') {
1080
- metrics.push(...applicationMetrics)
1114
+ // Duplicate process metrics with worker labels and add to output
1115
+ if (processMetricsJson) {
1116
+ this.#applyLabelsToMetrics(processMetricsJson, workerLabels, metrics)
1117
+ }
1118
+ // Add worker's thread-specific metrics
1119
+ for (let i = 0; i < applicationMetrics.length; i++) {
1120
+ metrics.push(applicationMetrics[i])
1121
+ }
1081
1122
  } else {
1123
+ // Text format: format process metrics with worker labels
1124
+ if (processMetricsJson) {
1125
+ metrics += this.#formatProcessMetricsText(processMetricsJson, workerLabels)
1126
+ }
1082
1127
  metrics += applicationMetrics
1083
1128
  }
1084
1129
  }
@@ -1099,6 +1144,65 @@ export class Runtime extends EventEmitter {
1099
1144
  return { metrics }
1100
1145
  }
1101
1146
 
1147
+ // Apply labels to process metrics and push to output array (for JSON format)
1148
+ #applyLabelsToMetrics (processMetrics, labels, outputArray) {
1149
+ for (let i = 0; i < processMetrics.length; i++) {
1150
+ const metric = processMetrics[i]
1151
+ const newValues = []
1152
+ const values = metric.values
1153
+ for (let j = 0; j < values.length; j++) {
1154
+ const v = values[j]
1155
+ newValues.push({
1156
+ value: v.value,
1157
+ labels: { ...labels, ...v.labels },
1158
+ metricName: v.metricName
1159
+ })
1160
+ }
1161
+ outputArray.push({
1162
+ name: metric.name,
1163
+ help: metric.help,
1164
+ type: metric.type,
1165
+ aggregator: metric.aggregator,
1166
+ values: newValues
1167
+ })
1168
+ }
1169
+ }
1170
+
1171
+ // Format process metrics as Prometheus text format with labels
1172
+ #formatProcessMetricsText (processMetricsJson, labels) {
1173
+ let output = ''
1174
+
1175
+ for (let i = 0; i < processMetricsJson.length; i++) {
1176
+ const metric = processMetricsJson[i]
1177
+ const name = metric.name
1178
+ const help = metric.help
1179
+ const type = metric.type
1180
+
1181
+ // Add HELP and TYPE lines
1182
+ output += `# HELP ${name} ${help}\n`
1183
+ output += `# TYPE ${name} ${type}\n`
1184
+
1185
+ const values = metric.values
1186
+ for (let j = 0; j < values.length; j++) {
1187
+ const v = values[j]
1188
+ const combinedLabels = { ...labels, ...v.labels }
1189
+ const labelParts = []
1190
+
1191
+ for (const [key, val] of Object.entries(combinedLabels)) {
1192
+ // Escape label values for Prometheus format
1193
+ const escapedVal = String(val).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')
1194
+ labelParts.push(`${key}="${escapedVal}"`)
1195
+ }
1196
+
1197
+ const labelStr = labelParts.length > 0 ? `{${labelParts.join(',')}}` : ''
1198
+ const metricName = v.metricName || name
1199
+ output += `${metricName}${labelStr} ${v.value}\n`
1200
+ }
1201
+ }
1202
+
1203
+ return output
1204
+ }
1205
+
1102
1206
  async getFormattedMetrics () {
1103
1207
  try {
1104
1208
  const { metrics } = await this.getMetrics()
@@ -1564,7 +1668,8 @@ export class Runtime extends EventEmitter {
1564
1668
  codeRangeSizeMb
1565
1669
  },
1566
1670
  stdout: true,
1567
- stderr: true
1671
+ stderr: true,
1672
+ name: workerId
1568
1673
  })
1569
1674
 
1570
1675
  this.#handleWorkerStandardStreams(worker, applicationId, index)
@@ -9,7 +9,7 @@ import { EventEmitter } from 'node:events'
9
9
  import { ServerResponse } from 'node:http'
10
10
  import inspector from 'node:inspector'
11
11
  import { hostname } from 'node:os'
12
- import { resolve } from 'node:path'
12
+ import { join, resolve } from 'node:path'
13
13
  import { pathToFileURL } from 'node:url'
14
14
  import { threadId, workerData } from 'node:worker_threads'
15
15
  import pino from 'pino'
@@ -85,6 +85,57 @@ async function performPreloading (...sources) {
85
85
  }
86
86
  }
87
87
 
88
+ // Enable compile cache if configured (Node.js 22.1.0+)
89
+ async function setupCompileCache (runtimeConfig, applicationConfig, logger) {
90
+ // Normalize boolean shorthand: true -> { enabled: true }
91
+ const normalizeConfig = cfg => {
92
+ if (cfg === true) return { enabled: true }
93
+ if (cfg === false) return { enabled: false }
94
+ return cfg
95
+ }
96
+
97
+ // Merge runtime and app-level config (app overrides runtime)
98
+ const runtimeCache = normalizeConfig(runtimeConfig.compileCache)
99
+ const appCache = normalizeConfig(applicationConfig.compileCache)
100
+ const config = { ...runtimeCache, ...appCache }
101
+
102
+ if (!config.enabled) {
103
+ return
104
+ }
105
+
106
+ // Check if API is available (Node.js 22.1.0+)
107
+ let moduleApi
108
+ try {
109
+ moduleApi = await import('node:module')
110
+ if (typeof moduleApi.enableCompileCache !== 'function') {
111
+ return
112
+ }
113
+ } catch {
114
+ return
115
+ }
116
+
117
+ // Determine cache directory - use applicationConfig.path for the app root
118
+ const cacheDir = config.directory ?? join(applicationConfig.path, '.plt', 'compile-cache')
119
+
120
+ try {
121
+ const result = moduleApi.enableCompileCache(cacheDir)
122
+
123
+ const { compileCacheStatus } = moduleApi.constants ?? {}
124
+
125
+ if (result.status === compileCacheStatus?.ENABLED) {
126
+ logger.debug({ directory: result.directory }, 'Module compile cache enabled')
127
+ } else if (result.status === compileCacheStatus?.ALREADY_ENABLED) {
128
+ logger.debug({ directory: result.directory }, 'Module compile cache already enabled')
129
+ } else if (result.status === compileCacheStatus?.FAILED) {
130
+ logger.warn({ message: result.message }, 'Failed to enable module compile cache')
131
+ } else if (result.status === compileCacheStatus?.DISABLED) {
132
+ logger.debug('Module compile cache disabled via NODE_DISABLE_COMPILE_CACHE')
133
+ }
134
+ } catch (err) {
135
+ logger.warn({ err }, 'Error enabling module compile cache')
136
+ }
137
+ }
138
+
88
139
  async function main () {
89
140
  globalThis.fetch = fetch
90
141
  globalThis[kId] = threadId
@@ -94,10 +145,12 @@ async function main () {
94
145
  })
95
146
 
96
147
  const runtimeConfig = workerData.config
148
+ const applicationConfig = workerData.applicationConfig
97
149
 
98
- await performPreloading(runtimeConfig, workerData.applicationConfig)
150
+ // Enable compile cache early before loading user modules
151
+ await setupCompileCache(runtimeConfig, applicationConfig, globalThis.platformatic.logger)
99
152
 
100
- const applicationConfig = workerData.applicationConfig
153
+ await performPreloading(runtimeConfig, applicationConfig)
101
154
 
102
155
  // Load env file and mixin env vars from application config
103
156
  let envfile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.30.0",
3
+ "version": "3.31.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -35,14 +35,14 @@
35
35
  "typescript": "^5.5.4",
36
36
  "undici-oidc-interceptor": "^0.5.0",
37
37
  "why-is-node-running": "^2.2.2",
38
- "@platformatic/composer": "3.30.0",
39
- "@platformatic/db": "3.30.0",
40
- "@platformatic/gateway": "3.30.0",
41
- "@platformatic/node": "3.30.0",
42
- "@platformatic/sql-graphql": "3.30.0",
43
- "@platformatic/sql-mapper": "3.30.0",
44
- "@platformatic/service": "3.30.0",
45
- "@platformatic/wattpm-pprof-capture": "3.30.0"
38
+ "@platformatic/composer": "3.31.0",
39
+ "@platformatic/db": "3.31.0",
40
+ "@platformatic/gateway": "3.31.0",
41
+ "@platformatic/node": "3.31.0",
42
+ "@platformatic/sql-graphql": "3.31.0",
43
+ "@platformatic/service": "3.31.0",
44
+ "@platformatic/wattpm-pprof-capture": "3.31.0",
45
+ "@platformatic/sql-mapper": "3.31.0"
46
46
  },
47
47
  "dependencies": {
48
48
  "@fastify/accepts": "^5.0.0",
@@ -71,12 +71,12 @@
71
71
  "undici": "^7.0.0",
72
72
  "undici-thread-interceptor": "^1.0.0",
73
73
  "ws": "^8.16.0",
74
- "@platformatic/foundation": "3.30.0",
75
- "@platformatic/basic": "3.30.0",
76
- "@platformatic/generators": "3.30.0",
77
- "@platformatic/itc": "3.30.0",
78
- "@platformatic/telemetry": "3.30.0",
79
- "@platformatic/metrics": "3.30.0"
74
+ "@platformatic/generators": "3.31.0",
75
+ "@platformatic/basic": "3.31.0",
76
+ "@platformatic/foundation": "3.31.0",
77
+ "@platformatic/itc": "3.31.0",
78
+ "@platformatic/metrics": "3.31.0",
79
+ "@platformatic/telemetry": "3.31.0"
80
80
  },
81
81
  "engines": {
82
82
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.30.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.31.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",
@@ -332,6 +332,28 @@
332
332
  }
333
333
  }
334
334
  }
335
+ },
336
+ "compileCache": {
337
+ "anyOf": [
338
+ {
339
+ "type": "boolean"
340
+ },
341
+ {
342
+ "type": "object",
343
+ "properties": {
344
+ "enabled": {
345
+ "type": "boolean",
346
+ "default": true,
347
+ "description": "Enable Node.js module compile cache for faster startup"
348
+ },
349
+ "directory": {
350
+ "type": "string",
351
+ "description": "Directory to store compile cache. Defaults to .plt/compile-cache in app root"
352
+ }
353
+ },
354
+ "additionalProperties": false
355
+ }
356
+ ]
335
357
  }
336
358
  }
337
359
  }
@@ -646,6 +668,28 @@
646
668
  }
647
669
  }
648
670
  }
671
+ },
672
+ "compileCache": {
673
+ "anyOf": [
674
+ {
675
+ "type": "boolean"
676
+ },
677
+ {
678
+ "type": "object",
679
+ "properties": {
680
+ "enabled": {
681
+ "type": "boolean",
682
+ "default": true,
683
+ "description": "Enable Node.js module compile cache for faster startup"
684
+ },
685
+ "directory": {
686
+ "type": "string",
687
+ "description": "Directory to store compile cache. Defaults to .plt/compile-cache in app root"
688
+ }
689
+ },
690
+ "additionalProperties": false
691
+ }
692
+ ]
649
693
  }
650
694
  }
651
695
  }
@@ -958,6 +1002,28 @@
958
1002
  }
959
1003
  }
960
1004
  }
1005
+ },
1006
+ "compileCache": {
1007
+ "anyOf": [
1008
+ {
1009
+ "type": "boolean"
1010
+ },
1011
+ {
1012
+ "type": "object",
1013
+ "properties": {
1014
+ "enabled": {
1015
+ "type": "boolean",
1016
+ "default": true,
1017
+ "description": "Enable Node.js module compile cache for faster startup"
1018
+ },
1019
+ "directory": {
1020
+ "type": "string",
1021
+ "description": "Directory to store compile cache. Defaults to .plt/compile-cache in app root"
1022
+ }
1023
+ },
1024
+ "additionalProperties": false
1025
+ }
1026
+ ]
961
1027
  }
962
1028
  }
963
1029
  }
@@ -1270,6 +1336,28 @@
1270
1336
  }
1271
1337
  }
1272
1338
  }
1339
+ },
1340
+ "compileCache": {
1341
+ "anyOf": [
1342
+ {
1343
+ "type": "boolean"
1344
+ },
1345
+ {
1346
+ "type": "object",
1347
+ "properties": {
1348
+ "enabled": {
1349
+ "type": "boolean",
1350
+ "default": true,
1351
+ "description": "Enable Node.js module compile cache for faster startup"
1352
+ },
1353
+ "directory": {
1354
+ "type": "string",
1355
+ "description": "Directory to store compile cache. Defaults to .plt/compile-cache in app root"
1356
+ }
1357
+ },
1358
+ "additionalProperties": false
1359
+ }
1360
+ ]
1273
1361
  }
1274
1362
  }
1275
1363
  }
@@ -1349,7 +1437,6 @@
1349
1437
  "properties": {
1350
1438
  "level": {
1351
1439
  "type": "string",
1352
- "default": "info",
1353
1440
  "oneOf": [
1354
1441
  {
1355
1442
  "enum": [
@@ -1496,9 +1583,6 @@
1496
1583
  "default": true
1497
1584
  }
1498
1585
  },
1499
- "required": [
1500
- "level"
1501
- ],
1502
1586
  "default": {},
1503
1587
  "additionalProperties": true
1504
1588
  },
@@ -2570,6 +2654,28 @@
2570
2654
  "deny"
2571
2655
  ],
2572
2656
  "additionalProperties": false
2657
+ },
2658
+ "compileCache": {
2659
+ "anyOf": [
2660
+ {
2661
+ "type": "boolean"
2662
+ },
2663
+ {
2664
+ "type": "object",
2665
+ "properties": {
2666
+ "enabled": {
2667
+ "type": "boolean",
2668
+ "default": true,
2669
+ "description": "Enable Node.js module compile cache for faster startup"
2670
+ },
2671
+ "directory": {
2672
+ "type": "string",
2673
+ "description": "Directory to store compile cache. Defaults to .plt/compile-cache in app root"
2674
+ }
2675
+ },
2676
+ "additionalProperties": false
2677
+ }
2678
+ ]
2573
2679
  }
2574
2680
  },
2575
2681
  "anyOf": [