diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f94c3b7..1acb6a9b 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## tip +* FEATURE: add support metrics with special characters in query builder. See [this issue](https://github.com/VictoriaMetrics/grafana-datasource/issues/131) + * BUGFIX: fix the default link to vmui. See [this issue](https://github.com/VictoriaMetrics/grafana-datasource/issues/132) * BUGFIX: fix the parsing logic in `renderLegendFormat` to correctly replace legend label names. See [this issue](https://github.com/VictoriaMetrics/grafana-datasource/issues/133) * BUGFIX: fix query editor which produce a lot of requests for alerting rule evaluation. See [this issue](https://github.com/VictoriaMetrics/grafana-datasource/issues/134) diff --git a/package.json b/package.json index 22e888ab..480b6499 100755 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "server": "docker-compose up --build", "tar": "tar -czf $npm_package_name-v$npm_package_version.tar.gz $npm_package_name && sha1sum ./$npm_package_name-v$npm_package_version.tar.gz >$npm_package_name-v$npm_package_version.tar.gz.sha1", "zip": "zip $npm_package_name-v$npm_package_version.zip $npm_package_name -r && sha1sum ./$npm_package_name-v$npm_package_version.zip >$npm_package_name-v$npm_package_version.zip.sha1", - "preinstall": "cd packages/lezer-metricsql && yarn install" + "preinstall": "cd packages/lezer-metricsql && yarn install && cd ../.. && yarn upgrade lezer-metricsql" }, "author": "VictoriaMetrics", "license": "AGPL-3.0-only", diff --git a/packages/lezer-metricsql/package.json b/packages/lezer-metricsql/package.json index 3c93af80..a3d54cdf 100644 --- a/packages/lezer-metricsql/package.json +++ b/packages/lezer-metricsql/package.json @@ -1,6 +1,6 @@ { "name": "lezer-metricsql", - "version": "0.1.1", + "version": "0.1.2", "main": "index.cjs", "type": "module", "exports": { diff --git a/packages/lezer-metricsql/src/metricsql.grammar b/packages/lezer-metricsql/src/metricsql.grammar index ced82c4c..9ef89eab 100644 --- a/packages/lezer-metricsql/src/metricsql.grammar +++ b/packages/lezer-metricsql/src/metricsql.grammar @@ -395,7 +395,9 @@ NumberLiteral { ( ( std.digit+ "y" )? ( std.digit+ "w" )? ( std.digit+ "d" )? ( std.digit+ "h" )? ( std.digit+ "m" )? ( std.digit+ "s" ) ( std.digit+ "ms" )? ) | ( ( std.digit+ "y" )? ( std.digit+ "w" )? ( std.digit+ "d" )? ( std.digit+ "h" )? ( std.digit+ "m" )? ( std.digit+ "s" )? ( std.digit+ "ms" ) ) } - Identifier { (std.asciiLetter | "_" | ":" | ".") (std.asciiLetter | std.digit | "_" | ":" | ".")*} + EscapedChar {("\\" AnyEscapesChar) ("\\" AnyEscapesChar)*} + AnyEscapesChar { "-" | "+" | "*" | "/" | "%" | "^" | "=" } + Identifier {(std.asciiLetter | "_" | ":" | "." | EscapedChar) (std.asciiLetter | std.digit | "_" | ":" | "." | "-" | EscapedChar)*} LabelName { (std.asciiLetter | "_") (std.asciiLetter | std.digit | "_")* } // Operator diff --git a/src/components/monaco-query-field/monaco-completion-provider/completions.ts b/src/components/monaco-query-field/monaco-completion-provider/completions.ts index 37116251..0a8ff844 100755 --- a/src/components/monaco-query-field/monaco-completion-provider/completions.ts +++ b/src/components/monaco-query-field/monaco-completion-provider/completions.ts @@ -23,7 +23,15 @@ import type { Situation, Label } from './situation'; import { NeverCaseError } from './util'; // FIXME: we should not load this from the "outside", but we cannot do that while we have the "old" query-field too -export type CompletionType = 'HISTORY' | 'FUNCTION' | 'METRIC_NAME' | 'WITH_TEMPLATE' | 'DURATION' | 'LABEL_NAME' | 'LABEL_VALUE'; +export enum CompletionType { + history = 'HISTORY', + function = 'FUNCTION', + metricName = 'METRIC_NAME', + withTemplate = 'WITH_TEMPLATE', + duration = 'DURATION', + labelName = 'LABEL_NAME', + labelValue = 'LABEL_VALUE', +} type Completion = { type: CompletionType; @@ -60,7 +68,7 @@ export type DataProvider = { async function getAllMetricNamesCompletions(dataProvider: DataProvider): Promise { const metrics = await dataProvider.getAllMetricNames(); return metrics.map((metric) => ({ - type: 'METRIC_NAME', + type: CompletionType.metricName, label: metric.name, insertText: metric.name, detail: `${metric.name} : ${metric.type}`, @@ -71,7 +79,7 @@ async function getAllMetricNamesCompletions(dataProvider: DataProvider): Promise async function getAllWithTemplatesCompletions(dataProvider: DataProvider): Promise { const metrics = await dataProvider.getAllWithTemplates(); return metrics.map((metric) => ({ - type: 'WITH_TEMPLATE', + type: CompletionType.withTemplate, label: metric.name, insertText: metric.name, detail: metric.value, @@ -80,7 +88,7 @@ async function getAllWithTemplatesCompletions(dataProvider: DataProvider): Promi } const FUNCTION_COMPLETIONS: Completion[] = FUNCTIONS.map((f) => ({ - type: 'FUNCTION', + type: CompletionType.function, label: f.label, insertText: f.insertText ?? '', // i don't know what to do when this is nullish. it should not be. detail: f.detail, @@ -104,7 +112,7 @@ const DURATION_COMPLETIONS: Completion[] = [ '1h', '1d', ].map((text) => ({ - type: 'DURATION', + type: CompletionType.duration, label: text, insertText: text, })); @@ -115,7 +123,7 @@ async function getAllHistoryCompletions(dataProvider: DataProvider): Promise ({ - type: 'HISTORY', + type: CompletionType.history, label: expr, insertText: expr, })); @@ -162,7 +170,7 @@ async function getLabelNamesForCompletions( ): Promise { const labelNames = await getLabelNames(metric, otherLabels, dataProvider); const labelNamesCompletion: Completion[] = labelNames.map((text) => ({ - type: 'LABEL_NAME', + type: CompletionType.labelName, label: text, insertText: `${text}${suffix}`, triggerOnInsert, @@ -215,7 +223,7 @@ async function getLabelValuesForMetricCompletions( ): Promise { const values = await getLabelValues(metric, labelName, otherLabels, dataProvider); return values.map((text) => ({ - type: 'LABEL_VALUE', + type: CompletionType.labelValue, label: text, insertText: betweenQuotes ? text : `"${text}"`, // FIXME: escaping strange characters? })); diff --git a/src/components/monaco-query-field/monaco-completion-provider/index.ts b/src/components/monaco-query-field/monaco-completion-provider/index.ts index b135ec20..800c9763 100755 --- a/src/components/monaco-query-field/monaco-completion-provider/index.ts +++ b/src/components/monaco-query-field/monaco-completion-provider/index.ts @@ -20,7 +20,9 @@ import { IMarkdownString } from "monaco-editor"; import type { Monaco, monacoTypes } from '@grafana/ui'; -import { getCompletions, DataProvider, CompletionType } from './completions'; +import { escapeMetricNameSpecialCharacters } from "../../../language_utils"; + +import { CompletionType, DataProvider, getCompletions } from './completions'; import { getSituation } from './situation'; import { NeverCaseError } from './util'; @@ -47,19 +49,19 @@ export function getSuggestOptions(): monacoTypes.editor.ISuggestOptions { function getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): monacoTypes.languages.CompletionItemKind { switch (type) { - case 'DURATION': + case CompletionType.duration: return monaco.languages.CompletionItemKind.Unit; - case 'FUNCTION': + case CompletionType.function: return monaco.languages.CompletionItemKind.Variable; - case 'HISTORY': + case CompletionType.history: return monaco.languages.CompletionItemKind.Snippet; - case 'LABEL_NAME': + case CompletionType.labelName: return monaco.languages.CompletionItemKind.Enum; - case 'LABEL_VALUE': + case CompletionType.labelValue: return monaco.languages.CompletionItemKind.EnumMember; - case 'METRIC_NAME': + case CompletionType.metricName: return monaco.languages.CompletionItemKind.Constructor; - case 'WITH_TEMPLATE': + case CompletionType.withTemplate: return monaco.languages.CompletionItemKind.Constant; default: throw new NeverCaseError(); @@ -100,7 +102,7 @@ export function getCompletionProvider( const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => ({ kind: getMonacoCompletionItemKind(item.type, monaco), label: item.label, - insertText: item.insertText, + insertText: item.type === CompletionType.metricName ? escapeMetricNameSpecialCharacters(item.insertText) : item.insertText, detail: item.detail, documentation: { value: item.documentation } as IMarkdownString, sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have diff --git a/src/language_utils.ts b/src/language_utils.ts index bae42ee6..59234808 100755 --- a/src/language_utils.ts +++ b/src/language_utils.ts @@ -282,6 +282,11 @@ export function escapeLabelValueInRegexSelector(labelValue: string): string { return escapeLabelValueInExactSelector(escapePrometheusRegexp(labelValue)); } +export function escapeMetricNameSpecialCharacters(metricName: string) { + const specialChars = /[-+*\/%^=]/g; + return metricName.replace(specialChars, (match) => '\\' + match); +} + export enum AbstractLabelOperator { Equal = "Equal", NotEqual = "NotEqual", diff --git a/src/querybuilder/components/MetricSelect.tsx b/src/querybuilder/components/MetricSelect.tsx index ac85e17f..60f3c060 100755 --- a/src/querybuilder/components/MetricSelect.tsx +++ b/src/querybuilder/components/MetricSelect.tsx @@ -24,6 +24,7 @@ import { SelectableValue, toOption, GrafanaTheme2 } from '@grafana/data'; import { Select, FormatOptionLabelMeta, useStyles2 } from '@grafana/ui'; import { EditorField, EditorFieldGroup } from '../../components/QueryEditor'; +import { escapeMetricNameSpecialCharacters } from "../../language_utils"; import { PromVisualQuery } from '../types'; // We are matching words split with space @@ -95,7 +96,7 @@ export function MetricSelect({ query, onChange, onGetMetrics }: Props) { options={state.metrics} onChange={({ value }) => { if (value) { - onChange({ ...query, metric: value }); + onChange({ ...query, metric: escapeMetricNameSpecialCharacters(value) }); } }} /> diff --git a/yarn.lock b/yarn.lock index 33a39e77..67727eed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6831,7 +6831,7 @@ levn@^0.4.1: type-check "~0.4.0" "lezer-metricsql@file:packages/lezer-metricsql": - version "0.1.0" + version "0.1.2" lines-and-columns@^1.1.6: version "1.2.4"