mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Merge pull request #15244 from prometheus/utf8-web-ui-support
Some checks are pending
buf.build / lint and publish (push) Waiting to run
CI / Go tests (push) Waiting to run
CI / More Go tests (push) Waiting to run
CI / Go tests with previous Go version (push) Waiting to run
CI / UI tests (push) Waiting to run
CI / Go tests on Windows (push) Waiting to run
CI / Mixins tests (push) Waiting to run
CI / Build Prometheus for common architectures (0) (push) Waiting to run
CI / Build Prometheus for common architectures (1) (push) Waiting to run
CI / Build Prometheus for common architectures (2) (push) Waiting to run
CI / Build Prometheus for all architectures (0) (push) Waiting to run
CI / Build Prometheus for all architectures (1) (push) Waiting to run
CI / Build Prometheus for all architectures (10) (push) Waiting to run
CI / Build Prometheus for all architectures (11) (push) Waiting to run
CI / Build Prometheus for all architectures (2) (push) Waiting to run
CI / Build Prometheus for all architectures (3) (push) Waiting to run
CI / Build Prometheus for all architectures (4) (push) Waiting to run
CI / Build Prometheus for all architectures (5) (push) Waiting to run
CI / Build Prometheus for all architectures (6) (push) Waiting to run
CI / Build Prometheus for all architectures (7) (push) Waiting to run
CI / Build Prometheus for all architectures (8) (push) Waiting to run
CI / Build Prometheus for all architectures (9) (push) Waiting to run
CI / Report status of build Prometheus for all architectures (push) Blocked by required conditions
CI / Check generated parser (push) Waiting to run
CI / golangci-lint (push) Waiting to run
CI / fuzzing (push) Waiting to run
CI / codeql (push) Waiting to run
CI / Publish main branch artifacts (push) Blocked by required conditions
CI / Publish release artefacts (push) Blocked by required conditions
CI / Publish UI on npm Registry (push) Blocked by required conditions
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run
Some checks are pending
buf.build / lint and publish (push) Waiting to run
CI / Go tests (push) Waiting to run
CI / More Go tests (push) Waiting to run
CI / Go tests with previous Go version (push) Waiting to run
CI / UI tests (push) Waiting to run
CI / Go tests on Windows (push) Waiting to run
CI / Mixins tests (push) Waiting to run
CI / Build Prometheus for common architectures (0) (push) Waiting to run
CI / Build Prometheus for common architectures (1) (push) Waiting to run
CI / Build Prometheus for common architectures (2) (push) Waiting to run
CI / Build Prometheus for all architectures (0) (push) Waiting to run
CI / Build Prometheus for all architectures (1) (push) Waiting to run
CI / Build Prometheus for all architectures (10) (push) Waiting to run
CI / Build Prometheus for all architectures (11) (push) Waiting to run
CI / Build Prometheus for all architectures (2) (push) Waiting to run
CI / Build Prometheus for all architectures (3) (push) Waiting to run
CI / Build Prometheus for all architectures (4) (push) Waiting to run
CI / Build Prometheus for all architectures (5) (push) Waiting to run
CI / Build Prometheus for all architectures (6) (push) Waiting to run
CI / Build Prometheus for all architectures (7) (push) Waiting to run
CI / Build Prometheus for all architectures (8) (push) Waiting to run
CI / Build Prometheus for all architectures (9) (push) Waiting to run
CI / Report status of build Prometheus for all architectures (push) Blocked by required conditions
CI / Check generated parser (push) Waiting to run
CI / golangci-lint (push) Waiting to run
CI / fuzzing (push) Waiting to run
CI / codeql (push) Waiting to run
CI / Publish main branch artifacts (push) Blocked by required conditions
CI / Publish release artefacts (push) Blocked by required conditions
CI / Publish UI on npm Registry (push) Blocked by required conditions
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run
Support UTF-8 metric names and labels in web UI
This commit is contained in:
commit
51866b9fee
|
@ -2,6 +2,7 @@ import { Badge, BadgeVariant, Group, MantineColor, Stack } from "@mantine/core";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { escapeString } from "../lib/escapeString";
|
import { escapeString } from "../lib/escapeString";
|
||||||
import badgeClasses from "../Badge.module.css";
|
import badgeClasses from "../Badge.module.css";
|
||||||
|
import { maybeQuoteLabelName } from "../promql/utils";
|
||||||
|
|
||||||
export interface LabelBadgesProps {
|
export interface LabelBadgesProps {
|
||||||
labels: Record<string, string>;
|
labels: Record<string, string>;
|
||||||
|
@ -30,7 +31,7 @@ export const LabelBadges: FC<LabelBadgesProps> = ({
|
||||||
}}
|
}}
|
||||||
key={k}
|
key={k}
|
||||||
>
|
>
|
||||||
{k}="{escapeString(v)}"
|
{maybeQuoteLabelName(k)}="{escapeString(v)}"
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,12 +1,24 @@
|
||||||
|
import {
|
||||||
|
maybeQuoteLabelName,
|
||||||
|
metricContainsExtendedCharset,
|
||||||
|
} from "../promql/utils";
|
||||||
import { escapeString } from "./escapeString";
|
import { escapeString } from "./escapeString";
|
||||||
|
|
||||||
|
// TODO: Maybe replace this with the new PromLens-derived serialization code in src/promql/serialize.ts?
|
||||||
export const formatSeries = (labels: { [key: string]: string }): string => {
|
export const formatSeries = (labels: { [key: string]: string }): string => {
|
||||||
if (labels === null) {
|
if (labels === null) {
|
||||||
return "scalar";
|
return "scalar";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metricContainsExtendedCharset(labels.__name__ || "")) {
|
||||||
|
return `{"${escapeString(labels.__name__)}",${Object.entries(labels)
|
||||||
|
.filter(([k]) => k !== "__name__")
|
||||||
|
.map(([k, v]) => `${maybeQuoteLabelName(k)}="${escapeString(v)}"`)
|
||||||
|
.join(", ")}}`;
|
||||||
|
}
|
||||||
|
|
||||||
return `${labels.__name__ || ""}{${Object.entries(labels)
|
return `${labels.__name__ || ""}{${Object.entries(labels)
|
||||||
.filter(([k]) => k !== "__name__")
|
.filter(([k]) => k !== "__name__")
|
||||||
.map(([k, v]) => `${k}="${escapeString(v)}"`)
|
.map(([k, v]) => `${maybeQuoteLabelName(k)}="${escapeString(v)}"`)
|
||||||
.join(", ")}}`;
|
.join(", ")}}`;
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,10 @@ import classes from "./SeriesName.module.css";
|
||||||
import { escapeString } from "../../lib/escapeString";
|
import { escapeString } from "../../lib/escapeString";
|
||||||
import { useClipboard } from "@mantine/hooks";
|
import { useClipboard } from "@mantine/hooks";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import {
|
||||||
|
maybeQuoteLabelName,
|
||||||
|
metricContainsExtendedCharset,
|
||||||
|
} from "../../promql/utils";
|
||||||
|
|
||||||
interface SeriesNameProps {
|
interface SeriesNameProps {
|
||||||
labels: { [key: string]: string } | null;
|
labels: { [key: string]: string } | null;
|
||||||
|
@ -15,8 +19,26 @@ const SeriesName: FC<SeriesNameProps> = ({ labels, format }) => {
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
|
|
||||||
const renderFormatted = (): React.ReactElement => {
|
const renderFormatted = (): React.ReactElement => {
|
||||||
|
const metricExtendedCharset =
|
||||||
|
labels && metricContainsExtendedCharset(labels.__name__ || "");
|
||||||
|
|
||||||
const labelNodes: React.ReactElement[] = [];
|
const labelNodes: React.ReactElement[] = [];
|
||||||
let first = true;
|
let first = true;
|
||||||
|
|
||||||
|
// If the metric name uses the extended new charset, we need to escape it,
|
||||||
|
// put it into the label matcher list, and make sure it's the first item.
|
||||||
|
if (metricExtendedCharset) {
|
||||||
|
labelNodes.push(
|
||||||
|
<span key="__name__">
|
||||||
|
<span className={classes.labelValue}>
|
||||||
|
"{escapeString(labels.__name__)}"
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
for (const label in labels) {
|
for (const label in labels) {
|
||||||
if (label === "__name__") {
|
if (label === "__name__") {
|
||||||
continue;
|
continue;
|
||||||
|
@ -37,7 +59,10 @@ const SeriesName: FC<SeriesNameProps> = ({ labels, format }) => {
|
||||||
}}
|
}}
|
||||||
title="Click to copy label matcher"
|
title="Click to copy label matcher"
|
||||||
>
|
>
|
||||||
<span className={classes.labelName}>{label}</span>=
|
<span className={classes.labelName}>
|
||||||
|
{maybeQuoteLabelName(label)}
|
||||||
|
</span>
|
||||||
|
=
|
||||||
<span className={classes.labelValue}>
|
<span className={classes.labelValue}>
|
||||||
"{escapeString(labels[label])}"
|
"{escapeString(labels[label])}"
|
||||||
</span>
|
</span>
|
||||||
|
@ -52,9 +77,11 @@ const SeriesName: FC<SeriesNameProps> = ({ labels, format }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<span className={classes.metricName}>
|
{!metricExtendedCharset && (
|
||||||
{labels ? labels.__name__ : ""}
|
<span className={classes.metricName}>
|
||||||
</span>
|
{labels ? labels.__name__ : ""}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{"{"}
|
{"{"}
|
||||||
{labelNodes}
|
{labelNodes}
|
||||||
{"}"}
|
{"}"}
|
||||||
|
|
|
@ -8,14 +8,21 @@ import ASTNode, {
|
||||||
MatrixSelector,
|
MatrixSelector,
|
||||||
} from "./ast";
|
} from "./ast";
|
||||||
import { formatPrometheusDuration } from "../lib/formatTime";
|
import { formatPrometheusDuration } from "../lib/formatTime";
|
||||||
import { maybeParenthesizeBinopChild, escapeString } from "./utils";
|
import {
|
||||||
|
maybeParenthesizeBinopChild,
|
||||||
|
escapeString,
|
||||||
|
maybeQuoteLabelName,
|
||||||
|
metricContainsExtendedCharset,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
export const labelNameList = (labels: string[]): React.ReactNode[] => {
|
export const labelNameList = (labels: string[]): React.ReactNode[] => {
|
||||||
return labels.map((l, i) => {
|
return labels.map((l, i) => {
|
||||||
return (
|
return (
|
||||||
<span key={i}>
|
<span key={i}>
|
||||||
{i !== 0 && ", "}
|
{i !== 0 && ", "}
|
||||||
<span className="promql-code promql-label-name">{l}</span>
|
<span className="promql-code promql-label-name">
|
||||||
|
{maybeQuoteLabelName(l)}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -69,27 +76,45 @@ const formatAtAndOffset = (
|
||||||
const formatSelector = (
|
const formatSelector = (
|
||||||
node: VectorSelector | MatrixSelector
|
node: VectorSelector | MatrixSelector
|
||||||
): ReactElement => {
|
): ReactElement => {
|
||||||
const matchLabels = node.matchers
|
const matchLabels: JSX.Element[] = [];
|
||||||
.filter(
|
|
||||||
(m) =>
|
// If the metric name contains the new extended charset, we need to escape it
|
||||||
!(
|
// and add it at the beginning of the matchers list in the curly braces.
|
||||||
m.name === "__name__" &&
|
const metricName =
|
||||||
m.type === matchType.equal &&
|
node.name ||
|
||||||
m.value === node.name
|
node.matchers.find(
|
||||||
)
|
(m) => m.name === "__name__" && m.type === matchType.equal
|
||||||
)
|
)?.value ||
|
||||||
.map((m, i) => (
|
"";
|
||||||
<span key={i}>
|
const metricExtendedCharset = metricContainsExtendedCharset(metricName);
|
||||||
{i !== 0 && ","}
|
if (metricExtendedCharset) {
|
||||||
<span className="promql-label-name">{m.name}</span>
|
matchLabels.push(
|
||||||
{m.type}
|
<span key="__name__">
|
||||||
<span className="promql-string">"{escapeString(m.value)}"</span>
|
<span className="promql-string">"{escapeString(metricName)}"</span>
|
||||||
</span>
|
</span>
|
||||||
));
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
matchLabels.push(
|
||||||
|
...node.matchers
|
||||||
|
.filter((m) => !(m.name === "__name__" && m.type === matchType.equal))
|
||||||
|
.map((m, i) => (
|
||||||
|
<span key={i}>
|
||||||
|
{(i !== 0 || metricExtendedCharset) && ","}
|
||||||
|
<span className="promql-label-name">
|
||||||
|
{maybeQuoteLabelName(m.name)}
|
||||||
|
</span>
|
||||||
|
{m.type}
|
||||||
|
<span className="promql-string">"{escapeString(m.value)}"</span>
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span className="promql-metric-name">{node.name}</span>
|
{!metricExtendedCharset && (
|
||||||
|
<span className="promql-metric-name">{metricName}</span>
|
||||||
|
)}
|
||||||
{matchLabels.length > 0 && (
|
{matchLabels.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{"{"}
|
{"{"}
|
||||||
|
|
|
@ -11,8 +11,14 @@ import {
|
||||||
aggregatorsWithParam,
|
aggregatorsWithParam,
|
||||||
maybeParenthesizeBinopChild,
|
maybeParenthesizeBinopChild,
|
||||||
escapeString,
|
escapeString,
|
||||||
|
metricContainsExtendedCharset,
|
||||||
|
maybeQuoteLabelName,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
|
const labelNameList = (labels: string[]): string => {
|
||||||
|
return labels.map((ln) => maybeQuoteLabelName(ln)).join(", ");
|
||||||
|
};
|
||||||
|
|
||||||
const serializeAtAndOffset = (
|
const serializeAtAndOffset = (
|
||||||
timestamp: number | null,
|
timestamp: number | null,
|
||||||
startOrEnd: StartOrEnd,
|
startOrEnd: StartOrEnd,
|
||||||
|
@ -28,15 +34,23 @@ const serializeAtAndOffset = (
|
||||||
|
|
||||||
const serializeSelector = (node: VectorSelector | MatrixSelector): string => {
|
const serializeSelector = (node: VectorSelector | MatrixSelector): string => {
|
||||||
const matchers = node.matchers
|
const matchers = node.matchers
|
||||||
.filter(
|
.filter((m) => !(m.name === "__name__" && m.type === matchType.equal))
|
||||||
(m) =>
|
.map(
|
||||||
!(
|
(m) => `${maybeQuoteLabelName(m.name)}${m.type}"${escapeString(m.value)}"`
|
||||||
m.name === "__name__" &&
|
);
|
||||||
m.type === matchType.equal &&
|
|
||||||
m.value === node.name
|
// If the metric name contains the new extended charset, we need to escape it
|
||||||
)
|
// and add it at the beginning of the matchers list in the curly braces.
|
||||||
)
|
const metricName =
|
||||||
.map((m) => `${m.name}${m.type}"${escapeString(m.value)}"`);
|
node.name ||
|
||||||
|
node.matchers.find(
|
||||||
|
(m) => m.name === "__name__" && m.type === matchType.equal
|
||||||
|
)?.value ||
|
||||||
|
"";
|
||||||
|
const metricExtendedCharset = metricContainsExtendedCharset(metricName);
|
||||||
|
if (metricExtendedCharset) {
|
||||||
|
matchers.unshift(`"${escapeString(metricName)}"`);
|
||||||
|
}
|
||||||
|
|
||||||
const range =
|
const range =
|
||||||
node.type === nodeType.matrixSelector
|
node.type === nodeType.matrixSelector
|
||||||
|
@ -48,7 +62,7 @@ const serializeSelector = (node: VectorSelector | MatrixSelector): string => {
|
||||||
node.offset
|
node.offset
|
||||||
);
|
);
|
||||||
|
|
||||||
return `${node.name}${matchers.length > 0 ? `{${matchers.join(",")}}` : ""}${range}${atAndOffset}`;
|
return `${!metricExtendedCharset ? metricName : ""}${matchers.length > 0 ? `{${matchers.join(",")}}` : ""}${range}${atAndOffset}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const serializeNode = (
|
const serializeNode = (
|
||||||
|
@ -68,9 +82,9 @@ const serializeNode = (
|
||||||
case nodeType.aggregation:
|
case nodeType.aggregation:
|
||||||
return `${initialInd}${node.op}${
|
return `${initialInd}${node.op}${
|
||||||
node.without
|
node.without
|
||||||
? ` without(${node.grouping.join(", ")}) `
|
? ` without(${labelNameList(node.grouping)}) `
|
||||||
: node.grouping.length > 0
|
: node.grouping.length > 0
|
||||||
? ` by(${node.grouping.join(", ")}) `
|
? ` by(${labelNameList(node.grouping)}) `
|
||||||
: ""
|
: ""
|
||||||
}(${childListSeparator}${
|
}(${childListSeparator}${
|
||||||
aggregatorsWithParam.includes(node.op) && node.param !== null
|
aggregatorsWithParam.includes(node.op) && node.param !== null
|
||||||
|
@ -119,16 +133,16 @@ const serializeNode = (
|
||||||
const vm = node.matching;
|
const vm = node.matching;
|
||||||
if (vm !== null && (vm.labels.length > 0 || vm.on)) {
|
if (vm !== null && (vm.labels.length > 0 || vm.on)) {
|
||||||
if (vm.on) {
|
if (vm.on) {
|
||||||
matching = ` on(${vm.labels.join(", ")})`;
|
matching = ` on(${labelNameList(vm.labels)})`;
|
||||||
} else {
|
} else {
|
||||||
matching = ` ignoring(${vm.labels.join(", ")})`;
|
matching = ` ignoring(${labelNameList(vm.labels)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
vm.card === vectorMatchCardinality.manyToOne ||
|
vm.card === vectorMatchCardinality.manyToOne ||
|
||||||
vm.card === vectorMatchCardinality.oneToMany
|
vm.card === vectorMatchCardinality.oneToMany
|
||||||
) {
|
) {
|
||||||
grouping = ` group_${vm.card === vectorMatchCardinality.manyToOne ? "left" : "right"}(${vm.include.join(",")})`;
|
grouping = ` group_${vm.card === vectorMatchCardinality.manyToOne ? "left" : "right"}(${labelNameList(vm.include)})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,7 @@ describe("serializeNode and formatNode", () => {
|
||||||
timestamp: null,
|
timestamp: null,
|
||||||
startOrEnd: null,
|
startOrEnd: null,
|
||||||
},
|
},
|
||||||
output: '{__name__="metric_name"} offset 1m',
|
output: "metric_name offset 1m",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Escaping in label values.
|
// Escaping in label values.
|
||||||
|
@ -642,6 +642,113 @@ describe("serializeNode and formatNode", () => {
|
||||||
== bool on(label1, label2) group_right(label3)
|
== bool on(label1, label2) group_right(label3)
|
||||||
…`,
|
…`,
|
||||||
},
|
},
|
||||||
|
// Test new Prometheus 3.0 UTF-8 support.
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
bool: false,
|
||||||
|
lhs: {
|
||||||
|
bool: false,
|
||||||
|
lhs: {
|
||||||
|
expr: {
|
||||||
|
matchers: [
|
||||||
|
{
|
||||||
|
name: "__name__",
|
||||||
|
type: matchType.equal,
|
||||||
|
value: "metric_ä",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "foo",
|
||||||
|
type: matchType.equal,
|
||||||
|
value: "bar",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
name: "",
|
||||||
|
offset: 0,
|
||||||
|
startOrEnd: null,
|
||||||
|
timestamp: null,
|
||||||
|
type: nodeType.vectorSelector,
|
||||||
|
},
|
||||||
|
grouping: ["a", "ä"],
|
||||||
|
op: aggregationType.sum,
|
||||||
|
param: null,
|
||||||
|
type: nodeType.aggregation,
|
||||||
|
without: false,
|
||||||
|
},
|
||||||
|
matching: {
|
||||||
|
card: vectorMatchCardinality.manyToOne,
|
||||||
|
include: ["c", "ü"],
|
||||||
|
labels: ["b", "ö"],
|
||||||
|
on: true,
|
||||||
|
},
|
||||||
|
op: binaryOperatorType.div,
|
||||||
|
rhs: {
|
||||||
|
expr: {
|
||||||
|
matchers: [
|
||||||
|
{
|
||||||
|
name: "__name__",
|
||||||
|
type: matchType.equal,
|
||||||
|
value: "metric_ö",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bar",
|
||||||
|
type: matchType.equal,
|
||||||
|
value: "foo",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
name: "",
|
||||||
|
offset: 0,
|
||||||
|
startOrEnd: null,
|
||||||
|
timestamp: null,
|
||||||
|
type: nodeType.vectorSelector,
|
||||||
|
},
|
||||||
|
grouping: ["d", "ä"],
|
||||||
|
op: aggregationType.sum,
|
||||||
|
param: null,
|
||||||
|
type: nodeType.aggregation,
|
||||||
|
without: true,
|
||||||
|
},
|
||||||
|
type: nodeType.binaryExpr,
|
||||||
|
},
|
||||||
|
matching: {
|
||||||
|
card: vectorMatchCardinality.oneToOne,
|
||||||
|
include: [],
|
||||||
|
labels: ["e", "ö"],
|
||||||
|
on: false,
|
||||||
|
},
|
||||||
|
op: binaryOperatorType.add,
|
||||||
|
rhs: {
|
||||||
|
expr: {
|
||||||
|
matchers: [
|
||||||
|
{
|
||||||
|
name: "__name__",
|
||||||
|
type: matchType.equal,
|
||||||
|
value: "metric_ü",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
name: "",
|
||||||
|
offset: 0,
|
||||||
|
startOrEnd: null,
|
||||||
|
timestamp: null,
|
||||||
|
type: nodeType.vectorSelector,
|
||||||
|
},
|
||||||
|
type: nodeType.parenExpr,
|
||||||
|
},
|
||||||
|
type: nodeType.binaryExpr,
|
||||||
|
},
|
||||||
|
output:
|
||||||
|
'sum by(a, "ä") ({"metric_ä",foo="bar"}) / on(b, "ö") group_left(c, "ü") sum without(d, "ä") ({"metric_ö",bar="foo"}) + ignoring(e, "ö") ({"metric_ü"})',
|
||||||
|
prettyOutput: ` sum by(a, "ä") (
|
||||||
|
{"metric_ä",foo="bar"}
|
||||||
|
)
|
||||||
|
/ on(b, "ö") group_left(c, "ü")
|
||||||
|
sum without(d, "ä") (
|
||||||
|
{"metric_ö",bar="foo"}
|
||||||
|
)
|
||||||
|
+ ignoring(e, "ö")
|
||||||
|
(
|
||||||
|
{"metric_ü"}
|
||||||
|
)`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
tests.forEach((t) => {
|
tests.forEach((t) => {
|
||||||
|
|
|
@ -267,6 +267,21 @@ export const humanizedValueType: Record<valueType, string> = {
|
||||||
[valueType.matrix]: "range vector",
|
[valueType.matrix]: "range vector",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const metricNameRe = /^[a-zA-Z_:][a-zA-Z0-9_:]*$/;
|
||||||
|
const labelNameCharsetRe = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||||
|
|
||||||
|
export const metricContainsExtendedCharset = (str: string) => {
|
||||||
|
return !metricNameRe.test(str);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const labelNameContainsExtendedCharset = (str: string) => {
|
||||||
|
return !labelNameCharsetRe.test(str);
|
||||||
|
};
|
||||||
|
|
||||||
export const escapeString = (str: string) => {
|
export const escapeString = (str: string) => {
|
||||||
return str.replace(/([\\"])/g, "\\$1");
|
return str.replace(/([\\"])/g, "\\$1");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const maybeQuoteLabelName = (str: string) => {
|
||||||
|
return labelNameContainsExtendedCharset(str) ? `"${escapeString(str)}"` : str;
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue