@platformatic/basic 3.13.1 → 3.15.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
@@ -13,7 +13,20 @@ export interface PlatformaticBasicConfig {
13
13
  services?: {
14
14
  [k: string]: unknown;
15
15
  }[];
16
- workers?: number | string;
16
+ workers?:
17
+ | number
18
+ | string
19
+ | {
20
+ static?: number;
21
+ dynamic?: boolean;
22
+ minimum?: number;
23
+ maximum?: number;
24
+ total?: number;
25
+ maxMemory?: number;
26
+ cooldown?: number;
27
+ gracePeriod?: number;
28
+ [k: string]: unknown;
29
+ };
17
30
  workersRestartDelay?: number | string;
18
31
  logger?: {
19
32
  level: (
@@ -97,6 +110,7 @@ export interface PlatformaticBasicConfig {
97
110
  rejectUnauthorized?: boolean;
98
111
  };
99
112
  };
113
+ reuseTcpPorts?: boolean;
100
114
  startTimeout?: number;
101
115
  restartOnError?: boolean | number;
102
116
  exitOnUnhandledErrors?: boolean;
@@ -221,6 +235,37 @@ export interface PlatformaticBasicConfig {
221
235
  };
222
236
  plugins?: string[];
223
237
  timeout?: number | string;
238
+ /**
239
+ * Configuration for exporting metrics to an OTLP endpoint
240
+ */
241
+ otlpExporter?: {
242
+ /**
243
+ * Enable or disable OTLP metrics export
244
+ */
245
+ enabled?: boolean | string;
246
+ /**
247
+ * OTLP endpoint URL (e.g., http://collector:4318/v1/metrics)
248
+ */
249
+ endpoint: string;
250
+ /**
251
+ * Interval in milliseconds between metric pushes
252
+ */
253
+ interval?: number | string;
254
+ /**
255
+ * Additional HTTP headers for authentication
256
+ */
257
+ headers?: {
258
+ [k: string]: string;
259
+ };
260
+ /**
261
+ * Service name for OTLP resource attributes
262
+ */
263
+ serviceName?: string;
264
+ /**
265
+ * Service version for OTLP resource attributes
266
+ */
267
+ serviceVersion?: string;
268
+ };
224
269
  };
225
270
  telemetry?: {
226
271
  enabled?: boolean | string;
@@ -304,13 +349,28 @@ export interface PlatformaticBasicConfig {
304
349
  maxTotalMemory?: number;
305
350
  minWorkers?: number;
306
351
  maxWorkers?: number;
352
+ cooldownSec?: number;
353
+ gracePeriod?: number;
354
+ /**
355
+ * @deprecated
356
+ */
307
357
  scaleUpELU?: number;
358
+ /**
359
+ * @deprecated
360
+ */
308
361
  scaleDownELU?: number;
362
+ /**
363
+ * @deprecated
364
+ */
309
365
  timeWindowSec?: number;
366
+ /**
367
+ * @deprecated
368
+ */
310
369
  scaleDownTimeWindowSec?: number;
311
- cooldownSec?: number;
370
+ /**
371
+ * @deprecated
372
+ */
312
373
  scaleIntervalSec?: number;
313
- gracePeriod?: number;
314
374
  };
315
375
  inspectorOptions?: {
316
376
  host?: string;
package/lib/capability.js CHANGED
@@ -2,13 +2,15 @@ import {
2
2
  buildPinoOptions,
3
3
  deepmerge,
4
4
  executeWithTimeout,
5
+ features,
5
6
  kHandledError,
6
7
  kMetadata,
7
8
  kTimeout
8
9
  } from '@platformatic/foundation'
9
- import { client, collectMetrics, ensureMetricsGroup } from '@platformatic/metrics'
10
+ import { client, collectMetrics, ensureMetricsGroup, setupOtlpExporter } from '@platformatic/metrics'
10
11
  import { parseCommandString } from 'execa'
11
12
  import { spawn } from 'node:child_process'
13
+ import { tracingChannel } from 'node:diagnostics_channel'
12
14
  import EventEmitter, { once } from 'node:events'
13
15
  import { existsSync } from 'node:fs'
14
16
  import { platform } from 'node:os'
@@ -34,6 +36,7 @@ export class BaseCapability extends EventEmitter {
34
36
  workerId
35
37
  telemetryConfig
36
38
  serverConfig
39
+ reuseTcpPorts
37
40
  openapiSchema
38
41
  graphqlSchema
39
42
  connectionString
@@ -50,11 +53,13 @@ export class BaseCapability extends EventEmitter {
50
53
  subprocessForceClose
51
54
  subprocessTerminationSignal
52
55
  logger
53
- metricsRegistr
56
+ metricsRegistry
57
+ otlpBridge
54
58
 
55
59
  #subprocessStarted
56
60
  #metricsCollected
57
61
  #pendingDependenciesWaits
62
+ #reuseTcpPortsSubscribers
58
63
 
59
64
  constructor (type, version, root, config, context, standardStreams = {}) {
60
65
  super()
@@ -88,6 +93,7 @@ export class BaseCapability extends EventEmitter {
88
93
  this.subprocessForceClose = false
89
94
  this.subprocessTerminationSignal = 'SIGINT'
90
95
  this.logger = this._initializeLogger()
96
+ this.reuseTcpPorts = this.config.reuseTcpPorts ?? this.runtimeConfig.reuseTcpPorts
91
97
  // True by default, can be overridden in subclasses. If false, it takes precedence over the runtime configuration
92
98
  this.exitOnUnhandledErrors = true
93
99
 
@@ -118,6 +124,7 @@ export class BaseCapability extends EventEmitter {
118
124
  this.registerGlobals({ prometheus: { client, registry: this.metricsRegistry } })
119
125
  }
120
126
 
127
+ this.otlpBridge = null
121
128
  this.#metricsCollected = false
122
129
  this.#pendingDependenciesWaits = new Set()
123
130
  }
@@ -127,6 +134,8 @@ export class BaseCapability extends EventEmitter {
127
134
  return
128
135
  }
129
136
 
137
+ // TODO@PI: Handle reusePort
138
+
130
139
  // Wait for explicit dependencies to start
131
140
  await this.waitForDependenciesStart(this.dependencies)
132
141
 
@@ -156,6 +165,17 @@ export class BaseCapability extends EventEmitter {
156
165
  if (this.#pendingDependenciesWaits.size > 0) {
157
166
  await Promise.allSettled(this.#pendingDependenciesWaits)
158
167
  }
168
+
169
+ if (this.#reuseTcpPortsSubscribers) {
170
+ tracingChannel('net.server.listen').unsubscribe(this.#reuseTcpPortsSubscribers)
171
+ this.#reuseTcpPortsSubscribers = null
172
+ }
173
+
174
+ // Stop OTLP bridge if running
175
+ if (this.otlpBridge) {
176
+ this.otlpBridge.stop()
177
+ this.otlpBridge = null
178
+ }
159
179
  }
160
180
 
161
181
  build () {
@@ -554,6 +574,7 @@ export class BaseCapability extends EventEmitter {
554
574
  basePath,
555
575
  logLevel: this.logger.level,
556
576
  isEntrypoint: this.isEntrypoint,
577
+ reuseTcpPorts: this.reuseTcpPorts,
557
578
  runtimeBasePath: this.runtimeConfig?.basePath ?? null,
558
579
  wantsAbsoluteUrls: meta.gateway?.wantsAbsoluteUrls ?? false,
559
580
  exitOnUnhandledErrors: this.runtimeConfig.exitOnUnhandledErrors ?? true,
@@ -607,6 +628,23 @@ export class BaseCapability extends EventEmitter {
607
628
  return pino(pinoOptions, this.standardStreams?.stdout)
608
629
  }
609
630
 
631
+ _start () {
632
+ if (this.reuseTcpPorts) {
633
+ if (!features.node.reusePort) {
634
+ this.reuseTcpPorts = false
635
+ this.logger.warn('Cannot enable reusePort as it is not available in your OS.')
636
+ } else {
637
+ this.#reuseTcpPortsSubscribers = {
638
+ asyncStart ({ options }) {
639
+ options.reusePort = true
640
+ }
641
+ }
642
+
643
+ tracingChannel('net.server.listen').subscribe(this.#reuseTcpPortsSubscribers)
644
+ }
645
+ }
646
+ }
647
+
610
648
  async _collectMetrics () {
611
649
  if (this.#metricsCollected) {
612
650
  return
@@ -620,6 +658,7 @@ export class BaseCapability extends EventEmitter {
620
658
 
621
659
  await this.#collectMetrics()
622
660
  this.#setHttpCacheMetrics()
661
+ await this.#setupOtlpExporter()
623
662
  }
624
663
 
625
664
  async #collectMetrics () {
@@ -736,6 +775,32 @@ export class BaseCapability extends EventEmitter {
736
775
  globalThis.platformatic.onActiveResourcesEventLoop = val => activeResourcesEventLoopMetric.set(val)
737
776
  }
738
777
 
778
+ async #setupOtlpExporter () {
779
+ const metricsConfig = this.context.metricsConfig
780
+ if (!metricsConfig || !metricsConfig.otlpExporter) {
781
+ return
782
+ }
783
+
784
+ // Wait for telemetry to be ready before loading promotel to avoid race condition
785
+ if (globalThis.platformatic?.telemetryReady) {
786
+ await globalThis.platformatic.telemetryReady
787
+ }
788
+
789
+ // Setup and start OTLP exporter bridge
790
+ this.otlpBridge = await setupOtlpExporter(this.metricsRegistry, metricsConfig.otlpExporter, this.applicationId)
791
+
792
+ if (this.otlpBridge) {
793
+ this.otlpBridge.start()
794
+ this.logger.info(
795
+ {
796
+ endpoint: metricsConfig.otlpExporter.endpoint,
797
+ interval: metricsConfig.otlpExporter.interval || 60000
798
+ },
799
+ 'OTLP metrics exporter started'
800
+ )
801
+ }
802
+ }
803
+
739
804
  async #invalidateHttpCache (opts = {}) {
740
805
  await globalThis[kITC].send('invalidateHttpCache', opts)
741
806
  }
@@ -2,8 +2,7 @@ import {
2
2
  buildPinoFormatters,
3
3
  buildPinoTimestamp,
4
4
  disablePinoDirectWrite,
5
- ensureLoggableError,
6
- features
5
+ ensureLoggableError
7
6
  } from '@platformatic/foundation'
8
7
  import { ITC } from '@platformatic/itc/lib/index.js'
9
8
  import { client, collectMetrics } from '@platformatic/metrics'
@@ -100,6 +99,10 @@ export class ChildProcess extends ITC {
100
99
  }
101
100
 
102
101
  if (!handled) {
102
+ this.#logger.warn(
103
+ `Please register a "close" event handler in globalThis.platformatic.events for application "${this.applicationId}" to make sure resources have been closed properly and avoid exit timeouts.`
104
+ )
105
+
103
106
  // No user event, just exit without errors
104
107
  setImmediate(() => {
105
108
  process.exit(process.exitCode ?? 0)
@@ -127,6 +130,10 @@ export class ChildProcess extends ITC {
127
130
  this.#setupServer()
128
131
  this.#setupInterceptors()
129
132
 
133
+ if (globalThis.platformatic.reuseTcpPorts) {
134
+ this.#setupTcpPortsHandling()
135
+ }
136
+
130
137
  this.registerGlobals({
131
138
  logger: this.#logger,
132
139
  setOpenapiSchema: this.setOpenapiSchema.bind(this),
@@ -355,10 +362,6 @@ export class ChildProcess extends ITC {
355
362
  if (port !== false) {
356
363
  const hasFixedPort = typeof port === 'number'
357
364
  options.port = hasFixedPort ? port : 0
358
-
359
- if (hasFixedPort && features.node.reusePort) {
360
- options.reusePort = true
361
- }
362
365
  }
363
366
 
364
367
  if (typeof host === 'string') {
@@ -395,6 +398,14 @@ export class ChildProcess extends ITC {
395
398
  }
396
399
  }
397
400
 
401
+ #setupTcpPortsHandling () {
402
+ tracingChannel('net.server.listen').subscribe({
403
+ asyncStart ({ options }) {
404
+ options.reusePort = true
405
+ }
406
+ })
407
+ }
408
+
398
409
  #setupInterceptors () {
399
410
  const globalDispatcher = new Agent().compose(createInterceptor(this))
400
411
  setGlobalDispatcher(globalDispatcher)
@@ -1,4 +1,3 @@
1
- import { features } from '@platformatic/foundation'
2
1
  import { subscribe, tracingChannel, unsubscribe } from 'node:diagnostics_channel'
3
2
 
4
3
  export function createServerListener (overridePort = true, overrideHost) {
@@ -14,10 +13,6 @@ export function createServerListener (overridePort = true, overrideHost) {
14
13
  if (overridePort !== false) {
15
14
  const hasFixedPort = typeof overridePort === 'number'
16
15
  options.port = hasFixedPort ? overridePort : 0
17
-
18
- if (hasFixedPort && features.node.reusePort) {
19
- options.reusePort = true
20
- }
21
16
  }
22
17
 
23
18
  if (typeof overrideHost === 'string') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/basic",
3
- "version": "3.13.1",
3
+ "version": "3.15.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -25,10 +25,10 @@
25
25
  "split2": "^4.2.0",
26
26
  "undici": "^7.0.0",
27
27
  "ws": "^8.18.0",
28
- "@platformatic/foundation": "3.13.1",
29
- "@platformatic/metrics": "3.13.1",
30
- "@platformatic/itc": "3.13.1",
31
- "@platformatic/telemetry": "3.13.1"
28
+ "@platformatic/foundation": "3.15.0",
29
+ "@platformatic/itc": "3.15.0",
30
+ "@platformatic/metrics": "3.15.0",
31
+ "@platformatic/telemetry": "3.15.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "cleaner-spec-reporter": "^0.5.0",
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/basic/3.13.1.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/basic/3.15.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Basic Config",
5
5
  "type": "object",
@@ -68,14 +68,34 @@
68
68
  "useHttp": {
69
69
  "type": "boolean"
70
70
  },
71
+ "reuseTcpPorts": {
72
+ "type": "boolean",
73
+ "default": true
74
+ },
71
75
  "workers": {
72
76
  "anyOf": [
73
77
  {
74
- "type": "number",
75
- "minimum": 1
78
+ "type": "number"
76
79
  },
77
80
  {
78
81
  "type": "string"
82
+ },
83
+ {
84
+ "type": "object",
85
+ "properties": {
86
+ "static": {
87
+ "type": "number",
88
+ "minimum": 1
89
+ },
90
+ "minimum": {
91
+ "type": "number",
92
+ "minimum": 1
93
+ },
94
+ "maximum": {
95
+ "type": "number",
96
+ "minimum": 0
97
+ }
98
+ }
79
99
  }
80
100
  ]
81
101
  },
@@ -296,6 +316,43 @@
296
316
  },
297
317
  {
298
318
  "type": "string"
319
+ },
320
+ {
321
+ "type": "object",
322
+ "properties": {
323
+ "static": {
324
+ "type": "number",
325
+ "minimum": 1
326
+ },
327
+ "dynamic": {
328
+ "type": "boolean",
329
+ "default": false
330
+ },
331
+ "minimum": {
332
+ "type": "number",
333
+ "minimum": 1
334
+ },
335
+ "maximum": {
336
+ "type": "number",
337
+ "minimum": 0
338
+ },
339
+ "total": {
340
+ "type": "number",
341
+ "minimum": 1
342
+ },
343
+ "maxMemory": {
344
+ "type": "number",
345
+ "minimum": 0
346
+ },
347
+ "cooldown": {
348
+ "type": "number",
349
+ "minimum": 0
350
+ },
351
+ "gracePeriod": {
352
+ "type": "number",
353
+ "minimum": 0
354
+ }
355
+ }
299
356
  }
300
357
  ]
301
358
  },
@@ -581,6 +638,10 @@
581
638
  },
582
639
  "additionalProperties": false
583
640
  },
641
+ "reuseTcpPorts": {
642
+ "type": "boolean",
643
+ "default": true
644
+ },
584
645
  "startTimeout": {
585
646
  "default": 30000,
586
647
  "type": "number",
@@ -1072,6 +1133,58 @@
1072
1133
  }
1073
1134
  ],
1074
1135
  "default": 10000
1136
+ },
1137
+ "otlpExporter": {
1138
+ "type": "object",
1139
+ "description": "Configuration for exporting metrics to an OTLP endpoint",
1140
+ "properties": {
1141
+ "enabled": {
1142
+ "anyOf": [
1143
+ {
1144
+ "type": "boolean"
1145
+ },
1146
+ {
1147
+ "type": "string"
1148
+ }
1149
+ ],
1150
+ "description": "Enable or disable OTLP metrics export"
1151
+ },
1152
+ "endpoint": {
1153
+ "type": "string",
1154
+ "description": "OTLP endpoint URL (e.g., http://collector:4318/v1/metrics)"
1155
+ },
1156
+ "interval": {
1157
+ "anyOf": [
1158
+ {
1159
+ "type": "integer"
1160
+ },
1161
+ {
1162
+ "type": "string"
1163
+ }
1164
+ ],
1165
+ "default": 60000,
1166
+ "description": "Interval in milliseconds between metric pushes"
1167
+ },
1168
+ "headers": {
1169
+ "type": "object",
1170
+ "additionalProperties": {
1171
+ "type": "string"
1172
+ },
1173
+ "description": "Additional HTTP headers for authentication"
1174
+ },
1175
+ "serviceName": {
1176
+ "type": "string",
1177
+ "description": "Service name for OTLP resource attributes"
1178
+ },
1179
+ "serviceVersion": {
1180
+ "type": "string",
1181
+ "description": "Service version for OTLP resource attributes"
1182
+ }
1183
+ },
1184
+ "required": [
1185
+ "endpoint"
1186
+ ],
1187
+ "additionalProperties": false
1075
1188
  }
1076
1189
  },
1077
1190
  "additionalProperties": false
@@ -1231,35 +1344,40 @@
1231
1344
  "type": "number",
1232
1345
  "minimum": 1
1233
1346
  },
1347
+ "cooldownSec": {
1348
+ "type": "number",
1349
+ "minimum": 0
1350
+ },
1351
+ "gracePeriod": {
1352
+ "type": "number",
1353
+ "minimum": 0
1354
+ },
1234
1355
  "scaleUpELU": {
1235
1356
  "type": "number",
1236
1357
  "minimum": 0,
1237
- "maximum": 1
1358
+ "maximum": 1,
1359
+ "deprecated": true
1238
1360
  },
1239
1361
  "scaleDownELU": {
1240
1362
  "type": "number",
1241
1363
  "minimum": 0,
1242
- "maximum": 1
1364
+ "maximum": 1,
1365
+ "deprecated": true
1243
1366
  },
1244
1367
  "timeWindowSec": {
1245
1368
  "type": "number",
1246
- "minimum": 0
1369
+ "minimum": 0,
1370
+ "deprecated": true
1247
1371
  },
1248
1372
  "scaleDownTimeWindowSec": {
1249
1373
  "type": "number",
1250
- "minimum": 0
1251
- },
1252
- "cooldownSec": {
1253
- "type": "number",
1254
- "minimum": 0
1374
+ "minimum": 0,
1375
+ "deprecated": true
1255
1376
  },
1256
1377
  "scaleIntervalSec": {
1257
1378
  "type": "number",
1258
- "minimum": 0
1259
- },
1260
- "gracePeriod": {
1261
- "type": "number",
1262
- "minimum": 0
1379
+ "minimum": 0,
1380
+ "deprecated": true
1263
1381
  }
1264
1382
  },
1265
1383
  "additionalProperties": false