@platformatic/watt-admin 0.6.0-alpha.1 → 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/README.md +1 -1
- package/cli.d.ts +3 -0
- package/cli.js +2 -2
- package/lib/start.d.ts +3 -0
- package/lib/start.js +6 -1
- package/package.json +19 -17
- 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
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ After installing the project dependencies, you can run the tool directly:
|
|
|
14
14
|
|
|
15
15
|
To run the CLI on a custom port, you can just pass it as an argument with `./cli.js --port 4321`.
|
|
16
16
|
|
|
17
|
-
To record a flamegraph session, and choose to profile either the `cpu` or the `heap`, you can run the CLI with `./cli.js --record --
|
|
17
|
+
To record a flamegraph session, and choose to profile either the `cpu` or the `heap`, you can run the CLI with `./cli.js --record --profile heap`. Once you will stop the process, an HTML one-file bundle will be auto-generated, and you will be able to navigate (even offline) the `watt-admin` app, looking at the flamegraph and at the metrics stored for the whole time you have run the CLI.
|
|
18
18
|
|
|
19
19
|
The tool is also available as a binary when installed globally or linked:
|
|
20
20
|
|
package/cli.d.ts
ADDED
package/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict'
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import esmain from 'es-main'
|
|
6
6
|
import { RuntimeApiClient } from '@platformatic/control'
|
|
7
7
|
import { select } from '@inquirer/prompts'
|
|
8
8
|
import { start } from './lib/start.js'
|
|
@@ -137,7 +137,7 @@ export default async function main () {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// Execute the main function if this script is run directly
|
|
140
|
-
if (import.meta
|
|
140
|
+
if (esmain(import.meta)) {
|
|
141
141
|
main().then((selectedRuntime) => {
|
|
142
142
|
if (!selectedRuntime) {
|
|
143
143
|
return
|
package/lib/start.d.ts
ADDED
package/lib/start.js
CHANGED
|
@@ -52,7 +52,9 @@ export async function start (client, selectedRuntime) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
const configFile = join(__dirname, '..', 'watt.json')
|
|
55
|
-
const server = await create(configFile
|
|
55
|
+
const server = await create(configFile, undefined, {
|
|
56
|
+
setupSignals: false
|
|
57
|
+
})
|
|
56
58
|
entrypointUrl = await server.start()
|
|
57
59
|
|
|
58
60
|
if (record) {
|
|
@@ -64,6 +66,9 @@ export async function start (client, selectedRuntime) {
|
|
|
64
66
|
|
|
65
67
|
// Always clean up the client
|
|
66
68
|
await client.close()
|
|
69
|
+
|
|
70
|
+
// Then stop the server
|
|
71
|
+
await server.close()
|
|
67
72
|
})
|
|
68
73
|
|
|
69
74
|
const { statusCode, body } = await requestRecord('start')
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@platformatic/watt-admin",
|
|
4
|
-
"version": "0.6.0-alpha.
|
|
4
|
+
"version": "0.6.0-alpha.3",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"prepublishOnly": "npm run clean && npm run build",
|
|
7
7
|
"dev": "wattpm dev",
|
|
@@ -28,46 +28,48 @@
|
|
|
28
28
|
"@platformatic/gateway": "^3.11.0",
|
|
29
29
|
"@platformatic/runtime": "^3.11.0",
|
|
30
30
|
"@platformatic/service": "^3.11.0",
|
|
31
|
+
"@platformatic/ui-components": "^0.19.1",
|
|
31
32
|
"@platformatic/vite": "^3.11.0",
|
|
33
|
+
"@platformatic/wattpm-pprof-capture": "^3.13.0",
|
|
32
34
|
"@scalar/api-reference-react": "^0.7.55",
|
|
35
|
+
"@vitejs/plugin-react": "^5.0.4",
|
|
36
|
+
"amaro": "^1.1.4",
|
|
37
|
+
"autoprefixer": "^10.4.21",
|
|
33
38
|
"close-with-grace": "^2.3.0",
|
|
39
|
+
"d3": "^7.9.0",
|
|
40
|
+
"dayjs": "^1.11.18",
|
|
41
|
+
"es-main": "^1.4.0",
|
|
34
42
|
"fastify": "^5.6.1",
|
|
35
43
|
"pprof-format": "^2.2.1",
|
|
44
|
+
"react": "^19.2.0",
|
|
45
|
+
"react-dom": "^19.2.0",
|
|
36
46
|
"react-pprof": "^1.1.0",
|
|
47
|
+
"react-router-dom": "^7.9.3",
|
|
37
48
|
"react-use-websocket": "^4.13.0",
|
|
38
49
|
"split2": "^4.2.0",
|
|
50
|
+
"tailwindcss": "^3.4.18",
|
|
39
51
|
"undici": "^7.16.0",
|
|
40
|
-
"
|
|
52
|
+
"use-error-boundary": "^2.0.6",
|
|
53
|
+
"vite": "^7.1.9",
|
|
54
|
+
"vite-plugin-singlefile": "^2.3.0",
|
|
55
|
+
"wattpm": "^3.11.0",
|
|
56
|
+
"zustand": "^5.0.8"
|
|
41
57
|
},
|
|
42
58
|
"devDependencies": {
|
|
43
59
|
"@fastify/type-provider-json-schema-to-ts": "^5.0.0",
|
|
44
|
-
"@platformatic/ui-components": "^0.19.1",
|
|
45
|
-
"@platformatic/wattpm-pprof-capture": "^3.13.0",
|
|
46
60
|
"@playwright/test": "^1.56.0",
|
|
47
61
|
"@types/d3": "^7.4.3",
|
|
48
62
|
"@types/node": "^22",
|
|
49
63
|
"@types/react-dom": "^19.2.1",
|
|
50
64
|
"@types/split2": "^4.2.3",
|
|
51
65
|
"@types/ws": "^8.18.1",
|
|
52
|
-
"@vitejs/plugin-react": "^5.0.4",
|
|
53
|
-
"autoprefixer": "^10.4.21",
|
|
54
|
-
"d3": "^7.9.0",
|
|
55
|
-
"dayjs": "^1.11.18",
|
|
56
66
|
"eslint": "^9.37.0",
|
|
57
67
|
"fastify-tsconfig": "^3.0.0",
|
|
58
68
|
"massimo-cli": "^1.0.1",
|
|
59
69
|
"neostandard": "^0.12.2",
|
|
60
70
|
"playwright": "^1.56.0",
|
|
61
|
-
"react": "^19.2.0",
|
|
62
|
-
"react-dom": "^19.2.0",
|
|
63
|
-
"react-router-dom": "^7.9.3",
|
|
64
|
-
"tailwindcss": "^3.4.18",
|
|
65
71
|
"typescript": "^5.9.3",
|
|
66
|
-
"
|
|
67
|
-
"vite": "^7.1.9",
|
|
68
|
-
"vite-plugin-singlefile": "^2.3.0",
|
|
69
|
-
"vitest": "^3.2.4",
|
|
70
|
-
"zustand": "^5.0.8"
|
|
72
|
+
"vitest": "^3.2.4"
|
|
71
73
|
},
|
|
72
74
|
"bin": {
|
|
73
75
|
"watt-admin": "./cli.js"
|
package/renovate.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
+
"extends": [
|
|
4
|
+
"config:base"
|
|
5
|
+
],
|
|
6
|
+
"packageRules": [
|
|
7
|
+
{
|
|
8
|
+
"groupName": "Safe automerge",
|
|
9
|
+
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
|
|
10
|
+
"automerge": true
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"lockFileMaintenance": {
|
|
14
|
+
"enabled": true,
|
|
15
|
+
"automerge": true
|
|
16
|
+
}
|
|
17
|
+
}
|
package/watt.json
CHANGED
|
@@ -7,13 +7,21 @@
|
|
|
7
7
|
"logger": {
|
|
8
8
|
"level": "info"
|
|
9
9
|
},
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
"entrypoint": "composer",
|
|
11
|
+
"applications": [
|
|
12
|
+
{
|
|
13
|
+
"id": "backend",
|
|
14
|
+
"path": "./web/backend",
|
|
15
|
+
"useHttp": true,
|
|
16
|
+
"nodeOptions": "--import=amaro/strip"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "composer",
|
|
20
|
+
"path": "./web/composer"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "frontend",
|
|
24
|
+
"path": "./web/frontend"
|
|
17
25
|
}
|
|
18
|
-
|
|
26
|
+
]
|
|
19
27
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
2
|
+
import type { FastifyInstance } from 'fastify'
|
|
3
|
+
import type { PlatformaticApp, PlatformaticServiceConfig } from '@platformatic/service'
|
|
4
|
+
import type { Mode, Profile } from './schemas/index.ts'
|
|
5
|
+
import type { MappedMetrics } from './utils/metrics-helpers.ts'
|
|
6
|
+
|
|
7
|
+
declare module 'fastify' {
|
|
8
|
+
interface FastifyInstance {
|
|
9
|
+
platformatic: PlatformaticApp<PlatformaticServiceConfig>
|
|
10
|
+
metricsInterval: NodeJS.Timeout
|
|
11
|
+
loaded: { mode?: Mode, metrics: MappedMetrics, type?: Profile }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FastifySchema {
|
|
15
|
+
hide?: boolean
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify'
|
|
2
|
+
import { getMetrics } from '../utils/metrics.ts'
|
|
3
|
+
import { MS_WAITING } from '../utils/constants.ts'
|
|
4
|
+
|
|
5
|
+
export default async function (fastify: FastifyInstance) {
|
|
6
|
+
fastify.decorate('loaded', { metrics: {} })
|
|
7
|
+
|
|
8
|
+
fastify.decorate('metricsInterval', setInterval(() => getMetrics(fastify), MS_WAITING))
|
|
9
|
+
|
|
10
|
+
fastify.addHook('onClose', async () => {
|
|
11
|
+
// If the following log is not called, please run the project directly through the `wattpm` binary (ref. https://github.com/platformatic/platformatic/issues/3751)
|
|
12
|
+
fastify.log.info('Closing the backend...')
|
|
13
|
+
clearInterval(fastify.metricsInterval)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify'
|
|
2
|
+
import type { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'
|
|
3
|
+
import { metricResponseSchema, pidParamSchema } from '../schemas/index.ts'
|
|
4
|
+
import type { MetricsResponse } from '../schemas/index.ts'
|
|
5
|
+
|
|
6
|
+
export default async function (fastify: FastifyInstance) {
|
|
7
|
+
const typedFastify = fastify.withTypeProvider<JsonSchemaToTsProvider>()
|
|
8
|
+
const emptyMetrics: MetricsResponse = { dataCpu: [], dataLatency: [], dataMem: [], dataReq: [], dataKafka: [], dataUndici: [], dataWebsocket: [], dataNodejs: [] }
|
|
9
|
+
|
|
10
|
+
typedFastify.get('/runtimes/:pid/metrics', {
|
|
11
|
+
schema: { params: pidParamSchema, response: { 200: metricResponseSchema } },
|
|
12
|
+
}, async ({ params: { pid } }) => fastify.loaded.metrics[pid]?.aggregated || emptyMetrics)
|
|
13
|
+
|
|
14
|
+
typedFastify.get('/runtimes/:pid/metrics/:serviceId', {
|
|
15
|
+
schema: {
|
|
16
|
+
params: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
pid: { type: 'number' },
|
|
20
|
+
serviceId: { type: 'string' }
|
|
21
|
+
},
|
|
22
|
+
required: ['pid', 'serviceId']
|
|
23
|
+
},
|
|
24
|
+
response: { 200: metricResponseSchema }
|
|
25
|
+
}
|
|
26
|
+
}, async ({ params: { pid, serviceId } }) => {
|
|
27
|
+
return fastify.loaded.metrics[pid]?.services[serviceId]?.all || emptyMetrics
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
typedFastify.get('/runtimes/:pid/metrics/:serviceId/:workerId', {
|
|
31
|
+
schema: {
|
|
32
|
+
params: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
pid: { type: 'number' },
|
|
36
|
+
serviceId: { type: 'string' },
|
|
37
|
+
workerId: { type: 'number' },
|
|
38
|
+
},
|
|
39
|
+
required: ['pid', 'serviceId', 'workerId']
|
|
40
|
+
},
|
|
41
|
+
response: { 200: metricResponseSchema }
|
|
42
|
+
}
|
|
43
|
+
}, async ({ params: { pid, serviceId, workerId } }) => {
|
|
44
|
+
return fastify.loaded.metrics[pid]?.services[serviceId]?.[workerId] || emptyMetrics
|
|
45
|
+
})
|
|
46
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'
|
|
2
|
+
import { RuntimeApiClient } from '@platformatic/control'
|
|
3
|
+
import type { FastifyInstance } from 'fastify'
|
|
4
|
+
|
|
5
|
+
export default async function (fastify: FastifyInstance) {
|
|
6
|
+
const typedFastify = fastify.withTypeProvider<JsonSchemaToTsProvider>()
|
|
7
|
+
const api = new RuntimeApiClient()
|
|
8
|
+
|
|
9
|
+
typedFastify.removeContentTypeParser(['application/json', 'text/*'])
|
|
10
|
+
typedFastify.addContentTypeParser('*', { parseAs: 'buffer' }, async (_request: unknown, body: unknown) => body)
|
|
11
|
+
|
|
12
|
+
typedFastify.all('/proxy/:pid/services/:serviceId/*', {
|
|
13
|
+
schema: {
|
|
14
|
+
hide: true, // needed since the client generation fails to properly handle the '*' wildcard
|
|
15
|
+
}
|
|
16
|
+
}, async (request, reply) => {
|
|
17
|
+
const { pid, serviceId, '*': requestUrl } = request.params as { pid: number, serviceId: string, '*': string } // cast needed because we can't define a valid json schema with the '*' wildcard
|
|
18
|
+
|
|
19
|
+
delete request.headers.connection
|
|
20
|
+
delete request.headers['content-length']
|
|
21
|
+
delete request.headers['content-encoding']
|
|
22
|
+
delete request.headers['transfer-encoding']
|
|
23
|
+
|
|
24
|
+
const injectParams = {
|
|
25
|
+
method: request.method,
|
|
26
|
+
url: '/' + requestUrl,
|
|
27
|
+
headers: request.headers,
|
|
28
|
+
query: request.query,
|
|
29
|
+
body: request.body
|
|
30
|
+
} as Parameters<typeof api.injectRuntime>[2]
|
|
31
|
+
|
|
32
|
+
fastify.log.info({ pid, serviceId, injectParams }, 'runtime request proxy')
|
|
33
|
+
|
|
34
|
+
const res = await api.injectRuntime(pid, serviceId, injectParams)
|
|
35
|
+
|
|
36
|
+
delete res.headers['content-length']
|
|
37
|
+
delete res.headers['transfer-encoding']
|
|
38
|
+
|
|
39
|
+
return reply.code(res.statusCode).headers(res.headers).send(res.body)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
@@ -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
|
+
}
|