@platformatic/runtime 3.4.1 → 3.5.1
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/config.d.ts +224 -77
- package/eslint.config.js +3 -5
- package/index.d.ts +73 -24
- package/index.js +173 -29
- package/lib/config.js +279 -197
- package/lib/errors.js +126 -34
- package/lib/generator.js +640 -0
- package/lib/logger.js +43 -41
- package/lib/management-api.js +109 -118
- package/lib/prom-server.js +202 -16
- package/lib/runtime.js +1963 -585
- package/lib/scheduler.js +119 -0
- package/lib/schema.js +22 -234
- package/lib/shared-http-cache.js +43 -0
- package/lib/upgrade.js +6 -8
- package/lib/utils.js +6 -61
- package/lib/version.js +7 -0
- package/lib/versions/v1.36.0.js +2 -4
- package/lib/versions/v1.5.0.js +2 -4
- package/lib/versions/v2.0.0.js +3 -5
- package/lib/versions/v3.0.0.js +16 -0
- package/lib/worker/controller.js +302 -0
- package/lib/worker/http-cache.js +171 -0
- package/lib/worker/interceptors.js +190 -10
- package/lib/worker/itc.js +146 -59
- package/lib/worker/main.js +220 -81
- package/lib/worker/messaging.js +182 -0
- package/lib/worker/round-robin-map.js +62 -0
- package/lib/worker/shared-context.js +22 -0
- package/lib/worker/symbols.js +14 -5
- package/package.json +47 -38
- package/schema.json +1383 -55
- package/help/compile.txt +0 -8
- package/help/help.txt +0 -5
- package/help/start.txt +0 -21
- package/index.test-d.ts +0 -41
- package/lib/build-server.js +0 -69
- package/lib/compile.js +0 -98
- package/lib/dependencies.js +0 -59
- package/lib/generator/README.md +0 -32
- package/lib/generator/errors.js +0 -10
- package/lib/generator/runtime-generator.d.ts +0 -37
- package/lib/generator/runtime-generator.js +0 -498
- package/lib/start.js +0 -190
- package/lib/worker/app.js +0 -278
- package/lib/worker/default-stackable.js +0 -33
- package/lib/worker/metrics.js +0 -122
- package/runtime.mjs +0 -54
package/lib/logger.js
CHANGED
|
@@ -1,55 +1,57 @@
|
|
|
1
|
-
|
|
1
|
+
import { buildPinoFormatters, buildPinoTimestamp } from '@platformatic/foundation'
|
|
2
|
+
import { isatty } from 'node:tty'
|
|
3
|
+
import pino from 'pino'
|
|
4
|
+
import pretty from 'pino-pretty'
|
|
5
|
+
|
|
6
|
+
export { abstractLogger } from '@platformatic/foundation'
|
|
7
|
+
|
|
8
|
+
const customPrettifiers = {
|
|
9
|
+
name (name, _, obj) {
|
|
10
|
+
if (typeof obj.worker !== 'undefined') {
|
|
11
|
+
name += ':' + obj.worker
|
|
12
|
+
obj.worker = undefined // Do not show the worker in a separate line
|
|
13
|
+
}
|
|
2
14
|
|
|
3
|
-
|
|
4
|
-
|
|
15
|
+
return name
|
|
16
|
+
}
|
|
17
|
+
}
|
|
5
18
|
|
|
6
|
-
|
|
7
|
-
|
|
19
|
+
// Create the runtime logger
|
|
20
|
+
export async function createLogger (config) {
|
|
21
|
+
const loggerConfig = { ...config.logger, transport: undefined }
|
|
22
|
+
if (config.logger.base === null) {
|
|
23
|
+
loggerConfig.base = undefined
|
|
24
|
+
}
|
|
8
25
|
|
|
9
|
-
|
|
10
|
-
const loggerConfig = { ...config.logger }
|
|
11
|
-
const cliStream = isatty(1) ? pretty() : pino.destination(1)
|
|
26
|
+
let cliStream
|
|
12
27
|
|
|
13
|
-
if (
|
|
14
|
-
|
|
28
|
+
if (config.logger.transport) {
|
|
29
|
+
cliStream = pino.transport(config.logger.transport)
|
|
30
|
+
} else {
|
|
31
|
+
cliStream = isatty(1) ? pretty({ customPrettifiers }) : pino.destination(1)
|
|
15
32
|
}
|
|
16
33
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
if (loggerConfig.formatters) {
|
|
35
|
+
loggerConfig.formatters = buildPinoFormatters(loggerConfig.formatters)
|
|
36
|
+
}
|
|
20
37
|
|
|
21
|
-
if (loggerConfig.
|
|
22
|
-
|
|
23
|
-
multiStream.add({ level: loggerConfig.level || 'info', stream: transport })
|
|
38
|
+
if (loggerConfig.timestamp) {
|
|
39
|
+
loggerConfig.timestamp = buildPinoTimestamp(loggerConfig.timestamp)
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
if (config.managementApi) {
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
if (!config.managementApi) {
|
|
43
|
+
return [pino(loggerConfig, cliStream), cliStream]
|
|
44
|
+
}
|
|
29
45
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
const multiStream = pino.multistream([{ stream: cliStream, level: loggerConfig.level }])
|
|
47
|
+
|
|
48
|
+
const logsFileMb = 5
|
|
49
|
+
const logsLimitMb = config.managementApi?.logs?.maxSize || 200
|
|
34
50
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
file: join(runtimeLogsDir, 'logs'),
|
|
39
|
-
mode: 0o600,
|
|
40
|
-
size: logsFileMb + 'm',
|
|
41
|
-
mkdir: true,
|
|
42
|
-
fsync: true,
|
|
43
|
-
limit: {
|
|
44
|
-
count: logsLimitCount,
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
multiStream.add({ level: 'trace', stream: pinoRoll })
|
|
51
|
+
let logsLimitCount = Math.ceil(logsLimitMb / logsFileMb) - 1
|
|
52
|
+
if (logsLimitCount < 1) {
|
|
53
|
+
logsLimitCount = 1
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
return [pino(
|
|
56
|
+
return [pino(loggerConfig, multiStream), multiStream]
|
|
53
57
|
}
|
|
54
|
-
|
|
55
|
-
module.exports = { createLogger }
|
package/lib/management-api.js
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const errors = require('./errors')
|
|
11
|
-
const { getRuntimeLogsDir } = require('./utils')
|
|
1
|
+
import fastifyAccepts from '@fastify/accepts'
|
|
2
|
+
import fastifyWebsocket from '@fastify/websocket'
|
|
3
|
+
import { createDirectory, safeRemove } from '@platformatic/foundation'
|
|
4
|
+
import fastify from 'fastify'
|
|
5
|
+
import { platform, tmpdir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
8
|
+
import { createWebSocketStream } from 'ws'
|
|
12
9
|
|
|
13
10
|
const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
|
|
14
11
|
|
|
15
|
-
async function managementApiPlugin (app, opts) {
|
|
16
|
-
app.
|
|
17
|
-
'Runtime Management API is in the experimental stage. ' +
|
|
18
|
-
'The feature is not subject to semantic versioning rules. ' +
|
|
19
|
-
'Non-backward compatible changes or removal may occur in any future release. ' +
|
|
20
|
-
'Use of the feature is not recommended in production environments.'
|
|
21
|
-
)
|
|
12
|
+
export async function managementApiPlugin (app, opts) {
|
|
13
|
+
app.register(fastifyAccepts)
|
|
22
14
|
|
|
23
15
|
const runtime = opts.runtime
|
|
24
16
|
|
|
17
|
+
app.get('/status', async () => {
|
|
18
|
+
const status = runtime.getRuntimeStatus()
|
|
19
|
+
return { status }
|
|
20
|
+
})
|
|
21
|
+
|
|
25
22
|
app.get('/metadata', async () => {
|
|
26
23
|
return runtime.getRuntimeMetadata()
|
|
27
24
|
})
|
|
@@ -35,62 +32,62 @@ async function managementApiPlugin (app, opts) {
|
|
|
35
32
|
})
|
|
36
33
|
|
|
37
34
|
app.post('/stop', async () => {
|
|
38
|
-
app.log.debug('stop
|
|
39
|
-
await runtime.close(
|
|
35
|
+
app.log.debug('stop applications')
|
|
36
|
+
await runtime.close()
|
|
40
37
|
})
|
|
41
38
|
|
|
42
39
|
app.post('/restart', async () => {
|
|
43
|
-
app.log.debug('restart
|
|
40
|
+
app.log.debug('restart applications')
|
|
44
41
|
await runtime.restart()
|
|
45
42
|
})
|
|
46
43
|
|
|
47
|
-
app.get('/
|
|
48
|
-
return runtime.
|
|
44
|
+
app.get('/applications', async () => {
|
|
45
|
+
return runtime.getApplications()
|
|
49
46
|
})
|
|
50
47
|
|
|
51
|
-
app.get('/
|
|
48
|
+
app.get('/applications/:id', async request => {
|
|
52
49
|
const { id } = request.params
|
|
53
|
-
app.log.debug('get
|
|
54
|
-
return runtime.
|
|
50
|
+
app.log.debug('get application details', { id })
|
|
51
|
+
return runtime.getApplicationDetails(id)
|
|
55
52
|
})
|
|
56
53
|
|
|
57
|
-
app.get('/
|
|
54
|
+
app.get('/applications/:id/config', async request => {
|
|
58
55
|
const { id } = request.params
|
|
59
|
-
app.log.debug('get
|
|
60
|
-
return runtime.
|
|
56
|
+
app.log.debug('get application config', { id })
|
|
57
|
+
return runtime.getApplicationConfig(id)
|
|
61
58
|
})
|
|
62
59
|
|
|
63
|
-
app.get('/
|
|
60
|
+
app.get('/applications/:id/env', async request => {
|
|
64
61
|
const { id } = request.params
|
|
65
|
-
app.log.debug('get
|
|
66
|
-
return runtime.
|
|
62
|
+
app.log.debug('get application config', { id })
|
|
63
|
+
return runtime.getApplicationEnv(id)
|
|
67
64
|
})
|
|
68
65
|
|
|
69
|
-
app.get('/
|
|
66
|
+
app.get('/applications/:id/openapi-schema', async request => {
|
|
70
67
|
const { id } = request.params
|
|
71
68
|
app.log.debug('get openapi-schema', { id })
|
|
72
|
-
return runtime.
|
|
69
|
+
return runtime.getApplicationOpenapiSchema(id)
|
|
73
70
|
})
|
|
74
71
|
|
|
75
|
-
app.get('/
|
|
72
|
+
app.get('/applications/:id/graphql-schema', async request => {
|
|
76
73
|
const { id } = request.params
|
|
77
74
|
app.log.debug('get graphql-schema', { id })
|
|
78
|
-
return runtime.
|
|
75
|
+
return runtime.getApplicationGraphqlSchema(id)
|
|
79
76
|
})
|
|
80
77
|
|
|
81
|
-
app.post('/
|
|
78
|
+
app.post('/applications/:id/start', async request => {
|
|
82
79
|
const { id } = request.params
|
|
83
|
-
app.log.debug('start
|
|
84
|
-
await runtime.
|
|
80
|
+
app.log.debug('start application', { id })
|
|
81
|
+
await runtime.startApplication(id)
|
|
85
82
|
})
|
|
86
83
|
|
|
87
|
-
app.post('/
|
|
84
|
+
app.post('/applications/:id/stop', async request => {
|
|
88
85
|
const { id } = request.params
|
|
89
|
-
app.log.debug('stop
|
|
90
|
-
await runtime.
|
|
86
|
+
app.log.debug('stop application', { id })
|
|
87
|
+
await runtime.stopApplication(id)
|
|
91
88
|
})
|
|
92
89
|
|
|
93
|
-
app.all('/
|
|
90
|
+
app.all('/applications/:id/proxy/*', async (request, reply) => {
|
|
94
91
|
const { id, '*': requestUrl } = request.params
|
|
95
92
|
app.log.debug('proxy request', { id, requestUrl })
|
|
96
93
|
|
|
@@ -115,6 +112,36 @@ async function managementApiPlugin (app, opts) {
|
|
|
115
112
|
reply.code(res.statusCode).headers(res.headers).send(res.body)
|
|
116
113
|
})
|
|
117
114
|
|
|
115
|
+
app.post('/applications/:id/pprof/start', async (request, reply) => {
|
|
116
|
+
const { id } = request.params
|
|
117
|
+
app.log.debug('start profiling', { id })
|
|
118
|
+
|
|
119
|
+
const options = request.body || {}
|
|
120
|
+
await runtime.startApplicationProfiling(id, options)
|
|
121
|
+
reply.code(200).send({})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
app.post('/applications/:id/pprof/stop', async (request, reply) => {
|
|
125
|
+
const { id } = request.params
|
|
126
|
+
app.log.debug('stop profiling', { id })
|
|
127
|
+
|
|
128
|
+
const profileData = await runtime.stopApplicationProfiling(id)
|
|
129
|
+
reply.type('application/octet-stream').code(200).send(profileData)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
app.get('/metrics', { logLevel: 'debug' }, async (req, reply) => {
|
|
133
|
+
const accepts = req.accepts()
|
|
134
|
+
|
|
135
|
+
if (!accepts.type('text/plain') && accepts.type('application/json')) {
|
|
136
|
+
const { metrics } = await runtime.getMetrics('json')
|
|
137
|
+
return metrics
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
reply.type('text/plain')
|
|
141
|
+
const { metrics } = await runtime.getMetrics('text')
|
|
142
|
+
return metrics
|
|
143
|
+
})
|
|
144
|
+
|
|
118
145
|
app.get('/metrics/live', { websocket: true }, async socket => {
|
|
119
146
|
const cachedMetrics = runtime.getCachedMetrics()
|
|
120
147
|
if (cachedMetrics.length > 0) {
|
|
@@ -139,93 +166,57 @@ async function managementApiPlugin (app, opts) {
|
|
|
139
166
|
})
|
|
140
167
|
|
|
141
168
|
app.get('/logs/live', { websocket: true }, async (socket, req) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (startLogId) {
|
|
145
|
-
const logIds = await runtime.getLogIds()
|
|
146
|
-
if (!logIds.includes(startLogId)) {
|
|
147
|
-
throw new errors.LogFileNotFound(startLogId)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const stream = ws.createWebSocketStream(socket)
|
|
152
|
-
runtime.pipeLogsStream(stream, req.log, startLogId)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
app.get('/logs/indexes', async req => {
|
|
156
|
-
const returnAllIds = req.query.all === 'true'
|
|
157
|
-
|
|
158
|
-
if (returnAllIds) {
|
|
159
|
-
const runtimesLogsIds = await runtime.getAllLogIds()
|
|
160
|
-
return runtimesLogsIds
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const runtimeLogsIds = await runtime.getLogIds()
|
|
164
|
-
return { indexes: runtimeLogsIds }
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
app.get('/logs/all', async (req, reply) => {
|
|
168
|
-
const runtimePID = parseInt(req.query.pid) || process.pid
|
|
169
|
-
|
|
170
|
-
const logsIds = await runtime.getLogIds(runtimePID)
|
|
171
|
-
const startLogId = logsIds.at(0)
|
|
172
|
-
const endLogId = logsIds.at(-1)
|
|
173
|
-
|
|
174
|
-
reply.hijack()
|
|
175
|
-
|
|
176
|
-
runtime.pipeLogsStream(reply.raw, req.log, startLogId, endLogId, runtimePID)
|
|
169
|
+
runtime.addLoggerDestination(createWebSocketStream(socket))
|
|
177
170
|
})
|
|
171
|
+
}
|
|
178
172
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const runtimePID = parseInt(req.query.pid) || process.pid
|
|
173
|
+
export async function startManagementApi (runtime) {
|
|
174
|
+
const runtimePID = process.pid
|
|
182
175
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
176
|
+
const runtimePIDDir = join(PLATFORMATIC_TMP_DIR, runtimePID.toString())
|
|
177
|
+
if (platform() !== 'win32') {
|
|
178
|
+
await createDirectory(runtimePIDDir, true)
|
|
179
|
+
}
|
|
187
180
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
181
|
+
let socketPath = null
|
|
182
|
+
if (platform() === 'win32') {
|
|
183
|
+
socketPath = '\\\\.\\pipe\\platformatic-' + runtimePID.toString()
|
|
184
|
+
} else {
|
|
185
|
+
socketPath = join(runtimePIDDir, 'socket')
|
|
186
|
+
}
|
|
192
187
|
|
|
193
|
-
|
|
194
|
-
|
|
188
|
+
const managementApi = fastify()
|
|
189
|
+
managementApi.register(fastifyWebsocket)
|
|
190
|
+
managementApi.register(managementApiPlugin, { runtime, prefix: '/api/v1' })
|
|
195
191
|
|
|
196
|
-
|
|
197
|
-
const runtimePIDDir = join(PLATFORMATIC_TMP_DIR, runtimePID.toString())
|
|
192
|
+
managementApi.addHook('onClose', async () => {
|
|
198
193
|
if (platform() !== 'win32') {
|
|
199
|
-
await
|
|
194
|
+
await safeRemove(runtimePIDDir)
|
|
200
195
|
}
|
|
196
|
+
})
|
|
201
197
|
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
// When the runtime closes, close the management API as well
|
|
199
|
+
runtime.on('closed', managementApi.close.bind(managementApi))
|
|
204
200
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
socketPath = join(runtimePIDDir, 'socket')
|
|
210
|
-
}
|
|
201
|
+
/*
|
|
202
|
+
If runtime are started multiple times in a short
|
|
203
|
+
period of time (like in tests), there is a chance that the pipe is still in use
|
|
204
|
+
as the manament API server is closed after the runtime is closed (see event handler above).
|
|
211
205
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
206
|
+
Since it's a very rare case, we simply retry couple of times.
|
|
207
|
+
*/
|
|
208
|
+
for (let i = 0; i < 5; i++) {
|
|
209
|
+
try {
|
|
210
|
+
await managementApi.listen({ path: socketPath })
|
|
211
|
+
break
|
|
212
|
+
} catch (e) {
|
|
213
|
+
if (i === 5) {
|
|
214
|
+
throw e
|
|
219
215
|
}
|
|
220
|
-
})
|
|
221
216
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
/* c8 ignore next 4 */
|
|
225
|
-
} catch (err) {
|
|
226
|
-
console.error(err)
|
|
227
|
-
process.exit(1)
|
|
217
|
+
await sleep(100)
|
|
218
|
+
}
|
|
228
219
|
}
|
|
229
|
-
}
|
|
230
220
|
|
|
231
|
-
|
|
221
|
+
return managementApi
|
|
222
|
+
}
|
package/lib/prom-server.js
CHANGED
|
@@ -1,46 +1,232 @@
|
|
|
1
|
-
|
|
1
|
+
import fastifyAccepts from '@fastify/accepts'
|
|
2
|
+
import fastifyBasicAuth from '@fastify/basic-auth'
|
|
3
|
+
import { loadModule } from '@platformatic/foundation'
|
|
4
|
+
import fastify from 'fastify'
|
|
5
|
+
import { createRequire } from 'node:module'
|
|
6
|
+
import { resolve } from 'node:path'
|
|
2
7
|
|
|
3
|
-
const
|
|
8
|
+
const DEFAULT_HOSTNAME = '0.0.0.0'
|
|
9
|
+
const DEFAULT_PORT = 9090
|
|
10
|
+
const DEFAULT_METRICS_ENDPOINT = '/metrics'
|
|
11
|
+
const DEFAULT_READINESS_ENDPOINT = '/ready'
|
|
12
|
+
const DEFAULT_READINESS_SUCCESS_STATUS_CODE = 200
|
|
13
|
+
const DEFAULT_READINESS_SUCCESS_BODY = 'OK'
|
|
14
|
+
const DEFAULT_READINESS_FAIL_STATUS_CODE = 500
|
|
15
|
+
const DEFAULT_READINESS_FAIL_BODY = 'ERR'
|
|
16
|
+
const DEFAULT_LIVENESS_ENDPOINT = '/status'
|
|
17
|
+
const DEFAULT_LIVENESS_SUCCESS_STATUS_CODE = 200
|
|
18
|
+
const DEFAULT_LIVENESS_SUCCESS_BODY = 'OK'
|
|
19
|
+
const DEFAULT_LIVENESS_FAIL_STATUS_CODE = 500
|
|
20
|
+
const DEFAULT_LIVENESS_FAIL_BODY = 'ERR'
|
|
4
21
|
|
|
5
|
-
async function
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
22
|
+
async function checkReadiness (runtime) {
|
|
23
|
+
const workers = await runtime.getWorkers()
|
|
24
|
+
|
|
25
|
+
// Make sure there is at least one started worker
|
|
26
|
+
const applications = new Set()
|
|
27
|
+
const started = new Set()
|
|
28
|
+
for (const worker of Object.values(workers)) {
|
|
29
|
+
applications.add(worker.application)
|
|
30
|
+
|
|
31
|
+
if (worker.status === 'started') {
|
|
32
|
+
started.add(worker.application)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (started.size !== applications.size) {
|
|
37
|
+
return { status: false }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// perform custom readiness checks, get custom response content if any
|
|
41
|
+
const checks = await runtime.getCustomReadinessChecks()
|
|
42
|
+
|
|
43
|
+
let response
|
|
44
|
+
const status = Object.values(checks).every(check => {
|
|
45
|
+
if (typeof check === 'boolean') {
|
|
46
|
+
return check
|
|
47
|
+
} else if (typeof check === 'object') {
|
|
48
|
+
response = check
|
|
49
|
+
return check.status
|
|
50
|
+
}
|
|
51
|
+
return false
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return { response, status }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function checkLiveness (runtime) {
|
|
58
|
+
const { status: ready, response: readinessResponse } = await checkReadiness(runtime)
|
|
59
|
+
if (!ready) {
|
|
60
|
+
return { status: false, readiness: readinessResponse }
|
|
61
|
+
}
|
|
62
|
+
// TODO test, doc
|
|
63
|
+
// in case of readiness check failure, if custom readiness response is set, we return the readiness check response on health check endpoint
|
|
64
|
+
|
|
65
|
+
const checks = await runtime.getCustomHealthChecks()
|
|
66
|
+
|
|
67
|
+
let response
|
|
68
|
+
const status = Object.values(checks).every(check => {
|
|
69
|
+
if (typeof check === 'boolean') {
|
|
70
|
+
return check
|
|
71
|
+
} else if (typeof check === 'object') {
|
|
72
|
+
response = check
|
|
73
|
+
return check.status
|
|
74
|
+
}
|
|
75
|
+
return false
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return { response, status }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function startPrometheusServer (runtime, opts) {
|
|
82
|
+
if (opts.enabled === false) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
const host = opts.hostname ?? DEFAULT_HOSTNAME
|
|
86
|
+
const port = opts.port ?? DEFAULT_PORT
|
|
87
|
+
const metricsEndpoint = opts.endpoint ?? DEFAULT_METRICS_ENDPOINT
|
|
9
88
|
const auth = opts.auth ?? null
|
|
10
89
|
|
|
11
90
|
const promServer = fastify({ name: 'Prometheus server' })
|
|
91
|
+
promServer.register(fastifyAccepts)
|
|
12
92
|
|
|
13
93
|
let onRequestHook
|
|
14
94
|
if (auth) {
|
|
15
95
|
const { username, password } = auth
|
|
16
96
|
|
|
17
|
-
await promServer.register(
|
|
97
|
+
await promServer.register(fastifyBasicAuth, {
|
|
18
98
|
validate: function (user, pass, req, reply, done) {
|
|
19
99
|
if (username !== user || password !== pass) {
|
|
20
100
|
return reply.code(401).send({ message: 'Unauthorized' })
|
|
21
101
|
}
|
|
22
102
|
return done()
|
|
23
|
-
}
|
|
103
|
+
}
|
|
24
104
|
})
|
|
25
105
|
onRequestHook = promServer.basicAuth
|
|
26
106
|
}
|
|
27
107
|
|
|
108
|
+
const readinessEndpoint = opts.readiness?.endpoint ?? DEFAULT_READINESS_ENDPOINT
|
|
109
|
+
const livenessEndpoint = opts.liveness?.endpoint ?? DEFAULT_LIVENESS_ENDPOINT
|
|
110
|
+
|
|
111
|
+
promServer.route({
|
|
112
|
+
url: '/',
|
|
113
|
+
method: 'GET',
|
|
114
|
+
logLevel: 'warn',
|
|
115
|
+
handler (req, reply) {
|
|
116
|
+
reply.type('text/plain')
|
|
117
|
+
let response = `Hello from Platformatic Prometheus Server!\nThe metrics are available at ${metricsEndpoint}.`
|
|
118
|
+
|
|
119
|
+
if (opts.readiness !== false) {
|
|
120
|
+
response += `\nThe readiness endpoint is available at ${readinessEndpoint}.`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (opts.liveness !== false) {
|
|
124
|
+
response += `\nThe liveness endpoint is available at ${livenessEndpoint}.`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return response
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
28
131
|
promServer.route({
|
|
29
132
|
url: metricsEndpoint,
|
|
30
133
|
method: 'GET',
|
|
31
134
|
logLevel: 'warn',
|
|
32
135
|
onRequest: onRequestHook,
|
|
33
136
|
handler: async (req, reply) => {
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
137
|
+
const accepts = req.accepts()
|
|
138
|
+
const reqType = !accepts.type('text/plain') && accepts.type('application/json') ? 'json' : 'text'
|
|
139
|
+
if (reqType === 'text') {
|
|
140
|
+
reply.type('text/plain')
|
|
141
|
+
}
|
|
142
|
+
return (await runtime.getMetrics(reqType)).metrics
|
|
143
|
+
}
|
|
38
144
|
})
|
|
39
145
|
|
|
146
|
+
if (opts.readiness !== false) {
|
|
147
|
+
const successStatusCode = opts.readiness?.success?.statusCode ?? DEFAULT_READINESS_SUCCESS_STATUS_CODE
|
|
148
|
+
const successBody = opts.readiness?.success?.body ?? DEFAULT_READINESS_SUCCESS_BODY
|
|
149
|
+
const failStatusCode = opts.readiness?.fail?.statusCode ?? DEFAULT_READINESS_FAIL_STATUS_CODE
|
|
150
|
+
const failBody = opts.readiness?.fail?.body ?? DEFAULT_READINESS_FAIL_BODY
|
|
151
|
+
|
|
152
|
+
promServer.route({
|
|
153
|
+
url: readinessEndpoint,
|
|
154
|
+
method: 'GET',
|
|
155
|
+
logLevel: 'warn',
|
|
156
|
+
handler: async (req, reply) => {
|
|
157
|
+
reply.type('text/plain')
|
|
158
|
+
|
|
159
|
+
const { status, response } = await checkReadiness(runtime)
|
|
160
|
+
|
|
161
|
+
if (typeof response === 'boolean') {
|
|
162
|
+
if (status) {
|
|
163
|
+
reply.status(successStatusCode).send(successBody)
|
|
164
|
+
} else {
|
|
165
|
+
reply.status(failStatusCode).send(failBody)
|
|
166
|
+
}
|
|
167
|
+
} else if (typeof response === 'object') {
|
|
168
|
+
const { status, body, statusCode } = response
|
|
169
|
+
if (status) {
|
|
170
|
+
reply.status(statusCode || successStatusCode).send(body || successBody)
|
|
171
|
+
} else {
|
|
172
|
+
reply.status(statusCode || failStatusCode).send(body || failBody)
|
|
173
|
+
}
|
|
174
|
+
} else if (!response) {
|
|
175
|
+
if (status) {
|
|
176
|
+
reply.status(successStatusCode).send(successBody)
|
|
177
|
+
} else {
|
|
178
|
+
reply.status(failStatusCode).send(failBody)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (opts.liveness !== false) {
|
|
186
|
+
const successStatusCode = opts.liveness?.success?.statusCode ?? DEFAULT_LIVENESS_SUCCESS_STATUS_CODE
|
|
187
|
+
const successBody = opts.liveness?.success?.body ?? DEFAULT_LIVENESS_SUCCESS_BODY
|
|
188
|
+
const failStatusCode = opts.liveness?.fail?.statusCode ?? DEFAULT_LIVENESS_FAIL_STATUS_CODE
|
|
189
|
+
const failBody = opts.liveness?.fail?.body ?? DEFAULT_LIVENESS_FAIL_BODY
|
|
190
|
+
|
|
191
|
+
promServer.route({
|
|
192
|
+
url: livenessEndpoint,
|
|
193
|
+
method: 'GET',
|
|
194
|
+
logLevel: 'warn',
|
|
195
|
+
handler: async (req, reply) => {
|
|
196
|
+
reply.type('text/plain')
|
|
197
|
+
|
|
198
|
+
const { status, response, readiness } = await checkLiveness(runtime)
|
|
199
|
+
|
|
200
|
+
if (typeof response === 'boolean') {
|
|
201
|
+
if (status) {
|
|
202
|
+
reply.status(successStatusCode).send(successBody)
|
|
203
|
+
} else {
|
|
204
|
+
reply.status(failStatusCode).send(readiness?.body || failBody)
|
|
205
|
+
}
|
|
206
|
+
} else if (typeof response === 'object') {
|
|
207
|
+
const { status, body, statusCode } = response
|
|
208
|
+
if (status) {
|
|
209
|
+
reply.status(statusCode || successStatusCode).send(body || successBody)
|
|
210
|
+
} else {
|
|
211
|
+
reply.status(statusCode || failStatusCode).send(body || readiness?.body || failBody)
|
|
212
|
+
}
|
|
213
|
+
} else if (!response) {
|
|
214
|
+
if (status) {
|
|
215
|
+
reply.status(successStatusCode).send(successBody)
|
|
216
|
+
} else {
|
|
217
|
+
reply.status(failStatusCode).send(readiness?.body || failBody)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const require = createRequire(resolve(import.meta.filename))
|
|
225
|
+
for (const pluginPath of opts.plugins ?? []) {
|
|
226
|
+
const plugin = await loadModule(require, pluginPath)
|
|
227
|
+
await promServer.register(plugin)
|
|
228
|
+
}
|
|
229
|
+
|
|
40
230
|
await promServer.listen({ port, host })
|
|
41
231
|
return promServer
|
|
42
232
|
}
|
|
43
|
-
|
|
44
|
-
module.exports = {
|
|
45
|
-
startPrometheusServer,
|
|
46
|
-
}
|