import { size } from 'lodash'; import { QueryHint, QueryFix } from '@grafana/data'; import { PrometheusDatasource } from './datasource'; /** * Number of time series results needed before starting to suggest sum aggregation hints */ export const SUM_HINT_THRESHOLD_COUNT = 20; export function getQueryHints(query: string, series?: any[], datasource?: PrometheusDatasource): QueryHint[] { const hints = []; // ..._bucket metric needs a histogram_quantile() const histogramMetric = query.trim().match(/^\w+_bucket$|^\w+_bucket{.*}$/); if (histogramMetric) { const label = 'Selected metric has buckets.'; hints.push({ type: 'HISTOGRAM_QUANTILE', label, fix: { label: 'Consider calculating aggregated quantile by adding histogram_quantile().', action: { type: 'ADD_HISTOGRAM_QUANTILE', query, }, } as QueryFix, }); } // Check for need of rate() if (query.indexOf('rate(') === -1 && query.indexOf('increase(') === -1) { // Use metric metadata for exact types const nameMatch = query.match(/\b(\w+_(total|sum|count))\b/); let counterNameMetric = nameMatch ? nameMatch[1] : ''; const metricsMetadata = datasource?.languageProvider?.metricsMetadata ?? {}; const metricMetadataKeys = Object.keys(metricsMetadata); let certain = false; if (metricMetadataKeys.length > 0) { counterNameMetric = metricMetadataKeys.find((metricName) => { // Only considering first type information, could be non-deterministic const metadata = metricsMetadata[metricName]; if (metadata.type.toLowerCase() === 'counter') { const metricRegex = new RegExp(`\\b${metricName}\\b`); if (query.match(metricRegex)) { certain = true; return true; } } return false; }) ?? ''; } 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 verb = certain ? 'is' : 'looks like'; let label = `Selected metric ${verb} a counter.`; let fix: QueryFix | undefined; if (fixableQuery) { fix = { label: 'Consider calculating rate of counter by adding rate().', action: { type: 'ADD_RATE', query, }, }; } else { label = `${label} Consider calculating rate of counter by adding rate().`; } hints.push({ type: 'APPLY_RATE', label, fix, }); } } // Check for recording rules expansion if (datasource && datasource.ruleMappings) { const mapping = datasource.ruleMappings; const mappingForQuery = Object.keys(mapping).reduce((acc, ruleName) => { if (query.search(ruleName) > -1) { return { ...acc, [ruleName]: mapping[ruleName], }; } return acc; }, {}); if (size(mappingForQuery) > 0) { const label = 'Query contains recording rules.'; hints.push({ type: 'EXPAND_RULES', label, fix: { label: 'Expand rules', action: { type: 'EXPAND_RULES', query, mapping: mappingForQuery, }, } as any as QueryFix, }); } } if (series && series.length >= SUM_HINT_THRESHOLD_COUNT) { const simpleMetric = query.trim().match(/^\w+$/); if (simpleMetric) { hints.push({ type: 'ADD_SUM', label: 'Many time series results returned.', fix: { label: 'Consider aggregating with sum().', action: { type: 'ADD_SUM', query: query, preventSubmit: true, }, } as QueryFix, }); } } return hints; } export function getInitHints(datasource: PrometheusDatasource): QueryHint[] { const hints = []; // Hint if using Loki as Prometheus data source if (datasource.directUrl.includes('/loki') && !datasource.languageProvider.metrics.length) { hints.push({ label: `Using Loki as a Prometheus data source is no longer supported. You must use the Loki data source for your Loki instance.`, type: 'INFO', }); } // Hint for big disabled lookups if (datasource.lookupsDisabled) { hints.push({ label: `Labels and metrics lookup was disabled in data source settings.`, type: 'INFO', }); } return hints; }