diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index 89c48b9b1f7..ac2e958eccf 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -743,6 +743,30 @@ export class PrometheusDatasource expression = `histogram_quantile(0.95, sum(rate(${expression}[$__rate_interval])) by (le))`; break; } + case 'ADD_HISTOGRAM_AVG': { + expression = `histogram_avg(rate(${expression}[$__rate_interval]))`; + break; + } + case 'ADD_HISTOGRAM_FRACTION': { + expression = `histogram_fraction(0,0.2,rate(${expression}[$__rate_interval]))`; + break; + } + case 'ADD_HISTOGRAM_COUNT': { + expression = `histogram_count(rate(${expression}[$__rate_interval]))`; + break; + } + case 'ADD_HISTOGRAM_SUM': { + expression = `histogram_sum(rate(${expression}[$__rate_interval]))`; + break; + } + case 'ADD_HISTOGRAM_STDDEV': { + expression = `histogram_stddev(rate(${expression}[$__rate_interval]))`; + break; + } + case 'ADD_HISTOGRAM_STDVAR': { + expression = `histogram_stdvar(rate(${expression}[$__rate_interval]))`; + break; + } case 'ADD_RATE': { expression = `rate(${expression}[$__rate_interval])`; break; diff --git a/packages/grafana-prometheus/src/query_hints.test.ts b/packages/grafana-prometheus/src/query_hints.test.ts index aa425a22f28..b292271ffa3 100644 --- a/packages/grafana-prometheus/src/query_hints.test.ts +++ b/packages/grafana-prometheus/src/query_hints.test.ts @@ -190,4 +190,44 @@ describe('getQueryHints()', () => { hints = getQueryHints('container_cpu_usage_seconds_total:irate_total', series); expect(hints!.length).toBe(0); }); + + // native histograms + it('returns hints for native histogram by metric type without suffix "_bucket"', () => { + const series = [ + { + datapoints: [ + [23, 1000], + [24, 1001], + ], + }, + ]; + const mock: unknown = { languageProvider: { metricsMetadata: { foo: { type: 'histogram' } } } }; + const datasource = mock as PrometheusDatasource; + + let hints = getQueryHints('foo', series, datasource); + expect(hints!.length).toBe(6); + const hintsString = JSON.stringify(hints); + expect(hintsString).toContain('ADD_HISTOGRAM_AVG'); + expect(hintsString).toContain('ADD_HISTOGRAM_COUNT'); + expect(hintsString).toContain('ADD_HISTOGRAM_SUM'); + expect(hintsString).toContain('ADD_HISTOGRAM_FRACTION'); + expect(hintsString).toContain('ADD_HISTOGRAM_AVG'); + }); + + it('returns no hints for native histogram when there are native histogram functions in the query', () => { + const queryWithNativeHistogramFunction = 'histogram_avg(foo)'; + const series = [ + { + datapoints: [ + [23, 1000], + [24, 1001], + ], + }, + ]; + const mock: unknown = { languageProvider: { metricsMetadata: { foo: { type: 'histogram' } } } }; + const datasource = mock as PrometheusDatasource; + + let hints = getQueryHints(queryWithNativeHistogramFunction, series, datasource); + expect(hints!.length).toBe(0); + }); }); diff --git a/packages/grafana-prometheus/src/query_hints.ts b/packages/grafana-prometheus/src/query_hints.ts index 10ec829f364..f75a52b2f8a 100644 --- a/packages/grafana-prometheus/src/query_hints.ts +++ b/packages/grafana-prometheus/src/query_hints.ts @@ -4,6 +4,7 @@ import { size } from 'lodash'; import { QueryFix, QueryHint } from '@grafana/data'; import { PrometheusDatasource } from './datasource'; +import { PromMetricsMetadata } from './types'; /** * Number of time series results needed before starting to suggest sum aggregation hints @@ -13,9 +14,12 @@ export const SUM_HINT_THRESHOLD_COUNT = 20; export function getQueryHints(query: string, series?: unknown[], datasource?: PrometheusDatasource): QueryHint[] { const hints = []; + const metricsMetadata = datasource?.languageProvider?.metricsMetadata; + // ..._bucket metric needs a histogram_quantile() - const histogramMetric = query.trim().match(/^\w+_bucket$|^\w+_bucket{.*}$/); - if (histogramMetric) { + // this regex also prevents hints from being shown when a query already has a function + const oldHistogramMetric = query.trim().match(/^\w+_bucket$|^\w+_bucket{.*}$/); + if (oldHistogramMetric) { const label = 'Selected metric has buckets.'; hints.push({ type: 'HISTOGRAM_QUANTILE', @@ -28,6 +32,94 @@ export function getQueryHints(query: string, series?: unknown[], datasource?: Pr }, }, }); + } else if (metricsMetadata && simpleQueryCheck(query)) { + // having migrated to native histograms + // there will be no more old histograms (no buckets) + // and we can identify a native histogram by the following + // type === 'histogram' + // metric name does not include '_bucket' + const queryTokens = getQueryTokens(query); + + // Determine whether any of the query identifier tokens refers to a native histogram metric + const { nameMetric } = checkMetricType(queryTokens, 'histogram', metricsMetadata, false); + + const nativeHistogramNameMetric = nameMetric; + + if (nativeHistogramNameMetric) { + // add hints: + // histogram_avg, histogram_count, histogram_sum, histogram_fraction, histogram_stddev, histogram_stdvar + const label = 'Selected metric is a native histogram.'; + hints.push( + { + type: 'HISTOGRAM_AVG', + label, + fix: { + label: 'Consider calculating the arithmetic average of observed values by adding histogram_avg().', + action: { + type: 'ADD_HISTOGRAM_AVG', + query, + }, + }, + }, + { + type: 'HISTOGRAM_COUNT', + label, + fix: { + label: 'Consider calculating the count of observations by adding histogram_count().', + action: { + type: 'ADD_HISTOGRAM_COUNT', + query, + }, + }, + }, + { + type: 'HISTOGRAM_SUM', + label, + fix: { + label: 'Consider calculating the sum of observations by adding histogram_sum().', + action: { + type: 'ADD_HISTOGRAM_SUM', + query, + }, + }, + }, + { + type: 'HISTOGRAM_FRACTION', + label, + fix: { + label: + 'Consider calculating the estimated fraction of observations between the provided lower and upper values by adding histogram_fraction().', + action: { + type: 'ADD_HISTOGRAM_FRACTION', + query, + }, + }, + }, + { + type: 'HISTOGRAM_STDDEV', + label, + fix: { + label: + 'Consider calculating the estimated standard deviation of observations by adding histogram_stddev().', + action: { + type: 'ADD_HISTOGRAM_STDDEV', + query, + }, + }, + }, + { + type: 'HISTOGRAM_STDVAR', + label, + fix: { + label: 'Consider calculating the estimated standard variance of observations by adding histogram_stdvar().', + action: { + type: 'ADD_HISTOGRAM_STDVAR', + query, + }, + }, + } + ); + } } // Check for need of rate() @@ -35,34 +127,21 @@ export function getQueryHints(query: string, series?: unknown[], datasource?: Pr // Use metric metadata for exact types const nameMatch = query.match(/\b((? match) - // Exclude variable identifiers - .filter((token) => !token.startsWith('$')) - // Split composite keys to match the tokens returned by the language provider - .flatMap((token) => token.split(':')); + const queryTokens = getQueryTokens(query); // Determine whether any of the query identifier tokens refers to a counter metric - counterNameMetric = - queryTokens.find((metricName) => { - // Only considering first type information, could be non-deterministic - const metadata = metricsMetadata[metricName]; - if (metadata && metadata.type.toLowerCase() === 'counter') { - certain = true; - return true; - } else { - return false; - } - }) ?? ''; + const metricTypeChecked = checkMetricType(queryTokens, 'counter', metricsMetadata, certain); + + counterNameMetric = metricTypeChecked.nameMetric; + certain = metricTypeChecked.certain; } if (counterNameMetric) { // FixableQuery consists of metric name and optionally label-value pairs. We are not offering fix for complex queries yet. - const fixableQuery = query.trim().match(/^\w+$|^\w+{.*}$/); + const fixableQuery = simpleQueryCheck(query); const verb = certain ? 'is' : 'looks like'; let label = `Selected metric ${verb} a counter.`; let fix: QueryFix | undefined; @@ -150,3 +229,44 @@ export function getInitHints(datasource: PrometheusDatasource): QueryHint[] { return hints; } + +function getQueryTokens(query: string) { + return ( + Array.from(query.matchAll(/\$?[a-zA-Z_:][a-zA-Z0-9_:]*/g)) + .map(([match]) => match) + // Exclude variable identifiers + .filter((token) => !token.startsWith('$')) + // Split composite keys to match the tokens returned by the language provider + .flatMap((token) => token.split(':')) + ); +} + +function checkMetricType( + queryTokens: string[], + metricType: string, + metricsMetadata: PromMetricsMetadata, + certain: boolean +) { + // update certain to change language for counters + const nameMetric = + queryTokens.find((metricName) => { + // Only considering first type information, could be non-deterministic + const metadata = metricsMetadata[metricName]; + if (metadata && metadata.type.toLowerCase() === metricType) { + certain = true; + return true; + } else { + return false; + } + }) ?? ''; + + return { nameMetric, certain }; +} + +/** + * This regex check looks for only metric name and label filters. + * This prevents hints from being shown when a query already has a functions or is complex. + * */ +function simpleQueryCheck(query: string) { + return query.trim().match(/^\w+$|^\w+{.*}$/); +}