mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-24 17:51:14 +02:00
feat: histogram impact metric ingestion
This commit is contained in:
parent
d082125a69
commit
4f1fdfe63b
@ -117,9 +117,9 @@ describe('BatchHistogram', () => {
|
|||||||
expect(metrics).toMatch(/test_histogram_count{label1="value1"} 8/);
|
expect(metrics).toMatch(/test_histogram_count{label1="value1"} 8/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should record different labels separately', async () => {
|
test('should record different labels separately and handle special characters', async () => {
|
||||||
histogram.recordBatch(
|
histogram.recordBatch(
|
||||||
{ service: 'api', app: 'my_app' },
|
{ service: 'api', url: 'http://example.com:8080/api' },
|
||||||
{
|
{
|
||||||
count: 3,
|
count: 3,
|
||||||
sum: 1.5,
|
sum: 1.5,
|
||||||
@ -131,7 +131,7 @@ describe('BatchHistogram', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
histogram.recordBatch(
|
histogram.recordBatch(
|
||||||
{ service: 'web', app: 'my_app' },
|
{ service: 'web', url: 'https://app.example.com/dashboard' },
|
||||||
{
|
{
|
||||||
count: 2,
|
count: 2,
|
||||||
sum: 3.0,
|
sum: 3.0,
|
||||||
@ -145,29 +145,30 @@ describe('BatchHistogram', () => {
|
|||||||
const metrics = await registry.metrics();
|
const metrics = await registry.metrics();
|
||||||
|
|
||||||
expect(metrics).toMatch(
|
expect(metrics).toMatch(
|
||||||
/test_histogram_bucket{app="my_app",service="api",le="1"} 2/,
|
/test_histogram_bucket{service="api",url="http:\/\/example\.com:8080\/api",le="1"} 2/,
|
||||||
);
|
);
|
||||||
expect(metrics).toMatch(
|
expect(metrics).toMatch(
|
||||||
/test_histogram_bucket{app="my_app",service="api",le="\+Inf"} 3/,
|
/test_histogram_bucket{service="api",url="http:\/\/example\.com:8080\/api",le="\+Inf"} 3/,
|
||||||
);
|
);
|
||||||
expect(metrics).toMatch(
|
expect(metrics).toMatch(
|
||||||
/test_histogram_sum{app="my_app",service="api"} 1\.5/,
|
/test_histogram_sum{service="api",url="http:\/\/example\.com:8080\/api"} 1\.5/,
|
||||||
);
|
);
|
||||||
expect(metrics).toMatch(
|
expect(metrics).toMatch(
|
||||||
/test_histogram_count{app="my_app",service="api"} 3/,
|
/test_histogram_count{service="api",url="http:\/\/example\.com:8080\/api"} 3/,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Web service metrics (with HTTPS URL)
|
||||||
expect(metrics).toMatch(
|
expect(metrics).toMatch(
|
||||||
/test_histogram_bucket{app="my_app",service="web",le="1"} 1/,
|
/test_histogram_bucket{service="web",url="https:\/\/app\.example\.com\/dashboard",le="1"} 1/,
|
||||||
);
|
);
|
||||||
expect(metrics).toMatch(
|
expect(metrics).toMatch(
|
||||||
/test_histogram_bucket{app="my_app",service="web",le="\+Inf"} 2/,
|
/test_histogram_bucket{service="web",url="https:\/\/app\.example\.com\/dashboard",le="\+Inf"} 2/,
|
||||||
);
|
);
|
||||||
expect(metrics).toMatch(
|
expect(metrics).toMatch(
|
||||||
/test_histogram_sum{app="my_app",service="web"} 3/,
|
/test_histogram_sum{service="web",url="https:\/\/app\.example\.com\/dashboard"} 3/,
|
||||||
);
|
);
|
||||||
expect(metrics).toMatch(
|
expect(metrics).toMatch(
|
||||||
/test_histogram_count{app="my_app",service="web"} 2/,
|
/test_histogram_count{service="web",url="https:\/\/app\.example\.com\/dashboard"} 2/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -193,4 +194,37 @@ describe('BatchHistogram', () => {
|
|||||||
expect(metrics).toMatch(/test_histogram_sum{client="sdk"} 12\.3/);
|
expect(metrics).toMatch(/test_histogram_sum{client="sdk"} 12\.3/);
|
||||||
expect(metrics).toMatch(/test_histogram_count{client="sdk"} 5/);
|
expect(metrics).toMatch(/test_histogram_count{client="sdk"} 5/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should handle unsorted bucket input', async () => {
|
||||||
|
histogram.recordBatch(
|
||||||
|
{ service: 'test' },
|
||||||
|
{
|
||||||
|
count: 5,
|
||||||
|
sum: 7.5,
|
||||||
|
buckets: [
|
||||||
|
{ le: '+Inf', count: 5 }, // Infinity first (unsorted)
|
||||||
|
{ le: 2.5, count: 4 }, // Out of order
|
||||||
|
{ le: 0.5, count: 2 }, // Out of order
|
||||||
|
{ le: 1, count: 3 }, // Out of order
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const metrics = await registry.metrics();
|
||||||
|
|
||||||
|
expect(metrics).toMatch(
|
||||||
|
/test_histogram_bucket{service="test",le="0.5"} 2/,
|
||||||
|
);
|
||||||
|
expect(metrics).toMatch(
|
||||||
|
/test_histogram_bucket{service="test",le="1"} 3/,
|
||||||
|
);
|
||||||
|
expect(metrics).toMatch(
|
||||||
|
/test_histogram_bucket{service="test",le="2.5"} 4/,
|
||||||
|
);
|
||||||
|
expect(metrics).toMatch(
|
||||||
|
/test_histogram_bucket{service="test",le="\+Inf"} 5/,
|
||||||
|
);
|
||||||
|
expect(metrics).toMatch(/test_histogram_sum{service="test"} 7\.5/);
|
||||||
|
expect(metrics).toMatch(/test_histogram_count{service="test"} 5/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,6 +15,7 @@ export class BatchHistogram {
|
|||||||
private name: string;
|
private name: string;
|
||||||
private help: string;
|
private help: string;
|
||||||
private registry: Registry;
|
private registry: Registry;
|
||||||
|
public labelNames: string[] = [];
|
||||||
|
|
||||||
// Store accumulated data for each label combination
|
// Store accumulated data for each label combination
|
||||||
private store: Map<
|
private store: Map<
|
||||||
@ -65,7 +66,7 @@ export class BatchHistogram {
|
|||||||
|
|
||||||
private createLabelKey(labels: Record<string, string | number>): string {
|
private createLabelKey(labels: Record<string, string | number>): string {
|
||||||
const sortedKeys = Object.keys(labels).sort();
|
const sortedKeys = Object.keys(labels).sort();
|
||||||
return sortedKeys.map((key) => `${key}:${labels[key]}`).join(',');
|
return JSON.stringify(sortedKeys.map((key) => [key, labels[key]]));
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
@ -78,10 +79,12 @@ export class BatchHistogram {
|
|||||||
for (const [labelKey, data] of this.store) {
|
for (const [labelKey, data] of this.store) {
|
||||||
const labels: Record<string, string | number> = {};
|
const labels: Record<string, string | number> = {};
|
||||||
if (labelKey) {
|
if (labelKey) {
|
||||||
labelKey.split(',').forEach((pair) => {
|
const parsedLabels = JSON.parse(labelKey);
|
||||||
const [key, value] = pair.split(':');
|
parsedLabels.forEach(
|
||||||
labels[key] = value;
|
([key, value]: [string, string | number]) => {
|
||||||
});
|
labels[key] = value;
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [le, cumulativeCount] of Array.from(
|
for (const [le, cumulativeCount] of Array.from(
|
||||||
|
@ -180,7 +180,6 @@ test('should store histogram metrics with batch data', async () => {
|
|||||||
name: 'response_time',
|
name: 'response_time',
|
||||||
help: 'Response time histogram',
|
help: 'Response time histogram',
|
||||||
type: 'histogram',
|
type: 'histogram',
|
||||||
buckets: [1],
|
|
||||||
samples: [
|
samples: [
|
||||||
{
|
{
|
||||||
labels: { foo: 'bar' },
|
labels: { foo: 'bar' },
|
||||||
@ -200,7 +199,6 @@ test('should store histogram metrics with batch data', async () => {
|
|||||||
name: 'response_time',
|
name: 'response_time',
|
||||||
help: 'Response time histogram',
|
help: 'Response time histogram',
|
||||||
type: 'histogram',
|
type: 'histogram',
|
||||||
buckets: [1],
|
|
||||||
samples: [
|
samples: [
|
||||||
{
|
{
|
||||||
labels: { foo: 'bar' },
|
labels: { foo: 'bar' },
|
||||||
|
@ -193,6 +193,22 @@ describe('MetricsTranslator', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'histogram_with_labels',
|
||||||
|
help: 'histogram with labels',
|
||||||
|
type: 'histogram' as const,
|
||||||
|
samples: [
|
||||||
|
{
|
||||||
|
labels: { service: 'api' },
|
||||||
|
count: 5,
|
||||||
|
sum: 2.5,
|
||||||
|
buckets: [
|
||||||
|
{ le: 1, count: 3 },
|
||||||
|
{ le: '+Inf' as const, count: 5 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result1 = await translator.translateAndSerializeMetrics(metrics1);
|
const result1 = await translator.translateAndSerializeMetrics(metrics1);
|
||||||
@ -202,6 +218,9 @@ describe('MetricsTranslator', () => {
|
|||||||
expect(result1).toContain(
|
expect(result1).toContain(
|
||||||
'unleash_gauge_gauge_with_labels{unleash_env="prod",unleash_origin="sdk"} 10',
|
'unleash_gauge_gauge_with_labels{unleash_env="prod",unleash_origin="sdk"} 10',
|
||||||
);
|
);
|
||||||
|
expect(result1).toContain(
|
||||||
|
'unleash_histogram_histogram_with_labels_count{unleash_origin="sdk",unleash_service="api"} 5',
|
||||||
|
);
|
||||||
|
|
||||||
const metrics2 = [
|
const metrics2 = [
|
||||||
{
|
{
|
||||||
@ -226,6 +245,23 @@ describe('MetricsTranslator', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'histogram_with_labels',
|
||||||
|
help: 'histogram with labels',
|
||||||
|
type: 'histogram' as const,
|
||||||
|
buckets: [1],
|
||||||
|
samples: [
|
||||||
|
{
|
||||||
|
labels: { service: 'api', region: 'us-east' }, // Added a new label
|
||||||
|
count: 3,
|
||||||
|
sum: 1.8,
|
||||||
|
buckets: [
|
||||||
|
{ le: 1, count: 2 },
|
||||||
|
{ le: '+Inf' as const, count: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result2 = await translator.translateAndSerializeMetrics(metrics2);
|
const result2 = await translator.translateAndSerializeMetrics(metrics2);
|
||||||
@ -236,5 +272,11 @@ describe('MetricsTranslator', () => {
|
|||||||
expect(result2).toContain(
|
expect(result2).toContain(
|
||||||
'unleash_gauge_gauge_with_labels{unleash_env="prod",unleash_region="us-east",unleash_origin="sdk"} 20',
|
'unleash_gauge_gauge_with_labels{unleash_env="prod",unleash_region="us-east",unleash_origin="sdk"} 20',
|
||||||
);
|
);
|
||||||
|
expect(result2).toContain(
|
||||||
|
'unleash_histogram_histogram_with_labels_count{unleash_origin="sdk",unleash_region="us-east",unleash_service="api"} 3',
|
||||||
|
);
|
||||||
|
expect(result2).not.toContain(
|
||||||
|
'unleash_histogram_histogram_with_labels_count{unleash_origin="sdk",unleash_service="api"} 5',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,6 @@ export interface BucketMetric {
|
|||||||
name: string;
|
name: string;
|
||||||
help: string;
|
help: string;
|
||||||
type: 'histogram';
|
type: 'histogram';
|
||||||
buckets: number[];
|
|
||||||
samples: BucketMetricSample[];
|
samples: BucketMetricSample[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +31,6 @@ export type Metric = NumericMetric | BucketMetric;
|
|||||||
|
|
||||||
export class MetricsTranslator {
|
export class MetricsTranslator {
|
||||||
private registry: Registry;
|
private registry: Registry;
|
||||||
private histograms: Map<string, BatchHistogram> = new Map();
|
|
||||||
|
|
||||||
constructor(registry: Registry) {
|
constructor(registry: Registry) {
|
||||||
this.registry = registry;
|
this.registry = registry;
|
||||||
@ -166,14 +164,26 @@ export class MetricsTranslator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let histogram = this.histograms.get(prefixedName);
|
let histogram: BatchHistogram;
|
||||||
if (!histogram) {
|
|
||||||
|
if (existingMetric && existingMetric instanceof BatchHistogram) {
|
||||||
|
if (this.hasNewLabels(existingMetric, labelNames)) {
|
||||||
|
this.registry.removeSingleMetric(prefixedName);
|
||||||
|
|
||||||
|
histogram = new BatchHistogram({
|
||||||
|
name: prefixedName,
|
||||||
|
help: metric.help,
|
||||||
|
registry: this.registry,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
histogram = existingMetric as BatchHistogram;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
histogram = new BatchHistogram({
|
histogram = new BatchHistogram({
|
||||||
name: prefixedName,
|
name: prefixedName,
|
||||||
help: metric.help,
|
help: metric.help,
|
||||||
registry: this.registry,
|
registry: this.registry,
|
||||||
});
|
});
|
||||||
this.histograms.set(prefixedName, histogram);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const sample of metric.samples) {
|
for (const sample of metric.samples) {
|
||||||
|
Loading…
Reference in New Issue
Block a user