@platformatic/watt-admin 0.6.0-alpha.1 → 0.6.0-alpha.3

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.
@@ -0,0 +1,226 @@
1
+ import { type FromSchema } from 'json-schema-to-ts'
2
+
3
+ const dateSchema = { type: 'string', format: 'date-time' } as const
4
+
5
+ export const modeSchema = { type: 'string', enum: ['start', 'stop'] } as const
6
+ export type Mode = FromSchema<typeof modeSchema>
7
+
8
+ const memoryDataPointSchema = {
9
+ type: 'object',
10
+ additionalProperties: false,
11
+ properties: {
12
+ date: dateSchema,
13
+ rss: { type: 'number' },
14
+ totalHeap: { type: 'number' },
15
+ usedHeap: { type: 'number' },
16
+ newSpace: { type: 'number' },
17
+ oldSpace: { type: 'number' }
18
+ },
19
+ required: ['date', 'rss', 'totalHeap', 'usedHeap', 'newSpace', 'oldSpace']
20
+ } as const
21
+ export type MemoryDataPoint = FromSchema<typeof memoryDataPointSchema>
22
+
23
+ const cpuDataPointSchema = {
24
+ type: 'object',
25
+ additionalProperties: false,
26
+ properties: {
27
+ date: dateSchema,
28
+ cpu: { type: 'number' },
29
+ eventLoop: { type: 'number' }
30
+ },
31
+ required: ['date', 'cpu', 'eventLoop']
32
+ } as const
33
+ export type CpuDataPoint = FromSchema<typeof cpuDataPointSchema>
34
+
35
+ const latencyDataPointSchema = {
36
+ type: 'object',
37
+ additionalProperties: false,
38
+ properties: {
39
+ date: { type: 'string', format: 'date-time' },
40
+ p90: { type: 'number' },
41
+ p95: { type: 'number' },
42
+ p99: { type: 'number' }
43
+ },
44
+ required: ['date', 'p90', 'p95', 'p99']
45
+ } as const
46
+ export type LatencyDataPoint = FromSchema<typeof latencyDataPointSchema>
47
+
48
+ const requestDataPointSchema = {
49
+ type: 'object',
50
+ additionalProperties: false,
51
+ properties: {
52
+ date: { type: 'string', format: 'date-time' },
53
+ count: { type: 'number' },
54
+ rps: { type: 'number' }
55
+ },
56
+ required: ['date', 'count', 'rps']
57
+ } as const
58
+ export type RequestDataPoint = FromSchema<typeof requestDataPointSchema>
59
+
60
+ const kafkaDataPointSchema = {
61
+ type: 'object',
62
+ additionalProperties: false,
63
+ properties: {
64
+ date: { type: 'string', format: 'date-time' },
65
+ producers: { type: 'number' },
66
+ producedMessages: { type: 'number' },
67
+ consumers: { type: 'number' },
68
+ consumersStreams: { type: 'number' },
69
+ consumersTopics: { type: 'number' },
70
+ consumedMessages: { type: 'number' },
71
+ hooksMessagesInFlight: { type: 'number' },
72
+ hooksDlqMessagesTotal: { type: 'number' },
73
+ },
74
+ required: [
75
+ 'date',
76
+ 'producers',
77
+ 'producedMessages',
78
+ 'consumers',
79
+ 'consumersStreams',
80
+ 'consumersTopics',
81
+ 'consumedMessages',
82
+ 'hooksMessagesInFlight',
83
+ 'hooksDlqMessagesTotal',
84
+ ]
85
+ } as const
86
+ export type KafkaDataPoint = FromSchema<typeof kafkaDataPointSchema>
87
+
88
+ const undiciDataPointSchema = {
89
+ type: 'object',
90
+ additionalProperties: false,
91
+ properties: {
92
+ date: { type: 'string', format: 'date-time' },
93
+ idleSockets: { type: 'number' },
94
+ openSockets: { type: 'number' },
95
+ pendingRequests: { type: 'number' },
96
+ queuedRequests: { type: 'number' },
97
+ activeRequests: { type: 'number' },
98
+ sizeRequests: { type: 'number' },
99
+ },
100
+ required: [
101
+ 'date',
102
+ 'idleSockets',
103
+ 'openSockets',
104
+ 'pendingRequests',
105
+ 'queuedRequests',
106
+ 'activeRequests',
107
+ 'sizeRequests',
108
+ ]
109
+ } as const
110
+ export type UndiciDataPoint = FromSchema<typeof undiciDataPointSchema>
111
+
112
+ const websocketDataPointSchema = {
113
+ type: 'object',
114
+ additionalProperties: false,
115
+ properties: {
116
+ date: { type: 'string', format: 'date-time' },
117
+ connections: { type: 'number' }
118
+ },
119
+ required: [
120
+ 'date',
121
+ 'connections'
122
+ ]
123
+ } as const
124
+ export type WebsocketDataPoint = FromSchema<typeof websocketDataPointSchema>
125
+
126
+ const nodejsDataPointSchema = {
127
+ type: 'object',
128
+ additionalProperties: false,
129
+ properties: {
130
+ date: { type: 'string', format: 'date-time' },
131
+ resources: { type: 'number' }
132
+ },
133
+ required: [
134
+ 'date',
135
+ 'resources'
136
+ ]
137
+ } as const
138
+ export type NodejsDataPoint = FromSchema<typeof nodejsDataPointSchema>
139
+
140
+ export const requiredMetricKeys = ['dataMem', 'dataCpu', 'dataKafka', 'dataReq', 'dataLatency', 'dataUndici', 'dataWebsocket', 'dataNodejs'] as const
141
+ export const metricResponseSchema = {
142
+ type: 'object',
143
+ additionalProperties: false,
144
+ properties: {
145
+ dataMem: { type: 'array', items: memoryDataPointSchema },
146
+ dataCpu: { type: 'array', items: cpuDataPointSchema },
147
+ dataLatency: { type: 'array', items: latencyDataPointSchema },
148
+ dataReq: { type: 'array', items: requestDataPointSchema },
149
+ dataKafka: { type: 'array', items: kafkaDataPointSchema },
150
+ dataUndici: { type: 'array', items: undiciDataPointSchema },
151
+ dataWebsocket: { type: 'array', items: websocketDataPointSchema },
152
+ dataNodejs: { type: 'array', items: nodejsDataPointSchema }
153
+ },
154
+ required: requiredMetricKeys
155
+ } as const
156
+ export type MetricsResponse = FromSchema<typeof metricResponseSchema>
157
+ export type SingleMetricResponse = {
158
+ [K in keyof MetricsResponse]: MetricsResponse[K] extends (infer T)[] ? T : never
159
+ }
160
+
161
+ export const pidParamSchema = { type: 'object', additionalProperties: false, properties: { pid: { type: 'number' } }, required: ['pid'] } as const
162
+ export type PidParam = FromSchema<typeof pidParamSchema>
163
+
164
+ export const profileSchema = { type: 'string', enum: ['cpu', 'heap'] } as const
165
+ export type Profile = FromSchema<typeof profileSchema>
166
+
167
+ export const selectableRuntimeSchema = {
168
+ type: 'object',
169
+ additionalProperties: false,
170
+ properties: {
171
+ pid: {
172
+ type: 'integer'
173
+ },
174
+ cwd: {
175
+ type: 'string'
176
+ },
177
+ argv: {
178
+ type: 'array',
179
+ items: {
180
+ type: 'string'
181
+ }
182
+ },
183
+ uptimeSeconds: {
184
+ type: 'number'
185
+ },
186
+ execPath: {
187
+ type: 'string'
188
+ },
189
+ nodeVersion: {
190
+ type: 'string'
191
+ },
192
+ projectDir: {
193
+ type: 'string'
194
+ },
195
+ packageName: {
196
+ type: 'string'
197
+ },
198
+ packageVersion: {
199
+ type: 'string'
200
+ },
201
+ url: {
202
+ type: 'string'
203
+ },
204
+ platformaticVersion: {
205
+ type: 'string'
206
+ },
207
+ selected: {
208
+ type: 'boolean'
209
+ }
210
+ },
211
+ required: [
212
+ 'pid',
213
+ 'cwd',
214
+ 'argv',
215
+ 'uptimeSeconds',
216
+ 'execPath',
217
+ 'nodeVersion',
218
+ 'projectDir',
219
+ 'packageName',
220
+ 'packageVersion',
221
+ 'url',
222
+ 'platformaticVersion',
223
+ 'selected'
224
+ ]
225
+ } as const
226
+ export type SelectableRuntime = FromSchema<typeof selectableRuntimeSchema>
@@ -0,0 +1 @@
1
+ export const bytesToMB = (bytes: number): number => Number((bytes / (1024 * 1024)).toFixed(2))
@@ -0,0 +1,29 @@
1
+ import fs from 'fs/promises'
2
+ import { create } from '@platformatic/service'
3
+ import { join, resolve } from 'node:path'
4
+
5
+ const __dirname = import.meta.dirname
6
+
7
+ async function clientOpenapi () {
8
+ const basePath = resolve(__dirname, join('..', 'platformatic.json'))
9
+ const server = await create(basePath, {
10
+ service: { openapi: true },
11
+ watch: false,
12
+ plugins: {
13
+ paths: [
14
+ {
15
+ path: resolve(__dirname, join('..', 'plugins')),
16
+ encapsulate: false
17
+ },
18
+ resolve(__dirname, join('..', 'routes'))
19
+ ]
20
+ }
21
+ })
22
+
23
+ await server.start({})
24
+ const { body } = await server.getApplication().inject('/documentation/json')
25
+ await fs.writeFile('openapi.json', body)
26
+ await server.close()
27
+ }
28
+
29
+ clientOpenapi()
@@ -0,0 +1,4 @@
1
+ export const MS_WAITING = 1000
2
+
3
+ // This is to avoid the mapped metrics array from growing indefinitely (and therefore a memory leak): store the last 10 minutes of metrics (1# * 10m * 60s).
4
+ export const MAX_STORED_METRICS = 600
@@ -0,0 +1,89 @@
1
+ import type { CpuDataPoint, KafkaDataPoint, LatencyDataPoint, MemoryDataPoint, MetricsResponse, NodejsDataPoint, RequestDataPoint, SingleMetricResponse, UndiciDataPoint, WebsocketDataPoint } from '../schemas/index.ts'
2
+ import { MAX_STORED_METRICS } from './constants.ts'
3
+
4
+ export type MappedMetrics = Record<number, {
5
+ aggregated: MetricsResponse,
6
+ services: Record<string, Record<'all' | number, MetricsResponse>>
7
+ }>
8
+
9
+ export const websocketMetricMap = {
10
+ active_ws_composer_connections: 'connections',
11
+ } as const
12
+ export const isWebsocketMetricName = (metricName: string): metricName is keyof typeof websocketMetricMap => metricName in websocketMetricMap
13
+
14
+ export const nodejsMetricMap = {
15
+ active_resources_event_loop: 'resources'
16
+ } as const
17
+ export const isNodejsMetricName = (metricName: string): metricName is keyof typeof nodejsMetricMap => metricName in nodejsMetricMap
18
+
19
+ export const undiciMetricMap = {
20
+ http_client_stats_free: 'idleSockets',
21
+ http_client_stats_connected: 'openSockets',
22
+ http_client_stats_pending: 'pendingRequests',
23
+ http_client_stats_queued: 'queuedRequests',
24
+ http_client_stats_running: 'activeRequests',
25
+ http_client_stats_size: 'sizeRequests'
26
+ } as const
27
+ export const isUndiciMetricName = (metricName: string): metricName is keyof typeof undiciMetricMap => metricName in undiciMetricMap
28
+
29
+ export const kafkaMetricMap = {
30
+ kafka_producers: 'producers',
31
+ kafka_produced_messages: 'producedMessages',
32
+ kafka_consumers: 'consumers',
33
+ kafka_consumers_streams: 'consumersStreams',
34
+ kafka_consumers_topics: 'consumersTopics',
35
+ kafka_consumed_messages: 'consumedMessages',
36
+ kafka_hooks_messages_in_flight: 'hooksMessagesInFlight',
37
+ kafka_hooks_dlq_messages_total: 'hooksDlqMessagesTotal'
38
+ } as const
39
+ export const isKafkaMetricName = (metricName: string): metricName is keyof typeof kafkaMetricMap => metricName in kafkaMetricMap
40
+
41
+ export const addMetricDataPoint = <T extends MemoryDataPoint | CpuDataPoint | LatencyDataPoint | RequestDataPoint | KafkaDataPoint | UndiciDataPoint | WebsocketDataPoint | NodejsDataPoint>(metrics: T[], dataPoint: T) => {
42
+ if (metrics.length >= MAX_STORED_METRICS) {
43
+ metrics.shift()
44
+ }
45
+ metrics.push(dataPoint)
46
+ }
47
+
48
+ export const initMetricsObject = (date: string): SingleMetricResponse => ({ dataMem: initMemData(date), dataCpu: initCpuData(date), dataKafka: initKafkaData(date), dataReq: initReqData(date), dataLatency: initLatencyData(date), dataUndici: initUndiciData(date), dataWebsocket: initWebsocketData(date), dataNodejs: initNodejsData(date) })
49
+
50
+ export const initMetricsResponse = (date?: string, length?: number): MetricsResponse => {
51
+ const isArray = date && length
52
+ return {
53
+ dataCpu: isArray ? Array.from({ length }, () => initCpuData(date)) : [],
54
+ dataLatency: isArray ? Array.from({ length }, () => initLatencyData(date)) : [],
55
+ dataMem: isArray ? Array.from({ length }, () => initMemData(date)) : [],
56
+ dataReq: isArray ? Array.from({ length }, () => initReqData(date)) : [],
57
+ dataKafka: isArray ? Array.from({ length }, () => initKafkaData(date)) : [],
58
+ dataUndici: isArray ? Array.from({ length }, () => initUndiciData(date)) : [],
59
+ dataWebsocket: isArray ? Array.from({ length }, () => initWebsocketData(date)) : [],
60
+ dataNodejs: isArray ? Array.from({ length }, () => initNodejsData(date)) : [],
61
+ }
62
+ }
63
+
64
+ const initMemData = (date: string): MemoryDataPoint => ({ date, rss: 0, totalHeap: 0, usedHeap: 0, newSpace: 0, oldSpace: 0 })
65
+ const initCpuData = (date: string): CpuDataPoint => ({ date, cpu: 0, eventLoop: 0 })
66
+ const initLatencyData = (date: string): LatencyDataPoint => ({ date, p90: 0, p95: 0, p99: 0 })
67
+ const initReqData = (date: string): RequestDataPoint => ({ date, count: 0, rps: 0, })
68
+ const initKafkaData = (date: string): KafkaDataPoint => ({ date, consumedMessages: 0, consumers: 0, consumersStreams: 0, consumersTopics: 0, hooksDlqMessagesTotal: 0, hooksMessagesInFlight: 0, producedMessages: 0, producers: 0 })
69
+ const initUndiciData = (date: string): UndiciDataPoint => ({ date, activeRequests: 0, idleSockets: 0, openSockets: 0, pendingRequests: 0, queuedRequests: 0, sizeRequests: 0 })
70
+ const initWebsocketData = (date: string): WebsocketDataPoint => ({ date, connections: 0 })
71
+ const initNodejsData = (date: string): NodejsDataPoint => ({ date, resources: 0 })
72
+
73
+ export const initServiceMetrics = ({ areMultipleWorkersEnabled, metrics, pid, serviceId, workers }: {
74
+ metrics: MappedMetrics,
75
+ pid: number,
76
+ serviceId: string,
77
+ workers: number,
78
+ areMultipleWorkersEnabled: boolean
79
+ }): void => {
80
+ if (!metrics[pid].services[serviceId]) {
81
+ metrics[pid].services[serviceId] = { all: initMetricsResponse() }
82
+
83
+ if (areMultipleWorkersEnabled) {
84
+ for (let i = 0; i < workers; i++) {
85
+ metrics[pid].services[serviceId][i] = initMetricsResponse()
86
+ }
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,202 @@
1
+ import { RuntimeApiClient } from '@platformatic/control'
2
+ import type { FastifyInstance } from 'fastify'
3
+ import { requiredMetricKeys } from '../schemas/index.ts'
4
+ import type { MemoryDataPoint, CpuDataPoint } from '../schemas/index.ts'
5
+ import { bytesToMB } from './bytes.ts'
6
+ import { getReqRps } from './rps.ts'
7
+ import { addMetricDataPoint, initMetricsObject, initMetricsResponse, initServiceMetrics, isKafkaMetricName, isNodejsMetricName, isUndiciMetricName, isWebsocketMetricName, kafkaMetricMap, nodejsMetricMap, undiciMetricMap, websocketMetricMap } from './metrics-helpers.ts'
8
+
9
+ export const getMetrics = async ({ loaded: { metrics }, log }: FastifyInstance): Promise<void> => {
10
+ try {
11
+ const api = new RuntimeApiClient()
12
+ const runtimes = await api.getRuntimes()
13
+ for (const { pid } of runtimes) {
14
+ const date = new Date().toISOString()
15
+ const aggregatedMetrics = initMetricsObject(date)
16
+ let aggregatedRss = 0
17
+
18
+ const runtimeMetrics = await api.getRuntimeMetrics(pid, { format: 'json' })
19
+ if (!metrics[pid]) {
20
+ metrics[pid] = { services: {}, aggregated: initMetricsResponse() }
21
+ }
22
+
23
+ const { applications, entrypoint } = await api.getRuntimeApplications(pid)
24
+ for (const service of applications) {
25
+ const { id: serviceId } = service
26
+ const workers = 'workers' in service && service?.workers ? service.workers : 1
27
+ const areMultipleWorkersEnabled = workers > 1
28
+ const isEntrypointService = entrypoint === serviceId
29
+ initServiceMetrics({ metrics, pid, serviceId, workers, areMultipleWorkersEnabled })
30
+ const workerMetrics = initMetricsResponse(date, workers)
31
+ const serviceMetrics = initMetricsObject(date)
32
+
33
+ const incDataMetric = <T extends 'dataMem' | 'dataCpu'>({ key, workerId, data, prop }: { key: T, workerId: number, data: number, prop: Exclude<T extends 'dataMem' ? keyof MemoryDataPoint : keyof CpuDataPoint, 'date'> }) => {
34
+ if (areMultipleWorkersEnabled) {
35
+ workerMetrics[key as 'dataMem'][workerId][prop as 'rss'] = data
36
+ }
37
+ serviceMetrics[key as 'dataMem'][prop as 'rss'] += data
38
+ aggregatedMetrics[key as 'dataMem'][prop as 'rss'] += serviceMetrics[key as 'dataMem'][prop as 'rss']
39
+ }
40
+
41
+ for (const metric of runtimeMetrics) {
42
+ if (metric.values.length > 0) {
43
+ const [{ value, labels }] = metric.values
44
+ const workerId = labels.workerId ?? 0
45
+
46
+ if (isEntrypointService) {
47
+ if (metric.name === 'process_resident_memory_bytes') {
48
+ aggregatedRss = bytesToMB(value)
49
+ }
50
+ }
51
+
52
+ if (serviceId === labels.applicationId) {
53
+ if (metric.name === 'nodejs_heap_size_total_bytes') {
54
+ incDataMetric({ key: 'dataMem', workerId, data: bytesToMB(value), prop: 'totalHeap' })
55
+ }
56
+
57
+ if (metric.name === 'nodejs_heap_size_used_bytes') {
58
+ incDataMetric({ key: 'dataMem', workerId, data: bytesToMB(value), prop: 'usedHeap' })
59
+ }
60
+
61
+ if (metric.name === 'nodejs_heap_space_size_used_bytes') {
62
+ metric.values.forEach(({ value, labels }) => {
63
+ if (labels?.space === 'new') {
64
+ incDataMetric({ key: 'dataMem', workerId, data: bytesToMB(value), prop: 'newSpace' })
65
+ } else if (labels?.space === 'old') {
66
+ incDataMetric({ key: 'dataMem', workerId, data: bytesToMB(value), prop: 'oldSpace' })
67
+ }
68
+ })
69
+ }
70
+
71
+ if (metric.name === 'thread_cpu_percent_usage') {
72
+ incDataMetric({ key: 'dataCpu', workerId, data: value / workers, prop: 'cpu' })
73
+ }
74
+
75
+ if (metric.name === 'nodejs_eventloop_utilization') {
76
+ incDataMetric({ key: 'dataCpu', workerId, data: (value * 100) / workers, prop: 'eventLoop' })
77
+ }
78
+
79
+ if (metric.name === 'http_request_all_summary_seconds') {
80
+ for (const metricValue of metric.values) {
81
+ const data = metricValue.value * 1000
82
+ if (data > 0) {
83
+ if (metricValue.labels?.quantile === 0.9) {
84
+ if (data > serviceMetrics.dataLatency.p90) {
85
+ if (areMultipleWorkersEnabled) {
86
+ workerMetrics.dataLatency[workerId].p90 = data
87
+ }
88
+ serviceMetrics.dataLatency.p90 = data
89
+ }
90
+ if (isEntrypointService) {
91
+ aggregatedMetrics.dataLatency.p90 = serviceMetrics.dataLatency.p90
92
+ }
93
+ }
94
+ if (metricValue.labels?.quantile === 0.95) {
95
+ if (data > serviceMetrics.dataLatency.p95) {
96
+ if (areMultipleWorkersEnabled) {
97
+ workerMetrics.dataLatency[workerId].p95 = data
98
+ }
99
+ serviceMetrics.dataLatency.p95 = data
100
+ }
101
+ if (isEntrypointService) {
102
+ aggregatedMetrics.dataLatency.p95 = serviceMetrics.dataLatency.p95
103
+ }
104
+ }
105
+ if (metricValue.labels?.quantile === 0.99) {
106
+ if (data > serviceMetrics.dataLatency.p99) {
107
+ if (areMultipleWorkersEnabled) {
108
+ workerMetrics.dataLatency[workerId].p99 = data
109
+ }
110
+ serviceMetrics.dataLatency.p99 = data
111
+ }
112
+ if (isEntrypointService) {
113
+ aggregatedMetrics.dataLatency.p99 = serviceMetrics.dataLatency.p99
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ if (metric.name === 'http_request_all_duration_seconds') {
121
+ const count = metric.values.reduce((acc, { metricName, value }) => {
122
+ if (metricName === 'http_request_all_duration_seconds_count') {
123
+ acc += value
124
+ }
125
+ return acc
126
+ }, 0)
127
+ if (!count) {
128
+ log.debug(metric.values, 'Empty HTTP request count')
129
+ } else {
130
+ if (areMultipleWorkersEnabled) {
131
+ workerMetrics.dataReq[workerId].count = count
132
+ const rps = getReqRps(count, metrics[pid].services[serviceId][workerId].dataReq)
133
+ workerMetrics.dataReq[workerId].rps = rps
134
+ }
135
+ serviceMetrics.dataReq.count += count
136
+ const rps = getReqRps(serviceMetrics.dataReq.count, metrics[pid].services[serviceId].all.dataReq)
137
+ serviceMetrics.dataReq.rps = rps
138
+
139
+ if (isEntrypointService) {
140
+ aggregatedMetrics.dataReq.count = serviceMetrics.dataReq.count
141
+ aggregatedMetrics.dataReq.rps = rps
142
+ }
143
+ }
144
+ }
145
+
146
+ if (isUndiciMetricName(metric.name)) {
147
+ const key = undiciMetricMap[metric.name]
148
+ serviceMetrics.dataUndici[key] = value
149
+ aggregatedMetrics.dataUndici[key] = value
150
+ if (areMultipleWorkersEnabled) {
151
+ workerMetrics.dataUndici[workerId][key] = value
152
+ }
153
+ }
154
+
155
+ if (isWebsocketMetricName(metric.name)) {
156
+ const key = websocketMetricMap[metric.name]
157
+ serviceMetrics.dataWebsocket[key] = value
158
+ aggregatedMetrics.dataWebsocket[key] = value
159
+ if (areMultipleWorkersEnabled) {
160
+ workerMetrics.dataWebsocket[workerId][key] = value
161
+ }
162
+ }
163
+
164
+ if (isNodejsMetricName(metric.name)) {
165
+ const key = nodejsMetricMap[metric.name]
166
+ serviceMetrics.dataNodejs[key] = value
167
+ aggregatedMetrics.dataNodejs[key] = value
168
+ if (areMultipleWorkersEnabled) {
169
+ workerMetrics.dataNodejs[workerId][key] = value
170
+ }
171
+ }
172
+
173
+ if (isKafkaMetricName(metric.name)) {
174
+ const key = kafkaMetricMap[metric.name]
175
+ serviceMetrics.dataKafka[key] = value
176
+ aggregatedMetrics.dataKafka[key] = value
177
+ if (areMultipleWorkersEnabled) {
178
+ workerMetrics.dataKafka[workerId][key] = value
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ serviceMetrics.dataMem.rss = aggregatedRss
186
+ aggregatedMetrics.dataMem.rss = aggregatedRss
187
+
188
+ if (areMultipleWorkersEnabled) {
189
+ for (let i = 0; i < workers; i++) {
190
+ workerMetrics.dataMem[i].rss = aggregatedRss
191
+ requiredMetricKeys.forEach(key => addMetricDataPoint(metrics[pid].services[serviceId][i][key], workerMetrics[key][i]))
192
+ }
193
+ }
194
+
195
+ requiredMetricKeys.forEach(key => addMetricDataPoint(metrics[pid].services[serviceId].all[key], serviceMetrics[key]))
196
+ }
197
+ requiredMetricKeys.forEach(key => addMetricDataPoint(metrics[pid].aggregated[key], aggregatedMetrics[key]))
198
+ }
199
+ } catch (error) {
200
+ log.warn(error, 'Unable to get runtime metrics. Retry will start soon...')
201
+ }
202
+ }
@@ -0,0 +1,3 @@
1
+ import type { RequestDataPoint } from '../schemas/index.ts'
2
+
3
+ export const getReqRps = (count: number, req: RequestDataPoint[]) => Math.abs(count - (req[req.length - 1]?.count || 0))
@@ -0,0 +1,21 @@
1
+ import type { Runtime } from '@platformatic/control'
2
+ import type { SelectableRuntime } from '../schemas/index.ts'
3
+
4
+ export const getSelectableRuntimes = (runtimes: Runtime[], includeAdmin: boolean): SelectableRuntime[] => {
5
+ const selectableRuntimes: SelectableRuntime[] = []
6
+ for (const runtime of runtimes) {
7
+ if (!includeAdmin && runtime.packageName === '@platformatic/watt-admin' && !process.env.INCLUDE_ADMIN) {
8
+ continue
9
+ }
10
+
11
+ let selected = true
12
+ if (process.env.SELECTED_RUNTIME) {
13
+ selected = process.env.SELECTED_RUNTIME === runtime.pid.toString()
14
+ }
15
+
16
+ selectableRuntimes.push({ ...runtime, packageName: runtime.packageName || '', packageVersion: runtime.packageVersion || '', url: runtime.url || '', selected })
17
+ }
18
+ return selectableRuntimes
19
+ }
20
+
21
+ export const getPidToLoad = (runtimes: SelectableRuntime[]): number => runtimes.find(({ selected }) => selected === true)?.pid || 0
@@ -0,0 +1,3 @@
1
+ import type { Mode } from '../schemas/index.ts'
2
+
3
+ export const checkRecordState = ({ from, to }: { from: Mode | undefined, to: Mode }): boolean => (from === undefined && to === 'start') || (from === 'start' && to === 'stop') || (from === 'stop' && to === 'start')