@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,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,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
|
+
}
|