@platformatic/watt-admin 0.6.0-alpha.2 → 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.
- package/cli.d.ts +3 -0
- package/lib/start.d.ts +3 -0
- package/lib/start.js +6 -1
- package/package.json +2 -1
- package/renovate.json +17 -0
- package/watt.json +16 -8
- package/web/backend/global.d.ts +17 -0
- package/web/backend/plugins/metrics.ts +15 -0
- package/web/backend/plugins/websocket.ts +6 -0
- package/web/backend/routes/metrics.ts +46 -0
- package/web/backend/routes/proxy.ts +41 -0
- package/web/backend/routes/root.ts +204 -0
- package/web/backend/routes/ws.ts +45 -0
- package/web/backend/schemas/index.ts +226 -0
- package/web/backend/utils/bytes.ts +1 -0
- package/web/backend/utils/client.openapi.ts +29 -0
- package/web/backend/utils/constants.ts +4 -0
- package/web/backend/utils/metrics-helpers.ts +89 -0
- package/web/backend/utils/metrics.ts +202 -0
- package/web/backend/utils/rps.ts +3 -0
- package/web/backend/utils/runtimes.ts +21 -0
- package/web/backend/utils/states.ts +3 -0
- package/web/frontend/dist/index.html +470 -466
- package/web/frontend/index.d.ts +4 -0
- package/web/frontend/playwright.config.ts +27 -0
- package/web/frontend/postcss.config.ts +14 -0
- package/CLAUDE.md +0 -32
- package/tsconfig.json +0 -22
- package/web/backend/tsconfig.json +0 -24
- package/web/frontend/tsconfig.json +0 -30
|
@@ -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,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,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
|