From 5820dbc1596bdbe497a285a78cc3fa49016a0e73 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 2 Oct 2025 16:45:48 +0200 Subject: [PATCH] properly detect histograms --- .../edit-pane/promQueries.test.ts | 124 ++++++++++++++++++ .../dashboard-scene/edit-pane/promQueries.ts | 40 +++++- 2 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 public/app/features/dashboard-scene/edit-pane/promQueries.test.ts diff --git a/public/app/features/dashboard-scene/edit-pane/promQueries.test.ts b/public/app/features/dashboard-scene/edit-pane/promQueries.test.ts new file mode 100644 index 00000000000..b42e65f1ea4 --- /dev/null +++ b/public/app/features/dashboard-scene/edit-pane/promQueries.test.ts @@ -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([]); + }); + }); +}); diff --git a/public/app/features/dashboard-scene/edit-pane/promQueries.ts b/public/app/features/dashboard-scene/edit-pane/promQueries.ts index a91f3b13695..56092b9bedf 100644 --- a/public/app/features/dashboard-scene/edit-pane/promQueries.ts +++ b/public/app/features/dashboard-scene/edit-pane/promQueries.ts @@ -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), })), })); }