prometheus/web/ui/mantine-ui/src/promql/serialize.ts

175 lines
5.6 KiB
TypeScript
Raw Normal View History

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;