mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
feat: translate impact metrics to prom format (#10147)
This commit is contained in:
parent
c165e87d74
commit
d2b9751e01
141
src/lib/features/metrics/impact/metrics-translator.test.ts
Normal file
141
src/lib/features/metrics/impact/metrics-translator.test.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { MetricsTranslator } from './metrics-translator.js';
|
||||||
|
import { Registry } from 'prom-client';
|
||||||
|
|
||||||
|
describe('MetricsTranslator', () => {
|
||||||
|
it('should handle metrics with labels', async () => {
|
||||||
|
const metrics = [
|
||||||
|
{
|
||||||
|
name: 'labeled_counter',
|
||||||
|
help: 'with labels',
|
||||||
|
type: 'counter' as const,
|
||||||
|
samples: [
|
||||||
|
{
|
||||||
|
labels: { foo: 'bar' },
|
||||||
|
value: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'test_gauge',
|
||||||
|
help: 'gauge test',
|
||||||
|
type: 'gauge' as const,
|
||||||
|
samples: [
|
||||||
|
{
|
||||||
|
labels: { env: 'prod' },
|
||||||
|
value: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const registry = new Registry();
|
||||||
|
const translator = new MetricsTranslator(registry);
|
||||||
|
const result = await translator.translateAndSerializeMetrics(metrics);
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
expect(result).toContain('# HELP labeled_counter with labels');
|
||||||
|
expect(result).toContain('# TYPE labeled_counter counter');
|
||||||
|
expect(result).toContain('labeled_counter{foo="bar"} 5');
|
||||||
|
expect(result).toContain('test_gauge{env="prod"} 10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore unsupported metric types', async () => {
|
||||||
|
const metrics = [
|
||||||
|
{
|
||||||
|
name: 'test_counter',
|
||||||
|
help: 'test counter',
|
||||||
|
type: 'counter' as const,
|
||||||
|
samples: [{ value: 1 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'unsupported',
|
||||||
|
help: 'unsupported type',
|
||||||
|
type: 'histogram' as any,
|
||||||
|
samples: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'test_gauge',
|
||||||
|
help: 'gauge test',
|
||||||
|
type: 'gauge' as const,
|
||||||
|
samples: [{ value: 2 }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const registry = new Registry();
|
||||||
|
const translator = new MetricsTranslator(registry);
|
||||||
|
const result = await translator.translateAndSerializeMetrics(metrics);
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
expect(result).toContain('# HELP test_counter test counter');
|
||||||
|
expect(result).toContain('# TYPE test_counter counter');
|
||||||
|
expect(result).toContain('# HELP test_gauge gauge test');
|
||||||
|
expect(result).toContain('# TYPE test_gauge gauge');
|
||||||
|
expect(result).not.toContain('unsupported');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle re-labeling of metrics', async () => {
|
||||||
|
const registry = new Registry();
|
||||||
|
const translator = new MetricsTranslator(registry);
|
||||||
|
|
||||||
|
const metrics1 = [
|
||||||
|
{
|
||||||
|
name: 'counter_with_labels',
|
||||||
|
help: 'counter with labels',
|
||||||
|
type: 'counter' as const,
|
||||||
|
samples: [
|
||||||
|
{
|
||||||
|
labels: { foo: 'bar' },
|
||||||
|
value: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gauge_with_labels',
|
||||||
|
help: 'gauge with labels',
|
||||||
|
type: 'gauge' as const,
|
||||||
|
samples: [
|
||||||
|
{
|
||||||
|
labels: { env: 'prod' },
|
||||||
|
value: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result1 = await translator.translateAndSerializeMetrics(metrics1);
|
||||||
|
expect(result1).toContain('counter_with_labels{foo="bar"} 5');
|
||||||
|
expect(result1).toContain('gauge_with_labels{env="prod"} 10');
|
||||||
|
|
||||||
|
const metrics2 = [
|
||||||
|
{
|
||||||
|
name: 'counter_with_labels',
|
||||||
|
help: 'counter with labels',
|
||||||
|
type: 'counter' as const,
|
||||||
|
samples: [
|
||||||
|
{
|
||||||
|
labels: { foo: 'bar', baz: 'qux' }, // Added a new label
|
||||||
|
value: 15,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gauge_with_labels',
|
||||||
|
help: 'gauge with labels',
|
||||||
|
type: 'gauge' as const,
|
||||||
|
samples: [
|
||||||
|
{
|
||||||
|
labels: { env: 'prod', region: 'us-east' }, // Added a new label
|
||||||
|
value: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result2 = await translator.translateAndSerializeMetrics(metrics2);
|
||||||
|
|
||||||
|
expect(result2).toContain(
|
||||||
|
'counter_with_labels{foo="bar",baz="qux"} 15',
|
||||||
|
);
|
||||||
|
expect(result2).toContain(
|
||||||
|
'gauge_with_labels{env="prod",region="us-east"} 20',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
135
src/lib/features/metrics/impact/metrics-translator.ts
Normal file
135
src/lib/features/metrics/impact/metrics-translator.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { Counter, Gauge, type Registry } from 'prom-client';
|
||||||
|
|
||||||
|
interface MetricSample {
|
||||||
|
labels?: Record<string, string | number>;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Metric {
|
||||||
|
name: string;
|
||||||
|
help: string;
|
||||||
|
type: 'counter' | 'gauge';
|
||||||
|
samples: MetricSample[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MetricsTranslator {
|
||||||
|
private registry: Registry;
|
||||||
|
|
||||||
|
constructor(registry: Registry) {
|
||||||
|
this.registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasNewLabels(
|
||||||
|
existingMetric: Counter<string> | Gauge<string>,
|
||||||
|
newLabelNames: string[],
|
||||||
|
): boolean {
|
||||||
|
const existingLabelNames = (existingMetric as any).labelNames || [];
|
||||||
|
|
||||||
|
return newLabelNames.some(
|
||||||
|
(label) => !existingLabelNames.includes(label),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
translateMetric(metric: Metric): Counter<string> | Gauge<string> | null {
|
||||||
|
const existingMetric = this.registry.getSingleMetric(metric.name);
|
||||||
|
|
||||||
|
const allLabelNames = new Set<string>();
|
||||||
|
for (const sample of metric.samples) {
|
||||||
|
if (sample.labels) {
|
||||||
|
Object.keys(sample.labels).forEach((label) =>
|
||||||
|
allLabelNames.add(label),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const labelNames = Array.from(allLabelNames);
|
||||||
|
|
||||||
|
if (metric.type === 'counter') {
|
||||||
|
let counter: Counter<string>;
|
||||||
|
|
||||||
|
if (existingMetric && existingMetric instanceof Counter) {
|
||||||
|
if (this.hasNewLabels(existingMetric, labelNames)) {
|
||||||
|
this.registry.removeSingleMetric(metric.name);
|
||||||
|
|
||||||
|
counter = new Counter({
|
||||||
|
name: metric.name,
|
||||||
|
help: metric.help,
|
||||||
|
registers: [this.registry],
|
||||||
|
labelNames,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
counter = existingMetric as Counter<string>;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
counter = new Counter({
|
||||||
|
name: metric.name,
|
||||||
|
help: metric.help,
|
||||||
|
registers: [this.registry],
|
||||||
|
labelNames,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sample of metric.samples) {
|
||||||
|
if (sample.labels) {
|
||||||
|
counter.inc(sample.labels, sample.value);
|
||||||
|
} else {
|
||||||
|
counter.inc(sample.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return counter;
|
||||||
|
} else if (metric.type === 'gauge') {
|
||||||
|
let gauge: Gauge<string>;
|
||||||
|
|
||||||
|
if (existingMetric && existingMetric instanceof Gauge) {
|
||||||
|
if (this.hasNewLabels(existingMetric, labelNames)) {
|
||||||
|
this.registry.removeSingleMetric(metric.name);
|
||||||
|
|
||||||
|
gauge = new Gauge({
|
||||||
|
name: metric.name,
|
||||||
|
help: metric.help,
|
||||||
|
registers: [this.registry],
|
||||||
|
labelNames,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
gauge = existingMetric as Gauge<string>;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gauge = new Gauge({
|
||||||
|
name: metric.name,
|
||||||
|
help: metric.help,
|
||||||
|
registers: [this.registry],
|
||||||
|
labelNames,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sample of metric.samples) {
|
||||||
|
if (sample.labels) {
|
||||||
|
gauge.set(sample.labels, sample.value);
|
||||||
|
} else {
|
||||||
|
gauge.set(sample.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gauge;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
translateMetrics(metrics: Metric[]): Registry {
|
||||||
|
for (const metric of metrics) {
|
||||||
|
this.translateMetric(metric);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeMetrics(): Promise<string> {
|
||||||
|
return this.registry.metrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
translateAndSerializeMetrics(metrics: Metric[]): Promise<string> {
|
||||||
|
this.translateMetrics(metrics);
|
||||||
|
return this.serializeMetrics();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user