@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,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')