diff --git a/CHANGELOG.md b/CHANGELOG.md index 11be4a31..8e4e115a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ project adheres to [Semantic Versioning](http://semver.org/). This release marks our first release under the Prometheus umbrella. +### Fixed + +- Fix escaping for non-string label values without regressing string hot path + ### Breaking - Drop support for Node.js versions 16, 18, 20, 21 and 23 diff --git a/lib/registry.js b/lib/registry.js index 9c9aeaf7..97b07205 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -303,11 +303,12 @@ function flattenSharedLabels(labels) { sharedLabelCache.set(labels, flattened); return flattened; } -function escapeLabelValue(str) { - if (typeof str !== 'string') { - return str; +function escapeLabelValue(value) { + // Fast-path strings; String() only for non-strings (e.g. Symbols) + if (typeof value !== 'string') { + value = String(value); } - return escapeString(str).replace(/"/g, '\\"'); + return escapeString(value).replace(/"/g, '\\"'); } function escapeString(str) { return str.replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); diff --git a/test/registerTest.js b/test/registerTest.js index ce4a8757..b107d172 100644 --- a/test/registerTest.js +++ b/test/registerTest.js @@ -340,6 +340,57 @@ describe('Register', () => { expect(escapedResult).toMatch(/\\"/); }); + it('should escape quotes and newlines in non-string label values', async () => { + register.registerMetric({ + async get() { + return { + name: 'test_metric', + type: 'gauge', + help: 'A test metric', + values: [ + { + value: 1, + labels: { + x: ['say "hi"'], + }, + }, + { + value: 2, + labels: { + y: ['a\nb'], + }, + }, + ], + }; + }, + }); + const escapedResult = await register.metrics(); + expect(escapedResult).toContain('x="say \\"hi\\""'); + expect(escapedResult).toContain('y="a\\nb"'); + }); + + it('should coerce Symbol label values without throwing', async () => { + register.registerMetric({ + async get() { + return { + name: 'test_metric', + type: 'gauge', + help: 'A test metric', + values: [ + { + value: 1, + labels: { + sym: Symbol('x'), + }, + }, + ], + }; + }, + }); + const escapedResult = await register.metrics(); + expect(escapedResult).toContain('sym="Symbol(x)"'); + }); + describe('should output metrics as JSON', () => { it('should output metrics as JSON', async () => { register.registerMetric(getMetric());