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[];
|
children?: CascaderOption[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
// Undocumented tooltip API
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CascaderProps {
|
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', () => {
|
it('returns options without prefix as toplevel option', () => {
|
||||||
expect(groupMetricsByPrefix(['metric'])).toMatchObject([
|
expect(groupMetricsByPrefix(['metric'])).toMatchObject([
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
@ -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[];
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user