mirror of https://github.com/requarks/wiki
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
64 lines
2.8 KiB
64 lines
2.8 KiB
import type { FastifyReply, FastifyRequest } from 'fastify'
|
|
|
|
import { METRICS_PERMISSION } from '../models/metrics.ts'
|
|
|
|
/**
|
|
* The Prometheus metrics endpoint.
|
|
*
|
|
* A hook rather than a route, because the path is a setting and a route table is fixed at boot. It
|
|
* is registered on the root instance in `index.ts` and does nothing at all unless metrics are turned
|
|
* on and the path matches — which is what lets a page live at `/metrics` while the endpoint is off,
|
|
* and is why the check has to come BEFORE the SEO hook: that one would otherwise redirect the scrape
|
|
* to the site's locale prefix, or strip a page extension off it.
|
|
*
|
|
* Authentication is either-or. An address in a class the operator opened scrapes anonymously; every
|
|
* other address has to carry `read:metrics`, as a bearer API key (verified by the hook above this
|
|
* one, which lets the metrics path through for exactly this reason) or as the session cookie of a
|
|
* logged-in browser. `manage:system` bypasses it, as it does everywhere.
|
|
*/
|
|
export async function metricsHook(req: FastifyRequest, reply: FastifyReply) {
|
|
// -> This runs ahead of every request the wiki serves, so the disabled case — which is most wikis,
|
|
// always — costs one property read and nothing else
|
|
if (!WIKI.models.metrics.isEnabled()) {
|
|
return
|
|
}
|
|
if (!WIKI.models.metrics.matches(req.raw.url!.split('?')[0]!)) {
|
|
return
|
|
}
|
|
|
|
// -> A scrape reads; nothing here answers a POST, and saying so is more useful than a 404 at a
|
|
// path that plainly exists
|
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
return reply.methodNotAllowed()
|
|
}
|
|
|
|
if (!WIKI.models.metrics.allowsAnonymous(req.ip)) {
|
|
const permissions = req.apiKey
|
|
? req.apiKey.permissions
|
|
: req.session?.authenticated
|
|
? req.session.permissions
|
|
: null
|
|
const isAllowed =
|
|
permissions?.includes(METRICS_PERMISSION) || permissions?.includes('manage:system')
|
|
if (!isAllowed) {
|
|
/*
|
|
401 rather than 403 even for a caller who is authenticated but unentitled: the answer a
|
|
scraper needs is "send a credential", and the two cases are not worth distinguishing to
|
|
somebody the endpoint is not open to anyway.
|
|
*/
|
|
return reply
|
|
.header('WWW-Authenticate', 'Bearer realm="metrics"')
|
|
.unauthorized('This endpoint requires the read:metrics permission.')
|
|
}
|
|
}
|
|
|
|
try {
|
|
const { contentType, body } = await WIKI.models.metrics.render()
|
|
// -> Never held: a scrape a minute old is worse than no scrape, and Prometheus asks again anyway
|
|
return reply.header('Cache-Control', 'no-store').type(contentType).send(body)
|
|
} catch (err: any) {
|
|
WIKI.logger.warn(`Failed to collect metrics: ${err.message}`)
|
|
return reply.internalServerError('Failed to collect metrics.')
|
|
}
|
|
}
|