Loki LogQL 查询参考:二元运算符、管道错误处理与 label_replace 函数完全指南
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
LogQL 是 Loki("Like Prometheus, but for logs")的查询语言,它允许你在日志流之上进行筛选、聚合与指标计算。本文以官方 query_reference.md 为核心骨架,系统讲解 LogQL 中全部运算符(算术、逻辑/集合、比较、模式匹配过滤)、on/ignoring/group_left/group_right向量匹配修饰符、#注释、__error__管道错误处理以及label_replace()函数,并结合仓库源码(如 pkg/logql/syntax/syntax.y 与 pkg/logql/syntax/ast.go)揭示语法定义与底层实现。读完本文,你将能写出精确、高效、可排查错误的 LogQL 查询,并理解其解析与求值原理。
二元运算符(Binary Operators)
Loki 在一条 LogQL 查询 中接受多种运算符。二元运算符按类别可分为:算术运算符、逻辑与集合运算符、比较运算符,以及 Loki 特有的模式匹配过滤运算符。这些运算符定义在 LogQL 语法文件 pkg/logql/syntax/syntax.y 的binOpExpr规则中,并被映射为 ast.go 中的BinOpExpr抽象语法树节点。
算术运算符
Loki 支持以下二元算术运算符:
+(加法)-(减法)*(乘法)/(除法)%(取模)^(幂/指数)
算术运算符可定义在三种操作数组合之间:两个字面量(标量 scalar)、字面量与向量、以及两个向量。
- 两个字面量之间:行为显而易见,直接对两个标量操作数求值得到另一个字面量,例如
1 + 1 = 2。 - 向量与字面量之间:运算符作用于向量中每个数据样本的值。例如将时间序列向量乘以 2,得到的新向量中每个样本值都是原值的 2 倍。
- 两个向量之间:算术运算符作用于左侧向量的每个元素与其在右侧向量中匹配到的元素。结果传播到结果向量中,分组标签成为输出标签集;在右侧向量中找不到匹配项的元素不会出现在结果中。
串联使用算术运算符时,请特别注意运算符顺序。
算术运算示例
用一条简单查询实现健康检查:
1 + 1将日志流条目的速率翻倍:
sum(rate({app="foo"}[1m])) * 2计算foo应用中 warning 日志与 error 日志的占比:
sum(rate({app="foo", level="warn"}[1m])) / sum(rate({app="foo", level="error"}[1m]))实现说明:
+、-、*、/、%、^在语法文件中分别对应ADD、SUB、MUL、DIV、MOD、POW词法记号(token),通过mustNewBinOpExpr("+", $3, $1, $4)等动作构造BinOpExpr,见 syntax.y。运算符的优先级由语法文件中的%left声明控制(详见下文"运算顺序")。
逻辑与集合运算符
以下逻辑/集合二元运算符仅定义在两个向量之间:
and(交集)or(并集)unless(补集)vector1 and vector2:结果为 vector1 中那些在 vector2 里存在标签集完全一致元素的子集,其余元素被丢弃。vector1 or vector2:结果包含 vector1 的全部原始元素(标签集 + 值),外加 vector2 中所有在 vector1 里没有匹配标签集的元素。vector1 unless vector2:结果为 vector1 中那些在 vector2 里不存在标签集完全一致元素的子集;两个向量中互相匹配的元素都被丢弃。
二元运算示例
下面这条刻意构造的查询返回这两个查询的交集,等价于rate({app="bar"}):
rate({app=~"foo|bar"}[1m]) and rate({app="bar"}[1m])实现说明:
and、unless、or在语法文件中分别对应AND、UNLESS、OR记号,见 syntax.y;对应的运算符字符串常量OpTypeOr = "or"、OpTypeAnd = "and"、OpTypeUnless = "unless"定义在 ast.go。ast.go 中的IsLogicalBinOp函数专门用于识别这三类逻辑/集合运算。
比较运算符
==(等于)!=(不等于)>(大于)>=(大于等于)<(小于)<=(小于等于)
比较运算符可定义在标量/标量、向量/标量、向量/向量的值对之间。默认情况下它们是过滤器(filter);可以在运算符后附加bool来改变行为——此时不再过滤,而是为值返回 0 或 1。
两个标量之间:结果是一个标量,根据比较结果取 0(false)或 1(true)。此时不得提供bool修饰符。
1 >= 1等价于1
向量与标量之间:运算符作用于向量中每个数据样本的值,比较结果为 false 的向量元素会被从结果向量中丢弃。如果提供了bool修饰符,原本会被丢弃的元素改为值 0,原本会被保留的元素值为 1。
过滤出最近一分钟内至少记录了 10 行日志的流:
count_over_time({foo="bar"}[1m]) > 10为记录行数少于/多于 10 行的流附加0/1值:
count_over_time({foo="bar"}[1m]) > bool 10两个向量之间:默认作为过滤器作用于匹配的元素。表达式不为真、或在表达式另一侧找不到匹配项的向量元素被丢弃,其余元素传播到结果向量。如果提供了bool修饰符,原本会被丢弃的元素改为值 0,原本会被保留的元素值为 1,分组标签成为输出标签集。
返回匹配app=foo且去掉 app 标签后、最近一分钟计数高于对应app=bar(同样去掉 app 标签)的流:
sum without(app) (count_over_time({app="foo"}[1m])) > sum without(app) (count_over_time({app="bar"}[1m]))与上例相同,但通过比较的向量值设为1、未通过(或本会被过滤掉)的设为0:
sum without(app) (count_over_time({app="foo"}[1m])) > bool sum without(app) (count_over_time({app="bar"}[1m]))实现说明:
bool修饰符在语法文件boolModifier规则中实现:不写bool时默认构造&BinOpOptions{VectorMatching: &VectorMatching{Card: CardOneToOne}};写了bool则额外设置ReturnBool: true,见 syntax.y 与 ast.go 中的BinOpOptions。比较运算符集合由 ast.go 的IsComparisonOperator统一判定。
模式匹配过滤运算符
|>(行匹配模式,line match pattern)!>(行不匹配模式,line match not pattern)
Pattern Filter(模式过滤器)不仅提升了查询效率,还简化了编写 LogQL 查询的过程:用户无需构造复杂的正则表达式,而是用更直观的语法写出查询,从而降低认知负担和出错概率。
在 pattern 语法中,<_>充当通配符,代表任意文本。这允许查询匹配"包含指定静态内容、中间夹杂可变内容"的日志行。模式过滤运算符在语法文件的filter规则中对应PIPE_PATTERN/NPA记号,映射为log.LineMatchPattern/log.LineMatchNotPattern,见 syntax.y。
行匹配模式示例:
{service_name=`distributor`} |> `<_> caller=http.go:194 level=debug <_> msg="POST /push.v1.PusherService/Push <_>`行不匹配模式示例:
{service_name=`distributor`} !> `<_> caller=http.go:194 level=debug <_> msg="POST /push.v1.PusherService/Push <_>`例如,上面的两个示例查询分别会匹配/不匹配来自distributor服务的下面这类日志行:
ts=2024-04-05T08:40:13.585911094Z caller=http.go:194 level=debug traceID=23e54a271db607cc orgID=3648 msg="POST /push.v1.PusherService/Push (200) 12.684035ms" ts=2024-04-05T08:41:06.551403339Z caller=http.go:194 level=debug traceID=54325a1a15b42e2d orgID=1218 msg="POST /push.v1.PusherService/Push (200) 1.664285ms" ts=2024-04-05T08:41:06.506524777Z caller=http.go:194 level=debug traceID=69d4271da1595bcb orgID=1218 msg="POST /push.v1.PusherService/Push (200) 1.783818ms" ts=2024-04-05T08:41:06.473740396Z caller=http.go:194 level=debug traceID=3b8ec973e6397814 orgID=3648 msg="POST /push.v1.PusherService/Push (200) 1.893987ms" ts=2024-04-05T08:41:05.88999067Z caller=http.go:194 level=debug traceID=6892d7ef67b4d65c orgID=3648 msg="POST /push.v1.PusherService/Push (200) 2.314337ms" ts=2024-04-05T08:41:05.826266414Z caller=http.go:194 level=debug traceID=0bb76e910cfd008d orgID=3648 msg="POST /push.v1.PusherService/Push (200) 3.625744ms"实现说明:
|>的 pattern 表达式由 pkg/logql/log/pattern 包解析执行,pattern/ast.go 定义了表达式树、捕获组(capture)与校验逻辑。pattern 解析器同样用于| pattern管线阶段——ast.go 在构造 pattern 解析器阶段时调用log.NewPatternParser(param)完成解析。
运算顺序(Order of Operations)
链式或组合运算符时,必须考虑运算符优先级(precedence)。一般而言,遵循常规数学约定,同一优先级上的运算符是左结合的。
1 + 2 / 3等于1 + ( 2 / 3 )。
2 * 3 % 2按(2 * 3) % 2求值。
实现说明:运算符优先级直接在语法文件 syntax.y 中用
%left声明实现,注释明确写着"Operators are listed with increasing precedence"(运算符按优先级递增列出):%left <binOp> AND UNLESS %left <binOp> CMP_EQ NEQ LT LTE GT GTE %left <binOp> ADD SUB即优先级从低到高依次为:逻辑/集合(
and/unless)→ 比较(==/!=/</<=/>/>=)→ 算术加减(+/-)→ 更高优先级的乘除/幂等。这种声明方式由 goyacc 生成 LALR 解析表,从根本上决定了表达式的结合与求值顺序。
关键字 on 和 ignoring
ignoring关键字使指定标签在匹配时被忽略。语法如下:
<vector expr> <bin-op> ignoring(<labels>) <vector expr>下面的示例返回在最近一分钟内总计数超过foo应用平均值的机器:
max by(machine) (count_over_time({app="foo"}[1m])) > bool ignoring(machine) avg(count_over_time({app="foo"}[1m]))on关键字将参与匹配的标签集缩减为指定的列表。语法如下:
<vector expr> <bin-op> on(<labels>) <vector expr>下面的示例返回foo应用中每台机器最近一分钟计数占总数的比例:
sum by(machine) (count_over_time({app="foo"}[1m])) / on() sum(count_over_time({app="foo"}[1m]))实现说明:
on/ignoring由语法文件onOrIgnoringModifier规则解析,见 syntax.y:ON (...)会设置VectorMatching.On = true并记录MatchingLabels,IGNORING (...)只设置MatchingLabels。对应的运行时结构体VectorMatching定义在 ast.go,其中On字段的注释说明:"On includes the given label names from matching, rather than excluding them"(on将给定标签名纳入匹配,而非排除)。
多对一与一对多向量匹配
多对一(many-to-one)和一对多(one-to-many)匹配发生在"一"侧的每个向量元素可以与"多"侧的多个元素匹配时。你必须显式使用group_left或group_right修饰符请求这种匹配,其中 left 或 right 决定哪一侧的向量基数(cardinality)更高。
语法如下:
<vector expr> <bin-op> ignoring(<labels>) group_left(<labels>) <vector expr> <vector expr> <bin-op> ignoring(<labels>) group_right(<labels>) <vector expr> <vector expr> <bin-op> on(<labels>) group_left(<labels>) <vector expr> <vector expr> <bin-op> on(<labels>) group_right(<labels>) <vector expr>group修饰符提供的标签列表包含来自"一"侧、需要包含进结果指标中的额外标签。一个标签只能出现在on和group_x指定的两个列表之一。结果向量的每个时间序列必须能被唯一标识。
分组修饰符只能用于比较和算术运算。默认情况下,系统以右侧向量的全部条目来匹配and、unless和or运算。
以下示例返回按app和status分区的请求速率占总请求的百分比:
sum by (app, status) ( rate( {job="http-server"} | json [5m] ) ) / on (app) group_left sum by (app) ( rate( {job="http-server"} | json [5m] ) ) => [ {app="foo", status="200"} => 0.8 {app="foo", status="400"} => 0.1 {app="foo", status="500"} => 0.1 ]下面的版本使用group_left(<labels>)将右侧的<labels>包含进结果,并返回每个用户、组织、命名空间下被丢弃事件的花费:
sum by (user, namespace) ( rate( {job="events"} | logfmt | discarded="true" [5m] ) ) * on (user) group_left(organization) max_over_time( {job="cost-calculator"} | logfmt | unwrap cost [5m] ) by (user, organization) => [ {user="foo", namespace="dev", organization="little-org"} => 10 {user="foo", namespace="prod", organization="little-org"} => 50 {user="bar", namespace="dev", organization="big-org"} => 70 {user="bar", namespace="prod", organization="big-org"} => 200 ]实现说明:
group_left与group_right在语法文件binOpModifier规则中解析,见 syntax.y:GROUP_LEFT设置Card = CardManyToOne并可通过GROUP_LEFT (labels)携带Include列表;GROUP_RIGHT设置Card = CardOneToMany。在 ast.go 中,Include字段的注释说明它包含"来自低基数一侧、需要包含进结果的额外标签"。另外值得注意的是 ast.go 的Shardable方法:当使用on分组或带非空标签集的ignoring时,会改变标签分组,因此这类查询会被禁止分片(sharding);而ignoring ()是零值、不改变标签,可以被分片。
注释(Comments)
LogQL 查询可以使用#字符添加注释:
{app="foo"} # anything that comes after will not be interpreted in your query对于多行 LogQL 查询,查询解析器可以使用#排除整行或部分行:
{app="foo"} | json # this line will be ignored | bar="baz" # this checks if bar = "baz"说明:注释在词法/语法解析阶段被剥离,因此可以安全地用于在 Grafana Explore 等界面中组织复杂的多行查询,而不影响语义。
管道错误(Pipeline Errors)
有多种原因会导致管道处理错误,例如:
- 数字标签过滤器(numeric label filter)无法将标签值转换为数字
- 标签的指标转换(metric conversion)失败
- 日志行不是合法的 JSON 文档
- 等等……
发生上述失败时,Loki不会过滤掉这些日志行,而是给它们附加一个名为__error__的新系统标签,并传递到管道的下一阶段。过滤错误的唯一方式是使用标签过滤表达式。__error__标签无法通过语言重命名。
例如,要移除 JSON 解析错误:
{cluster="ops-tools1",container="ingress-nginx"} | json | __error__ != "JSONParserErr"或者,你可以用__error__ = ""这样的全捕获匹配器移除所有错误,甚至用__error__ != ""只显示错误。
过滤器应放在产生该错误的阶段之后。这意味着如果需要移除unwrap表达式产生的错误,过滤器必须放在unwrap之后:
quantile_over_time( 0.99, {container="ingress-nginx",service="hosted-grafana"} | json | unwrap response_latency_seconds | __error__=""[1m] ) by (cluster)注意:指标查询不能包含错误;如果在执行期间发现错误,Loki 会返回错误和相应的状态码。
实现说明:
__error__在代码中被定义为logqlmodel.ErrorLabel常量。pkg/logql/log/drop_labels.go 的isErrorLabel(name)通过比较name == logqlmodel.ErrorLabel来识别该标签,防止它在drop/keep等标签操作中被意外重命名或移除(见同文件 L52、L68 处的防护逻辑)。其行为在 pkg/logql/log/drop_labels_test.go 中有完整测试覆盖,例如drop by __error__、drop with wrong __error__ value等用例。| json阶段产生的典型错误值JSONParserErr、| logfmt的LogfmtParserErr等错误类型可在 pkg/logql/log 包中查找对应错误常量。
函数(Functions)
Loki 支持多种对数据进行操作的函数,其中本文介绍与向量标签重写直接相关的label_replace()。其余聚合与范围函数(如rate、count_over_time、quantile_over_time、max_over_time等)在 LogQL 查询文档 中另有说明。
label_replace()
对于v中的每个时间序列:
label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)将正则表达式regex与标签src_label匹配。如果匹配成功,则返回将标签dst_label替换为replacement展开结果后的时间序列。
$1被替换为第一个匹配子组,$2为第二个,以此类推。如果正则表达式不匹配,则时间序列原样返回。
下面的示例返回一个向量,其中每个时间序列都被添加了值为a的foo标签:
label_replace(rate({job="api-server",service="a:c"} |= "err" [1m]), "foo", "$1", "service", "(.*):.*")实现说明:
label_replace在语法文件中由labelReplaceExpr规则解析,语法要求恰好五个字符串参数(目标标签、替换模板、源标签、正则),见 syntax.y:labelReplaceExpr: LABEL_REPLACE OPEN_PARENTHESIS metricExpr COMMA STRING COMMA STRING COMMA STRING COMMA STRING CLOSE_PARENTHESIS { $$ = mustNewLabelReplaceExpr($3, $5, $7, $9, $11)} ;运算符常量
OpLabelReplace = "label_replace"定义在 ast.go。ast.go 会对正则表达式做预校验,非法正则会返回形如invalid regex in label_replace: error parsing regexp: ...的解析错误;该行为在 pkg/logql/syntax/parser_test.go 中有测试用例佐证(例如非法转义序列^^^^x43\q会被拒绝)。label_replace的序列化、克隆与 prettier 格式化逻辑分别位于 serialize.go 与 prettier.go,并在 extractor_test.go、ast_test.go 等测试文件中得到验证。
深入研读指引
若想从源码层面进一步理解 LogQL 运算符与函数:
- 语法定义:pkg/logql/syntax/syntax.y 是 LogQL 的 goyacc 文法源文件,包含全部运算符优先级(L86-L89)、
binOpExpr(L373-L389)、boolModifier(L391-L399)、onOrIgnoringModifier(L401-L422)、binOpModifier(L424-L452)与labelReplaceExpr(L184-L187)。 - AST 与语义:pkg/logql/syntax/ast.go 定义了
BinOpExpr、BinOpOptions、VectorMatching(L1988-L2015)等节点,以及IsComparisonOperator(L1383-L1390)、IsLogicalBinOp(L1392-L1400)等判定函数。 - 求值与优化:pkg/logql/engine.go、pkg/logql/vector 与 pkg/logql/evaluator.go 负责向量匹配与求值;pkg/logql/shardmapper.go 中可看到分组修改与分片的关系。
- 管道错误与模式过滤:pkg/logql/log/drop_labels.go 处理
__error__标签防护;pkg/logql/log/pattern/ast.go 是|>/!>与| pattern的 pattern 解析实现。 - 测试用例:pkg/logql/syntax/parser_test.go 与 pkg/logql/syntax/extractor_test.go 覆盖了各类运算符与
label_replace的解析与执行,是学习 LogQL 语义边界的绝佳参考。
掌握本文所述的运算符优先级、bool修饰符语义、向量匹配规则与__error__处理方式,你就拥有了阅读和编写复杂 LogQL 查询的完整工具箱——无论是做 SRE 指标告警、成本分析,还是日志清洗与错误排查,都能直接落地到实际的 Loki 查询中。
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考