@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.
- package/README.md +162 -34
- package/cli.d.ts +3 -0
- package/cli.js +2 -2
- package/lib/start.d.ts +3 -0
- package/lib/start.js +22 -7
- package/package.json +30 -28
- 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 +645 -610
- 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,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
|