首页
/ Prometheus 的 PromQL 测试脚本语言 promqltest:从 load/eval 语法到 expect 断言与迁移工具的完整实战指南

Prometheus 的 PromQL 测试脚本语言 promqltest:从 load/eval 语法到 expect 断言与迁移工具的完整实战指南

2026-09-04 12:28:20作者:邬祺芯Juliet

本文围绕 Prometheus 仓库中的 promql/promqltest 测试脚本语言展开:它是一套用纯文本描述「造数据、跑查询、断言结果」的引擎无关测试框架,并附带一套随源码分发的内置 PromQL 测试集。读完本文,你将掌握 .test 脚本的完整语法(load/clear/evalexpect 断言)、注解位置与 step 不变性检查等隐含机制,以及如何用官方迁移工具把旧版 eval_fail/eval_warn 语法批量升级为新的 expect 写法。

promqltest 是什么:一个可复用的 PromQL 引擎验收套件

promql/promqltest 包包含两样东西(见 README):

  1. PromQL 引擎的测试脚本语言实现:把一段纯文本脚本解析为命令序列,在内存测试存储上加载数据、执行即时/范围查询,并逐条断言结果与注解;
  2. 一套预定义的测试集:位于 promql/promqltest/testdata/ 的 21 个 .test 文件(如 aggregators.testinfo.testnative_histograms.test 等),覆盖聚合、选择器、直方图、staleness、子查询等主题。

两个对外入口分别是:

  • promqltest.RunBuiltinTests():把全部内置 .test 文件当作验收套件跑在任意 PromQL 引擎实现上;
  • promqltest.RunTest():执行你自己写的任意测试脚本。

从源码看,内置测试集通过 //go:embed testdata 直接嵌入编译产物(test.go#L275-L276),因此 RunBuiltinTests 可以脱离具体仓库目录运行。PromQL 引擎自身的测试就是这样调用它的:promql/promql_test.go#L36 中有一行 promqltest.RunBuiltinTests(t, newTestEngine(t))

包还导出了构造测试引擎的辅助函数 NewTestEngine / NewTestEngineWithOptstest.go#L99-L125)。默认引擎启用了内置测试文件用到的全部实验特性,见 TestParserOptstest.go#L91-L97):

var TestParserOpts = parser.Options{
    EnableExperimentalFunctions:  true,
    ExperimentalDurationExpr:     true,
    EnableExtendedRangeSelectors: true,
    EnableBinopFillModifiers:     true,
}

RunBuiltinTestsWithStorage 的注释明确要求:被测引擎必须用启用所有实验特性的 ParserOptions 创建,否则测试文件中的实验表达式会解析失败。若你想自定义存储(例如开启 start timestamp 或特定 chunk 编码),可以直接调用 RunBuiltinTestsWithStorage,默认版本 RunBuiltinTests 底层用的就是它,并开启了 EnableSTStorageFloatChunkEncoding = chunkenc.EncXOR2EnableHistogramSTEncodingtest.go#L160-L184)。

脚本整体结构:注释、命令与执行顺序

每个测试脚本都是纯文本文件。以 # 开头的行是注释,例如:

# This is a comment.

源码层面,getLines 会先按行切分、去掉首尾空白,并把以 # 开头的整行置空(test.go#L803-L814)。每个测试文件由一组命令按文件出现顺序依次执行,共三种命令:

  • load:向测试环境追加数据;
  • clear:清空已加载的全部数据;
  • eval:执行查询并断言结果。

parse 函数按行扫描,用命令首词分派到 parseLoad / parseEval,未知命令直接报错(test.go#L816-L844)。

注意(README 原文强调): eval 的旧变体 eval_faileval_warneval_infoeval_ordered 已被弃用,应改用新的 expect 行(见下文 eval 命令);同理,expected_fail_messageexpected_fail_regexp 也已弃用。

load 命令:用扩展记号造数据

load 向测试环境添加数据,语法如下:

load <interval>
    <series> <points>
    ...
    <series> <points>

参数含义:

  • <interval>:采样点之间的步长,如 1m30s
  • <series>:常规 Prometheus 时序名,metric{label="value"} 形式;
  • <points>:为该时序添加的点,使用与 promtool unittest 相同的扩展记号,详见 规则单元测试文档的 series 一节

例如:

load 1m
    my_metric{env="prod"} 5 2+3x2 _ stale {{schema:1 sum:3 count:22 buckets:[5 10 7]}}

将创建一条标签为 my_metric{env="prod"} 的时序,产生如下点:

  • t=0:值 5;
  • t=1m:值 2;
  • t=2m:值 5;
  • t=3m:值 8;
  • t=4m:无点(_ 表示缺失样本);
  • t=5m:stale 标记;
  • t=6m:原生直方图,schema 1,sum 3,count 22,桶计数 5、10、7。

扩展记号的核心规则(与 unit_testing_rules.md 一致):a+bxn 展开为 a a+b a+(2*b) … a+(n*b),即从 a 起再有 n 个步长为 b 的样本;a-bxn 为递减;axna+0xn 的简写,表示 a 重复 n+1 次;_ 表示缺失、stale 表示 stale 样本。原生直方图可用 {{schema:1 sum:3 count:22 buckets:[5 10 7]}} 这类花括号记号,支持全部属性(schemasumcountz_bucketz_bucket_wbucketsoffsetn_bucketsn_offsetcounter_reset_hintcustom_values),且与浮点数一样支持 axna+bxn 等扩展记号。

源码中 parseLoad 复用解析器的 ParseSeriesDesc 来解析每一行时序定义(test.go#L285-L344);loadCmd.set 把序列展开为带毫秒时间戳的 promql.Sample,起始时间统一锚定在 Unix epoch(testStartTime = time.Unix(0, 0).UTC()test.go#L72)。

关键点:load 是追加式的,clear 用于重置

每条 load 命令都是追加语义——它不会替换之前 load 的数据。需要重置环境时显式执行 clear,它会关闭当前存储并重新打开一个空的测试存储(t.clear() 实现,test.go#L1870-L1881)。testdata 中大量文件(如 info.test)正是反复用 clear 分隔出独立的测试场景。

load _with_nhcb:经典直方图批量转 NHCB

加载一批经典直方图浮点时序时,可以在 load 后追加 _with_nhcb 后缀,把它们转换成「带自定义桶的原生直方图」(Native Histograms with Custom Buckets,NHCB),并同时加载原始浮点时序与转换后的直方图时序。

从源码看,这条路径由 loadCmd.appendCustomHistogram 实现(test.go#L969-L1030):它按时间戳把 _bucket/_count/_sum 后缀的经典直方图样本汇总成 convertnhcb.TempHistogram,调用 Convert() 生成 schema 为自定义桶(-53)的原生直方图后逐点写入存储。解析侧则由正则 ^load(?:_(with_nhcb))?\s+(.+?)$ 捕获该可选后缀(test.go#L52)。

clear 命令

clear 移除此前所有 load 加载的数据。注意它不是删除样本,而是整体更换测试存储实例,因此 clear 之后所有 load/eval 都基于全新空环境。

eval 命令:Instant 与 Range 两种查询

eval 对测试环境执行查询并断言结果。除非提供了 expect fail 行,否则要求查询成功;旧版 eval 默认期望没有任何 info/warn 注解,而现在必须显式提供 expect no_infoexpect no_warn来表达这一约束。即时查询与范围查询都受支持,语法:

# 即时查询
eval instant at <time> <query>
    <expect>
    ...
    <expect>
    <series> <points>
    ...
    <series> <points>

# 范围查询
eval range from <start> to <end> step <step> <query>
    <expect>
    ...
    <expect>
    <series> <points>
    ...
    <series> <points>

参数说明:

  • <time>:即时查询的评估时间戳,如 1m(相对 Unix epoch 的持续时间);
  • <start>/<end>:范围查询的起止时间,语法同 <time>
  • <step>:范围查询步长,语法同 <time>,如 30s
  • <expect>(可选):声明期望的注解、错误或结果顺序;
  • expect range vector(可选):即时查询时可声明期望的范围向量时间戳;
  • expect string "<string>"(可选):断言字符串字面量结果;
  • <series><points>:期望值,语法同 load

解析实现见 parseEvaltest.go#L623-L781),它用正则区分即时/范围两种形态,并把期望行按类型分桶存入 evalCmd.expectedCmds。一个真实用例(摘自 aggregators.test):

load 5m
  http_requests{job="api-server", instance="0", group="production"} 0+10x10
  http_requests{job="api-server", instance="1", group="production"} 0+20x10
  http_requests{job="api-server", instance="0", group="canary"}   0+30x10
  http_requests{job="api-server", instance="1", group="canary"}   0+40x10

# Simple sum.
eval instant at 50m SUM BY (group) (http_requests{job="api-server"})
  {group="canary"} 700
  {group="production"} 300

原生直方图的 counter reset hint 特殊处理

<points> 中的原生直方图可以也可以带显式 counter_reset_hint 属性:

  • 显式提供时(unknownresetnot_resetgauge),测试会校验直方图的 reset hint 恰为该值;
  • 未指定时,reset hint 完全不参与校验(而不是按默认值 unknown 校验)。

源码印证:compareNativeHistogram 接收一个 counterResetHintSet 布尔参数,只有当期望值里显式写了 hint 时才比较 CounterResetHint 字段,注释明确写着「当 counterResetHintSet 为 false 时表示『不关心』」(test.go#L1393-L1443)。同时该函数对 Count/Sum 等浮点字段使用 almost.Equal 做相对误差容忍比较,容忍度由常量 defaultEpsilon = 0.000001 控制(test.go#L61)。

expect string:断言字符串字面量

用于断言查询返回的字符串字面量,仅支持即时查询

eval instant at 50m ("Foo")
 expect string "Foo"

期望字符串必须加引号,支持双引号或反引号。解析侧 parseAsStringLiteralstrconv.Unquote 解引号,缺少引号会直接报「a quoted string literal is required」(test.go#L783-L801)。

expect range vector:断言即时查询返回的范围向量

即时查询默认只能返回单点向量;若要断言「返回的是一个范围向量」,需要显式声明其时间戳窗口:

expect range vector <start> to <end> step <step>

完整例子(摘自 README):

load 10s
  some_metric{env="a"} 1+1x5
  some_metric{env="b"} 2+2x5
eval instant at 1m some_metric[1m]
  expect range vector from 10s to 1m step 10s
  some_metric{env="a"} 2 3 4 5 6
  some_metric{env="b"} 4 6 8 10 12

源码中的实现细节值得注意:一旦某条即时查询出现 expect range vector 行,该查询会被打上 excludeFromRangeQuery 标记,跳过框架默认的「范围模式复查」,且未声明时若期望多个点会直接报错提示使用 expect range vectortest.go#L724-L738test.go#L774-L777)。

expect 语法详解:fail、info、warn、no_info、no_warn、ordered

新版断言统一写作:

expect <type> <match_type>: <string>

参数说明:

  • <type> 是期望类型:
    • fail:期望查询失败;
    • info:期望查询返回至少一条 info 注解;
    • warn:期望查询返回至少一条 warn 注解;
    • no_info:期望查询不返回任何 info 注解;
    • no_warn:期望查询不返回任何 warn 注解;
    • ordered:期望查询按指定顺序返回结果。
  • <match_type>(可选)指定注解消息的匹配方式:
    • msg:精确字符串匹配;
    • regex:正则匹配;
    • orderedno_infono_warn 不适用。
  • <string> 是期望的注解消息;对 fail 则是期望的错误消息。warninfo 的注解字符串以位置信息结尾,精确 msg 匹配时必须带上位置,见下文 注解位置

完整示例(摘自 README):

eval instant at 1m sum by (env) (my_metric)
    expect warn
    expect no_info
    {env="prod"} 5
    {env="test"} 20

eval range from 0 to 3m step 1m sum by (env) (my_metric)
    expect warn msg: PromQL warning: something went wrong (1:15)
    expect info regex: PromQL info: something went (wrong|boom) \(1:15\)
    {env="prod"} 2 5 10 20
    {env="test"} 10 20 30 45

eval instant at 1m ceil({__name__=~'testmetric1|testmetric2'})
expect fail

eval instant at 1m ceil({__name__=~'testmetric1|testmetric2'})
expect fail msg: "vector cannot contain metrics with the same labelset"

eval instant at 1m ceil({__name__=~'testmetric1|testmetric2'})
expect fail regex: "vector cannot contain metrics .*|something else went wrong"

eval instant at 1m sum by (env) (my_metric)
expect ordered
{env="prod"} 5
{env="test"} 20

同一条 eval 可以对同一 <type> 写多行 expect:每一行各自校验对应注解、错误或顺序,互不干扰;但每一行 <expect> 都必须至少命中一条对应注解或错误。一旦存在至少一行 warninfo 类型的 expect,则所有对应注解都必须有匹配的 expect 行——即双向包含校验。这一点在 validateExpectedAnnotationsOfType 中实现:先遍历期望列表确认每条都有命中,再遍历实际注解确认每条都有对应期望,否则报「unexpected … annotation」(test.go#L1181-L1210)。

validateExpectedCmds 还施加了组合约束(test.go#L559-L570):

  • infono_info 不能同时使用;
  • warnno_warn 不能同时使用;
  • expect fail 只允许出现一行。

真实测试文件里也能看到用法,例如 info.test 末尾:

eval range from 0m to 2m step 1m info(metric, {__name__=~".+_info"})
	expect fail regex: conflicting label

以及「查询应成功且无注解」的标准写法——expect no_warn + expect no_info 成对出现(这正是迁移工具 strict 模式会自动补上的两行)。

注解位置(Annotation Positions)

warninfo 期望是拿注解在 API 中的渲染形态来匹配的,而该形态以触发注解的表达式位置结尾:

eval instant at 0m sum(metric)
    expect warn msg: PromQL warning: encountered a mix of histograms and floats for aggregation (1:5)

其中 (1:5)metric 所在的行号与列号——也就是注解所针对的表达式位置。断言位置可以确保注解仍然指向查询的正确部分。fail 期望匹配的是查询错误,错误本身不携带位置。

有两类情况让精确位置难以维护,解法都是用 regex

  1. 位置附加的详细信息可能随评估时间变化。例如修复非单调直方图的 info 注解在展开后包含细节:
eval instant at 50m histogram_quantiles(nonmonotonic_bucket, "q", 0.01, 0.5, 0.99)
    expect info regex: PromQL info: input to histogram_quantile needed to be fixed for monotonicity .* \(1:21\)
  1. 即时查询会被框架以追加 @ 修饰符的形式重跑一遍。重新序列化表达式会规范空格并使每个选择器变长,导致第一个选择器之后的列号全部右移。因此精确位置只在「被注解表达式不晚于第一个选择器开始」时才稳定(上述两例都满足),否则应宽松匹配位置:
eval instant at 50m histogram_quantiles(non_existent, "q", NaN)
    expect warn regex: PromQL warning: quantile value should be between 0 and 1, got NaN \([0-9]+:[0-9]+\)

第 2 条机制有直接的源码依据:atModifierTestCases 会遍历表达式 AST,给所有尚未带 @VectorSelectorMatrixSelectorSubqueryExpr 补上评估时间,然后生成多个改写后的用例在额外时间点上重跑(test.go#L1594-L1654);execInstantEval 则把原始查询排在这些改写用例之前逐个执行(test.go#L1748-L1761)。这正是 README 所说「即时查询还会以每个选择器追加 @ 修饰符的形式重新执行,以检查结果与步长无关」的实现。

源码级补充:框架还替你做了哪些隐式断言

文档语法之外,阅读 test.go 可以发现几条影响写法行为的隐式规则:

  1. step 不变性 + 范围模式复查:除 @ 改写重跑外,每条即时查询还会被转成一个范围为 [eval-1m, eval+1m]、步长 1m 的范围查询重跑,并与中间步的结果比对,确保即时查询在范围模式下结果一致(runInstantQuerytest.go#L1802-L1851)。因此测试文件里出现的表达式必须「任意步长下结果一致」,这也是部分函数(如 start()/end()/range()/step() 相关查询)被显式跳过复查的原因(test.go#L1590-L1591)。
  2. 矩阵结果必须按标签有序:范围查询结果会经过 assertMatrixSorted 检查,确保引擎输出始终按标签排序(test.go#L1853-L1868);而 expect ordered 则进一步断言即时向量的具体顺序,范围模式复查时对 ordered 用例会跳过(test.go#L1817-L1820)。
  3. 浮点比较带相对误差:所有标量比较走 almost.Equal(actual, expected, 0.000001),即相对误差 1e-6 内视为相等(test.go#L61test.go#L1306-L1308)。
  4. 每条 eval 成为独立子测试:非测试模式下,每条 eval 会以 line <行号>/<表达式> 为名称注册为一个子测试(test.go#L1693-L1713),失败定位天然精确到脚本行。

此外,LazyLoadertest.go#L1895 起)是同一脚本语言的「只读数据加载器」变体,专门用于规则单元测试——它只接受 load 命令,被 promtool test rules 用来把 promtool unittest 文档 中的 input_series 喂进存储,这解释了为什么两者的 <points> 扩展记号完全同源。

旧语法迁移工具:三种模式把 eval_* 升级为 expect 行

README 提供了官方迁移工具,用于把旧版脚本原地升级为新语法:

  • 目录由 --dir 指定(默认即内置测试集目录),其中所有 .test 文件会被原地更新
  • 弃用语法会被替换为推荐的 expect 行。

用法:

go run ./promql/promqltest/cmd/migrate/main.go --mode=strict [--dir=<directory>]

--mode 控制迁移策略(cmd/migrate/main.gomode 默认为 strict):

  • strict:把所有期望严格迁移到新语法。由于旧语法隐含了许多常常并不需要的约束,迁移结果可能比预期更啰嗦(例如普通 eval 会被补上 expect no_warnexpect no_info);
  • basic:类似 strict,但从不生成 no_info/no_warn 期望。适合作为手动补充这两类期望、或按需删除 info/warn 期望的起点;
  • tolerant:仅在合适处生成 expect failexpect ordered,所有关于 info/warn 存在与否的期望都需手动添加。

三种模式对「原本通过的测试」都会产出「依然通过的测试」;basictolerant 只是校验的期望更少。

test_migrate.go 的映射表可以看到各模式的精确行为,例如 strict 模式下:

旧命令 生成的 expect 行
eval_fail expect fail + expect no_warn + expect no_info
eval_warn expect warn + expect no_info
eval_info expect info + expect no_warn
eval_ordered expect ordered + expect no_warn + expect no_info
普通 eval expect no_warn + expect no_info

同时,旧的 expected_fail_message/expected_fail_regexp 行会被改写成 expect fail msg: …/expect fail regex: …test_migrate.go#L173-L193);若某个 eval 块里已经存在 expect 行,迁移器会跳过该块不做改动,避免重复处理(test_migrate.go#L132-L147)。迁移器还会从首个缩进行自动探测 tab/空格风格并保持一致。

上手路径小结:如何运行内置测试集

结合源码,验证任何 PromQL 引擎实现的标准流程是:

  1. promqltest.NewTestEngineWithOpts(或等价方式)构造启用全部实验解析特性的引擎;
  2. 调用 promqltest.RunBuiltinTests(t, engine),或需要自定义存储时调用 RunBuiltinTestsWithStorage
  3. 对自有表达式,编写 .test 脚本(load 造数 → eval + expect 断言),通过 promqltest.RunTest 执行。

一个可直接参考的最小脚本结构(综合 aggregators.testREADME 示例):

# 注释以 # 开头
load 1m
    my_metric{env="prod"} 2 5 10 20
    my_metric{env="test"} 10 20 30 45

eval instant at 50m sum by (env) (my_metric)
    expect no_warn
    expect no_info
    {env="prod"} 5
    {env="test"} 20

eval range from 0 to 3m step 1m sum by (env) (my_metric)
    expect no_warn
    expect no_info
    {env="prod"} 2 5 10 20
    {env="test"} 10 20 30 45

clear
# clear 之后是全新空环境,可继续 load/eval

需要留意的适用前提:本文描述的行为均以当前仓库 promql/promqltest/ 下的实现为准——expect 新语法与「显式 expect no_info/expect no_warn」的强制性是近期版本才成为默认契约的,旧文档或第三方引擎适配层若仍使用 eval_fail 等弃用写法,可优先用上述迁移工具对齐。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341