资讯动态

Prometheus Prettifying PromQL:promql/parser 中表达式美化规则的源码级解读

发布时间:2026/9/6 17:59:38 来源:尧图企业网站定制
Prometheus Prettifying PromQLpromql/parser 中表达式美化规则的源码级解读【免费下载链接】prometheusThe Prometheus monitoring system and time series database.项目地址: https://gitcode.com/GitHub_Trending/pr/prometheusPromQL 表达式一旦写复杂往往长达数百字符挤在一行里难以阅读和 diff。Prometheus 的promql/parser包内置了一个表达式美化器prettifier规则文档定义了它如何对解析后的 AST 节点做换行、缩进和归一化重写。本文以这份规则文档为主线逐条拆解四条美化规则并结合 prettier.go 的实现与 prettier_test.go 的测试用例说明Prettify的调用方式、max_characters_per_line关键字的底层含义以及节点深度level与缩进机制的工作原理帮助你在二次开发 Prometheus 解析器或自研查询工具时直接复用这套能力。核心概念Prettify 与 max_characters_per_line规则文档首先定义了一个关键字max_characters_per_line美化后的单行表达式允许的最大字符数。对应到源码这是一个包级变量默认值为 100var maxCharactersPerLine 100见 prettier.go#L44。对外入口是Prettify函数它把任意 AST 节点从根level 0开始递归格式化func Prettify(n Node) string { return n.Pretty(0) }见 prettier.go#L46-L48。它调用的是Node接口上的Pretty(level int)方法——每个节点类型AggregateExpr、BinaryExpr、Call、ParenExpr等各自实现一份。文件头部的注释prettier.go#L21-L42解释了整个算法思路解析后的 PromQL 是一棵嵌套的 AST每个节点带一个level距根节点的深度由父节点传递格式化时每个节点只考虑两件事父节点有没有为它换行level 为 0 表示没换行则不加缩进前缀level 0 则加level个两空格缩进当前节点是否需要被拆行判据是其规范化长度len(n.String())是否超过maxCharactersPerLine超过则拆行并把 level1 传给子节点否则原样返回n.String()。判定逻辑集中在needsSplit中// needsSplit normalizes the node and then checks if the node needs any split. func needsSplit(n Node) bool { if n nil { return false } return len(n.String()) maxCharactersPerLine }见 prettier.go#L176-L183。注意判断用的是而非即恰好等于上限时不拆行。缩进常量是两空格prettier.go#L185-L189const indentString 规则一超长节点才拆行向量选择器与区间选择器是例外规则原文A node exceeding themax_characters_per_linewill qualify for splitunlessIt is aMatrixSelectorIt is aVectorSelector. Label sets in aVectorSelectorwill be in the same line as metric_name, separated by commas and a space源码中这两类节点的实现刻意不走needsSplit分支而是统一走“加前缀缩进 原样输出”的路径func (e *MatrixSelector) Pretty(level int) string { return getCommonPrefixIndent(level, e) } func (e *VectorSelector) Pretty(level int) string { return getCommonPrefixIndent(level, e) }见 prettier.go#L142-L155。getCommonPrefixIndent只是indent(level) current.String()。这个设计的实际效果是即使node_filesystem_avail_bytes{jobnode,fstype!}这样的选择器很长它的指标名和标签集也始终保持在一行内、以,分隔不会因为超长而被拆碎。这一点在测试用例中有直接体现如 prettier_test.go#L466-L488 的混入mixin风格真实告警查询里node_filesystem_avail_bytes{fstype!,jobnode}完整保留在同一行。另外规则附带了一条 NoteLabel groupings likeby,without,on,ignoringwill remain on the same line as their parent node即分组/匹配标签子句与父节点操作符保持同行。以二元表达式为例Pretty的拆行形式是“左操作数一行、操作符加 matching 串一行、右操作数一行”matching : e.getMatchingStr() return fmt.Sprintf(%s\n%s%s%s%s\n%s, e.LHS.Pretty(level1), indent(level), e.Op, returnBool, matching, e.RHS.Pretty(level1))见 prettier.go#L67-L80。on/ignoring/group_left/group_right等字符串由getMatchingStr()生成后紧跟在e.Op之后输出因此始终与操作符同行而不会单独换行。测试 prettier_test.go#L176-L183 展示了多组匹配子句的效果foo_1 ignoring (foo) foo_2 ignoring (job) group_left () foo_3 on (instance) group_right () foo_4匹配子句串on (instance) group_right ()的生成逻辑在 printer.go#L148-L190 的getMatchingStr()中它按顺序拼接on/ignoring (labels)、group_left/right (include)以及fill/fill_left/fill_right填充值子句。规则二嵌套节点只在超长时单独美化规则原文Nodes that are nested within another node will be prettified only if they exceed themax_characters_per_line这条规则对应的就是needsSplit的递归语义一个节点只有自身的String()长度超限时才拆行并把level1传给子节点否则它直接以单行字符串参与父节点的拼接子树整体不被触碰。测试用例foo_1 foo_2 foo_3limit10直观展示了这种“逐层独立判定”foo_1 foo_2 foo_3见 prettier_test.go#L152-L158。注意foo_1 foo_2长度 11超限被拆而更内层的短表达式则保持单行二元运算的结合顺序决定了左结合树形缩进层级也随之变化。TestExprPretty中还收录了来自 monitoring mixin 的复杂真实查询prettier_test.go#L464-L513是验证多层嵌套拆行行为的完整样本。规则三聚合表达式归一化为sum without (labels) (expression)形式规则原文Expressions likesum(expression) without (label_matchers)will be modified tosum without(label_matchers) (expression)这条规则的关键在于归一化并不发生在 prettier 里而是在更底层的String()输出上。AggregateExpr的字符串化始终把by (...)/without (...)前置到函数名之后func (node *AggregateExpr) writeAggOpStr(b *bytes.Buffer) { b.WriteString(node.Op.String()) switch { case node.Without: b.WriteString( without () writeLabels(b, node.Grouping) b.WriteString() ) case len(node.Grouping) 0: b.WriteString( by () writeLabels(b, node.Grouping) b.WriteString() ) } }见 printer.go#L92-L104。由于needsSplit和所有不拆行路径都依赖n.String()因此任何写法sum(expr) by (job)、sum by (job) (expr)、sum(expr) without (job)只要进入格式化流程输出都会被统一成sum by (job) (expr)这一规范形态且标签之间用,分隔。测试用例直接验证了这一重写行为limit10 下输入sum(task:errors:rate10s{jobs}) without(job,foo) 输出sum without (job, foo) ( task:errors:rate10s{jobs} )见 prettier_test.go#L44-L49。超限时拆行后的形态由AggregateExpr.Pretty生成ShortString()即sum without (job, foo)(\n 子表达式 \n)若聚合操作符带参数IsAggregatorWithParam()如topk、quantile参数会作为单独一行插在子表达式之前s e.ShortString() s (\n if e.Op.IsAggregatorWithParam() { s fmt.Sprintf(%s,\n, e.Param.Pretty(level1)) } s fmt.Sprintf(%s\n%s), e.Expr.Pretty(level1), indent(level))见 prettier.go#L50-L65。对应效果prettier_test.go#L62-L68topk( 10, ask:errors:rate10s{jobs} )规则四函数调用参数逐个换行规则原文Functional call args will be split to different lines if they exceed themax_characters_per_lineCall.Pretty在超限时把每个参数放到独立一行func (e *Call) Pretty(level int) string { s : indent(level) if !needsSplit(e) { s e.String() return s } s fmt.Sprintf(%s(\n%s\n%s), e.Func.Name, e.Args.Pretty(level1), indent(level)) return s }见 prettier.go#L101-L109。参数列表是Expressions类型其Pretty实现把每个参数用,\n连接prettier.go#L115-L127parts : make([]string, len(e)) for i : range e { parts[i] e[i].Pretty(level) } return strings.Join(parts, ,\n)典型效果label_replace五个参数逐行排列label_replace( up{jobapi-server,servicea:c}, foo, $1, service, (.*):.* )见 prettier_test.go#L236-L245。注意子参数若自身也超限会递归拆行——测试中嵌套两层的label_replace(label_replace(...))prettier_test.go#L247-L261就展示了参数内部再拆函数的完整嵌套形态。level 与缩进机制父节点决定子节点的缩进前面各规则最终都落到同一套缩进约定上这里把机制完整讲清楚便于自行扩展Pretty实现。文件头部注释prettier.go#L21-L42给出的算法是父节点没有换行传入的 level 为 0时当前节点不得添加缩进前缀父节点换行了level 0时当前节点以level × 作为前缀缩进当前节点超限时输出缩进等于自身深度并把 level1 传给子节点。各节点类型在这一机制下的行为差异值得注意SubqueryExpr.Pretty不添加缩进前缀而是把时间后缀[range:step] ... offset ...原样追加在内部表达式之后见 prettier.go#L146-L151测试效果为rate(\n long_vector_selector[10m:1m] start() offset 1m\n)prettier_test.go#L214-L218StepInvariantExpr表达式透传 levelprettier.go#L138-L140UnaryExpr.Pretty会先TrimSpace掉子表达式的前导缩进再把缩进挂到一元操作符前prettier.go#L165-L170保证-rate(...)这类表达式中负号不换行实验性的DurationExprOptions{ExperimentalDurationExpr: true}解析出的时长运算永不拆行只按 level 加缩进见 prettier.go#L82-L99 与测试 prettier_test.go#L672-L704。已知限制注释不会被保留规则文档开头特别注明Note: The current version of prettier does not preserve comments.这是因为美化基于String()重新序列化 AST而注释并不属于 AST 节点解析后即丢失。测试用例给出了确证带两行注释的输入prettier_test.go#L105-L114输入 sum by(job,foo) # Comment 1. (sum by(job,foo) ( # Comment 2. task:errors:rate10s{jobs})) 输出注释全部消失 sum by (job, foo) ( sum by (job, foo) ( task:errors:rate10s{jobs} ) )因此在依赖美化输出的场景中例如规则文件展示、告警表达式回显不能期望用户注释被保留。如何调用与验证Prettify 与测试套件Prettify是promql/parser包的导出 API入参为任意Node通常先由Parser.ParseExpr解析得到表达式。当前仓库内该 API 的消费方是解析器自身的测试套件 prettier_test.go其中每个测试都会把上限调小以便在小表达式上触发拆行func TestAggregateExprPretty(t *testing.T) { maxCharactersPerLine 10 ... expr, err : testParser.ParseExpr(test.in) require.NoError(t, err) require.Equal(t, test.out, Prettify(expr)) }从源码结构看maxCharactersPerLine是包内可变变量无导出 setter生产路径下固定为 100测试通过直接赋值来模拟不同行宽。测试文件按节点类型组织TestAggregateExprPretty聚合表达式、by/without 归一化、嵌套聚合、注释丢弃TestBinaryExprPretty二元运算、bool、on/ignoring/group_left/group_right 匹配子句TestCallExprPretty函数调用参数拆行、子查询后缀TestParenExprPretty 与 TestExprPretty括号包裹与复杂真实查询的多层嵌套TestUnaryPretty 与 TestDurationExprPretty一元负号与实验性时长表达式。小结promql/parser/prettier_rules.md用四条规则定义了 Prometheus 表达式美化器的行为边界超限才拆行needsSplit以String()长度对maxCharactersPerLine100判定、选择器节点永不拆碎MatrixSelector/VectorSelector走getCommonPrefixIndent、聚合分组子句归一化前置由printer.go的writeAggOpStr保证规范形态、函数参数逐行排列Expressions.Pretty以,\n连接。整个机制围绕level递归传递与两空格缩进展开且当前版本不保留注释。对维护查询工具链、规则文件可视化或需要稳定 diff 格式的团队而言理解这四条规则与 prettier.go 中各Pretty方法的实现即可准确预判任意 PromQL 表达式经Prettify后的输出形态。【免费下载链接】prometheusThe Prometheus monitoring system and time series database.项目地址: https://gitcode.com/GitHub_Trending/pr/prometheus创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价