mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Fixes most of https://github.com/prometheus/prometheus/issues/15202 This should address all areas of the UI except for the autocompletion in the codemirror-promql text editor. The strategy here is that any time we print or internally serialize (like for the PromLens tree view) either a metric name or a label name as part of a selector or in other relevant parts of PromQL, we check whether it contains characters beyond what was previously supported, and if so, quote and escape it. In the case of metric names, we also have to move them from the beginning of the selector into the curly braces. Signed-off-by: Julius Volz <julius.volz@gmail.com>
175 lines
5.6 KiB
TypeScript
175 lines
5.6 KiB
TypeScript
import { formatPrometheusDuration } from "../lib/formatTime";
|
|
import ASTNode, {
|
|
VectorSelector,
|
|
matchType,
|
|
vectorMatchCardinality,
|
|
nodeType,
|
|
StartOrEnd,
|
|
MatrixSelector,
|
|
} from "./ast";
|
|
import {
|
|
aggregatorsWithParam,
|
|
maybeParenthesizeBinopChild,
|
|
escapeString,
|
|
metricContainsExtendedCharset,
|
|
maybeQuoteLabelName,
|
|
} from "./utils";
|
|
|
|
const labelNameList = (labels: string[]): string => {
|
|
return labels.map((ln) => maybeQuoteLabelName(ln)).join(", ");
|
|
};
|
|
|
|
const serializeAtAndOffset = (
|
|
timestamp: number | null,
|
|
startOrEnd: StartOrEnd,
|
|
offset: number
|
|
): string =>
|
|
`${timestamp !== null ? ` @ ${(timestamp / 1000).toFixed(3)}` : startOrEnd !== null ? ` @ ${startOrEnd}()` : ""}${
|
|
offset === 0
|
|
? ""
|
|
: offset > 0
|
|
? ` offset ${formatPrometheusDuration(offset)}`
|
|
: ` offset -${formatPrometheusDuration(-offset)}`
|
|
}`;
|
|
|
|
const serializeSelector = (node: VectorSelector | MatrixSelector): string => {
|
|
const matchers = node.matchers
|
|
.filter((m) => !(m.name === "__name__" && m.type === matchType.equal))
|
|
.map(
|
|
(m) => `${maybeQuoteLabelName(m.name)}${m.type}"${escapeString(m.value)}"`
|
|
);
|
|
|
|
// 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 =
|
|
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 =
|
|
node.type === nodeType.matrixSelector
|
|
? `[${formatPrometheusDuration(node.range)}]`
|
|
: "";
|
|
const atAndOffset = serializeAtAndOffset(
|
|
node.timestamp,
|
|
node.startOrEnd,
|
|
node.offset
|
|
);
|
|
|
|
return `${!metricExtendedCharset ? metricName : ""}${matchers.length > 0 ? `{${matchers.join(",")}}` : ""}${range}${atAndOffset}`;
|
|
};
|
|
|
|
const serializeNode = (
|
|
node: ASTNode,
|
|
indent = 0,
|
|
pretty = false,
|
|
initialIndent = true
|
|
): string => {
|
|
const childListSeparator = pretty ? "\n" : "";
|
|
const childSeparator = pretty ? "\n" : " ";
|
|
const childIndent = indent + 2;
|
|
const ind = pretty ? " ".repeat(indent) : "";
|
|
// Needed for unary operators.
|
|
const initialInd = initialIndent ? ind : "";
|
|
|
|
switch (node.type) {
|
|
case nodeType.aggregation:
|
|
return `${initialInd}${node.op}${
|
|
node.without
|
|
? ` without(${labelNameList(node.grouping)}) `
|
|
: node.grouping.length > 0
|
|
? ` by(${labelNameList(node.grouping)}) `
|
|
: ""
|
|
}(${childListSeparator}${
|
|
aggregatorsWithParam.includes(node.op) && node.param !== null
|
|
? `${serializeNode(node.param, childIndent, pretty)},${childSeparator}`
|
|
: ""
|
|
}${serializeNode(node.expr, childIndent, pretty)}${childListSeparator}${ind})`;
|
|
|
|
case nodeType.subquery:
|
|
return `${initialInd}${serializeNode(node.expr, indent, pretty)}[${formatPrometheusDuration(node.range)}:${
|
|
node.step !== 0 ? formatPrometheusDuration(node.step) : ""
|
|
}]${serializeAtAndOffset(node.timestamp, node.startOrEnd, node.offset)}`;
|
|
|
|
case nodeType.parenExpr:
|
|
return `${initialInd}(${childListSeparator}${serializeNode(
|
|
node.expr,
|
|
childIndent,
|
|
pretty
|
|
)}${childListSeparator}${ind})`;
|
|
|
|
case nodeType.call: {
|
|
const sep = node.args.length > 0 ? childListSeparator : "";
|
|
|
|
return `${initialInd}${node.func.name}(${sep}${node.args
|
|
.map((arg) => serializeNode(arg, childIndent, pretty))
|
|
.join("," + childSeparator)}${sep}${node.args.length > 0 ? ind : ""})`;
|
|
}
|
|
|
|
case nodeType.matrixSelector:
|
|
return `${initialInd}${serializeSelector(node)}`;
|
|
|
|
case nodeType.vectorSelector:
|
|
return `${initialInd}${serializeSelector(node)}`;
|
|
|
|
case nodeType.numberLiteral:
|
|
return `${initialInd}${node.val}`;
|
|
|
|
case nodeType.stringLiteral:
|
|
return `${initialInd}"${escapeString(node.val)}"`;
|
|
|
|
case nodeType.unaryExpr:
|
|
return `${initialInd}${node.op}${serializeNode(node.expr, indent, pretty, false)}`;
|
|
|
|
case nodeType.binaryExpr: {
|
|
let matching = "";
|
|
let grouping = "";
|
|
const vm = node.matching;
|
|
if (vm !== null && (vm.labels.length > 0 || vm.on)) {
|
|
if (vm.on) {
|
|
matching = ` on(${labelNameList(vm.labels)})`;
|
|
} else {
|
|
matching = ` ignoring(${labelNameList(vm.labels)})`;
|
|
}
|
|
|
|
if (
|
|
vm.card === vectorMatchCardinality.manyToOne ||
|
|
vm.card === vectorMatchCardinality.oneToMany
|
|
) {
|
|
grouping = ` group_${vm.card === vectorMatchCardinality.manyToOne ? "left" : "right"}(${labelNameList(vm.include)})`;
|
|
}
|
|
}
|
|
|
|
return `${serializeNode(maybeParenthesizeBinopChild(node.op, node.lhs), childIndent, pretty)}${childSeparator}${ind}${
|
|
node.op
|
|
}${node.bool ? " bool" : ""}${matching}${grouping}${childSeparator}${serializeNode(
|
|
maybeParenthesizeBinopChild(node.op, node.rhs),
|
|
childIndent,
|
|
pretty
|
|
)}`;
|
|
}
|
|
|
|
case nodeType.placeholder:
|
|
// TODO: Should we just throw an error when trying to serialize an AST containing a placeholder node?
|
|
// (that would currently break editing-as-text of ASTs that contain placeholders)
|
|
return `${initialInd}…${
|
|
node.children.length > 0
|
|
? `(${childListSeparator}${node.children
|
|
.map((child) => serializeNode(child, childIndent, pretty))
|
|
.join("," + childSeparator)}${childListSeparator}${ind})`
|
|
: ""
|
|
}`;
|
|
|
|
default:
|
|
throw new Error("unsupported node type");
|
|
}
|
|
};
|
|
|
|
export default serializeNode;
|