1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-31 13:47:02 +02:00

feat: sanitize impact metrics (#10364)

This commit is contained in:
Mateusz Kwasniewski 2025-07-17 12:59:50 +02:00 committed by GitHub
parent 84748aaff0
commit f04dd454d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 99 additions and 3 deletions

View File

@ -2,6 +2,38 @@ import { MetricsTranslator } from './metrics-translator.js';
import { Registry } from 'prom-client'; import { Registry } from 'prom-client';
describe('MetricsTranslator', () => { describe('MetricsTranslator', () => {
describe('Sanitize name', () => {
let translator: MetricsTranslator;
beforeEach(() => {
const registry = new Registry();
translator = new MetricsTranslator(registry);
});
it('should not modify valid names', () => {
expect(translator.sanitizeName('valid_name')).toBe('valid_name');
expect(translator.sanitizeName('validName')).toBe('validName');
expect(translator.sanitizeName('_valid_name')).toBe('_valid_name');
});
it('should replace invalid characters with underscores', () => {
expect(translator.sanitizeName('invalid-name')).toBe(
'invalid_name',
);
expect(translator.sanitizeName('invalid.name')).toBe(
'invalid_name',
);
expect(translator.sanitizeName('invalid@name')).toBe(
'invalid_name',
);
expect(translator.sanitizeName('invalid name')).toBe(
'invalid_name',
);
expect(translator.sanitizeName('invalid:name')).toBe(
'invalid_name',
);
});
});
it('should handle metrics with labels', async () => { it('should handle metrics with labels', async () => {
const metrics = [ const metrics = [
{ {
@ -81,6 +113,59 @@ describe('MetricsTranslator', () => {
expect(result).not.toContain('unsupported'); expect(result).not.toContain('unsupported');
}); });
it('should sanitize metric names and label names', async () => {
const registry = new Registry();
const translator = new MetricsTranslator(registry);
const metrics = [
{
name: 'invalid-metric-name',
help: 'metric with invalid name',
type: 'counter' as const,
samples: [
{
labels: { 'invalid-label': 'value', '1numeric': 123 },
value: 5,
},
],
},
{
name: '1numeric-metric',
help: 'metric with numeric prefix',
type: 'gauge' as const,
samples: [
{
labels: {
'invalid:colon': 'value',
'space label': 'test',
},
value: 10,
},
],
},
];
const result = await translator.translateAndSerializeMetrics(metrics);
expect(result).toContain(
'# HELP unleash_counter_invalid_metric_name metric with invalid name',
);
expect(result).toContain(
'# TYPE unleash_counter_invalid_metric_name counter',
);
expect(result).toContain(
'# HELP unleash_gauge_1numeric_metric metric with numeric prefix',
);
expect(result).toContain('# TYPE unleash_gauge_1numeric_metric gauge');
expect(result).toContain(
'unleash_counter_invalid_metric_name{unleash_invalid_label="value",unleash_1numeric="123",unleash_origin="sdk"} 5',
);
expect(result).toContain(
'unleash_gauge_1numeric_metric{unleash_invalid_colon="value",unleash_space_label="test",unleash_origin="sdk"} 10',
);
});
it('should handle re-labeling of metrics', async () => { it('should handle re-labeling of metrics', async () => {
const registry = new Registry(); const registry = new Registry();
const translator = new MetricsTranslator(registry); const translator = new MetricsTranslator(registry);

View File

@ -19,6 +19,14 @@ export class MetricsTranslator {
this.registry = registry; this.registry = registry;
} }
sanitizeName(name: string): string {
const regex = /[^a-zA-Z0-9_]/g;
const sanitized = name.replace(regex, '_');
return sanitized;
}
private hasNewLabels( private hasNewLabels(
existingMetric: Counter<string> | Gauge<string>, existingMetric: Counter<string> | Gauge<string>,
newLabelNames: string[], newLabelNames: string[],
@ -35,7 +43,7 @@ export class MetricsTranslator {
): Record<string, string | number> { ): Record<string, string | number> {
return Object.fromEntries( return Object.fromEntries(
Object.entries(labels).map(([labelKey, value]) => [ Object.entries(labels).map(([labelKey, value]) => [
`unleash_${labelKey}`, `unleash_${this.sanitizeName(labelKey)}`,
value, value,
]), ]),
); );
@ -51,7 +59,8 @@ export class MetricsTranslator {
} }
translateMetric(metric: Metric): Counter<string> | Gauge<string> | null { translateMetric(metric: Metric): Counter<string> | Gauge<string> | null {
const prefixedName = `unleash_${metric.type}_${metric.name}`; const sanitizedName = this.sanitizeName(metric.name);
const prefixedName = `unleash_${metric.type}_${sanitizedName}`;
const existingMetric = this.registry.getSingleMetric(prefixedName); const existingMetric = this.registry.getSingleMetric(prefixedName);
const allLabelNames = new Set<string>(); const allLabelNames = new Set<string>();
@ -63,7 +72,9 @@ export class MetricsTranslator {
); );
} }
} }
const labelNames = Array.from(allLabelNames); const labelNames = Array.from(allLabelNames).map((labelName) =>
this.sanitizeName(labelName),
);
if (metric.type === 'counter') { if (metric.type === 'counter') {
let counter: Counter<string>; let counter: Counter<string>;