properly detect histograms
CodeQL checks / Detect whether code changed (push) Waiting to run Details
CodeQL checks / Analyze (actions) (push) Blocked by required conditions Details
CodeQL checks / Analyze (go) (push) Blocked by required conditions Details
CodeQL checks / Analyze (javascript) (push) Blocked by required conditions Details

This commit is contained in:
ismail simsek 2025-10-02 16:45:48 +02:00
parent 3f064b9ff0
commit 5820dbc159
No known key found for this signature in database
2 changed files with 159 additions and 5 deletions

View File

@ -0,0 +1,124 @@
import { getQueriesForMetric, findMetadataForMetric } from './promQueries';
describe('promQueries', () => {
const mockMetadata = {
'http_request_duration': {
type: 'histogram',
help: 'Duration of HTTP requests',
},
'http_request_duration_bucket': {
type: 'counter',
help: 'Cumulative counters for the observation buckets (Duration of HTTP requests)',
},
'http_request_duration_count': {
type: 'counter',
help: 'Count of events that have been observed for the histogram metric (Duration of HTTP requests)',
},
'http_request_duration_sum': {
type: 'counter',
help: 'Total sum of all observed values for the histogram metric (Duration of HTTP requests)',
},
'cpu_usage': {
type: 'gauge',
help: 'CPU usage percentage',
},
'requests_total': {
type: 'counter',
help: 'Total number of requests',
},
};
describe('findMetadataForMetric', () => {
it('should return histogram metadata for _bucket suffix', () => {
const result = findMetadataForMetric('http_request_duration_bucket', mockMetadata);
expect(result?.type).toBe('histogram');
expect(result?.help).toBe('Duration of HTTP requests');
});
it('should return histogram metadata for _count suffix', () => {
const result = findMetadataForMetric('http_request_duration_count', mockMetadata);
expect(result?.type).toBe('histogram');
expect(result?.help).toBe('Duration of HTTP requests');
});
it('should return histogram metadata for _sum suffix', () => {
const result = findMetadataForMetric('http_request_duration_sum', mockMetadata);
expect(result?.type).toBe('histogram');
expect(result?.help).toBe('Duration of HTTP requests');
});
it('should return exact match for non-histogram metrics', () => {
const result = findMetadataForMetric('cpu_usage', mockMetadata);
expect(result?.type).toBe('gauge');
expect(result?.help).toBe('CPU usage percentage');
});
it('should return counter metadata for counter metrics', () => {
const result = findMetadataForMetric('requests_total', mockMetadata);
expect(result?.type).toBe('counter');
expect(result?.help).toBe('Total number of requests');
});
it('should return undefined for non-existent metrics', () => {
const result = findMetadataForMetric('non_existent_metric', mockMetadata);
expect(result).toBeUndefined();
});
});
describe('getQueriesForMetric', () => {
it('should return histogram queries for _bucket suffix metrics', () => {
const result = getQueriesForMetric('http_request_duration_bucket', mockMetadata);
expect(result.length).toBeGreaterThan(0);
// Check that we get histogram-specific queries
const percentileQuery = result.find(panel => panel.name === '95th percentile');
expect(percentileQuery).toBeDefined();
expect(percentileQuery?.targets[0].expr).toBe('histogram_quantile(0.95, rate(http_request_duration_bucket[5m]))');
});
it('should return histogram queries for _count suffix metrics', () => {
const result = getQueriesForMetric('http_request_duration_count', mockMetadata);
expect(result.length).toBeGreaterThan(0);
// Check that we get histogram-specific queries with base metric name
const percentileQuery = result.find(panel => panel.name === '95th percentile');
expect(percentileQuery).toBeDefined();
expect(percentileQuery?.targets[0].expr).toBe('histogram_quantile(0.95, rate(http_request_duration_bucket[5m]))');
});
it('should return histogram queries for _sum suffix metrics', () => {
const result = getQueriesForMetric('http_request_duration_sum', mockMetadata);
expect(result.length).toBeGreaterThan(0);
// Check that we get histogram-specific queries with base metric name
const averageQuery = result.find(panel => panel.name === 'Average');
expect(averageQuery).toBeDefined();
expect(averageQuery?.targets[0].expr).toBe('rate(http_request_duration_sum[5m]) / rate(http_request_duration_count[5m])');
});
it('should return gauge queries for gauge metrics', () => {
const result = getQueriesForMetric('cpu_usage', mockMetadata);
expect(result.length).toBeGreaterThan(0);
// Check that we get gauge-specific queries
const currentValueQuery = result.find(panel => panel.name === 'Current value');
expect(currentValueQuery).toBeDefined();
expect(currentValueQuery?.targets[0].expr).toBe('cpu_usage');
});
it('should return counter queries for counter metrics', () => {
const result = getQueriesForMetric('requests_total', mockMetadata);
expect(result.length).toBeGreaterThan(0);
// Check that we get counter-specific queries
const rateQuery = result.find(panel => panel.name === 'Rate per second');
expect(rateQuery).toBeDefined();
expect(rateQuery?.targets[0].expr).toBe('rate(requests_total[5m])');
});
it('should return empty array for non-existent metrics', () => {
const result = getQueriesForMetric('non_existent_metric', mockMetadata);
expect(result).toEqual([]);
});
});
});

View File

@ -274,13 +274,31 @@ function findMetadataForMetric(
metricName: string,
metricsMetadata: PromMetricsMetadata
): PromMetricsMetadataItem | undefined {
// First try exact match
// For histogram/summary suffixes, prioritize finding the base metric first
// This ensures that metrics like 'http_request_duration_bucket' return 'histogram' type
// instead of the synthetic 'counter' type created for the suffixed metrics
const histogramSummarySuffixes = ['_bucket', '_count', '_sum'];
for (const suffix of histogramSummarySuffixes) {
if (metricName.endsWith(suffix)) {
const baseMetricName = metricName.slice(0, -suffix.length);
if (metricsMetadata[baseMetricName]) {
const baseMetadata = metricsMetadata[baseMetricName];
// Only return base metadata if it's actually a histogram or summary
if (baseMetadata.type === 'histogram' || baseMetadata.type === 'summary') {
return baseMetadata;
}
}
}
}
// Try exact match for non-histogram/summary metrics or when base metric isn't found
if (metricsMetadata[metricName]) {
return metricsMetadata[metricName];
}
// Try removing common Prometheus suffixes to find the base metric
for (const suffix of PROMETHEUS_SUFFIXES) {
// Try removing other common Prometheus suffixes to find the base metric
const otherSuffixes = PROMETHEUS_SUFFIXES.filter(s => !histogramSummarySuffixes.includes(s));
for (const suffix of otherSuffixes) {
if (metricName.endsWith(suffix)) {
const baseMetricName = metricName.slice(0, -suffix.length);
if (metricsMetadata[baseMetricName]) {
@ -309,12 +327,24 @@ export function getQueriesForMetric(metricName: string, metricsMetadata: PromMet
return [];
}
// For histogram/summary metrics with suffixes, use the base metric name in queries
// since the query templates expect the base name (e.g., 'http_request_duration' not 'http_request_duration_bucket')
let queryMetricName = metricName;
if (metadata.type === 'histogram' || metadata.type === 'summary') {
const histogramSummarySuffixes = ['_bucket', '_count', '_sum'];
for (const suffix of histogramSummarySuffixes) {
if (metricName.endsWith(suffix)) {
queryMetricName = metricName.slice(0, -suffix.length);
break;
}
}
}
return panels.map((panel) => ({
...panel,
// name: `${metricName} - ${panel.name}`,
targets: panel.targets.map((target) => ({
...target,
expr: target.expr.replace(/\{\{metric_name\}\}/g, metricName),
expr: target.expr.replace(/\{\{metric_name\}\}/g, queryMetricName),
})),
}));
}