Grafana Loki 查询指南:从 LogQL 流选择器、日志管道到指标聚合的完整实战
2026/9/12 4:51:45 网站建设 项目流程

Grafana Loki 查询指南:从 LogQL 流选择器、日志管道到指标聚合的完整实战

【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki

Grafana Loki 的日志查询能力统一建立在 LogQL(Loki Query Language)之上,本文以官方查询文档为核心,系统讲解 Loki 的数据查询模型、LogQL 的流选择器与日志管道语法、日志查询与指标查询两大类型,并结合仓库源码与配套文档给出可直接上手的配置与示例。读完本文,你将掌握用 LogQL 从海量日志中筛选、解析、格式化日志行,以及将日志转化为指标进行聚合分析的完整方法。

查询 Loki 的整体流程:流、块与索引

当你想要在 Loki 中查找特定日志时,需要指定一组标签来标识它们。Loki 在接收日志条目时,会将其分组为日志流(log stream);存储时,日志流会被压缩并写入块(chunk),随后 Loki 为这些块建立索引,相当于一本"目录"(table of contents)。当你执行一条查询时,Loki 会先在索引中检索,确定需要从存储中取回哪些块用于展示。

因此,Loki 的查询本质上是"先按标签找流、再对流内日志做处理"的两段式过程:标签决定检索范围(影响性能),管道决定处理逻辑(影响结果)。这也解释了为什么把标签设计得足够精确、把行过滤尽量前置,是优化查询性能的关键。

查询 Loki 的几种方式:Grafana、Logs Drilldown 与 LogCLI

Loki 本身没有内置用户界面,所有查询方式在底层都使用 LogQL:

  • Grafana Explore:最常见的交互式查询入口,支持即席(ad-hoc)探索日志、构建并打磨 LogQL 查询,然后将查询嵌入 Dashboard。
  • Grafana Logs Drilldown:使用默认查询自动生成一组初始可视化,帮助你无需手写查询即可快速浏览日志。
  • LogCLI:Loki 的命令行客户端,适合脚本化、批量下载日志、做分析型运维任务(如统计日志流数量以评估标签基数)。注意 logcli 是纯查询工具,不能用于写入日志

LogCLI 的安装与使用详见 LogCLI 入门 与 LogCLI 教程,其入口实现在仓库 cmd/logcli/main.go,客户端封装在 pkg/logcli/client。基础用法示例:

# 连接本地 Loki export LOKI_ADDR=http://localhost:3100 logcli query '{service_name="website"}' # 连接需要认证的实例 export LOKI_ADDR=https://logs-us-west1.grafana.net export LOKI_USERNAME=<username> export LOKI_PASSWORD=<password> logcli query '{service_name="website"}'

常用参数包括--addr/LOKI_ADDR--username/--password--org-id/LOKI_ORG_ID(指定租户)、-o(输出模式default|raw|jsonl)、-q(静默输出查询元数据)、--stats(显示查询统计)等,所有参数均可通过同名环境变量覆盖,且环境变量优先于命令行参数

LogQL:查询时动态定义"schema"

LogQL 是 Grafana Loki 的查询语言。由于 Loki 在采集日志时不强制要求预定义 schema,LogQL 实现了"查询时 schema"(schema at query)——日志行的结构是在你书写查询的那一刻被推断出来的,而不是在日志摄入(ingest)时。LogQL 借鉴了 PromQL 的设计,但你不必先掌握 PromQL 也能写出 LogQL。

一条 Loki 日志由三部分组成:

  • 时间戳(timestamp)
  • 标签/选择器(labels/selectors)
  • 日志行内容(content)

Loki 只对时间戳和标签建立索引,日志行的其余内容不做索引。LogQL 查询的基本格式为:

{ log stream selector } | log pipeline

其中日志流选择器是必填的日志管道是可选的。该语法对应的 AST 节点可以在仓库 pkg/logql/syntax/ast.go 中看到:LogSelectorExpr(第 69 行起)定义了选择器接口,MatchersExpr(第 328 行起)承载标签匹配表达式,PipelineExpr(第 371 行起)承载管道阶段序列。

日志流选择器(Log stream selector)

日志流选择器(也称标签选择器)是包含键值对的字符串,例如:

{service_name="nginx", status="500"}

所有键值对的唯一组合被称为一个流(stream)。选择器的目标是通过使用预定义(在 Loki 配置中)或自动检测的标签,缩小日志管道需要处理的数据集范围。因此,传给选择器的标签会直接影响查询执行的相对性能。

提示:service_name是 Loki 创建的默认标签之一,它会尝试从日志行中填充像服务名一样的内容,常被 Logs Drilldown 用于发现和探索日志;该默认行为可以在 Loki 配置中修改。

流选择器支持的运算符:

运算符含义
=标签与选择器完全相等
!=标签与选择器不相等
=~标签与正则表达式匹配
!~标签与正则表达式不匹配

其中~表示使用正则表达式。示例:

  • {name =~ "mysql.+"}
  • {name !~ "mysql.+"}
  • {name !~ `mysql-\d+`}

注意:与行过滤正则不同,=~!~是**完全锚定(fully anchored)**的,正则必须匹配整个字符串(含换行)。正则中的.默认不匹配换行;如需匹配换行可用单行标志,如(?s)search_term.+,或用[\S\s]组合匹配任意字符(含换行):

  • {name =~ ".*mysql.*"}:不匹配含换行的标签值
  • {name =~ "(?s).*mysql.*"}:匹配含换行的标签值
  • {name =~ "[\S\s]*mysql[\S\s]*"}:匹配含换行的标签值

选择器语义与 Prometheus 标签选择器一致:查询会包含所有同时满足"app值为mysqlname值为mysql-backup"的流,流中即使还有其它标签对也不影响入选判定。

日志管道(Log pipeline)

日志管道可以附加在流选择器之后,对选中的日志流做进一步处理和过滤。它由一组阶段表达式组成,从左到右对每一行日志依次执行;一旦某个表达式过滤掉了某行,管道便停止处理该行并转向下一行。某些表达式(如| line_format "{{.status_code}}")会改写日志内容与对应标签,改写结果可供后续阶段继续过滤或处理。

管道表达式分为四类:

  1. 过滤表达式:行过滤表达式(line filter)与标签过滤表达式(label filter)
  2. 解析表达式(parser)
  3. 格式化表达式:行格式化(line format)与标签格式化(label format)
  4. 标签表达式:丢弃标签(drop labels)与保留标签(keep labels)
行过滤表达式(Line filter)

行过滤表达式在匹配到的日志流聚合结果上执行一次分布式grep,按区分大小写的表达式丢弃不匹配的行。每个行过滤表达式由过滤运算符加文本或正则组成:

运算符含义
\|=日志行包含该字符串
!=日志行不包含该字符串
\|~日志行匹配该正则表达式
!~日志行不匹配该正则表达式

与选择器正则不同,|~!~不是完全锚定的,.可以匹配包括换行在内的所有字符。

示例:

{job="mysql"} |= "error" # 保留包含 "error" 的行 {instance=~"kafka-[23]",name="kafka"} != "kafka.server:type=ReplicaManager" {name="kafka"} |~ "tsdb-ops.*io:2003" # 正则包含匹配 {name="cassandra"} |~ `error=\w+` # 反引号避免转义 {job="mysql"} |= "error" != "timeout" # 过滤器可链式串联

使用|~!~时可用 Go 的 RE2 语法,默认区分大小写,可用前缀(?i)切换为不区分大小写。

性能要点:行过滤表达式可以放在管道任意位置,但几乎总是应该放在最前面——放在开头意味着只有匹配的行才进入后续处理。例如下面两条查询结果相同,但前者总是更快:

{job="mysql"} |= "error" | json | line_format "{{.err}}" # 更快 {job="mysql"} | json | line_format "{{.message}}" |= "error"

流选择器应用之后,行过滤表达式是过滤日志最快的方式。此外,行过滤表达式还支持去除 ANSI 颜色码:

{job="example"} | decolorize
标签过滤表达式(Label filter)

标签过滤表达式基于原始标签或解析出的标签过滤日志行,可包含多个谓词。每个谓词由标签标识符(恒在运算符左侧)、运算符组成,例如cluster="namespace"。值类型会根据查询输入自动推断:

  • 字符串(String):双引号或反引号包裹,如"200"`us-central1`。其行为与流选择器中的标签匹配完全相同,支持=!==~!~
  • 时长(Duration):形如"300ms""1.5h""2h45m";查询字面量接受nsus(或µs)、mssmhdwy,而标签值比较时只接受ns~h,因此若标签值是d/w/y单位,可先用label_format转换。
  • 数字(Number):64 位浮点数,如25089.923
  • 字节(Bytes):如"42MB""1.5KiB""20B",合法单位有BkBMBGBTBPBKBKiBMiBGiBTiBPiB

Duration、Number、Bytes 类型在比较前会转换标签值,支持==/=!=>/>=</<=

| logfmt | duration > 1m and bytes_consumed > 20MB

如果标签值转换失败,该行不会被过滤掉,而是被打上__error__标签;处理这类错误见管道错误一节。多个谓词可以用andor链接(and也可用逗号或空格表达),Loki 先计算and再计算or,可用括号强制分组:

| duration >= 20ms or size == 20KB and method!~"2.." | duration >= 20ms or size == 20KB , method!~"2.." | duration >= 20ms or size == 20KB method!~"2.." | duration >= 20ms or (size == 20KB and method!~"2..") | (duration >= 20ms or size == 20KB) and method!~"2.."

注意|会开启新的管道阶段而不是新的谓词,因此下面两条等价:

| duration >= 20ms or size == 20KB | method!~"2.." | (duration >= 20ms or size == 20KB) and method!~"2.."

标签过滤表达式是unwrap 表达式之后唯一允许出现的表达式,主要用于过滤指标提取过程中产生的错误。

解析表达式(Parser)

解析表达式可以从日志内容中提取标签,提取出的标签可用于标签过滤或指标聚合。所有解析器都会自动清洗提取出的标签键以符合 Prometheus 指标命名规范(仅含 ASCII 字母、数字、下划线和冒号,且不能以数字开头)。例如| json会把{ "a.b": {c: "d"}, e: "f" }解析为{a_b_c="d", e="f"}。解析出错时日志行不会被过滤,而是附加__error__标签;若提取的标签键与原始流标签重名,会加_extracted后缀以区分,可用标签格式化表达式强制覆盖,同键重复提取时只保留第一个值。

Loki 支持五种解析器:JSON、logfmt、pattern、regexp、unpack。能用预定义的jsonlogfmt就优先使用;结构特殊的日志用pattern(比regexp更易写且更快)或regexp。一个管道可以组合多个解析器解析复杂日志,示例见多解析器示例。

JSON 解析器有两种模式:

  1. 不带参数:| json提取所有 JSON 属性为标签,嵌套属性用_连接打平,数组会被跳过。例如对下面的文档:
{ "protocol": "HTTP/2.0", "servers": ["129.0.1.1","10.2.1.3"], "request": {"time": "6.032", "method": "GET", "host": "foo.grafana.net", "size": "55", "headers": {"Accept": "*/*", "User-Agent": "curl/7.68.0"}}, "response": {"status": 401, "size": "228", "latency_seconds": "6.031"} }

会提取出protocolrequest_timerequest_methodrequest_hostrequest_sizerequest_headers_Acceptrequest_headers_User_Agentresponse_statusresponse_sizeresponse_latency_seconds等标签。

  1. 带参数:| json label="expression", another="expression"只提取指定字段,支持字段访问(my.fieldmy["field"])与数组访问(list[0])及其任意嵌套组合。例如| json first_server="servers[0]", ua="request.headers[\"User-Agent\"]";如果标签名与 JSON 字段同名可直接写| json servers(等价于servers="servers");若表达式返回数组或对象,则会以 JSON 格式赋给标签。

logfmt 解析器同样有两种模式:| logfmt提取所有键值对(例如把at=info method=GET path=/ host=grafana.net fwd="124.133.124.161" service=8ms status=200提取为atmethodpathhostfwdservicestatus等标签);带参数形式| logfmt host, fwd_ip="fwd"可只提取指定字段并重命名。它还支持两个标志:

  • --strict:启用严格解析,遇到格式不佳的键值对立即停止并返回错误;不加该标志则跳过无效对继续解析(尽力而为)。
  • --keep-empty:保留无值独立键(值为空字符串)为标签。
| logfmt --strict | logfmt --strict host, fwd_ip="fwd" | logfmt --keep-empty --strict host

标志必须紧跟在logfmt之后、标签提取参数之前。

pattern 解析器通过模式表达式| pattern "<pattern-expression>"显式提取字段,模式由捕获(captures)与字面量(literals)组成。捕获是<>包裹的字段名(如<example>),匿名捕获<_>用于跳过内容;字面量可以是任意 UTF-8 字符序列(含空白)。捕获从行首或上一组字面量匹配到行尾或下一组字面量,未匹配则解析停止。模式默认锚定行首,不想锚定就在表达式开头用<_>。例如对 NGINX 日志:

0.191.12.2 - - [10/Jun/2021:09:14:29 +0000] "GET /api/plugins/versioncheck HTTP/1.1" 200 2 "-" "Go-http-client/2.0" "13.76.247.102, 34.120.177.193" "TLSv1.2" "US" ""

可用<ip> - - <_> "<method> <uri> <_>" <status> <size> <_> "<agent>" <_>提取ipmethoduristatussizeagent字段。模式不含任何命名捕获,或包含两个未被空白分隔的连续捕获时,属于无效表达式。

regexp 解析器接收单个参数| regexp "<re>"(Go RE2 语法),正则必须至少包含一个命名子匹配(如(?P<name>re)),每个子匹配提取一个标签。例如:

| regexp "(?P<method>\\w+) (?P<path>[\\w|/]+) \\((?P<status>\\d+?)\\) (?P<duration>.*)"

可把POST /api/prom/api/v1/query_range (200) 1.5s提取为method="POST"path="/api/prom/api/v1/query_range"status="200"duration="1.5s"

unpack 解析器解析 JSON 日志行,解包所有嵌入标签(对应采集端pack阶段打包的数据),并用特殊属性_entry替换原始日志行。例如| unpack会把{"container": "myapp", "pod": "pod-3223f", "_entry": "original log message"}提取出containerpod标签,并把original log message设为新的日志行。若嵌入的日志行是特定格式,还可与json等其它解析器组合使用。

行格式化表达式(Line format)

| line_format "{{.label_name}}"使用 Go text/template 格式改写日志行内容(不修改底层源数据,只影响查询返回结果)。所有标签都被注入模板变量,可用{{.label_name}}引用:

{container="frontend"} | logfmt | line_format "{{.query}} {{.duration}}"

模板可用双引号或反引号(`{{.label_name}}`)避免转义。line_format还支持math函数,例如把毫秒duration除以 1000 转成秒:

{container="frontend"} | logfmt | line_format "{{.ip}} {{.status}} {{div .duration 1000}}"

此外可通过__line____timestamp__函数访问原始日志行与时间戳,全部可用模板函数见模板函数文档。

标签格式化表达式(Labels format)

| label_format可重命名、修改或新增标签,参数为逗号分隔的等式列表:

  • 两侧均为标签标识符(如dst=src)时,把src重命名为dst;若dst不存在则新建,重命名后src会被丢弃。
  • 右侧为模板字符串(如dst="{{.status}} {{.query}}")时,用 text/template 求值结果替换dst,此时会保留被引用的标签(dst="{{.src}}"会让dstsrc同值并存)。

单个标签名在每个表达式中只能出现一次,例如| label_format foo=bar,foo="new"不合法,需拆成两次:| label_format foo=bar | label_format foo="new"

丢弃标签与保留标签表达式

Drop Labels(语法|drop name, other_name, some_name="some_value")丢弃指定标签,也支持正则(如app=~"some-api.*"),还可用于丢弃__error__标签。例如:

{job="varlogs"}|json|drop level, method="GET" {job="varlogs"}|json|drop __error__ {job="varlogs"}|json|drop level, path, app=~"some-api.*"

Keep Labels(语法|keep name, other_name, some_name="some_value")只保留指定标签并丢弃其余标签。注意 keep 阶段不会丢弃 Loki 在查询时添加的__error____error_details__标签,如需丢弃请用|drop

两种查询类型:Log queries 与 Metric queries

LogQL 查询分为两类:

  • 日志查询(Log queries):返回日志行的内容(结构化或非结构化),使用流选择器与日志管道,且可以链式拼接形成更长的查询。详细语法见日志查询。
  • 指标查询(Metric queries):基于日志查询结果计算数值,把日志变成指标。

日志查询的完整示例

{container="query-frontend",namespace="loki-dev"} |= "metrics.go" | logfmt | duration > 10s and throughput_mb < 500

它由两部分构成:流选择器{container="query-frontend",namespace="loki-dev"}锁定loki-dev命名空间下的query-frontend容器;管道|= "metrics.go" | logfmt | duration > 10s and throughput_mb < 500先过滤出包含metrics.go的行,再用 logfmt 解析出更多标签,最后按durationthroughput_mb做标签过滤。查询的组成结构可参考查询构成示意图。

技巧:为避免转义特殊字符,可用反引号`代替双引号,例如`\w+`等价于"\\w+",在写含多个反斜杠的正则时特别有用。

指标查询:Range Vector 聚合

指标查询扩展了日志查询——对日志查询结果应用函数即可从日志中制造指标,例如计算错误消息的速率,或统计最近 3 小时产日志最多的 Top N 日志源;配合解析器,还可以从日志行中的采样值(如延迟、请求大小)计算指标。所有标签(含提取标签)都可用于聚合和生成新序列。

LogQL 与 Prometheus 共享 range vector 概念,在 Loki 中,所选样本范围是选中的日志或标签值范围。Loki 支持两类 range vector 聚合:

日志范围聚合(Log range aggregations):查询后跟时长,函数在时长内聚合。时长可放在流选择器之后或管道末尾。支持的函数:

  • rate(log-range):每秒条目数
  • count_over_time(log-range):给定范围内各日志流的条目数
  • bytes_rate(log-range):各流每秒字节数
  • bytes_over_time(log-range):给定范围内各流消耗的字节量
  • absent_over_time(log-range):范围向量有元素时返回空向量,无元素时返回值为 1 的单元素向量(适合对"某段时间内不存在某标签组合的日志流"告警)

示例:

count_over_time({job="mysql"}[5m]) sum by (host) (rate({job="mysql"} |= "error" != "timeout" | json | duration > 10s [1m]))

Offset 修饰符可改变单个 range vector 的时间偏移,且必须紧跟 range vector 之后:

count_over_time({job="mysql"}[5m] offset 5m) // 正确:统计 10 分钟前到 5 分钟前的日志 count_over_time({job="mysql"}[5m]) offset 5m // 非法

解包范围聚合(Unwrapped range aggregations):用提取标签作为样本值而非日志行。日志查询须以 unwrap 表达式结尾,可选标签过滤丢弃错误:

<aggr-op>([parameter,] <unwrapped-range>) [without|by (<label list>)]

| unwrap label_identifier默认把字符串标签值转换为 64 位浮点数,转换失败会打__error__标签;也可用转换函数| unwrap <function>(label_identifier)

  • duration_seconds(label)(短写duration):把 Go duration 格式(如5m24s30ms)转为秒
  • bytes(label):按字节单位(如5 MiB3k1G)转为原始字节数

解包范围支持的聚合函数:rate(每秒所有值之和)、rate_counter(按计数器语义计算每秒速率)、sum_over_timeavg_over_timemax_over_timemin_over_timefirst_over_timelast_over_timestdvar_over_timestddev_over_timequantile_over_time(scalar, unwrapped-range)(φ 分位数,0≤φ≤1)、absent_over_time。除sum_over_timeabsent_over_timeraterate_counter外均支持by/without分组:without从结果向量中移除列出的标签并保留其余;by反之,丢弃未列出的标签。更多示例见 unwrap 示例。

内置聚合操作符

与 PromQL 类似,LogQL 支持对单个向量的元素做聚合,生成元素更少但带聚合值的新向量:

sumavgminmaxstddevstdvarcounttopkbottomksort(按样本值升序)、sort_desc(降序)。

<aggr-op>([parameter,] <vector expression>) [without|by (<label list>)]

其中topk/bottomk必须提供参数,且与其它聚合器不同,它们返回的是包含原始标签的输入样本子集;by/without只用于对输入向量分组。

函数与概率聚合

vector(s scalar):把标量s作为无标签向量返回(与 Prometheusvector()行为一致),主要用于让原本无结果的查询返回一个值,便于告警:

sum(count_over_time({namespace="traefik"}[5m])) # 无结果 or vector(0) # 返回 0

approx_count_distinct:近似统计某个标签或提取字段的去重数量,而不会为每个去重值生成一条序列,适合count by会撑爆基数(series cardinality)的场景。使用前提:在 Loki 配置的limits_config.shard_aggregations中加入approx_count_distinct,并要求frontend.encoding: protobuf。语法:

approx_count_distinct( <counted field>, <log expression> [<duration>] ) [by (<grouping fields>)]

支持即时与范围查询(范围时长必填),分组可选(省略by保留剩余流标签,by ()得到单条无标签序列),不要按被统计字段分组。底层为每个输出组构建精度 14 的 HyperLogLog 与 pkg/logql/count_min_sketch.go。

approx_topk(实验特性,无 SLA):topk的概率近似替代,适合topk超时或触及最大序列数限制、以及"更快的近似答案优于更慢的精确答案"的场景。仅支持即时查询,不支持分组(应由内层sum by/sum without处理)。底层基于分片 + count-min sketch + 堆实现,精度取决于max_count_min_sketch_heap_size(默认堆大小 10000),k越接近堆大小精度越低。

结果排序

指标查询结果不保证任何顺序,除非查询使用sortsort_desc(仅影响即时查询结果;范围查询即使用了sort/sort_desc顺序也不保证)。

二进制操作符速览

在 LogQL 指标查询中还可使用二进制操作符(详见 LogQL 参考):

  • 算术操作符+-*/%^,可作用于标量/标量、向量/标量、向量/向量。示例:1 + 1sum(rate({app="foo"}[1m])) * 2sum(rate({app="foo", level="warn"}[1m])) / sum(rate({app="foo", level="error"}[1m]))
  • 逻辑/集合操作符(仅向量间):and(交集)、or(并集)、unless(补集)。
  • 比较操作符==!=>>=<<=,默认起过滤作用,可在其后加bool改为返回 0/1 而非过滤,例如count_over_time({foo="bar"}[1m]) > 10... > bool 10

从源码看 LogQL 的实现骨架

LogQL 的语法解析与执行在仓库 pkg/logql 中:语法树节点定义于 pkg/logql/syntax/ast.go(LogSelectorExprMatchersExprPipelineExprVectorAggregationExpr等),词法/语法解析与查询优化位于同目录下的 pkg/logql/syntax,执行引擎与评估器见 pkg/logql/engine.go、pkg/logql/evaluator.go,向量与范围向量实现见 pkg/logql/vector 与 pkg/logql/range_vector.go。理解这些结构有助于排查查询问题或阅读配套的查询故障排查、查询示例与查询加速文档。

小结

Loki 查询体系的核心是一以贯之的 LogQL:先用流选择器按标签收敛数据范围,再用管道逐阶段过滤、解析、格式化日志,最后根据需求选择日志查询(取回日志行)或指标查询(聚合出指标)。把行过滤前置、精心设计标签、合理使用解析器与unwrap,是让 Loki 查询既快又准的三大关键实践。

【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询