Commit 13073fa6 by David Committed by GitHub

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
parent 7d218689
......@@ -9,6 +9,8 @@ export interface CascaderOption {
children?: CascaderOption[];
disabled?: boolean;
// Undocumented tooltip API
title?: string;
}
export interface CascaderProps {
......
......@@ -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([
{
......
......@@ -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<PromQueryFieldProps, PromQueryF
};
onUpdateLanguage = () => {
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
......
......@@ -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);
});
});
});
......
......@@ -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<any>;
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<any> => {
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<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.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<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 };
};
......@@ -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);
......
......@@ -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[];
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment