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

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,204 @@
1
+ import type { FastifyInstance } from 'fastify'
2
+ import type { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'
3
+ import { RuntimeApiClient } from '@platformatic/control'
4
+ import { getPidToLoad, getSelectableRuntimes } from '../utils/runtimes.ts'
5
+ import { writeFile, readFile } from 'fs/promises'
6
+ import { checkRecordState } from '../utils/states.ts'
7
+ import { join } from 'path'
8
+ import { pidParamSchema, selectableRuntimeSchema, modeSchema, profileSchema } from '../schemas/index.ts'
9
+
10
+ const __dirname = import.meta.dirname
11
+
12
+ export default async function (fastify: FastifyInstance) {
13
+ const typedFastify = fastify.withTypeProvider<JsonSchemaToTsProvider>()
14
+
15
+ // FIXME: types have not been properly implemented in `@platformatic/control` and they should be updated as form the cast in the following line
16
+ const api = new RuntimeApiClient() as RuntimeApiClient & { startApplicationProfiling: (...args: unknown[]) => Promise<unknown>, stopApplicationProfiling: (...args: unknown[]) => Promise<string> }
17
+
18
+ typedFastify.get('/runtimes', {
19
+ schema: {
20
+ querystring: {
21
+ type: 'object',
22
+ properties: {
23
+ includeAdmin: {
24
+ type: 'boolean',
25
+ default: false,
26
+ },
27
+ },
28
+ },
29
+ response: { 200: { type: 'array', items: selectableRuntimeSchema } }
30
+ }
31
+ }, async (request) => getSelectableRuntimes(await api.getRuntimes(), request.query.includeAdmin))
32
+
33
+ typedFastify.get('/runtimes/:pid/health', {
34
+ schema: {
35
+ params: pidParamSchema,
36
+ response: {
37
+ 200: {
38
+ type: 'object',
39
+ additionalProperties: false,
40
+ properties: {
41
+ status: {
42
+ type: 'string',
43
+ enum: ['OK', 'KO'],
44
+ description: "Status can only be 'OK' or 'KO'"
45
+ }
46
+ },
47
+ required: ['status']
48
+ }
49
+ }
50
+ }
51
+ }, async ({ params: { pid } }) => {
52
+ const ok = { status: 'OK' as const }
53
+ const ko = { status: 'KO' as const }
54
+
55
+ try {
56
+ const result = await api.getMatchingRuntime({ pid: pid.toString() })
57
+ return (result.pid === pid) ? ok : ko
58
+ } catch {
59
+ return ko
60
+ }
61
+ })
62
+
63
+ typedFastify.get('/runtimes/:pid/services', {
64
+ schema: {
65
+ params: pidParamSchema,
66
+ response: {
67
+ 200: {
68
+ type: 'object',
69
+ additionalProperties: false,
70
+ required: ['entrypoint', 'production', 'applications'],
71
+ properties: {
72
+ entrypoint: {
73
+ type: 'string'
74
+ },
75
+ production: {
76
+ type: 'boolean'
77
+ },
78
+ applications: {
79
+ type: 'array',
80
+ items: {
81
+ anyOf: [
82
+ {
83
+ additionalProperties: false,
84
+ type: 'object',
85
+ required: ['id', 'type', 'status', 'version', 'localUrl', 'entrypoint', 'dependencies'],
86
+ properties: {
87
+ id: {
88
+ type: 'string'
89
+ },
90
+ type: {
91
+ type: 'string'
92
+ },
93
+ status: {
94
+ type: 'string'
95
+ },
96
+ version: {
97
+ type: 'string'
98
+ },
99
+ localUrl: {
100
+ type: 'string'
101
+ },
102
+ entrypoint: {
103
+ type: 'boolean'
104
+ },
105
+ workers: {
106
+ type: 'number'
107
+ },
108
+ url: {
109
+ type: 'string'
110
+ },
111
+ dependencies: {
112
+ type: 'array',
113
+ items: { type: 'string' }
114
+ }
115
+ }
116
+ },
117
+ {
118
+ additionalProperties: false,
119
+ type: 'object',
120
+ required: ['id', 'status'],
121
+ properties: {
122
+ id: {
123
+ type: 'string'
124
+ },
125
+ status: {
126
+ type: 'string'
127
+ }
128
+ }
129
+ }
130
+ ]
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }, async (request) => api.getRuntimeApplications(request.params.pid))
138
+
139
+ typedFastify.get('/runtimes/:pid/openapi/:serviceId', {
140
+ schema: {
141
+ params: { type: 'object', properties: { pid: { type: 'number' }, serviceId: { type: 'string' } }, required: ['pid', 'serviceId'] }
142
+ }
143
+ }, async ({ params: { pid, serviceId } }) => api.getRuntimeOpenapi(pid, serviceId))
144
+
145
+ typedFastify.post('/runtimes/:pid/restart', {
146
+ schema: { params: pidParamSchema, body: { type: 'object' } }
147
+ }, async (request) => {
148
+ try {
149
+ await api.restartRuntime(request.params.pid)
150
+ } catch (err) {
151
+ fastify.log.warn({ err }, 'Issue restarting the runtime')
152
+ }
153
+ })
154
+
155
+ typedFastify.post('/record/:pid', {
156
+ schema: {
157
+ params: pidParamSchema,
158
+ body: {
159
+ type: 'object',
160
+ additionalProperties: false,
161
+ properties: { mode: modeSchema, profile: profileSchema },
162
+ required: ['mode', 'profile']
163
+ }
164
+ }
165
+ }, async ({ body: { mode, profile: type }, params: { pid } }) => {
166
+ const from = fastify.loaded.mode
167
+ const to = mode
168
+ if (!checkRecordState({ from, to })) {
169
+ return fastify.log.error({ from, to }, 'Invalid record state machine transition')
170
+ }
171
+
172
+ const { applications } = await api.getRuntimeApplications(pid)
173
+ fastify.loaded.mode = mode
174
+ if (mode === 'start') {
175
+ for (const { id } of applications) {
176
+ await api.startApplicationProfiling(pid, id, { type })
177
+ }
178
+ fastify.loaded.type = type
179
+ fastify.loaded.metrics = {}
180
+ }
181
+
182
+ if (mode === 'stop') {
183
+ try {
184
+ const runtimes = getSelectableRuntimes(await api.getRuntimes(), false)
185
+ const services = await api.getRuntimeApplications(getPidToLoad(runtimes))
186
+
187
+ const profile: Record<string, Uint8Array> = {}
188
+ for (const { id } of applications) {
189
+ const profileData = Buffer.from(await api.stopApplicationProfiling(pid, id, { type }))
190
+ await writeFile(join(__dirname, '..', '..', 'frontend', 'dist', `${fastify.loaded.type}-profile-${id}.pb`), profileData)
191
+ profile[id] = new Uint8Array(profileData)
192
+ }
193
+
194
+ const loadedJson = JSON.stringify({ runtimes, services, metrics: fastify.loaded.metrics[getPidToLoad(runtimes)], profile, type })
195
+
196
+ const scriptToAppend = ` <script>window.LOADED_JSON=${loadedJson}</script>\n</body>`
197
+ const bundlePath = join(__dirname, '..', '..', 'frontend', 'dist', 'index.html')
198
+ await writeFile(bundlePath, (await readFile(bundlePath, 'utf8')).replace('</body>', scriptToAppend), 'utf8')
199
+ } catch (err) {
200
+ fastify.log.error({ err }, 'Unable to save the loaded JSON')
201
+ }
202
+ }
203
+ })
204
+ }
@@ -0,0 +1,45 @@
1
+ import type { WebSocket } from 'ws'
2
+ import split2 from 'split2'
3
+ import type { FastifyInstance } from 'fastify'
4
+ import type { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'
5
+ import { RuntimeApiClient } from '@platformatic/control'
6
+ import { pidParamSchema } from '../schemas/index.ts'
7
+ import type { PidParam } from '../schemas/index.ts'
8
+ import { pipeline } from 'node:stream/promises'
9
+
10
+ export default async function (fastify: FastifyInstance) {
11
+ const typedFastify = fastify.withTypeProvider<JsonSchemaToTsProvider>()
12
+ const api = new RuntimeApiClient()
13
+
14
+ const wsSendAsync = (socket: WebSocket, data: string): Promise<void> => new Promise((resolve, reject) => setTimeout(() => socket.send(data, (err) => (err)
15
+ ? reject(err)
16
+ : resolve()
17
+ ), 100)
18
+ )
19
+
20
+ typedFastify.get<{ Params: PidParam }>('/runtimes/:pid/logs/ws', {
21
+ schema: { params: pidParamSchema },
22
+ websocket: true
23
+ }, async (socket, { params: { pid } }) => {
24
+ try {
25
+ const clientStream = api.getRuntimeLiveLogsStream(pid)
26
+
27
+ socket.on('close', () => {
28
+ clientStream.destroy()
29
+ })
30
+
31
+ await pipeline(
32
+ clientStream,
33
+ split2(),
34
+ async function * (source: AsyncIterable<string>) {
35
+ for await (const line of source) {
36
+ await wsSendAsync(socket, line)
37
+ }
38
+ }
39
+ )
40
+ } catch (error) {
41
+ fastify.log.error({ error }, 'fatal error on runtime logs ws')
42
+ socket.close()
43
+ }
44
+ })
45
+ }
@@ -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
+ }