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;