fix(mcp): cache bound config; surface shutdown errors

initMetrics() idempotent re-entry now returns the values bound at first
init rather than re-reading env (which could lie if env mutated between
calls). shutdownMetrics() writes exporter/provider shutdown failures to
stderr instead of swallowing — silent failure during exit hid lost
final-flush data. Adds tests for idempotent re-init and forceEnabled.
This commit is contained in:
Nathan Hangen 2026-05-10 21:33:32 -04:00
parent 8bb0508ded
commit 8e2c3dce93
2 changed files with 38 additions and 9 deletions

View file

@ -22,6 +22,7 @@ let requestsCounter: Counter | null = null;
let durationHistogram: Histogram | null = null;
let resultBytesHistogram: Histogram | null = null;
let inflightGauge: UpDownCounter | null = null;
let boundConfig: { port: number; host: string; endpoint: string } | null = null;
function envFlag(value: string | undefined): boolean {
if (typeof value !== 'string') return false;
@ -48,13 +49,8 @@ export async function initMetrics(
const enabled = opts.forceEnabled ?? envFlag(env['GITNEXUS_OTEL_METRICS']);
if (!enabled) return { enabled: false };
if (provider !== null) {
return {
enabled: true,
port: Number(env['GITNEXUS_OTEL_METRICS_PORT'] ?? 9464),
host: env['GITNEXUS_OTEL_METRICS_HOST'] ?? '127.0.0.1',
endpoint: env['GITNEXUS_OTEL_METRICS_ENDPOINT'] ?? '/metrics',
};
if (provider !== null && boundConfig !== null) {
return { enabled: true, ...boundConfig };
}
const port = Number(env['GITNEXUS_OTEL_METRICS_PORT'] ?? 9464);
@ -115,6 +111,7 @@ export async function initMetrics(
});
await exporter.startServer();
boundConfig = { port, host, endpoint };
return { enabled: true, port, host, endpoint };
}
@ -128,8 +125,17 @@ export async function shutdownMetrics(): Promise<void> {
durationHistogram = null;
resultBytesHistogram = null;
inflightGauge = null;
if (e) await e.shutdown().catch(() => {});
if (p) await p.shutdown().catch(() => {});
boundConfig = null;
if (e) {
await e.shutdown().catch((err) => {
process.stderr.write(`[gitnexus metrics] exporter shutdown error: ${err?.message ?? err}\n`);
});
}
if (p) {
await p.shutdown().catch((err) => {
process.stderr.write(`[gitnexus metrics] provider shutdown error: ${err?.message ?? err}\n`);
});
}
}
/** hasError must be a boolean; never accept the error string — cypher errors echo user input. */

View file

@ -69,6 +69,29 @@ describe('initMetrics gating', () => {
expect(isMetricsEnabled()).toBe(true);
});
});
it('idempotent re-init returns the originally bound values, not current env', async () => {
process.env['GITNEXUS_OTEL_METRICS'] = 'on';
await withRandomPort(async () => {
const first = await initMetrics();
process.env['GITNEXUS_OTEL_METRICS_PORT'] = '65535';
process.env['GITNEXUS_OTEL_METRICS_HOST'] = '0.0.0.0';
process.env['GITNEXUS_OTEL_METRICS_ENDPOINT'] = '/changed';
const second = await initMetrics();
expect(second.port).toBe(first.port);
expect(second.host).toBe(first.host);
expect(second.endpoint).toBe(first.endpoint);
});
});
it('honors forceEnabled regardless of env', async () => {
delete process.env['GITNEXUS_OTEL_METRICS'];
await withRandomPort(async () => {
const result = await initMetrics({ forceEnabled: true });
expect(result.enabled).toBe(true);
expect(isMetricsEnabled()).toBe(true);
});
});
});
describe('observe() — success path', () => {