diff --git a/src/server/routes/metrics/prometheus.get.ts b/src/server/routes/metrics/prometheus.get.ts index f67a93b2..9da7bee1 100644 --- a/src/server/routes/metrics/prometheus.get.ts +++ b/src/server/routes/metrics/prometheus.get.ts @@ -3,6 +3,7 @@ import { setHeader } from 'h3'; import Database from '#server/utils/Database'; import WireGuard from '#server/utils/WireGuard'; import { defineMetricsHandler } from '#server/utils/handler'; +import { formatPrometheusLabels } from '#server/utils/prometheus'; import { isPeerConnected } from '#shared/utils/time'; export default defineMetricsHandler('prometheus', async ({ event }) => { @@ -27,7 +28,13 @@ async function getPrometheusResponse() { wireguardConnectedPeersCount++; } - const id = `interface="${wgInterface.name}",enabled="${client.enabled}",ipv4Address="${client.ipv4Address}",ipv6Address="${client.ipv6Address}",name="${client.name}"`; + const id = formatPrometheusLabels({ + interface: wgInterface.name, + enabled: client.enabled, + ipv4Address: client.ipv4Address, + ipv6Address: client.ipv6Address, + name: client.name, + }); wireguardSentBytes.push( `wireguard_sent_bytes{${id}} ${client.transferTx ?? 0}` @@ -41,7 +48,7 @@ async function getPrometheusResponse() { ); } - const id = `interface="${wgInterface.name}"`; + const id = formatPrometheusLabels({ interface: wgInterface.name }); const returnText = [ '# HELP wireguard_configured_peers', diff --git a/src/server/utils/prometheus.ts b/src/server/utils/prometheus.ts new file mode 100644 index 00000000..ca903f07 --- /dev/null +++ b/src/server/utils/prometheus.ts @@ -0,0 +1,17 @@ +export function escapePrometheusLabelValue(value: string): string { + return value + .replaceAll('\\', '\\\\') + .replaceAll('\n', '\\n') + .replaceAll('"', '\\"'); +} + +export function formatPrometheusLabels( + labels: Record +): string { + return Object.entries(labels) + .map( + ([name, value]) => + `${name}="${escapePrometheusLabelValue(String(value))}"` + ) + .join(','); +} diff --git a/src/test/unit/prometheus.spec.ts b/src/test/unit/prometheus.spec.ts new file mode 100644 index 00000000..09b930b8 --- /dev/null +++ b/src/test/unit/prometheus.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from 'vitest'; + +import { + escapePrometheusLabelValue, + formatPrometheusLabels, +} from '#server/utils/prometheus'; + +describe('Prometheus label formatting', () => { + test('escapes quotes, backslashes, and newlines in label values', () => { + expect(escapePrometheusLabelValue('vpn"client')).toBe('vpn\\"client'); + expect(escapePrometheusLabelValue('path\\client')).toBe('path\\\\client'); + expect(escapePrometheusLabelValue('line one\nline two')).toBe( + 'line one\\nline two' + ); + }); + + test('formats escaped values without changing scalar values', () => { + expect( + formatPrometheusLabels({ + interface: 'wg"0', + enabled: true, + name: 'home\\office\npeer', + }) + ).toBe('interface="wg\\"0",enabled="true",name="home\\\\office\\npeer"'); + }); +});