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
pull/99087/head
ismail simsek 6 months ago committed by GitHub
parent 4c0fa629da
commit 31deddafb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      .betterer.results
  2. 20
      packages/grafana-prometheus/src/add_label_to_query.test.ts
  3. 79
      packages/grafana-prometheus/src/querybuilder/PromQueryModeller.test.ts
  4. 99
      packages/grafana-prometheus/src/querybuilder/parsing.test.ts
  5. 38
      packages/grafana-prometheus/src/querybuilder/parsing.ts
  6. 19
      packages/grafana-prometheus/src/querybuilder/shared/LokiAndPromQueryModellerBase.ts
  7. 20
      public/app/features/explore/PrometheusListView/RawListItem.tsx

@ -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);

@ -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

Loading…
Cancel
Save