Prometheus: Support utf8 labels and metrics on visual query builder (#98274)
* utf8 metrics for prometheus devenv * introduce utf8 support * completions and suggestions * don't wrap the utf8 label in quotes * linting * support utf8 labels and metrics on visual query builder * lint * update raw view for utf8 metric syntax * betterer * support series endpoint * support series endpoint * betterer
This commit is contained in:
+3
-1
@@ -4799,7 +4799,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/explore/PrometheusListView/RawListItem.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/explore/PrometheusListView/RawListItemAttributes.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
|
||||
@@ -112,4 +112,24 @@ describe('addLabelToQuery()', () => {
|
||||
it('should not add ad-hoc filter bool operator', () => {
|
||||
expect(addLabelToQuery('ALERTS < bool 1', 'bar', 'baz')).toBe('ALERTS{bar="baz"} < bool 1');
|
||||
});
|
||||
|
||||
it('should add a utf8 label', () => {
|
||||
expect(addLabelToQuery('{"metric.name"}', 'cenk.erdem', 'muhabbet')).toBe(
|
||||
'{"metric.name", "cenk.erdem"="muhabbet"}'
|
||||
);
|
||||
|
||||
expect(addLabelToQuery('metric{label="val"}', 'cenk.erdem', 'muhabbet')).toBe(
|
||||
'metric{label="val", "cenk.erdem"="muhabbet"}'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add a utf8 label when it is already applied', () => {
|
||||
expect(addLabelToQuery('{"metric.name", "cenk.erdem"="muhabbet"}', 'cenk.erdem', 'muhabbet')).toBe(
|
||||
'{"metric.name", "cenk.erdem"="muhabbet"}'
|
||||
);
|
||||
|
||||
expect(addLabelToQuery('metric{label="val", "cenk.erdem"="muhabbet"}', 'cenk.erdem', 'muhabbet')).toBe(
|
||||
'metric{label="val", "cenk.erdem"="muhabbet"}'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -334,3 +334,82 @@ describe('PromQueryModeller', () => {
|
||||
).toBe('cluster_namespace_slug_dialer_name <= bool 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PromQueryModeller with utf8 support', () => {
|
||||
const modeller = new PromQueryModeller();
|
||||
|
||||
it('should render nothing if there is nothing', () => {
|
||||
expect(
|
||||
modeller.renderQuery({
|
||||
metric: undefined,
|
||||
labels: [],
|
||||
operations: [],
|
||||
})
|
||||
).toBe('');
|
||||
|
||||
expect(
|
||||
modeller.renderQuery({
|
||||
metric: '',
|
||||
labels: [],
|
||||
operations: [],
|
||||
})
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
it('should render legacy metric name as usual', () => {
|
||||
expect(
|
||||
modeller.renderQuery({
|
||||
metric: 'not_a_utf8_metric',
|
||||
labels: [],
|
||||
operations: [],
|
||||
})
|
||||
).toBe('not_a_utf8_metric');
|
||||
});
|
||||
|
||||
it('can render utf8 metric name in curly braces', () => {
|
||||
expect(
|
||||
modeller.renderQuery({
|
||||
metric: 'a.utf8.metric',
|
||||
labels: [],
|
||||
operations: [],
|
||||
})
|
||||
).toBe('{"a.utf8.metric"}');
|
||||
});
|
||||
|
||||
it('can render utf8 metric name in curly braces with legacy labels', () => {
|
||||
expect(
|
||||
modeller.renderQuery({
|
||||
metric: 'a.utf8.metric',
|
||||
labels: [
|
||||
{
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
op: '=',
|
||||
},
|
||||
],
|
||||
operations: [],
|
||||
})
|
||||
).toBe('{"a.utf8.metric", label="value"}');
|
||||
});
|
||||
|
||||
it('can render utf8 metric name in curly braces with legacy and utf8 labels', () => {
|
||||
expect(
|
||||
modeller.renderQuery({
|
||||
metric: 'a.utf8.metric',
|
||||
labels: [
|
||||
{
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
op: '=',
|
||||
},
|
||||
{
|
||||
label: 'utf8.label',
|
||||
value: 'value',
|
||||
op: '=',
|
||||
},
|
||||
],
|
||||
operations: [],
|
||||
})
|
||||
).toBe('{"a.utf8.metric", label="value", "utf8.label"="value"}');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,80 @@ import { buildVisualQueryFromString } from './parsing';
|
||||
import { PromOperationId, PromVisualQuery } from './types';
|
||||
|
||||
describe('buildVisualQueryFromString', () => {
|
||||
describe('utf8 support', () => {
|
||||
it('supports uts-8 label names', () => {
|
||||
expect(buildVisualQueryFromString('{"glück:🍀.dot"="luck"} == 11')).toEqual({
|
||||
query: {
|
||||
labels: [
|
||||
{
|
||||
label: 'glück:🍀.dot',
|
||||
op: '=',
|
||||
value: 'luck',
|
||||
},
|
||||
],
|
||||
metric: '',
|
||||
operations: [
|
||||
{
|
||||
id: PromOperationId.EqualTo,
|
||||
params: [11, false],
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('supports uts-8 metric names', () => {
|
||||
expect(buildVisualQueryFromString('{"I am a metric"}')).toEqual({
|
||||
query: {
|
||||
labels: [],
|
||||
metric: 'I am a metric',
|
||||
operations: [],
|
||||
},
|
||||
errors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('supports uts-8 metric names with labels', () => {
|
||||
expect(buildVisualQueryFromString('{"metric.name", label_field="label value"}')).toEqual({
|
||||
query: {
|
||||
labels: [
|
||||
{
|
||||
label: 'label_field',
|
||||
op: '=',
|
||||
value: 'label value',
|
||||
},
|
||||
],
|
||||
metric: 'metric.name',
|
||||
operations: [],
|
||||
},
|
||||
errors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('supports uts-8 metric names with utf8 labels', () => {
|
||||
expect(buildVisualQueryFromString('{"metric.name", "glück:🍀.dot"="luck"} == 11')).toEqual({
|
||||
query: {
|
||||
labels: [
|
||||
{
|
||||
label: 'glück:🍀.dot',
|
||||
op: '=',
|
||||
value: 'luck',
|
||||
},
|
||||
],
|
||||
metric: 'metric.name',
|
||||
operations: [
|
||||
{
|
||||
id: PromOperationId.EqualTo,
|
||||
params: [11, false],
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('creates no errors for empty query', () => {
|
||||
expect(buildVisualQueryFromString('')).toEqual(
|
||||
noErrors({
|
||||
@@ -246,6 +320,31 @@ describe('buildVisualQueryFromString', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('parses query with aggregation by utf8 labels', () => {
|
||||
const visQuery = {
|
||||
metric: 'metric_name',
|
||||
labels: [
|
||||
{
|
||||
label: 'instance',
|
||||
op: '=',
|
||||
value: 'internal:3000',
|
||||
},
|
||||
],
|
||||
operations: [
|
||||
{
|
||||
id: '__sum_by',
|
||||
params: ['cluster', '"app.version"'],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(
|
||||
buildVisualQueryFromString('sum(metric_name{instance="internal:3000"}) by ("app.version", cluster)')
|
||||
).toEqual(noErrors(visQuery));
|
||||
expect(
|
||||
buildVisualQueryFromString('sum by ("app.version", cluster)(metric_name{instance="internal:3000"})')
|
||||
).toEqual(noErrors(visQuery));
|
||||
});
|
||||
|
||||
it('parses aggregation with params', () => {
|
||||
expect(buildVisualQueryFromString('topk(5, http_requests_total)')).toEqual(
|
||||
noErrors({
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
GroupingLabels,
|
||||
Identifier,
|
||||
LabelName,
|
||||
QuotedLabelName,
|
||||
MatchingModifierClause,
|
||||
MatchOp,
|
||||
NumberDurationLiteral,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
ParenExpr,
|
||||
parser,
|
||||
StringLiteral,
|
||||
QuotedLabelMatcher,
|
||||
UnquotedLabelMatcher,
|
||||
VectorSelector,
|
||||
Without,
|
||||
@@ -145,9 +147,33 @@ export function handleExpression(expr: string, node: SyntaxNode, context: Contex
|
||||
break;
|
||||
}
|
||||
|
||||
case QuotedLabelName: {
|
||||
// Usually we got the metric name above in the Identifier case.
|
||||
// If we didn't get the name that's potentially we have it in curly braces as quoted string.
|
||||
// It must be quoted because that's how utf8 metric names should be defined
|
||||
// See proposal https://github.com/prometheus/proposals/blob/main/proposals/2023-08-21-utf8.md
|
||||
if (visQuery.metric === '') {
|
||||
const strLiteral = node.getChild(StringLiteral);
|
||||
const quotedMetric = getString(expr, strLiteral);
|
||||
visQuery.metric = quotedMetric.slice(1, -1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case QuotedLabelMatcher: {
|
||||
const quotedLabel = getLabel(expr, node, QuotedLabelName);
|
||||
quotedLabel.label = quotedLabel.label.slice(1, -1);
|
||||
visQuery.labels.push(quotedLabel);
|
||||
const err = node.getChild(ErrorId);
|
||||
if (err) {
|
||||
context.errors.push(makeError(expr, err));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case UnquotedLabelMatcher: {
|
||||
// Same as MetricIdentifier should be just one per query.
|
||||
visQuery.labels.push(getLabel(expr, node));
|
||||
visQuery.labels.push(getLabel(expr, node, LabelName));
|
||||
const err = node.getChild(ErrorId);
|
||||
if (err) {
|
||||
context.errors.push(makeError(expr, err));
|
||||
@@ -202,8 +228,12 @@ function isIntervalVariableError(node: SyntaxNode) {
|
||||
return node.prevSibling?.firstChild?.type.id === VectorSelector;
|
||||
}
|
||||
|
||||
function getLabel(expr: string, node: SyntaxNode): QueryBuilderLabelFilter {
|
||||
const label = getString(expr, node.getChild(LabelName));
|
||||
function getLabel(
|
||||
expr: string,
|
||||
node: SyntaxNode,
|
||||
labelType: typeof LabelName | typeof QuotedLabelName
|
||||
): QueryBuilderLabelFilter {
|
||||
const label = getString(expr, node.getChild(labelType));
|
||||
const op = getString(expr, node.getChild(MatchOp));
|
||||
const value = getString(expr, node.getChild(StringLiteral)).replace(/^["'`]|["'`]$/g, '');
|
||||
return {
|
||||
@@ -281,7 +311,7 @@ function handleAggregation(expr: string, node: SyntaxNode, context: Context) {
|
||||
funcName = `__${funcName}_without`;
|
||||
}
|
||||
|
||||
labels.push(...getAllByType(expr, modifier, LabelName));
|
||||
labels.push(...getAllByType(expr, modifier, LabelName), ...getAllByType(expr, modifier, QuotedLabelName));
|
||||
}
|
||||
|
||||
const body = node.getChild(FunctionCallBody);
|
||||
|
||||
+17
-2
@@ -3,6 +3,7 @@ import { Registry } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { prometheusRegularEscape } from '../../datasource';
|
||||
import { isValidLegacyName, utf8Support } from '../../utf8_support';
|
||||
import { PromVisualQueryOperationCategory } from '../types';
|
||||
|
||||
import { QueryBuilderLabelFilter, QueryBuilderOperation, QueryBuilderOperationDef, VisualQueryModeller } from './types';
|
||||
@@ -97,14 +98,28 @@ export abstract class LokiAndPromQueryModellerBase implements VisualQueryModelle
|
||||
if (config.featureToggles.prometheusSpecialCharsInLabelValues && !usingRegexOperator) {
|
||||
labelValue = prometheusRegularEscape(labelValue);
|
||||
}
|
||||
expr += `${filter.label}${filter.op}"${labelValue}"`;
|
||||
expr += `${utf8Support(filter.label)}${filter.op}"${labelValue}"`;
|
||||
}
|
||||
|
||||
return expr + `}`;
|
||||
}
|
||||
|
||||
renderQuery(query: PromLokiVisualQuery, nested?: boolean) {
|
||||
let queryString = `${query.metric ?? ''}${this.renderLabels(query.labels)}`;
|
||||
let queryString = '';
|
||||
const labels = this.renderLabels(query.labels);
|
||||
if (query.metric) {
|
||||
if (isValidLegacyName(query.metric)) {
|
||||
// This is a legacy metric, put outside the curl legacy_query{label="value"}
|
||||
queryString = `${query.metric}${labels}`;
|
||||
} else {
|
||||
// This is a utf8 metric, put inside the curly and quotes {"utf8.metric", label="value"}
|
||||
queryString = `{"${query.metric}"${labels.length > 0 ? `, ${labels.substring(1)}` : `}`}`;
|
||||
}
|
||||
} else {
|
||||
// No metric just use labels {label="value"}
|
||||
queryString = labels;
|
||||
}
|
||||
|
||||
queryString = this.renderOperations(queryString, query.operations);
|
||||
|
||||
if (!nested && this.hasBinaryOp(query) && Boolean(query.binaryQueries?.length)) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
import { Field, GrafanaTheme2 } from '@grafana/data/';
|
||||
import { isValidLegacyName, utf8Support } from '@grafana/prometheus/src/utf8_support';
|
||||
import { reportInteraction } from '@grafana/runtime/src';
|
||||
import { IconButton, useStyles2 } from '@grafana/ui/';
|
||||
|
||||
@@ -94,6 +95,8 @@ function getQueryValues(allLabels: Pick<instantQueryRawVirtualizedListData, 'Val
|
||||
|
||||
const RawListItem = ({ listItemData, listKey, totalNumberOfValues, valueLabels, isExpandedView }: RawListProps) => {
|
||||
const { __name__, ...allLabels } = listItemData;
|
||||
// We must know whether it is a utf8 metric name or not
|
||||
const isLegacyMetric = isValidLegacyName(__name__);
|
||||
const [_, copyToClipboard] = useCopyToClipboard();
|
||||
const displayLength = valueLabels?.length ?? totalNumberOfValues;
|
||||
const styles = useStyles2(getStyles, displayLength, isExpandedView);
|
||||
@@ -110,10 +113,12 @@ const RawListItem = ({ listItemData, listKey, totalNumberOfValues, valueLabels,
|
||||
};
|
||||
|
||||
// Convert the object back into a string
|
||||
const stringRep = `${__name__}{${attributeValues.map((value) => {
|
||||
// For histograms the string representation currently in this object is not directly queryable in all situations, leading to broken copied queries. Omitting the attribute from the copied result gives a query which returns all le values, which I assume to be a more common use case.
|
||||
return `${value.key}="${transformCopyValue(value.value)}"`;
|
||||
})}}`;
|
||||
const stringRep = `${isLegacyMetric ? __name__ : ''}{${isLegacyMetric ? '' : `"${__name__}", `}${attributeValues.map(
|
||||
(value) => {
|
||||
// For histograms the string representation currently in this object is not directly queryable in all situations, leading to broken copied queries. Omitting the attribute from the copied result gives a query which returns all le values, which I assume to be a more common use case.
|
||||
return `${utf8Support(value.key)}="${transformCopyValue(value.value)}"`;
|
||||
}
|
||||
)}}`;
|
||||
|
||||
const hideFieldsWithoutValues = Boolean(valueLabels && valueLabels?.length);
|
||||
|
||||
@@ -135,8 +140,13 @@ const RawListItem = ({ listItemData, listKey, totalNumberOfValues, valueLabels,
|
||||
</span>
|
||||
<span role={'cell'} className={styles.rowLabelWrapWrap}>
|
||||
<div className={styles.rowLabelWrap}>
|
||||
<span>{__name__}</span>
|
||||
{isLegacyMetric && <span>{__name__}</span>}
|
||||
<span>{`{`}</span>
|
||||
{!isLegacyMetric && __name__ !== '' && (
|
||||
<span>
|
||||
"{__name__}"{', '}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{attributeValues.map((value, index) => (
|
||||
<RawListItemAttributes
|
||||
|
||||
Reference in New Issue
Block a user