Prometheus: Add title to metrics in the metric select with metric help text (#46406)

* Add title to metrics in the select

* Add some comments

* Fix tests
This commit is contained in:
Andrej Ocenas 2022-03-10 16:58:25 +01:00 committed by GitHub
parent 146745b3bb
commit b3f8079f4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 78 additions and 29 deletions

View File

@ -7,6 +7,11 @@ export interface SelectableValue<T = any> {
value?: T;
imgUrl?: string;
icon?: string;
// Secondary text under the the title of the option.
description?: string;
// Adds a simple native title attribute to each option.
title?: string;
// Optional component that will be shown together with other options. Does not get past any props.
component?: React.ComponentType<any>;
[key: string]: any;
}

View File

@ -61,6 +61,7 @@ export const SelectMenuOptions: FC<SelectMenuOptionProps<any>> = ({
)}
{...innerProps}
aria-label="Select option"
title={data.title}
>
{data.icon && <Icon name={data.icon as IconName} className={styles.optionIcon} />}
{data.imgUrl && <img className={styles.optionImage} src={data.imgUrl} alt={data.label || data.value} />}

View File

@ -12,4 +12,5 @@ export class EmptyLanguageProviderMock {
fetchSeries = jest.fn().mockReturnValue([]);
fetchSeriesLabels = jest.fn().mockReturnValue([]);
fetchLabels = jest.fn();
loadMetricsMetadata = jest.fn();
}

View File

@ -63,12 +63,19 @@ 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];
item.documentation = `${type.toUpperCase()}: ${help}`;
item.documentation = getMetadataString(metric, metadata);
}
return item;
}
export function getMetadataString(metric: string, metadata: PromMetricsMetadata): string | undefined {
if (!metadata[metric]) {
return undefined;
}
const { type, help } = metadata[metric];
return `${type.toUpperCase()}: ${help}`;
}
const PREFIX_DELIMITER_REGEX =
/(="|!="|=~"|!~"|\{|\[|\(|\+|-|\/|\*|%|\^|\band\b|\bor\b|\bunless\b|==|>=|!=|<=|>|<|=|~|,)/;
@ -133,11 +140,15 @@ export default class PromQlLanguageProvider extends LanguageProvider {
// TODO #33976: make those requests parallel
await this.fetchLabels();
this.metrics = (await this.fetchLabelValues('__name__')) || [];
this.metricsMetadata = fixSummariesMetadata(await this.request('/api/v1/metadata', {}));
await this.loadMetricsMetadata();
this.histogramMetrics = processHistogramMetrics(this.metrics).sort();
return [];
};
async loadMetricsMetadata() {
this.metricsMetadata = fixSummariesMetadata(await this.request('/api/v1/metadata', {}));
}
getLabelKeys(): string[] {
return this.labelKeys;
}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import { MetricSelect } from './MetricSelect';
import { PromVisualQuery } from '../types';
import { LabelFilters } from '../shared/LabelFilters';
@ -11,6 +11,7 @@ import { QueryBuilderLabelFilter } from '../shared/types';
import { DataSourceApi, PanelData, SelectableValue } from '@grafana/data';
import { OperationsEditorRow } from '../shared/OperationsEditorRow';
import { PromQueryBuilderHints } from './PromQueryBuilderHints';
import { getMetadataString } from '../../language_provider';
export interface Props {
query: PromVisualQuery;
@ -26,18 +27,27 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
onChange({ ...query, labels });
};
const withTemplateVariableOptions = async (optionsPromise: Promise<string[]>): Promise<SelectableValue[]> => {
const variables = datasource.getVariables();
const options = await optionsPromise;
return [...variables, ...options].map((value) => ({ label: value, value }));
};
/**
* Map metric metadata to SelectableValue for Select component and also adds defined template variables to the list.
*/
const withTemplateVariableOptions = useCallback(
async (optionsPromise: Promise<Array<{ value: string; description?: string }>>): Promise<SelectableValue[]> => {
const variables = datasource.getVariables();
const options = await optionsPromise;
return [
...variables.map((value) => ({ label: value, value })),
...options.map((option) => ({ label: option.value, value: option.value, title: option.description })),
];
},
[datasource]
);
const onGetLabelNames = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<string[]> => {
const onGetLabelNames = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<Array<{ value: string }>> => {
// If no metric we need to use a different method
if (!query.metric) {
// Todo add caching but inside language provider!
await datasource.languageProvider.fetchLabels();
return datasource.languageProvider.getLabelKeys();
return datasource.languageProvider.getLabelKeys().map((k) => ({ value: k }));
}
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
@ -46,9 +56,9 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
const labelsIndex = await datasource.languageProvider.fetchSeriesLabels(expr);
// filter out already used labels
return Object.keys(labelsIndex).filter(
(labelName) => !labelsToConsider.find((filter) => filter.label === labelName)
);
return Object.keys(labelsIndex)
.filter((labelName) => !labelsToConsider.find((filter) => filter.label === labelName))
.map((k) => ({ value: k }));
};
const onGetLabelValues = async (forLabel: Partial<QueryBuilderLabelFilter>) => {
@ -58,7 +68,7 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
// If no metric we need to use a different method
if (!query.metric) {
return await datasource.languageProvider.getLabelValues(forLabel.label);
return (await datasource.languageProvider.getLabelValues(forLabel.label)).map((v) => ({ value: v }));
}
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
@ -66,26 +76,17 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
const expr = promQueryModeller.renderLabels(labelsToConsider);
const result = await datasource.languageProvider.fetchSeriesLabels(expr);
const forLabelInterpolated = datasource.interpolateString(forLabel.label);
return result[forLabelInterpolated] ?? [];
return result[forLabelInterpolated].map((v) => ({ value: v })) ?? [];
};
const onGetMetrics = async () => {
if (query.labels.length > 0) {
const expr = promQueryModeller.renderLabels(query.labels);
return (await datasource.languageProvider.getSeries(expr, true))['__name__'] ?? [];
} else {
return (await datasource.languageProvider.getLabelValues('__name__')) ?? [];
}
};
const onGetMetrics = useCallback(() => {
return withTemplateVariableOptions(getMetrics(datasource, query));
}, [datasource, query, withTemplateVariableOptions]);
return (
<>
<EditorRow>
<MetricSelect
query={query}
onChange={onChange}
onGetMetrics={() => withTemplateVariableOptions(onGetMetrics())}
/>
<MetricSelect query={query} onChange={onChange} onGetMetrics={onGetMetrics} />
<LabelFilters
labelsFilters={query.labels}
onChange={onChangeLabels}
@ -114,4 +115,34 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
);
});
/**
* Returns list of metrics, either all or filtered by query param. It also adds description string to each metric if it
* exists.
* @param datasource
* @param query
*/
async function getMetrics(
datasource: PrometheusDatasource,
query: PromVisualQuery
): Promise<Array<{ value: string; description?: string }>> {
// Makes sure we loaded the metadata for metrics. Usually this is done in the start() method of the provider but we
// don't use it with the visual builder and there is no need to run all the start() setup anyway.
if (!datasource.languageProvider.metricsMetadata) {
await datasource.languageProvider.loadMetricsMetadata();
}
let metrics;
if (query.labels.length > 0) {
const expr = promQueryModeller.renderLabels(query.labels);
metrics = (await datasource.languageProvider.getSeries(expr, true))['__name__'] ?? [];
} else {
metrics = (await datasource.languageProvider.getLabelValues('__name__')) ?? [];
}
return metrics.map((m) => ({
value: m,
description: getMetadataString(m, datasource.languageProvider.metricsMetadata!),
}));
}
PromQueryBuilder.displayName = 'PromQueryBuilder';