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
This commit is contained in:
David 2019-12-17 11:06:43 +01:00 committed by GitHub
parent 7d21868931
commit 13073fa6ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 70 additions and 15 deletions

View File

@ -9,6 +9,8 @@ export interface CascaderOption {
children?: CascaderOption[]; children?: CascaderOption[];
disabled?: boolean; disabled?: boolean;
// Undocumented tooltip API
title?: string;
} }
export interface CascaderProps { export interface CascaderProps {

View File

@ -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', () => { it('returns options without prefix as toplevel option', () => {
expect(groupMetricsByPrefix(['metric'])).toMatchObject([ expect(groupMetricsByPrefix(['metric'])).toMatchObject([
{ {

View File

@ -15,7 +15,7 @@ import {
import Prism from 'prismjs'; import Prism from 'prismjs';
// dom also includes Element polyfills // 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 { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
import { ExploreQueryFieldProps, QueryHint, isDataFrame, toLegacyResponseData, HistoryItem } from '@grafana/data'; import { ExploreQueryFieldProps, QueryHint, isDataFrame, toLegacyResponseData, HistoryItem } from '@grafana/data';
import { DOMUtil, SuggestionsState } from '@grafana/ui'; import { DOMUtil, SuggestionsState } from '@grafana/ui';
@ -36,7 +36,16 @@ function getChooserText(hasSyntax: boolean, metrics: string[]) {
return 'Metrics'; 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 // Filter out recording rules and insert as first option
const ruleRegex = /:\w+:/; const ruleRegex = /:\w+:/;
const ruleNames = metrics.filter(metric => ruleRegex.test(metric)); 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 options = ruleNames.length > 0 ? [rulesOption] : [];
const delimiter = '_';
const metricsOptions = _.chain(metrics) const metricsOptions = _.chain(metrics)
.filter((metric: string) => !ruleRegex.test(metric)) .filter((metric: string) => !ruleRegex.test(metric))
.groupBy((metric: string) => metric.split(delimiter)[0]) .groupBy((metric: string) => metric.split(delimiter)[0])
.map( .map(
(metricsForPrefix: string[], prefix: string): CascaderOption => { (metricsForPrefix: string[], prefix: string): CascaderOption => {
const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix; 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 { return {
children, children,
label: prefix, label: prefix,
@ -228,13 +238,19 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
}; };
onUpdateLanguage = () => { onUpdateLanguage = () => {
const { histogramMetrics, metrics, lookupsDisabled, lookupMetricsThreshold } = this.languageProvider; const {
histogramMetrics,
metrics,
metricsMetadata,
lookupsDisabled,
lookupMetricsThreshold,
} = this.languageProvider;
if (!metrics) { if (!metrics) {
return; return;
} }
// Build metrics tree // Build metrics tree
const metricsByPrefix = groupMetricsByPrefix(metrics); const metricsByPrefix = groupMetricsByPrefix(metrics, metricsMetadata);
const histogramOptions = histogramMetrics.map((hm: any) => ({ label: hm, value: hm })); const histogramOptions = histogramMetrics.map((hm: any) => ({ label: hm, value: hm }));
const metricsOptions = const metricsOptions =
histogramMetrics.length > 0 histogramMetrics.length > 0

View File

@ -526,9 +526,11 @@ describe('Language completion provider', () => {
expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(0); expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(0);
await instance.start(); await instance.start();
expect(instance.lookupsDisabled).toBeTruthy(); 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); await instance.provideCompletionItems(args);
expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(1); expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(callCount);
}); });
}); });
}); });

View File

@ -8,7 +8,7 @@ import { parseSelector, processLabels, processHistogramLabels } from './language
import PromqlSyntax, { FUNCTIONS, RATE_RANGES } from './promql'; import PromqlSyntax, { FUNCTIONS, RATE_RANGES } from './promql';
import { PrometheusDatasource } from './datasource'; import { PrometheusDatasource } from './datasource';
import { PromQuery } from './types'; import { PromQuery, PromMetricsMetadata } from './types';
const DEFAULT_KEYS = ['job', 'instance']; const DEFAULT_KEYS = ['job', 'instance'];
const EMPTY_SELECTOR = '{}'; 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 { export default class PromQlLanguageProvider extends LanguageProvider {
histogramMetrics?: string[]; histogramMetrics?: string[];
timeRange?: { start: number; end: number }; timeRange?: { start: number; end: number };
metrics?: string[]; metrics?: string[];
metricsMetadata?: PromMetricsMetadata;
startTask: Promise<any>; startTask: Promise<any>;
datasource: PrometheusDatasource; datasource: PrometheusDatasource;
lookupMetricsThreshold: number; lookupMetricsThreshold: number;
@ -85,7 +95,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
return PromqlSyntax; return PromqlSyntax;
} }
request = async (url: string) => { request = async (url: string, defaultValue: any): Promise<any> => {
try { try {
const res = await this.datasource.metadataRequest(url); const res = await this.datasource.metadataRequest(url);
const body = await (res.data || res.json()); const body = await (res.data || res.json());
@ -95,12 +105,13 @@ export default class PromQlLanguageProvider extends LanguageProvider {
console.error(error); console.error(error);
} }
return []; return defaultValue;
}; };
start = async (): Promise<any[]> => { start = async (): Promise<any[]> => {
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.lookupsDisabled = this.metrics.length > this.lookupMetricsThreshold;
this.metricsMetadata = await this.request('/api/v1/metadata', {});
this.processHistogramMetrics(this.metrics); this.processHistogramMetrics(this.metrics);
return []; return [];
}; };
@ -197,7 +208,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
}; };
getTermCompletionItems = (): TypeaheadOutput => { getTermCompletionItems = (): TypeaheadOutput => {
const { metrics } = this; const { metrics, metricsMetadata } = this;
const suggestions = []; const suggestions = [];
suggestions.push({ suggestions.push({
@ -209,7 +220,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
if (metrics && metrics.length) { if (metrics && metrics.length) {
suggestions.push({ suggestions.push({
label: 'Metrics', 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<Record<string, string[]>> => { fetchLabelValues = async (key: string): Promise<Record<string, string[]>> => {
const data = await this.request(`/api/v1/label/${key}/values`); const data = await this.request(`/api/v1/label/${key}/values`, []);
return { [key]: data }; return { [key]: data };
}; };
@ -386,7 +397,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
)}&end=${this.roundToMinutes(tRange['end'])}&withName=${!!withName}`; )}&end=${this.roundToMinutes(tRange['end'])}&withName=${!!withName}`;
let value = this.labelsCache.get(cacheKey); let value = this.labelsCache.get(cacheKey);
if (!value) { if (!value) {
const data = await this.request(url); const data = await this.request(url, []);
const { values } = processLabels(data, withName); const { values } = processLabels(data, withName);
value = values; value = values;
this.labelsCache.set(cacheKey, value); this.labelsCache.set(cacheKey, value);

View File

@ -35,3 +35,13 @@ export interface PromQueryRequest extends PromQuery {
end: number; end: number;
headers?: any; headers?: any;
} }
export interface PromMetricsMetadataItem {
type: string;
help: string;
unit?: string;
}
export interface PromMetricsMetadata {
[metric: string]: PromMetricsMetadataItem[];
}