diff --git a/gitnexus/src/mcp/metrics.ts b/gitnexus/src/mcp/metrics.ts index 1925315dc..ee428341f 100644 --- a/gitnexus/src/mcp/metrics.ts +++ b/gitnexus/src/mcp/metrics.ts @@ -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 { 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. */ diff --git a/gitnexus/test/unit/mcp/metrics.test.ts b/gitnexus/test/unit/mcp/metrics.test.ts index edaea736a..3bfdbbed2 100644 --- a/gitnexus/test/unit/mcp/metrics.test.ts +++ b/gitnexus/test/unit/mcp/metrics.test.ts @@ -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', () => {