@platformatic/runtime 2.65.0 → 2.66.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/lib/runtime.js CHANGED
@@ -12,6 +12,7 @@ const { Worker } = require('node:worker_threads')
12
12
  const ts = require('tail-file-stream')
13
13
  const { Agent, interceptors: undiciInterceptors, request } = require('undici')
14
14
  const { createThreadInterceptor } = require('undici-thread-interceptor')
15
+ const SonicBoom = require('sonic-boom')
15
16
 
16
17
  const { checkDependencies, topologicalSort } = require('./dependencies')
17
18
  const errors = require('./errors')
@@ -76,6 +77,7 @@ class Runtime extends EventEmitter {
76
77
  #sharedHttpCache
77
78
  servicesConfigsPatches
78
79
  #scheduler
80
+ #stdio
79
81
 
80
82
  constructor (configManager, runtimeLogsDir, env) {
81
83
  super()
@@ -96,6 +98,13 @@ class Runtime extends EventEmitter {
96
98
  this.#restartingWorkers = new Map()
97
99
  this.#sharedHttpCache = null
98
100
  this.servicesConfigsPatches = new Map()
101
+
102
+ if (!this.#configManager.current.logger.captureStdio) {
103
+ this.#stdio = {
104
+ stdout: new SonicBoom({ fd: process.stdout.fd }),
105
+ stderr: new SonicBoom({ fd: process.stderr.fd })
106
+ }
107
+ }
99
108
  }
100
109
 
101
110
  async init () {
@@ -1720,26 +1729,26 @@ class Runtime extends EventEmitter {
1720
1729
 
1721
1730
  worker.stdout.setEncoding('utf8')
1722
1731
  worker.stdout.on('data', raw => {
1723
- let selector = selectors.stdout
1724
-
1725
1732
  if (raw.includes(kStderrMarker)) {
1726
- selector = selectors.stderr
1727
- raw = raw.replaceAll(kStderrMarker, '')
1733
+ this.#forwardThreadLog(logger, selectors.stderr, raw.replaceAll(kStderrMarker, ''), 'stderr')
1734
+ } else {
1735
+ this.#forwardThreadLog(logger, selectors.stdout, raw, 'stdout')
1728
1736
  }
1729
-
1730
- this.#forwardThreadLog(logger, selector, raw)
1731
1737
  })
1732
1738
 
1733
1739
  // Whatever is outputted here, it come from a direct process.stderr.write in the thread.
1734
1740
  // There's nothing we can do about it in regard of out of order logs due to a Node bug.
1735
1741
  worker.stderr.setEncoding('utf8')
1736
1742
  worker.stderr.on('data', raw => {
1737
- this.#forwardThreadLog(logger, selectors.stderr, raw)
1743
+ this.#forwardThreadLog(logger, selectors.stderr, raw, 'stderr')
1738
1744
  })
1739
1745
  }
1740
1746
 
1741
- #forwardThreadLog (logger, { level, caller }, data) {
1742
- if (!this.#loggerDestination) {
1747
+ // label is the key in the logger object, either 'stdout' or 'stderr'
1748
+ #forwardThreadLog (logger, { level, caller }, data, label) {
1749
+ // When captureStdio is false, write directly to the logger destination
1750
+ if (!this.#configManager.current.logger.captureStdio) {
1751
+ this.#stdio[label].write(data)
1743
1752
  return
1744
1753
  }
1745
1754
 
@@ -1747,35 +1756,43 @@ class Runtime extends EventEmitter {
1747
1756
  for (const raw of data.split('\n')) {
1748
1757
  // First of all, try to parse the message as JSON
1749
1758
  let message
1759
+ let json
1750
1760
  // The message is a JSON object if it has at least 2 bytes
1751
1761
  if (raw.length >= 2) {
1752
1762
  try {
1753
1763
  message = JSON.parse(raw)
1754
- } catch (e) {
1764
+ json = true
1765
+ } catch {
1755
1766
  // No-op, we assume the message is raw
1756
1767
  }
1757
1768
  }
1758
1769
 
1759
- // Not a Pino JSON, accumulate the message and continue
1760
- if (typeof message?.level !== 'number' || (typeof message?.time !== 'number' && typeof message?.msg !== 'string')) {
1761
- plainMessages += (plainMessages.length ? '\n' : '') + raw
1770
+ const pinoLog = typeof message?.level === 'number' && typeof message?.time === 'number' && typeof message?.msg === 'string'
1771
+
1772
+ // Directly write to the Pino destination
1773
+ if (pinoLog) {
1774
+ if (!this.#loggerDestination) {
1775
+ continue
1776
+ }
1777
+
1778
+ this.#loggerDestination.lastLevel = message.level
1779
+ this.#loggerDestination.lastTime = message.time
1780
+ this.#loggerDestination.lastMsg = message.msg
1781
+ this.#loggerDestination.lastObj = message
1782
+ this.#loggerDestination.lastLogger = logger
1783
+ this.#loggerDestination.write(raw + '\n')
1762
1784
  continue
1763
1785
  }
1764
1786
 
1765
- // Before continuing, write any previous plain messages
1766
- if (plainMessages.length > 0) {
1767
- logger[level]({ caller }, plainMessages.replace(/\n$/, ''))
1768
- plainMessages = ''
1787
+ if (json) {
1788
+ logger[level]({ caller, [label]: message })
1789
+ continue
1769
1790
  }
1770
1791
 
1771
- // Now we directly write to the Pino destination
1772
- this.#loggerDestination.lastLevel = message.level
1773
- this.#loggerDestination.lastTime = message.time
1774
- this.#loggerDestination.lastMsg = message.msg
1775
- this.#loggerDestination.lastObj = message
1776
- this.#loggerDestination.lastLogger = logger
1777
-
1778
- this.#loggerDestination.write(raw + '\n')
1792
+ // Not a Pino JSON nor a JSON object, accumulate the message
1793
+ if (!pinoLog && !json) {
1794
+ plainMessages += (plainMessages.length ? '\n' : '') + raw
1795
+ }
1779
1796
  }
1780
1797
 
1781
1798
  // Write whatever is left
package/lib/schema.js CHANGED
@@ -1,547 +1,30 @@
1
1
  #! /usr/bin/env node
2
2
  'use strict'
3
3
 
4
- const telemetry = require('@platformatic/telemetry').schema
5
- const {
6
- schemaComponents: { server, logger, health, healthWithoutDefaults }
7
- } = require('@platformatic/utils')
4
+ const { schemaComponents } = require('@platformatic/utils')
8
5
 
9
- const env = {
10
- type: 'object',
11
- additionalProperties: {
12
- type: 'string'
13
- }
14
- }
15
-
16
- const workers = {
17
- anyOf: [
18
- {
19
- type: 'number',
20
- minimum: 1
21
- },
22
- { type: 'string' }
23
- ]
24
- }
25
-
26
- const preload = {
27
- anyOf: [
28
- { type: 'string', resolvePath: true },
29
- {
30
- type: 'array',
31
- items: {
32
- type: 'string',
33
- resolvePath: true
34
- }
35
- }
36
- ]
37
- }
6
+ const pkg = require('../package.json')
38
7
 
39
- const services = {
40
- type: 'array',
41
- items: {
42
- type: 'object',
43
- anyOf: [{ required: ['id', 'path'] }, { required: ['id', 'url'] }],
44
- properties: {
45
- id: {
46
- type: 'string'
47
- },
48
- path: {
49
- type: 'string',
50
- // This is required for the resolve command to allow empty paths after environment variable replacement
51
- allowEmptyPaths: true,
52
- resolvePath: true
53
- },
54
- config: {
55
- type: 'string'
56
- },
57
- url: {
58
- type: 'string'
59
- },
60
- gitBranch: {
61
- type: 'string',
62
- default: 'main'
63
- },
64
- useHttp: {
65
- type: 'boolean'
66
- },
67
- workers,
68
- health: { ...healthWithoutDefaults, default: undefined },
69
- arguments: {
70
- type: 'array',
71
- items: {
72
- type: 'string'
73
- }
74
- },
75
- env,
76
- envfile: {
77
- type: 'string'
78
- },
79
- sourceMaps: {
80
- type: 'boolean',
81
- default: false
82
- },
83
- packageManager: {
84
- type: 'string',
85
- enum: ['npm', 'pnpm', 'yarn']
86
- },
87
- preload,
88
- nodeOptions: {
89
- type: 'string'
90
- },
91
- telemetry: {
92
- type: 'object',
93
- properties: {
94
- instrumentations: {
95
- type: 'array',
96
- description: 'An array of instrumentations loaded if telemetry is enabled',
97
- items: {
98
- oneOf: [
99
- {
100
- type: 'string'
101
- },
102
- {
103
- type: 'object',
104
- properties: {
105
- package: {
106
- type: 'string'
107
- },
108
- exportName: {
109
- type: 'string'
110
- },
111
- options: {
112
- type: 'object',
113
- additionalProperties: true
114
- }
115
- },
116
- required: ['package']
117
- }
118
- ]
119
- }
120
- }
121
- }
122
- }
8
+ const runtimeLogger = {
9
+ ...schemaComponents.runtimeProperties.logger,
10
+ properties: {
11
+ ...schemaComponents.runtimeProperties.logger.properties,
12
+ captureStdio: {
13
+ type: 'boolean',
14
+ default: true
123
15
  }
124
16
  }
125
17
  }
126
18
 
127
- const pkg = require('../package.json')
19
+ schemaComponents.runtimeProperties.logger = runtimeLogger
128
20
 
129
21
  const platformaticRuntimeSchema = {
130
22
  $id: `https://schemas.platformatic.dev/@platformatic/runtime/${pkg.version}.json`,
131
23
  $schema: 'http://json-schema.org/draft-07/schema#',
132
24
  type: 'object',
133
- properties: {
134
- $schema: {
135
- type: 'string'
136
- },
137
- preload,
138
- entrypoint: {
139
- type: 'string'
140
- },
141
- basePath: {
142
- type: 'string'
143
- },
144
- autoload: {
145
- type: 'object',
146
- additionalProperties: false,
147
- required: ['path'],
148
- properties: {
149
- path: {
150
- type: 'string',
151
- resolvePath: true
152
- },
153
- exclude: {
154
- type: 'array',
155
- default: [],
156
- items: {
157
- type: 'string'
158
- }
159
- },
160
- mappings: {
161
- type: 'object',
162
- additionalProperties: {
163
- type: 'object',
164
- additionalProperties: false,
165
- required: ['id'],
166
- properties: {
167
- id: {
168
- type: 'string'
169
- },
170
- config: {
171
- type: 'string'
172
- },
173
- useHttp: {
174
- type: 'boolean'
175
- },
176
- workers,
177
- health: { ...healthWithoutDefaults, default: undefined },
178
- preload,
179
- arguments: {
180
- type: 'array',
181
- items: {
182
- type: 'string'
183
- }
184
- },
185
- nodeOptions: {
186
- type: 'string'
187
- }
188
- }
189
- }
190
- }
191
- }
192
- },
193
- services,
194
- workers: { ...workers, default: 1 },
195
- web: services,
196
- logger,
197
- server,
198
- startTimeout: {
199
- default: 30000,
200
- type: 'number',
201
- minimum: 0
202
- },
203
- restartOnError: {
204
- default: true,
205
- anyOf: [
206
- { type: 'boolean' },
207
- {
208
- type: 'number',
209
- minimum: 0
210
- }
211
- ]
212
- },
213
- gracefulShutdown: {
214
- type: 'object',
215
- properties: {
216
- runtime: {
217
- anyOf: [
218
- {
219
- type: 'number',
220
- minimum: 1
221
- },
222
- { type: 'string' }
223
- ],
224
- default: 10000
225
- },
226
- service: {
227
- anyOf: [
228
- {
229
- type: 'number',
230
- minimum: 1
231
- },
232
- { type: 'string' }
233
- ],
234
- default: 10000
235
- }
236
- },
237
- default: {},
238
- required: ['runtime', 'service'],
239
- additionalProperties: false
240
- },
241
- health,
242
- undici: {
243
- type: 'object',
244
- properties: {
245
- agentOptions: {
246
- type: 'object',
247
- additionalProperties: true
248
- },
249
- interceptors: {
250
- anyOf: [
251
- {
252
- type: 'array',
253
- items: {
254
- $ref: '#/$defs/undiciInterceptor'
255
- }
256
- },
257
- {
258
- type: 'object',
259
- properties: {
260
- Client: {
261
- type: 'array',
262
- items: {
263
- $ref: '#/$defs/undiciInterceptor'
264
- }
265
- },
266
- Pool: {
267
- type: 'array',
268
- items: {
269
- $ref: '#/$defs/undiciInterceptor'
270
- }
271
- },
272
- Agent: {
273
- type: 'array',
274
- items: {
275
- $ref: '#/$defs/undiciInterceptor'
276
- }
277
- }
278
- }
279
- }
280
- ]
281
- }
282
- }
283
- },
284
- httpCache: {
285
- oneOf: [
286
- {
287
- type: 'boolean'
288
- },
289
- {
290
- type: 'object',
291
- properties: {
292
- store: {
293
- type: 'string'
294
- },
295
- methods: {
296
- type: 'array',
297
- items: {
298
- type: 'string'
299
- },
300
- default: ['GET', 'HEAD'],
301
- minItems: 1
302
- },
303
- cacheTagsHeader: {
304
- type: 'string'
305
- },
306
- maxSize: {
307
- type: 'integer'
308
- },
309
- maxEntrySize: {
310
- type: 'integer'
311
- },
312
- maxCount: {
313
- type: 'integer'
314
- }
315
- }
316
- }
317
- ]
318
- },
319
- watch: {
320
- anyOf: [
321
- {
322
- type: 'boolean'
323
- },
324
- {
325
- type: 'string'
326
- }
327
- ]
328
- },
329
- managementApi: {
330
- anyOf: [
331
- { type: 'boolean' },
332
- { type: 'string' },
333
- {
334
- type: 'object',
335
- properties: {
336
- logs: {
337
- type: 'object',
338
- properties: {
339
- maxSize: {
340
- type: 'number',
341
- minimum: 5,
342
- default: 200
343
- }
344
- },
345
- additionalProperties: false
346
- }
347
- },
348
- additionalProperties: false
349
- }
350
- ],
351
- default: true
352
- },
353
- metrics: {
354
- anyOf: [
355
- { type: 'boolean' },
356
- {
357
- type: 'object',
358
- properties: {
359
- port: {
360
- anyOf: [{ type: 'integer' }, { type: 'string' }]
361
- },
362
- enabled: {
363
- anyOf: [{
364
- type: 'boolean'
365
- }, {
366
- type: 'string'
367
- }]
368
- },
369
- hostname: { type: 'string' },
370
- endpoint: { type: 'string' },
371
- auth: {
372
- type: 'object',
373
- properties: {
374
- username: { type: 'string' },
375
- password: { type: 'string' }
376
- },
377
- additionalProperties: false,
378
- required: ['username', 'password']
379
- },
380
- labels: {
381
- type: 'object',
382
- additionalProperties: { type: 'string' }
383
- },
384
- readiness: {
385
- anyOf: [
386
- { type: 'boolean' },
387
- {
388
- type: 'object',
389
- properties: {
390
- endpoint: { type: 'string' },
391
- success: {
392
- type: 'object',
393
- properties: {
394
- statusCode: { type: 'number' },
395
- body: { type: 'string' }
396
- },
397
- additionalProperties: false
398
- },
399
- fail: {
400
- type: 'object',
401
- properties: {
402
- statusCode: { type: 'number' },
403
- body: { type: 'string' }
404
- },
405
- additionalProperties: false
406
- }
407
- },
408
- additionalProperties: false
409
- }
410
- ]
411
- },
412
- liveness: {
413
- anyOf: [
414
- { type: 'boolean' },
415
- {
416
- type: 'object',
417
- properties: {
418
- endpoint: { type: 'string' },
419
- success: {
420
- type: 'object',
421
- properties: {
422
- statusCode: { type: 'number' },
423
- body: { type: 'string' }
424
- },
425
- additionalProperties: false
426
- },
427
- fail: {
428
- type: 'object',
429
- properties: {
430
- statusCode: { type: 'number' },
431
- body: { type: 'string' }
432
- },
433
- additionalProperties: false
434
- }
435
- },
436
- additionalProperties: false
437
- }
438
- ]
439
- },
440
- additionalProperties: false
441
- }
442
- }
443
- ]
444
- },
445
- telemetry,
446
- inspectorOptions: {
447
- type: 'object',
448
- properties: {
449
- host: {
450
- type: 'string'
451
- },
452
- port: {
453
- type: 'number'
454
- },
455
- breakFirstLine: {
456
- type: 'boolean'
457
- },
458
- watchDisabled: {
459
- type: 'boolean'
460
- }
461
- }
462
- },
463
- serviceTimeout: {
464
- anyOf: [
465
- {
466
- type: 'number',
467
- minimum: 1
468
- },
469
- { type: 'string' }
470
- ],
471
- default: 300000 // 5 minutes
472
- },
473
- resolvedServicesBasePath: {
474
- type: 'string',
475
- default: 'external'
476
- },
477
- env,
478
- sourceMaps: {
479
- type: 'boolean',
480
- default: false
481
- },
482
- scheduler: {
483
- type: 'array',
484
- items: {
485
- type: 'object',
486
- properties: {
487
- enabled: {
488
- anyOf: [{
489
- type: 'boolean'
490
- }, {
491
- type: 'string'
492
- }],
493
- default: true
494
- },
495
- name: {
496
- type: 'string'
497
- },
498
- cron: {
499
- type: 'string'
500
- },
501
- callbackUrl: {
502
- type: 'string'
503
- },
504
- method: {
505
- type: 'string',
506
- enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
507
- default: 'GET'
508
- },
509
- headers: {
510
- type: 'object',
511
- additionalProperties: {
512
- type: 'string'
513
- }
514
- },
515
- body: {
516
- anyOf: [{ type: 'string' }, { type: 'object', additionalProperties: true }]
517
- },
518
- maxRetries: {
519
- type: 'number',
520
- minimum: 0,
521
- default: 3
522
- }
523
- },
524
- required: ['name', 'cron', 'callbackUrl']
525
- }
526
- }
527
- },
25
+ properties: schemaComponents.runtimeProperties,
528
26
  anyOf: [{ required: ['autoload'] }, { required: ['services'] }, { required: ['web'] }],
529
- additionalProperties: false,
530
- $defs: {
531
- undiciInterceptor: {
532
- type: 'object',
533
- properties: {
534
- module: {
535
- type: 'string'
536
- },
537
- options: {
538
- type: 'object',
539
- additionalProperties: true
540
- }
541
- },
542
- required: ['module', 'options']
543
- }
544
- }
27
+ additionalProperties: false
545
28
  }
546
29
 
547
30
  module.exports.schema = platformaticRuntimeSchema