From 13073fa6baf2ed2c9023cf3ce6d78568718d1529 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 17 Dec 2019 11:06:43 +0100 Subject: [PATCH] Prometheus: Display HELP and TYPE of metrics if available (#21124) * Prometheus: Display HELP and TYPE of metrics if available - Prometheus recently added a metadata API around HELP and TYPE of metrics - request metadata when datasource instance is created - use metadata to show help and type in typeahead suggestions and in metrics selector as tooltip * Fix types --- .../src/components/Cascader/Cascader.tsx | 2 ++ .../components/PromQueryField.test.tsx | 14 ++++++++++ .../prometheus/components/PromQueryField.tsx | 26 ++++++++++++++---- .../prometheus/language_provider.test.ts | 6 +++-- .../prometheus/language_provider.ts | 27 +++++++++++++------ .../plugins/datasource/prometheus/types.ts | 10 +++++++ 6 files changed, 70 insertions(+), 15 deletions(-) diff --git a/packages/grafana-ui/src/components/Cascader/Cascader.tsx b/packages/grafana-ui/src/components/Cascader/Cascader.tsx index 50fa30cce68..a30d69fcc79 100644 --- a/packages/grafana-ui/src/components/Cascader/Cascader.tsx +++ b/packages/grafana-ui/src/components/Cascader/Cascader.tsx @@ -9,6 +9,8 @@ export interface CascaderOption { children?: CascaderOption[]; disabled?: boolean; + // Undocumented tooltip API + title?: string; } export interface CascaderProps { diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx index bc7eb301be1..876ed0005fd 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx @@ -18,6 +18,20 @@ describe('groupMetricsByPrefix()', () => { ]); }); + it('returns options grouped by prefix with metadata', () => { + expect(groupMetricsByPrefix(['foo_metric'], { foo_metric: [{ type: 'TYPE', help: 'my help' }] })).toMatchObject([ + { + value: 'foo', + children: [ + { + value: 'foo_metric', + title: 'foo_metric\nTYPE\nmy help', + }, + ], + }, + ]); + }); + it('returns options without prefix as toplevel option', () => { expect(groupMetricsByPrefix(['metric'])).toMatchObject([ { diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index cffcaeaf223..b27dd7bc358 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -15,7 +15,7 @@ import { import Prism from 'prismjs'; // dom also includes Element polyfills -import { PromQuery, PromContext, PromOptions } from '../types'; +import { PromQuery, PromContext, PromOptions, PromMetricsMetadata } from '../types'; import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise'; import { ExploreQueryFieldProps, QueryHint, isDataFrame, toLegacyResponseData, HistoryItem } from '@grafana/data'; import { DOMUtil, SuggestionsState } from '@grafana/ui'; @@ -36,7 +36,16 @@ function getChooserText(hasSyntax: boolean, metrics: string[]) { return 'Metrics'; } -export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): CascaderOption[] { +function addMetricsMetadata(metric: string, metadata?: PromMetricsMetadata): CascaderOption { + const option: CascaderOption = { label: metric, value: metric }; + if (metadata && metadata[metric]) { + const { type = '', help } = metadata[metric][0]; + option.title = [metric, type.toUpperCase(), help].join('\n'); + } + return option; +} + +export function groupMetricsByPrefix(metrics: string[], metadata?: PromMetricsMetadata): CascaderOption[] { // Filter out recording rules and insert as first option const ruleRegex = /:\w+:/; const ruleNames = metrics.filter(metric => ruleRegex.test(metric)); @@ -51,13 +60,14 @@ export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): Cascad const options = ruleNames.length > 0 ? [rulesOption] : []; + const delimiter = '_'; const metricsOptions = _.chain(metrics) .filter((metric: string) => !ruleRegex.test(metric)) .groupBy((metric: string) => metric.split(delimiter)[0]) .map( (metricsForPrefix: string[], prefix: string): CascaderOption => { const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix; - const children = prefixIsMetric ? [] : metricsForPrefix.sort().map(m => ({ label: m, value: m })); + const children = prefixIsMetric ? [] : metricsForPrefix.sort().map(m => addMetricsMetadata(m, metadata)); return { children, label: prefix, @@ -228,13 +238,19 @@ class PromQueryField extends React.PureComponent { - const { histogramMetrics, metrics, lookupsDisabled, lookupMetricsThreshold } = this.languageProvider; + const { + histogramMetrics, + metrics, + metricsMetadata, + lookupsDisabled, + lookupMetricsThreshold, + } = this.languageProvider; if (!metrics) { return; } // Build metrics tree - const metricsByPrefix = groupMetricsByPrefix(metrics); + const metricsByPrefix = groupMetricsByPrefix(metrics, metricsMetadata); const histogramOptions = histogramMetrics.map((hm: any) => ({ label: hm, value: hm })); const metricsOptions = histogramMetrics.length > 0 diff --git a/public/app/plugins/datasource/prometheus/language_provider.test.ts b/public/app/plugins/datasource/prometheus/language_provider.test.ts index 1cf3420b997..08df3ba0d5a 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.test.ts @@ -526,9 +526,11 @@ describe('Language completion provider', () => { expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(0); await instance.start(); expect(instance.lookupsDisabled).toBeTruthy(); - expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(1); + // Capture request count to metadata + const callCount = (datasource.metadataRequest as Mock).mock.calls.length; + expect((datasource.metadataRequest as Mock).mock.calls.length).toBeGreaterThan(0); await instance.provideCompletionItems(args); - expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(1); + expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(callCount); }); }); }); diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index 15394c9f8bf..a3170731805 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -8,7 +8,7 @@ import { parseSelector, processLabels, processHistogramLabels } from './language import PromqlSyntax, { FUNCTIONS, RATE_RANGES } from './promql'; import { PrometheusDatasource } from './datasource'; -import { PromQuery } from './types'; +import { PromQuery, PromMetricsMetadata } from './types'; const DEFAULT_KEYS = ['job', 'instance']; const EMPTY_SELECTOR = '{}'; @@ -41,10 +41,20 @@ export function addHistoryMetadata(item: CompletionItem, history: any[]): Comple }; } +function addMetricsMetadata(metric: string, metadata?: PromMetricsMetadata): CompletionItem { + const item: CompletionItem = { label: metric }; + if (metadata && metadata[metric]) { + const { type, help } = metadata[metric][0]; + item.documentation = `${type.toUpperCase()}: ${help}`; + } + return item; +} + export default class PromQlLanguageProvider extends LanguageProvider { histogramMetrics?: string[]; timeRange?: { start: number; end: number }; metrics?: string[]; + metricsMetadata?: PromMetricsMetadata; startTask: Promise; datasource: PrometheusDatasource; lookupMetricsThreshold: number; @@ -85,7 +95,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { return PromqlSyntax; } - request = async (url: string) => { + request = async (url: string, defaultValue: any): Promise => { try { const res = await this.datasource.metadataRequest(url); const body = await (res.data || res.json()); @@ -95,12 +105,13 @@ export default class PromQlLanguageProvider extends LanguageProvider { console.error(error); } - return []; + return defaultValue; }; start = async (): Promise => { - this.metrics = await this.request('/api/v1/label/__name__/values'); + this.metrics = await this.request('/api/v1/label/__name__/values', []); this.lookupsDisabled = this.metrics.length > this.lookupMetricsThreshold; + this.metricsMetadata = await this.request('/api/v1/metadata', {}); this.processHistogramMetrics(this.metrics); return []; }; @@ -197,7 +208,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { }; getTermCompletionItems = (): TypeaheadOutput => { - const { metrics } = this; + const { metrics, metricsMetadata } = this; const suggestions = []; suggestions.push({ @@ -209,7 +220,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { if (metrics && metrics.length) { suggestions.push({ label: 'Metrics', - items: metrics.map(wrapLabel), + items: metrics.map(m => addMetricsMetadata(m, metricsMetadata)), }); } @@ -360,7 +371,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { } fetchLabelValues = async (key: string): Promise> => { - const data = await this.request(`/api/v1/label/${key}/values`); + const data = await this.request(`/api/v1/label/${key}/values`, []); return { [key]: data }; }; @@ -386,7 +397,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { )}&end=${this.roundToMinutes(tRange['end'])}&withName=${!!withName}`; let value = this.labelsCache.get(cacheKey); if (!value) { - const data = await this.request(url); + const data = await this.request(url, []); const { values } = processLabels(data, withName); value = values; this.labelsCache.set(cacheKey, value); diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts index 92d6140e664..4199370a39f 100644 --- a/public/app/plugins/datasource/prometheus/types.ts +++ b/public/app/plugins/datasource/prometheus/types.ts @@ -35,3 +35,13 @@ export interface PromQueryRequest extends PromQuery { end: number; headers?: any; } + +export interface PromMetricsMetadataItem { + type: string; + help: string; + unit?: string; +} + +export interface PromMetricsMetadata { + [metric: string]: PromMetricsMetadataItem[]; +}