diff --git a/packages/cli/src/expression-observability/__tests__/expression-observability.provider.test.ts b/packages/cli/src/expression-observability/__tests__/expression-observability.provider.test.ts index 9a7075569d3..8923cd93b48 100644 --- a/packages/cli/src/expression-observability/__tests__/expression-observability.provider.test.ts +++ b/packages/cli/src/expression-observability/__tests__/expression-observability.provider.test.ts @@ -110,6 +110,48 @@ describe('ExpressionObservabilityProvider', () => { }); }); + describe('survives a global registry clear', () => { + it('re-registers expression metrics on emission without warning after a clear', async () => { + const provider = new ExpressionObservabilityProvider( + buildConfig(), + buildLogger(), + buildGlobalConfig(), + ); + + promClient.register.clear(); + + provider.metrics.counter(EXPRESSION_METRICS.poolAcquired.name, 3); + provider.metrics.gauge(EXPRESSION_METRICS.codeCacheSize.name, 7); + provider.metrics.histogram(EXPRESSION_METRICS.evaluationDuration.name, 0.02, { + status: 'success', + type: 'none', + }); + + expect(scopedLogger.warn).not.toHaveBeenCalled(); + + const output = await promClient.register.metrics(); + expect(output).toContain('n8n_expression_pool_acquired_total 3'); + expect(output).toContain('n8n_expression_code_cache_size 7'); + expect(output).toContain('n8n_expression_evaluation_duration_seconds_count'); + }); + + it('still warns for genuinely unknown metric names after a clear', () => { + const provider = new ExpressionObservabilityProvider( + buildConfig(), + buildLogger(), + buildGlobalConfig(), + ); + + promClient.register.clear(); + + provider.metrics.counter('test.unknown', 1); + + expect(scopedLogger.warn).toHaveBeenCalledWith('Emitted unknown expression metric', { + name: 'test.unknown', + }); + }); + }); + describe('tail sampling', () => { const startSpanMock = jest.fn().mockReturnValue({ setStatus: jest.fn(), diff --git a/packages/cli/src/expression-observability/expression-observability.provider.ts b/packages/cli/src/expression-observability/expression-observability.provider.ts index 2148d67df5f..3c3bc399a44 100644 --- a/packages/cli/src/expression-observability/expression-observability.provider.ts +++ b/packages/cli/src/expression-observability/expression-observability.provider.ts @@ -42,6 +42,10 @@ export class ExpressionObservabilityProvider implements ObservabilityProvider { private tracer?: Tracer; + private readonly metricDefs = new Map( + (Object.values(EXPRESSION_METRICS) as MetricDef[]).map((def) => [def.name, def]), + ); + constructor( private readonly config: ExpressionEngineConfig, private readonly logger: Logger, @@ -58,7 +62,9 @@ export class ExpressionObservabilityProvider implements ObservabilityProvider { this.prefix = globalConfig.endpoints.metrics.prefix; - this.registerMetrics(); + for (const metric of this.metricDefs.values()) { + this.getOrRegisterMetric(metric); + } this.metrics = { counter: (name, value, tags) => this.counter(name, value, tags), @@ -78,71 +84,65 @@ export class ExpressionObservabilityProvider implements ObservabilityProvider { }; } - private registerMetrics(): void { - for (const def of Object.values(EXPRESSION_METRICS) as MetricDef[]) { - const promName = toPromName(def.name, def.kind, this.prefix); - switch (def.kind) { - case 'counter': - new promClient.Counter({ - name: promName, - help: def.help, - labelNames: def.labels, - }); - break; - case 'gauge': - new promClient.Gauge({ - name: promName, - help: def.help, - labelNames: def.labels, - }); - break; - case 'histogram': - new promClient.Histogram({ - name: promName, - help: def.help, - labelNames: def.labels, - buckets: DURATION_BUCKETS_SECONDS, - }); - break; - default: { - const _exhaustive: never = def.kind; - throw new UnexpectedError(`Unknown metric kind: ${String(_exhaustive)}`); - } + private getOrRegisterMetric(def: MetricDef) { + const promName = toPromName(def.name, def.kind, this.prefix); + const existing = promClient.register.getSingleMetric(promName); + if (existing) return existing; + + switch (def.kind) { + case 'counter': + return new promClient.Counter({ + name: promName, + help: def.help, + labelNames: def.labels, + }); + case 'gauge': + return new promClient.Gauge({ + name: promName, + help: def.help, + labelNames: def.labels, + }); + case 'histogram': + return new promClient.Histogram({ + name: promName, + help: def.help, + labelNames: def.labels, + buckets: DURATION_BUCKETS_SECONDS, + }); + default: { + const _exhaustive: never = def.kind; + throw new UnexpectedError(`Unknown metric kind: ${String(_exhaustive)}`); } } } + private getMetricDef(name: string, kind: MetricDef['kind']) { + const def = this.metricDefs.get(name); + if (def?.kind === kind) return def; + this.scopedLogger.warn('Emitted unknown expression metric', { name }); + return undefined; + } + private counter(name: string, value: number, tags?: Record): void { - const promName = toPromName(name, 'counter', this.prefix); - const counter = promClient.register.getSingleMetric(promName) as Counter | undefined; - if (!counter) { - this.scopedLogger.warn('Emitted unknown expression metric', { name }); - return; - } + const def = this.getMetricDef(name, 'counter'); + if (!def) return; + const counter = this.getOrRegisterMetric(def) as Counter; if (tags) counter.inc(tags, value); else counter.inc(value); } private gauge(name: string, value: number, tags?: Record): void { - const promName = toPromName(name, 'gauge', this.prefix); - const gauge = promClient.register.getSingleMetric(promName) as Gauge | undefined; - if (!gauge) { - this.scopedLogger.warn('Emitted unknown expression metric', { name }); - return; - } + const def = this.getMetricDef(name, 'gauge'); + if (!def) return; + const gauge = this.getOrRegisterMetric(def) as Gauge; if (tags) gauge.set(tags, value); else gauge.set(value); } private histogram(name: string, value: number, tags?: Record): void { - const promName = toPromName(name, 'histogram', this.prefix); - const histogram = promClient.register.getSingleMetric(promName) as - | Histogram - | undefined; - if (!histogram) { - this.scopedLogger.warn('Emitted unknown expression metric', { name }); - return; - } + const def = this.getMetricDef(name, 'histogram'); + if (!def) return; + const histogram = this.getOrRegisterMetric(def) as Histogram; if (tags) histogram.observe(tags, value); else histogram.observe(value);