Files
gitmost/apps/server/src/integrations/metrics/metrics.server.ts
T
agent_coder 5d8364bb5f fix(#355 review round-2 F9-F11): register-gate test + shutdown idle-close + DB-path metrics gate
- 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>
2026-07-05 00:20:26 +03:00

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();
});
}