@platformatic/runtime 2.0.0-alpha.6 → 2.0.0-alpha.7

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
@@ -5,7 +5,7 @@
5
5
  * and run json-schema-to-typescript to regenerate this file.
6
6
  */
7
7
 
8
- export type HttpsSchemasPlatformaticDevPlatformaticRuntime200Alpha6Json = {
8
+ export type HttpsSchemasPlatformaticDevPlatformaticRuntime200Alpha7Json = {
9
9
  [k: string]: unknown;
10
10
  } & {
11
11
  $schema?: string;
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://schemas.platformatic.dev/@platformatic/runtime/1.52.0.json",
2
+ "$schema": "https://schemas.platformatic.dev/@platformatic/runtime/2.0.0-alpha.5.json",
3
3
  "entrypoint": "service-1",
4
4
  "watch": false,
5
5
  "autoload": {
@@ -13,5 +13,10 @@
13
13
  "logs": {
14
14
  "maxSize": 6
15
15
  }
16
+ },
17
+ "metrics": {
18
+ "labels": {
19
+ "custom_label": "custom-value"
20
+ }
16
21
  }
17
22
  }
package/index.js CHANGED
@@ -6,11 +6,12 @@ const errors = require('./lib/errors')
6
6
  const { platformaticRuntime, wrapConfigInRuntimeConfig } = require('./lib/config')
7
7
  const RuntimeGenerator = require('./lib/generator/runtime-generator')
8
8
  const { Runtime } = require('./lib/runtime')
9
- const { start, startCommand } = require('./lib/start')
9
+ const { buildRuntime, start, startCommand } = require('./lib/start')
10
10
  const symbols = require('./lib/worker/symbols')
11
11
  const { loadConfig, getRuntimeLogsDir } = require('./lib/utils')
12
12
 
13
13
  module.exports.buildServer = buildServer
14
+ module.exports.buildRuntime = buildRuntime
14
15
  module.exports.compile = compile
15
16
  module.exports.errors = errors
16
17
  module.exports.Generator = RuntimeGenerator
package/lib/errors.js CHANGED
@@ -14,6 +14,7 @@ module.exports = {
14
14
  FailedToRetrieveOpenAPISchemaError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_OPENAPI_SCHEMA`, 'Failed to retrieve OpenAPI schema for service with id "%s": %s'),
15
15
  FailedToRetrieveGraphQLSchemaError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_GRAPHQL_SCHEMA`, 'Failed to retrieve GraphQL schema for service with id "%s": %s'),
16
16
  FailedToRetrieveMetaError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_META`, 'Failed to retrieve metadata for service with id "%s": %s'),
17
+ FailedToRetrieveMetricsError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_METRICS`, 'Failed to retrieve metrics for service with id "%s": %s'),
17
18
  ApplicationAlreadyStartedError: createError(`${ERROR_PREFIX}_APPLICATION_ALREADY_STARTED`, 'Application is already started'),
18
19
  ApplicationNotStartedError: createError(`${ERROR_PREFIX}_APPLICATION_NOT_STARTED`, 'Application has not been started'),
19
20
  ConfigPathMustBeStringError: createError(`${ERROR_PREFIX}_CONFIG_PATH_MUST_BE_STRING`, 'Config path must be a string'),
package/lib/runtime.js CHANGED
@@ -151,7 +151,7 @@ class Runtime extends EventEmitter {
151
151
  return this.#url
152
152
  }
153
153
 
154
- async stop () {
154
+ async stop (silent = false) {
155
155
  if (this.#status === 'starting') {
156
156
  await once(this, 'started')
157
157
  }
@@ -159,7 +159,7 @@ class Runtime extends EventEmitter {
159
159
  this.#updateStatus('stopping')
160
160
  this.#startedServices.clear()
161
161
 
162
- await Promise.all(this.#servicesIds.map(service => this._stopService(service)))
162
+ await Promise.all(this.#servicesIds.map(service => this._stopService(service, silent)))
163
163
 
164
164
  this.#updateStatus('stopped')
165
165
  }
@@ -175,12 +175,12 @@ class Runtime extends EventEmitter {
175
175
  return this.#url
176
176
  }
177
177
 
178
- async close (fromManagementApi) {
178
+ async close (fromManagementApi = false, silent = false) {
179
179
  this.#updateStatus('closing')
180
180
 
181
181
  clearInterval(this.#metricsTimeout)
182
182
 
183
- await this.stop()
183
+ await this.stop(silent)
184
184
 
185
185
  if (this.#managementApi) {
186
186
  if (fromManagementApi) {
@@ -263,7 +263,7 @@ class Runtime extends EventEmitter {
263
263
  }
264
264
 
265
265
  // Do not rename to #stopService as this is used in tests
266
- async _stopService (id) {
266
+ async _stopService (id, silent) {
267
267
  const service = await this.#getServiceById(id, false, false)
268
268
 
269
269
  if (!service) {
@@ -272,7 +272,9 @@ class Runtime extends EventEmitter {
272
272
 
273
273
  this.#startedServices.set(id, false)
274
274
 
275
- this.logger?.info(`Stopping service "${id}"...`)
275
+ if (!silent) {
276
+ this.logger?.info(`Stopping service "${id}"...`)
277
+ }
276
278
 
277
279
  // Always send the stop message, it will shut down workers that only had ITC and interceptors setup
278
280
  try {
@@ -524,7 +526,6 @@ class Runtime extends EventEmitter {
524
526
  }
525
527
 
526
528
  const serviceMetrics = await sendViaITC(service, 'getMetrics', format)
527
-
528
529
  if (serviceMetrics) {
529
530
  if (metrics === null) {
530
531
  metrics = format === 'json' ? [] : ''
@@ -576,17 +577,26 @@ class Runtime extends EventEmitter {
576
577
  let p99Value = 0
577
578
 
578
579
  const metricName = 'http_request_all_summary_seconds'
579
- const httpLatencyMetrics = metrics.find(metric => metric.name === metricName)
580
-
581
- p50Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.5).value || 0
582
- p90Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.9).value || 0
583
- p95Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.95).value || 0
584
- p99Value = httpLatencyMetrics.values.find(value => value.labels.quantile === 0.99).value || 0
580
+ const httpLatencyMetrics = metrics.filter(
581
+ metric => metric.name === metricName
582
+ )
585
583
 
586
- p50Value = Math.round(p50Value * 1000)
587
- p90Value = Math.round(p90Value * 1000)
588
- p95Value = Math.round(p95Value * 1000)
589
- p99Value = Math.round(p99Value * 1000)
584
+ if (httpLatencyMetrics) {
585
+ const entrypointMetrics = httpLatencyMetrics.find(
586
+ metric => metric.values?.[0]?.labels?.serviceId === this.#entrypointId
587
+ )
588
+ if (entrypointMetrics) {
589
+ p50Value = entrypointMetrics.values.find(value => value.labels.quantile === 0.5)?.value || 0
590
+ p90Value = entrypointMetrics.values.find(value => value.labels.quantile === 0.9)?.value || 0
591
+ p95Value = entrypointMetrics.values.find(value => value.labels.quantile === 0.95)?.value || 0
592
+ p99Value = entrypointMetrics.values.find(value => value.labels.quantile === 0.99)?.value || 0
593
+
594
+ p50Value = Math.round(p50Value * 1000)
595
+ p90Value = Math.round(p90Value * 1000)
596
+ p95Value = Math.round(p95Value * 1000)
597
+ p99Value = Math.round(p99Value * 1000)
598
+ }
599
+ }
590
600
 
591
601
  const cpu = cpuMetric.values[0].value
592
602
  const rss = rssMetric.values[0].value
package/lib/worker/app.js CHANGED
@@ -7,6 +7,7 @@ const debounce = require('debounce')
7
7
 
8
8
  const errors = require('../errors')
9
9
  const defaultStackable = require('./default-stackable')
10
+ const { collectMetrics } = require('./metrics')
10
11
  const { getServiceUrl, loadConfig, loadEmptyConfig } = require('../utils')
11
12
 
12
13
  class PlatformaticApp extends EventEmitter {
@@ -15,6 +16,7 @@ class PlatformaticApp extends EventEmitter {
15
16
  #listening
16
17
  #watch
17
18
  #fileWatcher
19
+ #metricsRegistry
18
20
  #debouncedRestart
19
21
  #context
20
22
 
@@ -27,6 +29,7 @@ class PlatformaticApp extends EventEmitter {
27
29
  this.#listening = false
28
30
  this.stackable = null
29
31
  this.#fileWatcher = null
32
+ this.#metricsRegistry = null
30
33
 
31
34
  this.#context = {
32
35
  serviceId: this.appConfig.id,
@@ -93,6 +96,15 @@ class PlatformaticApp extends EventEmitter {
93
96
  })
94
97
  this.stackable = this.#wrapStackable(stackable)
95
98
 
99
+ const metricsConfig = this.#context.metricsConfig
100
+ if (metricsConfig !== false) {
101
+ this.#metricsRegistry = await collectMetrics(
102
+ this.stackable,
103
+ this.appConfig.id,
104
+ metricsConfig
105
+ )
106
+ }
107
+
96
108
  this.#updateDispatcher()
97
109
  } catch (err) {
98
110
  this.#logAndExit(err)
@@ -164,6 +176,14 @@ class PlatformaticApp extends EventEmitter {
164
176
  await this.stackable.start({ listen: true })
165
177
  }
166
178
 
179
+ async getMetrics ({ format }) {
180
+ if (!this.#metricsRegistry) return null
181
+
182
+ return format === 'json'
183
+ ? this.#metricsRegistry.getMetricsAsJSON()
184
+ : this.#metricsRegistry.metrics()
185
+ }
186
+
167
187
  #fetchServiceUrl (key, { parent, context: service }) {
168
188
  if (service.localServiceEnvVars.has(key)) {
169
189
  return service.localServiceEnvVars.get(key)
@@ -14,7 +14,10 @@ const defaultStackable = {
14
14
  getOpenapiSchema: () => null,
15
15
  getGraphqlSchema: () => null,
16
16
  getMeta: () => ({}),
17
- getMetrics: () => null,
17
+ collectMetrics: () => ({
18
+ defaultMetrics: true,
19
+ httpMetrics: true,
20
+ }),
18
21
  inject: () => {
19
22
  throw new Error('Stackable inject not implemented')
20
23
  },
package/lib/worker/itc.js CHANGED
@@ -120,8 +120,12 @@ function setupITC (app, service, dispatcher) {
120
120
  }
121
121
  },
122
122
 
123
- getMetrics (format) {
124
- return app.stackable.getMetrics({ format })
123
+ async getMetrics (format) {
124
+ try {
125
+ return await app.getMetrics({ format })
126
+ } catch (err) {
127
+ throw new errors.FailedToRetrieveMetricsError(service.id, err.message)
128
+ }
125
129
  },
126
130
 
127
131
  inject (injectParams) {
@@ -0,0 +1,106 @@
1
+ 'use strict'
2
+
3
+ const os = require('node:os')
4
+ const { eventLoopUtilization } = require('node:perf_hooks').performance
5
+ const { Registry, Gauge, collectDefaultMetrics } = require('prom-client')
6
+ const collectHttpMetrics = require('@platformatic/http-metrics')
7
+
8
+ async function collectMetrics (stackable, serviceId, opts = {}) {
9
+ const registry = new Registry()
10
+ const metricsConfig = await stackable.collectMetrics({ registry })
11
+
12
+ const labels = opts.labels ?? {}
13
+ registry.setDefaultLabels({ ...labels, serviceId })
14
+
15
+ if (metricsConfig.defaultMetrics) {
16
+ collectDefaultMetrics({ register: registry })
17
+ collectEluMetric(registry)
18
+ }
19
+
20
+ if (metricsConfig.httpMetrics) {
21
+ collectHttpMetrics(registry, {
22
+ customLabels: ['telemetry_id'],
23
+ getCustomLabels: (req) => {
24
+ const telemetryId = req.headers['x-plt-telemetry-id'] ?? 'unknown'
25
+ return { telemetry_id: telemetryId }
26
+ }
27
+ })
28
+
29
+ // TODO: check if it's a nodejs environment
30
+ // Needed for the Meraki metrics
31
+ collectHttpMetrics(registry, {
32
+ customLabels: ['telemetry_id'],
33
+ getCustomLabels: (req) => {
34
+ const telemetryId = req.headers['x-plt-telemetry-id'] ?? 'unknown'
35
+ return { telemetry_id: telemetryId }
36
+ },
37
+ histogram: {
38
+ name: 'http_request_all_duration_seconds',
39
+ help: 'request duration in seconds summary for all requests',
40
+ collect: function () {
41
+ process.nextTick(() => this.reset())
42
+ },
43
+ },
44
+ summary: {
45
+ name: 'http_request_all_summary_seconds',
46
+ help: 'request duration in seconds histogram for all requests',
47
+ collect: function () {
48
+ process.nextTick(() => this.reset())
49
+ },
50
+ },
51
+ })
52
+ }
53
+
54
+ return registry
55
+ }
56
+
57
+ function collectEluMetric (register) {
58
+ let startELU = eventLoopUtilization()
59
+ const eluMetric = new Gauge({
60
+ name: 'nodejs_eventloop_utilization',
61
+ help: 'The event loop utilization as a fraction of the loop time. 1 is fully utilized, 0 is fully idle.',
62
+ collect: () => {
63
+ const endELU = eventLoopUtilization()
64
+ const result = eventLoopUtilization(endELU, startELU).utilization
65
+ eluMetric.set(result)
66
+ startELU = endELU
67
+ },
68
+ registers: [register],
69
+ })
70
+ register.registerMetric(eluMetric)
71
+
72
+ let previousIdleTime = 0
73
+ let previousTotalTime = 0
74
+ const cpuMetric = new Gauge({
75
+ name: 'process_cpu_percent_usage',
76
+ help: 'The process CPU percent usage.',
77
+ collect: () => {
78
+ const cpus = os.cpus()
79
+ let idleTime = 0
80
+ let totalTime = 0
81
+
82
+ cpus.forEach(cpu => {
83
+ for (const type in cpu.times) {
84
+ totalTime += cpu.times[type]
85
+ if (type === 'idle') {
86
+ idleTime += cpu.times[type]
87
+ }
88
+ }
89
+ })
90
+
91
+ const idleDiff = idleTime - previousIdleTime
92
+ const totalDiff = totalTime - previousTotalTime
93
+
94
+ const usagePercent = 100 - ((100 * idleDiff) / totalDiff)
95
+ const roundedUsage = Math.round(usagePercent * 100) / 100
96
+ cpuMetric.set(roundedUsage)
97
+
98
+ previousIdleTime = idleTime
99
+ previousTotalTime = totalTime
100
+ },
101
+ registers: [register],
102
+ })
103
+ register.registerMetric(cpuMetric)
104
+ }
105
+
106
+ module.exports = { collectMetrics }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "2.0.0-alpha.6",
3
+ "version": "2.0.0-alpha.7",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -34,16 +34,17 @@
34
34
  "typescript": "^5.5.4",
35
35
  "undici-oidc-interceptor": "^0.5.0",
36
36
  "why-is-node-running": "^2.2.2",
37
- "@platformatic/composer": "2.0.0-alpha.6",
38
- "@platformatic/service": "2.0.0-alpha.6",
39
- "@platformatic/sql-graphql": "2.0.0-alpha.6",
40
- "@platformatic/sql-mapper": "2.0.0-alpha.6",
41
- "@platformatic/db": "2.0.0-alpha.6"
37
+ "@platformatic/composer": "2.0.0-alpha.7",
38
+ "@platformatic/db": "2.0.0-alpha.7",
39
+ "@platformatic/service": "2.0.0-alpha.7",
40
+ "@platformatic/sql-graphql": "2.0.0-alpha.7",
41
+ "@platformatic/sql-mapper": "2.0.0-alpha.7"
42
42
  },
43
43
  "dependencies": {
44
44
  "@fastify/error": "^3.4.1",
45
45
  "@fastify/websocket": "^10.0.0",
46
46
  "@hapi/topo": "^6.0.2",
47
+ "@platformatic/http-metrics": "^0.1.0",
47
48
  "@watchable/unpromise": "^1.0.2",
48
49
  "boring-name-generator": "^1.0.3",
49
50
  "change-case-all": "^2.1.0",
@@ -62,18 +63,19 @@
62
63
  "pino": "^8.19.0",
63
64
  "pino-pretty": "^11.0.0",
64
65
  "pino-roll": "^1.0.0",
66
+ "prom-client": "^15.1.2",
65
67
  "semgrator": "^0.3.0",
66
68
  "tail-file-stream": "^0.2.0",
67
69
  "undici": "^6.9.0",
68
70
  "undici-thread-interceptor": "^0.5.1",
69
71
  "ws": "^8.16.0",
70
- "@platformatic/basic": "2.0.0-alpha.6",
71
- "@platformatic/generators": "2.0.0-alpha.6",
72
- "@platformatic/config": "2.0.0-alpha.6",
73
- "@platformatic/utils": "2.0.0-alpha.6",
74
- "@platformatic/itc": "2.0.0-alpha.6",
75
- "@platformatic/ts-compiler": "2.0.0-alpha.6",
76
- "@platformatic/telemetry": "2.0.0-alpha.6"
72
+ "@platformatic/basic": "2.0.0-alpha.7",
73
+ "@platformatic/config": "2.0.0-alpha.7",
74
+ "@platformatic/generators": "2.0.0-alpha.7",
75
+ "@platformatic/itc": "2.0.0-alpha.7",
76
+ "@platformatic/telemetry": "2.0.0-alpha.7",
77
+ "@platformatic/ts-compiler": "2.0.0-alpha.7",
78
+ "@platformatic/utils": "2.0.0-alpha.7"
77
79
  },
78
80
  "scripts": {
79
81
  "test": "npm run lint && borp --concurrency=1 --timeout=180000 && tsd",
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.0.0-alpha.6.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.0.0-alpha.7.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "type": "object",
5
5
  "properties": {