拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Grafana Loki 列式计算测试 DSL 详解:pkg/compute 领域特定语言与选择向量测试机制

Grafana Loki 列式计算测试 DSL 详解pkg/compute 领域特定语言与选择向量测试机制【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/lokiGrafana Loki 的pkg/compute是一个无状态列式数据计算包它通过一组纯函数对列式数据columnar.Datum执行等于、比较、逻辑运算、子串匹配、集合成员判断等操作。为了对这些计算函数进行系统、紧凑、可读的测试Loki 在 pkg/compute/testdata 下定义了一套专属的领域特定语言DSL把一组计算操作与期望结果写成一行一行的测试用例。本文以 pkg/compute/testdata/README.md 为骨架完整讲解这套 DSL 的语法、数据类型、测试用例组织方式并结合compute_test.go、computetest/parser.go等源码揭示其解析与执行原理。读完本文你将能够阅读、编写和扩展这类 DSL 测试文件理解选择向量selection vector如何参与计算并影响结果以及如何在新增计算函数时把 DSL 测试接入测试框架。一、背景compute 包与列式测试 DSL 的定位pkg/compute是 Loki 内部的列式计算包其包注释pkg/compute/doc.go明确说明它实现无状态的计算操作作用对象是一个或多个columnar.Datum——一种不透明表示既可以是一个数组array也可以是一个标量scalar值并带有特定的值类型kind。除非特别说明所有 compute 函数返回的数据都使用调用方传入的memory.Allocator分配。该包目前标注为EXPERIMENTAL实验性当前仅被 pkg/dataobj 使用。compute 包内部按功能拆分为多个源码文件equality.go等于/不等于/大小比较、logical.goNOT/AND/OR、filter.goFILTER、set.goIsMember、utf8.go子串与正则匹配外加compat.go、stats.go、shape.go、assertions.go、results_cache.go等外围文件。围绕这些计算函数pkg/compute/testdata/目录存放了用 DSL 编写的测试文件它们与 Go 测试互补DSL 擅长表达输入参数组合 期望输出这种高密度的数据驱动用例Go 测试则负责 DSL 难以表达的复杂场景如期望报错、record batch、struct 操作等。二、DSL 语法规范pkg/compute/testdata/README.md给出了完整的 DSL 文法grammarCases : Case* Case : Function Datum* Selection? - Datum TERMINATOR Datum : TypedValue Selection : select : [ Scalar* ] TypedValue : Type : Value Type : bool | int32 | int64 | uint32 | uint64 | utf8 | null Value : Scalar | Array Scalar : literal | null | _ Array : [ Scalar* ] TERMINATOR : \n逐条拆解Cases一个测试文件由零个或多个 Case 组成。Case一行测试用例结构为函数名 若干 Datum 参数 可选的选择向量 - 期望结果 Datum 行结束符。例如EQ int32:5 int32:5 - bool:true。Datum带类型的值形如类型:值例如int32:[1 2 3]、utf8:hello、null:[null null]。Selection可选的选择向量形如select:[true false true]必须是布尔数组用于指示哪些行参与计算、哪些行在结果中被标记为未定义见第四节。TERMINATOR一条用例以换行符结束。注释以#开头持续到行尾。所有空白字符都被忽略因此排版比较自由例如函数名与参数之间可以用多个空格对齐便于阅读。2.1 从文法到解析器实现这套文法在 pkg/compute/internal/computetest/parser.go 中有逐一的实现对应parseCases()parser.go 第 54 行循环调用parseCase()直至遇到tokenEOFparseCase()第 70 行先读取函数名然后循环解析 Datum 参数直到遇到-tokenArrow或selecttokenSelect接着解析可选的选择向量再解析-后的期望结果最后要求一个TERMINATOR收尾parseDatum()第 117 行根据类型标识分发到parseBoolDatum、parseInt32Datum、parseInt64Datum、parseUint32Datum、parseUint64Datum、parseUTF8Datum、parseNullDatumparseSelection()第 596 行解析select:后的布尔数组并且校验选择向量中不能包含 null 值否则报错。底层还依赖词法分析器scanner.go与 token 定义token.go整数字面量支持可选负号-42字符串字面量必须加引号并支持转义序列标识符用于函数名、类型名以及true/false/null/_等字面量。三、数据类型详解DSL 支持 7 种类型对应 parser.go 中parseDatum的分发逻辑以及一个特殊的未定义占位符类型含义示例bool布尔值true、false、nullint32有符号 32 位整数-42、123int64有符号 64 位整数-42、123uint32无符号 32 位整数0、456uint64无符号 64 位整数0、456utf8UTF-8 字符串必须加引号hello、test string支持转义序列null显式的空值类型用于纯 null 值null:[null null null]_未定义值undefined表示某个位置的值是未定义的例如选择向量中未被选中的行关于_有一个重要细节它不是一个独立的类型而是值层面的占位符。从 parser.go 可以看到_在标量位置会被解析为对应类型的空标量如columnar.BoolScalar{}、columnar.NumberScalar[int32]{}、columnar.UTF8Scalar{}在数组位置如bool:[true _ true]则向 builder 追加一个占位值。因此_可以出现在任意类型的值位置上表示该槽位无有效值通常用来与选择向量配合指示未选中的行。解析器还校验数值的字面量范围int32使用strconv.ParseInt(..., 32)uint64使用strconv.ParseUint(..., 64)超范围会在解析阶段直接报错而不是等到运行时。四、选择向量Selection Vector语义选择向量是这套测试 DSL 最具特色的机制也是 pkg/compute/ARCHITECTURE.md 详细阐述的核心设计。它让计算操作能够高效地做行级过滤——只对满足条件的行执行计算而不必物化出过滤后的子集。4.1 表示方式与约定选择向量在运行时用 memory.Bitmap 表示bitmap 中的每一位对应输入数据的一行true bit该行被选中参与计算false bit该行未被选中在结果中按 null/未定义处理。关键约定摘自 ARCHITECTURE.md全部选中当 selection 参数Len() 0时表示所有行都被选中。这是默认行为且相比不支持选择向量的操作零额外性能开销allSelected : memory.Bitmap{} // Len() 0 result : compute.Equals(mem, left, right, allSelected) // all rows selected部分选中当 selection bitmap 的Len() 0时只有对应位为 true 的行参与计算其余行在结果中被置为未定义selection : memory.NewBitmap(mem, 10) // 10 rows selection.Set(0) // select first row selection.Set(5) // select sixth row result : compute.Equals(mem, left, right, selection) // only rows 0 and 5 are computed, others are null4.2 调度模式Dispatch Patterncompute 函数针对输入组合采用统一的调度模式见 ARCHITECTURE.md 的 Dispatch Pattern 一节选择向量只作用于数组结果输入组合缩写选择向量处理标量-标量SS不应用选择向量结果是标量标量-数组SA对结果数组应用选择向量数组-标量AS对结果数组应用选择向量数组-数组AA对结果数组应用选择向量设计动机包括懒求值被过滤的数据不复制、不物化、内存高效未选中的行原地保留、可组合选择向量可通过位运算合并、一致的空值处理未选中的行按 null 处理。4.3 DSL 中的选择向量写法在 DSL 中选择向量写作select:[true false true false]这种形式位于参数之后、-之前。以 pkg/compute/testdata/selection.test 中的用例为例EQ int32:[1 2 3 4] int32:[1 0 3 0] select:[true false true false] - bool:[true _ true _]解读左右两个数组逐行比较只有第 0 行和第 2 行被选中因此这两行产生真实比较结果true、true第 1、3 行未被选中结果槽位写为_未定义。所以期望输出为bool:[true _ true _]。选择向量还有几个值得注意的边界行为均可在 selection.test 与 logical.test 中看到对应用例全不选中select:[false false false false]时输出全部为_例如AND ... select:[false false false false] - bool:[_ _ _ _]单选只选中一行时只有该行有计算结果其余为_带 null 的输入 选择向量选中行中如果有 null结果为 null未选中行仍为_二者语义不同但可共存例如AND bool:[true null true false] bool:[true true false null] select:[true true false false] - bool:[true null _ _]。五、测试文件组织与运行方式5.1 目录结构pkg/compute/testdata 下共有 6 个.test文件按计算函数分组文件覆盖的函数说明equality.testEQ / NEQ / LT / LTE / GT / GTE等于、不等于、小于、小于等于、大于、大于等于logical.testNOT / AND / OR逻辑非、与、或filter.testFILTER按掩码过滤数组set.testISMEMBER集合成员判断utf8.testSUBSTR / SUBSTRI / REGEXP子串、大小写不敏感子串、正则匹配selection.test混合函数专门演示选择向量的遮蔽行为5.2 运行入口TestCompute所有这些.test文件由 pkg/compute/compute_test.go 中的TestCompute函数统一驱动第 20 行起func TestCompute(t *testing.T) { require.NoError(t, filepath.WalkDir(testdata, func(path string, d fs.DirEntry, walkErr error) error { // 只处理 *.test 文件 cases, err : computetest.ParseCases(f) ... for _, tc : range cases { t.Run(fmt.Sprintf(%s/source%s:%d, tc.Function, d.Name(), tc.Line), func(t *testing.T) { var alloc memory.Allocator result, err : evalCaseFunction(t, alloc, tc) require.NoError(t, err) mask : tc.Selection if tc.Function FILTER { // FILTER 会把掩码物化因此不再把 mask 传给 RequireDatumsEqual mask memory.Bitmap{} } columnartest.RequireDatumsEqual(t, tc.Expect, result, mask) }) } ... })) }运行方式cd pkg/compute go test -run TestCompute ./...TestCompute会递归遍历testdata目录读取所有.test后缀文件用computetest.ParseCases解析出全部用例然后逐条调用evalCaseFunction执行对应的 compute 函数最后用columnartest.RequireDatumsEqual对比实际结果与期望结果并考虑选择向量掩码。每个用例的测试名包含函数名、源文件名与行号tc.Line失败时可以精确定位到具体某一行 DSL。其中FILTER有特殊处理FILTER 会物化掩码、真正压缩出结果数组所以校验阶段不再回传 mask避免重复遮蔽。六、核心计算函数与 DSL 用例解析evalCaseFunctioncompute_test.go 第 58 行维护着 DSL 函数名到 compute 包函数的映射表这是理解 DSL 的关键索引DSL 函数名底层实现pkg/compute参数特殊处理EQEquals2要求两侧类型一致NEQNotEquals2同上LTLessThan2要求可排序类型LTELessOrEqual2同上GTGreaterThan2同上GTEGreaterOrEqual2同上NOTNot1仅布尔ANDAnd2仅布尔OROr2仅布尔SUBSTRSubstr2大小写敏感子串SUBSTRISubstrInsensitive2大小写不敏感子串REGEXPRegexpMatch2第二个参数必须是 utf8 标量被编译为正则FILTERFilter1用选择向量做掩码物化过滤ISMEMBERIsMember2第二个参数数组被转换为columnar.Set下面按文件逐一说明各类函数在 DSL 中的用例写法。6.1 等值与比较运算equality.testequality.test是最大的一个测试文件系统覆盖了 EQ / NEQ / LT / LTE / GT / GTE 六种比较函数每种函数都覆盖标量-标量scalar, scalarEQ bool:true bool:false - bool:false EQ int32:5 int32:5 - bool:true LT utf8:a utf8:b - bool:true标量-数组scalar, arrayEQ bool:true bool:[true false null] - bool:[true false null] LT int32:5 int32:[3 5 10 null] - bool:[false false true null]数组-标量array, scalarNEQ int32:[5 10 null] int32:10 - bool:[true false null] GTE utf8:[apple banana cherry null] utf8:banana - bool:[false true true null]数组-数组array, arrayEQ int32:[1 2 3 4 null null null] int32:[1 3 3 5 1 2 null] - bool:[true false true false null null null] GT int32:[5 5 10 10 null null null] int32:[1 5 5 15 1 2 null] - bool:[true false true false null null null]带选择向量EQ int32:[10 20 30 40] int32:[10 21 30 41] select:[true false true false] - bool:[true _ true _] LTE int32:[10 20 10] int32:[10 15 10] select:[true false true] - bool:[true _ true]从 equality.go 的Equals实现可以看到一个明确的规则两侧类型不一致直接报错both inputs must be the same kind且任何一侧出现 null结果就是 null。LessThan等比较运算要求类型可排序ordered对null同样传播空值。值得注意的用例细节在标量-数组和数组-标量组合中null的传播行为与标量-标量一致——只要参与比较的对应位置出现 null结果就是 null例如EQ int32:null int32:[5 10 null] - bool:[null null null]。6.2 逻辑运算logical.testlogical.test覆盖 NOT / AND / OR 三种逻辑运算。核心语义同样遵循三值逻辑true / false / nullNOT bool:true - bool:false NOT bool:null - bool:null AND bool:true bool:null - bool:null OR bool:false bool:null - bool:null AND bool:[true false null] bool:false - bool:[false false null] OR bool:[true false null] bool:true - bool:[true true null]带选择向量的逻辑运算用例也覆盖了四种组合数组-数组、标量-数组、数组-标量AND bool:[true false true false] bool:[true true false false] select:[true false true false] - bool:[true _ false _] AND bool:true bool:[true false true false] select:[true false true false] - bool:[true _ true _] OR bool:[true false true false] bool:false select:[false true false true] - bool:[_ false _ false]从源码看Notlogical.go直接忽略选择向量参数参数名为_ memory.Bitmap因为 NOT 是单目运算选择向量的遮蔽由上层统一处理And/Or第 72、83 行则接收并应用选择向量。6.3 过滤运算filter.testFILTER与其它函数不同它的作用不是生成布尔结果而是根据布尔掩码真正压缩出子数组——只保留掩码为 true 的行FILTER bool:[true false true false] select:[true false true false] - bool:[true true] FILTER int32:[1 2 3 4 5] select:[true false true false true] - int32:[1 3 5] FILTER int32:[10 20 30] select:[false false false] - int32:[] FILTER utf8:[null hello world] select:[true true false] - utf8:[null hello] FILTER null:[null null null] select:[true false true] - null:[null null]几个要点全不选中时结果为空数组[]而不是全_数组输入中的 null 值在选中时会原样保留int32:[10 null 30] - int32:[10 null]这就是compute_test.go中对 FILTER 特殊处理mask memory.Bitmap{}的原因——掩码已经被物化消费掉了。从 filter.go 的实现看Filter接收input columnar.Datum和mask memory.Bitmap返回按掩码挑选后的新 Datum。6.4 集合成员判断set.testISMEMBER判断第一个数组中的每个值是否属于第二个数组作为集合ISMEMBER utf8:[test1 test2 test3] utf8:[test1 test2 test4] - bool:[true true false] ISMEMBER int32:[1 2 3] int32:[4 5 6] - bool:[false false false] ISMEMBER int32:[] int32:[1 2 3] - bool:[] ISMEMBER utf8:[null] utf8:[test1 test2 test3] - bool:[null]带选择向量的用例ISMEMBER utf8:[apple null cherry null] utf8:[apple cherry] select:[true true true true] - bool:[true null true null] ISMEMBER utf8:[apple null cherry null] utf8:[apple cherry] select:[true false true false] - bool:[true _ true _]在evalCaseFunction中ISMEMBER的第二个参数数组会被转换为columnar.SetNewUTF8Set或NewNumberSet且集合不允许包含 null 值require.Equal(t, 0, arr.Nulls(), ...)转换后调用 IsMember。注意集合语义是去重后的成员判断因此第二个数组即使有重复值也只影响集合构建不影响结果。6.5 字符串与正则utf8.testutf8.test覆盖三种字符串匹配函数SUBSTR大小写敏感的子串匹配。utf8:test匹配test但不匹配TESTSUBSTRI大小写不敏感的子串匹配test也能匹配TESTREGEXP正则表达式匹配第二个参数必须是标量正则模式且由测试框架编译为*regexp.Regexp后传入 RegexpMatch。基础用例SUBSTR utf8:[test] utf8:test - bool:[true] SUBSTR utf8:[test] utf8:TEST - bool:[false] SUBSTRI utf8:[test] utf8:TEST - bool:[true] REGEXP utf8:[test] utf8:test - bool:[true] REGEXP utf8:[test] utf8:NOTtest - bool:[false]空字符串与 null 的边界行为SUBSTR utf8:[test] utf8: - bool:[true] // 空串是任意字符串的子串 SUBSTR utf8:[null] utf8:test - bool:[null] REGEXP utf8:[test] utf8:null - bool:[null]带选择向量的正则用例utf8.test中给出了 full / partial / middle / single / none 五种遮蔽形态的完整矩阵REGEXP utf8:[foo bar baz qux test] utf8:ba. select:[true true true true true] - bool:[false true true false false] # Full selection REGEXP utf8:[foo bar baz qux test] utf8:ba. select:[false true true true false] - bool:[_ true true false _] # Partial (middle)REGEXP 的特殊处理逻辑位于 compute_test.go第二参数必须断言为*columnar.UTF8Scalar否则测试失败非 null 时用regexp.Compile编译编译失败即测试失败若模式为 null则传入 nil 正则此时结果全为 null。七、如何新增一个 compute 函数并接入 DSL 测试README 对扩展流程给出了明确指引结合源码可整理为如下步骤实现 compute 函数在pkg/compute下新增函数签名遵循统一约定func(alloc *memory.Allocator, args ..., selection memory.Bitmap) (columnar.Datum, error)返回数据使用传入的 allocator 分配见 doc.go 的包级约定。更新evalCaseFunction在 pkg/compute/compute_test.go 的switch tc.Function中新增分支把 DSL 函数名映射到新函数并用require.Len校验参数个数。处理特殊参数如果新函数需要非常规参数例如 REGEXP 需要把columnar.Datum转成编译后的正则、ISMEMBER 需要把数组转成Set需要像 compute_test.go 那样做类型断言与转换。编写 DSL 用例在testdata下新增或扩展.test文件按第五节语法编写用例覆盖标量/数组的四种组合以及选择向量场景。运行测试go test -run TestCompute ./pkg/compute/即可验证全部 DSL 用例。八、DSL 的边界错误测试与高级测试README 明确划定了 DSL 的能力边界这两类场景应放在 Go 测试文件中错误测试Error testingDSL无法表达计算函数应当失败的用例例如类型不匹配、不可排序类型、非法参数。这类用例应写在 Go 测试文件中直接断言错误返回值例如compute.Equals对两侧 kind 不一致会返回fmt.Errorf(both inputs must be the same kind, got %s and %s, ...)equality.go。高级测试Advanced tests作用于 record batch 或 struct 的更复杂计算函数例如 equality_struct.go、filter_struct_test.go、equality_bench_test.go 所覆盖的场景它们需要更复杂的构造和前置条件更适合直接用 Go 测试代码搭建。这一分工让 DSL 保持一行一用例的高密度表达力同时把异常路径和复杂结构测试留在表达能力更强的 Go 层。九、小结pkg/compute/testdata下的 DSL 是 Loki 列式计算引擎测试体系中的一块数据驱动拼图语法上它用函数名 类型化 Datum 参数 可选选择向量 - 期望结果的紧凑文法一行描述一个完整用例注释与空白规则让它天然易读语义上它忠实反映了 compute 包的运行时约定——三值逻辑null 传播、类型一致性校验、四种输入组合的调度模式以及选择向量的懒求值与未定义槽位标记工程上TestComputecomputetest解析器scanner/parser/token共同构成一个低摩擦的扩展通道新增计算函数只需实现函数、在evalCaseFunction注册映射、在.test文件补充用例即可。理解这套 DSL等于同时理解了 Loki 列式数据模型columnar.Datum、memory.Bitmap与计算层的测试方法论——这对于阅读 pkg/dataobj 等使用 compute 包的上游代码以及为 Loki 贡献新的列式计算能力都是必要的一课。【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门