Prometheus Prettifying PromQL:promql/parser 中表达式美化规则的源码级解读
2026/9/6 17:59:19 网站建设 项目流程

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

【免费下载链接】prometheusThe Prometheus monitoring system and time series database.项目地址: https://gitcode.com/GitHub_Trending/pr/prometheus

PromQL 表达式一旦写复杂,往往长达数百字符挤在一行里,难以阅读和 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:美化后的单行表达式允许的最大字符数。

对应到源码,这是一个包级变量,默认值为 100:

var 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)方法——每个节点类型(AggregateExprBinaryExprCallParenExpr等)各自实现一份。文件头部的注释(prettier.go#L21-L42)解释了整个算法思路:

  • 解析后的 PromQL 是一棵嵌套的 AST,每个节点带一个level(距根节点的深度),由父节点传递;
  • 格式化时每个节点只考虑两件事:父节点有没有为它换行(level 为 0 表示没换行,则不加缩进前缀;level > 0 则加level个两空格缩进);
  • 当前节点是否需要被拆行:判据是其规范化长度len(n.String())是否超过maxCharactersPerLine,超过则拆行并把 level+1 传给子节点,否则原样返回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-L189):

const indentString = " "

规则一:超长节点才拆行,向量选择器与区间选择器是例外

规则原文:

  1. A node exceeding themax_characters_per_linewill qualify for splitunless
    1. It is aMatrixSelector
    2. It 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{job="node",fstype!=""}这样的选择器很长,它的指标名和标签集也始终保持在一行内、以,分隔,不会因为超长而被拆碎。这一点在测试用例中有直接体现,如 prettier_test.go#L466-L488 的混入(mixin)风格真实告警查询里,node_filesystem_avail_bytes{fstype!="",job="node"}完整保留在同一行。

另外规则附带了一条 Note:

Label 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(level+1), indent(level), e.Op, returnBool, matching, e.RHS.Pretty(level+1))

见 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填充值子句。

规则二:嵌套节点只在超长时单独美化

规则原文:

  1. Nodes that are nested within another node will be prettified only if they exceed themax_characters_per_line

这条规则对应的就是needsSplit的递归语义:一个节点只有自身的String()长度超限时才拆行,并把level+1传给子节点;否则它直接以单行字符串参与父节点的拼接,子树整体不被触碰。测试用例foo_1 + foo_2 + foo_3(limit=10)直观展示了这种“逐层独立判定”:

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)形式

规则原文:

  1. 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)这一规范形态,且标签之间用,分隔。测试用例直接验证了这一重写行为(limit=10 下):

输入:sum(task:errors:rate10s{job="s"}) without(job,foo) 输出:sum without (job, foo) ( task:errors:rate10s{job="s"} )

见 prettier_test.go#L44-L49。超限时拆行后的形态由AggregateExpr.Pretty生成:ShortString()(即sum without (job, foo))+(\n+ 子表达式 +\n);若聚合操作符带参数(IsAggregatorWithParam(),如topkquantile),参数会作为单独一行插在子表达式之前:

s += e.ShortString() s += "(\n" if e.Op.IsAggregatorWithParam() { s += fmt.Sprintf("%s,\n", e.Param.Pretty(level+1)) } s += fmt.Sprintf("%s\n%s)", e.Expr.Pretty(level+1), indent(level))

见 prettier.go#L50-L65。对应效果(prettier_test.go#L62-L68):

topk( 10, ask:errors:rate10s{job="s"} )

规则四:函数调用参数逐个换行

规则原文:

  1. Functional call args will be split to different lines if they exceed themax_characters_per_line

Call.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(level+1), indent(level)) return s }

见 prettier.go#L101-L109。参数列表是Expressions类型,其Pretty实现把每个参数用,\n连接(prettier.go#L115-L127):

parts := make([]string, len(e)) for i := range e { parts[i] = e[i].Pretty(level) } return strings.Join(parts, ",\n")

典型效果(label_replace五个参数逐行排列):

label_replace( up{job="api-server",service="a:c"}, "foo", "$1", "service", "(.*):.*" )

见 prettier_test.go#L236-L245。注意子参数若自身也超限会递归拆行——测试中嵌套两层的label_replace(label_replace(...))(prettier_test.go#L247-L261)就展示了参数内部再拆函数的完整嵌套形态。

level 与缩进机制:父节点决定子节点的缩进

前面各规则最终都落到同一套缩进约定上,这里把机制完整讲清楚,便于自行扩展Pretty实现。文件头部注释(prettier.go#L21-L42)给出的算法是:

  1. 父节点没有换行(传入的 level 为 0)时,当前节点不得添加缩进前缀;
  2. 父节点换行了(level > 0)时,当前节点以level × " "作为前缀缩进;
  3. 当前节点超限时,输出缩进等于自身深度,并把 level+1 传给子节点。

各节点类型在这一机制下的行为差异值得注意:

  • SubqueryExpr.Pretty不添加缩进前缀,而是把时间后缀([range:step] @ ... offset ...)原样追加在内部表达式之后,见 prettier.go#L146-L151,测试效果为rate(\n long_vector_selector[10m:1m] @ start() offset 1m\n)(prettier_test.go#L214-L218);
  • StepInvariantExpr@表达式)透传 level(prettier.go#L138-L140);
  • UnaryExpr.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{job="s"})) 输出(注释全部消失): sum by (job, foo) ( sum by (job, foo) ( task:errors:rate10s{job="s"} ) )

因此在依赖美化输出的场景中(例如规则文件展示、告警表达式回显),不能期望用户注释被保留。

如何调用与验证:Prettify 与测试套件

Prettifypromql/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 表达式美化器的行为边界:超限才拆行needsSplitString()长度对maxCharactersPerLine=100判定)、选择器节点永不拆碎MatrixSelector/VectorSelectorgetCommonPrefixIndent)、聚合分组子句归一化前置(由printer.gowriteAggOpStr保证规范形态)、函数参数逐行排列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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询