5d8364bb5f
- F10 [stability]: closeMetricsServer() now calls server.closeIdleConnections()
+ server.unref() after server.close(). server.close()'s callback doesn't fire
until keep-alive sockets drain, and the scraper (VictoriaMetrics/vmagent) holds
an idle keep-alive socket — so onModuleDestroy's awaited close would hang until
the scraper disconnects or the orchestrator SIGKILLs on the kill-grace window.
closeIdleConnections() drops idle keep-alive sockets so shutdown completes
immediately (Node 22, per the Dockerfile base).
- F9 [test]: client-telemetry.module.spec.ts pins the E1=B register() gate — the
core of the "public endpoint OFF by default" decision: flag unset / any non-
"true" value ("false"/""/"0"/…) → empty controllers+providers (route absent);
"true"/"TRUE" → registers VitalsController + VitalsService. A flag-inversion or
truthiness regression that reopened the anonymous disk-fill surface now fails.
- F11 [regression/perf]: the db_query_duration_seconds token work (firstSqlToken
regex + Set lookup) is now gated on isMetricsEnabled() in database.module.ts, so
a non-metrics deployment pays NOTHING per query (previously observeDbQuery
no-op'd but the token was still computed on every query). Also hoisted the
13-element known-token Set to a module const (KNOWN_SQL_TOKENS) so it's built
once, not per query.
Gate: server tsc 0; metrics + vitals + client-telemetry suites pass (incl. the
new register-gate test).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
87 lines
3.2 KiB
TypeScript
87 lines
3.2 KiB
TypeScript
import { createServer, Server } from 'node:http';
|
|
import { Logger } from '@nestjs/common';
|
|
import { getMetricsRegistry, isMetricsEnabled } from './metrics.registry';
|
|
|
|
/**
|
|
* Start the Prometheus scrape endpoint on a SEPARATE port, taken from
|
|
* `METRICS_PORT`. There is NO default port: when `METRICS_PORT` is unset the
|
|
* whole metrics subsystem is OFF and this returns null. This is a bare node:http
|
|
* server, NOT part of the Fastify app, so `/metrics` never exists on the public
|
|
* :3000 listener.
|
|
*
|
|
* Returns the http.Server (so callers can close it on shutdown) or null when
|
|
* metrics are disabled. The reference is also kept module-side so the Nest
|
|
* lifecycle (see MetricsModule) can close it on application shutdown without
|
|
* threading the handle back through the non-DI bootstrap.
|
|
*/
|
|
let metricsServer: Server | null = null;
|
|
|
|
export function startMetricsServer(): Server | null {
|
|
if (!isMetricsEnabled()) return null;
|
|
|
|
const logger = new Logger('MetricsServer');
|
|
const register = getMetricsRegistry();
|
|
if (!register) return null;
|
|
|
|
const port = Number(process.env.METRICS_PORT);
|
|
if (!Number.isInteger(port) || port <= 0) {
|
|
logger.warn(
|
|
`Invalid METRICS_PORT="${process.env.METRICS_PORT}", metrics endpoint not started`,
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const server = createServer(async (req, res) => {
|
|
if (req.method === 'GET' && req.url === '/metrics') {
|
|
try {
|
|
const body = await register.metrics();
|
|
res.setHeader('Content-Type', register.contentType);
|
|
res.statusCode = 200;
|
|
res.end(body);
|
|
} catch (err) {
|
|
res.statusCode = 500;
|
|
res.end(String((err as Error)?.message ?? 'error'));
|
|
}
|
|
return;
|
|
}
|
|
res.statusCode = 404;
|
|
res.end();
|
|
});
|
|
|
|
// Bind on all interfaces: the scraper (VictoriaMetrics) reaches this from
|
|
// another container as docmost:9464. The port is not published to the host.
|
|
server.listen(port, '0.0.0.0', () => {
|
|
logger.log(`Metrics endpoint listening on :${port}/metrics`);
|
|
});
|
|
|
|
server.on('error', (err) => {
|
|
logger.error(`Metrics server error: ${err?.message}`);
|
|
});
|
|
|
|
metricsServer = server;
|
|
return server;
|
|
}
|
|
|
|
/**
|
|
* Close the metrics scrape server if one is running. Idempotent and safe to call
|
|
* when metrics are disabled (no server was ever started). Wired into Nest's
|
|
* shutdown lifecycle so the listener is not left dangling on shutdown.
|
|
*/
|
|
export function closeMetricsServer(): Promise<void> {
|
|
const server = metricsServer;
|
|
metricsServer = null;
|
|
if (!server) return Promise.resolve();
|
|
return new Promise((resolve) => {
|
|
server.close(() => resolve());
|
|
// server.close() stops accepting NEW connections but its callback does not
|
|
// fire until existing keep-alive sockets drain. The scraper (VictoriaMetrics/
|
|
// vmagent) holds an idle HTTP keep-alive socket, so without this the callback
|
|
// — and thus shutdown — would hang until the scraper disconnects or the
|
|
// orchestrator escalates to SIGKILL on the kill-grace window. Force-close idle
|
|
// keep-alive sockets so close() completes immediately, and unref so this
|
|
// server never keeps the event loop alive on its own.
|
|
server.closeIdleConnections();
|
|
server.unref();
|
|
});
|
|
}
|