mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
7d21868931
commit
13073fa6ba
@ -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[];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user