Loki查询语⾔LogQL使⽤
前⾯我们在学习到使⽤ Loki 的 Ruler 进⾏报警的时候,使⽤了⼀种查询语⾔来定义报警规则,这个就是受 PromQL 的启发,Loki ⾃⼰推出的查询语⾔,称为LogQL,它就像⼀个分布式的 grep,可以聚合查看⽇志。和的查询功能:
查询返回⽇志⾏内容
通过过滤规则在⽇志流中计算相关的度量指标
1⽇志查询
⼀个基本的⽇志查询由两部分组成。
log stream selector(⽇志流选择器)
log pipeline(⽇志管道)
log stream selector
由于 Loki 的设计,所有 LogQL 查询必须包含⼀个⽇志流选择器。
⽇志流选择器决定了有多少⽇志流(⽇志内容的唯⼀来源,如⽂件)将被搜索到,⼀个更细粒度的⽇志流选择器将搜索到流的数量减少到⼀个可管理的数量。所以传递给⽇志流选择器的标签将影响查询执⾏的性能。⽽⽇志流选择器后⾯的⽇志管道是可选的,⽇志管道是⼀组阶段表达式,它们被串联在⼀起应⽤于所过滤的⽇志流,每个表达式都可以过滤、解析和改变⽇志⾏内容以及各⾃的标签。
下⾯的例⼦显⽰了⼀个完整的⽇志查询的操作:
{container="query-frontend",namespace="loki-dev"} |= "" | logfmt | duration > 10s and throughput_mb < 500
该查询语句由以下⼏个部分组成:
⼀个⽇志流选择器{container="query-frontend",namespace="loki-dev"},⽤于过滤loki-dev命名空间下⾯的query-frontend容器的⽇志
然后后⾯跟着⼀个⽇志管道|= "" | logfmt | duration > 10s and throughput_mb < 500,这管道表⽰将筛选出包含这个词的⽇志,然后解析每⼀⾏⽇志提取更多的表达并进⾏过滤
为了避免转义特⾊字符,你可以在引⽤字符串的时候使⽤单引号,⽽不是双引号,⽐如 `\w+1` 与 "\w+" 是相同的。
2Log Stream Selector
⽇志流选择器决定了哪些⽇志流应该被包含在你的查询结果中,选择器由⼀个或多个键值对组成,其中每个键是⼀个⽇志标签,每个值是该标签的值。
⽇志流选择器是通过将键值对包裹在⼀对⼤括号中编写的,⽐如:
{app="mysql",name="mysql-backup"}
上⾯这个⽰例表⽰,所有标签为 app 且其值为mysql和标签为 name 且其值为 mysql-backup 的⽇志流将被包括在查询结果中。
其中标签名后⾯的=运算符是⼀个标签匹配运算符,LogQL 中⼀共⽀持以下⼏种标签匹配运算符:
=: 完全匹配
!=: 不相等
=~: 正则表达式匹配
!~: 正则表达式不匹配
例如:
{name=~"mysql.+"}
{name!~"mysql.+"}
{name!~mysql-\d+}
适⽤于 Prometheus 标签选择器的规则同样适⽤于 Loki ⽇志流选择器。
偏移量修饰符
偏移修饰符允许改变查询中范围向量的时间偏移。例如,以下表达式对 MySQL 作业的最近 10 分钟到 5 分钟(⽽不是最近 5 分钟)内的所有⽇志进⾏计数。注意,偏移量修饰符总是需要紧跟在范围向量选择器之后。3Log Pipeline
⽇志管道可以附加到⽇志流选择器上,以进⼀步处理和过滤⽇志流。它通常由⼀个或多个表达式组成,每个表达式针对每个⽇志⾏依次执⾏。如果⼀个表达式过滤掉了⽇志⾏,则管道将在此处停⽌并开始处理下⼀⾏。后续表达式或指标查询。
⼀个⽇志管道可以由以下部分组成。
⽇志⾏过滤表达式
解析器表达式
标签过滤表达式
⽇志⾏格式化表达式
标签格式化表达式
Unwrap 表达式
其中 unwrap 表达式是⼀个特殊的表达式,只能在度量查询中使⽤。
⽇志⾏过滤表达式
⽇志⾏过滤表达式⽤于对匹配⽇志流中的聚合⽇志进⾏分布式 grep。
编写⼊⽇志流选择器后,可以使⽤⼀个搜索表达式进⼀步过滤得到的⽇志数据集,搜索表达式可以是⽂本或正则表达式,⽐如:
{job="mysql"} |= "error"
{name="kafka"} |~ "tsdb-ops.*io:2003"
{name="cassandra"} |~ "error=\\w+"
{instance=~"kafka-[23]",name="kafka"} != "kafka.server:type=ReplicaManager"
上⾯⽰例中的|=、|~和!=是过滤运算符,⽀持下⾯⼏种:
|=:⽇志⾏包含的字符串
!=:⽇志⾏不包含的字符串
|~:⽇志⾏匹配正则表达式
!~:⽇志⾏与正则表达式不匹配
过滤运算符可以是链式的,并将按顺序过滤表达式,产⽣的⽇志⾏必须满⾜每个过滤器,⽐如:
{job="mysql"} |= "error" != "timeout"
当使⽤|~和!~时,可以使⽤ Golang 的 RE2 语法的正则表达式,默认情况下,匹配是区分⼤⼩写的,可以⽤(?i)作为正则表达式的前缀,切换为不区分⼤⼩写。
虽然⽇志⾏过滤表达式可以放在管道的任何地⽅,但最好把它们放在开头,这样可以提⾼查询的性能,当某⼀⾏匹配时才做进⼀步的后续处理。例如,虽然结果是⼀样的,但下⾯的查询{job="mysql"}|="error"|json |= "error" 快,⽇志⾏过滤表达式是继⽇志流选择器之后过滤⽇志的最快⽅式。
解析器表达式
解析器表达式可以解析和提取⽇志内容中的标签,这些提取的标签可以⽤于标签过滤表达式进⾏过滤,或者⽤于指标聚合。
提取的标签键将由解析器进⾏⾃动格式化,以遵循 Prometheus 指标名称的约定(它们只能包含 ASCII 字母和数字,以及下划线和冒号,不能以数字开头)。
例如下⾯的⽇志经过管道| json将产⽣以下 Map 数据:
{ "a.b": { "c": "d" }, "e": "f" }
->
{a_b_c="d", e="f"}
在出现错误的情况下,例如,如果该⾏不是预期的格式,该⽇志⾏不会被过滤,⽽是会被添加⼀个新的__error__标签。
需要注意的是如果⼀个提取的标签键名已经存在于原始⽇志流中,那么提取的标签键将以_extracted作为后缀,以区分两个标签,你可以使⽤⼀个标签格式化表达式来强⾏覆盖原始标签,但是如果⼀个提取的键出现了⽬前⽀持json、logfmt、regexp和unpack这⼏种解析器。
我们应该尽可能使⽤json和logfmt等预定义的解析器,这会更加容易,⽽当⽇志⾏结构异常时,可以使⽤regexp,可以在同⼀⽇志管道中使⽤多个解析器,这在你解析复杂⽇志时很有⽤。
JSON
json 解析器有两种模式运⾏。
1. 没有参数。如果⽇志⾏是⼀个有效的 json ⽂档,在你的管道中添加| json将提取所有 json 属性作为标签,嵌套的属性会使⽤_分隔符被平铺到标签键中。
注意:数组会被忽略。
例如,使⽤ json 解析器从以下⽂件内容中提取标签。
{
"protocol": "HTTP/2.0",
"servers": ["129.0.1.1", "10.2.1.3"],
"request": {
"time": "6.032",
"method": "GET",
"host": "afana",
"size": "55",
"headers": {
"Accept": "*/*",
"User-Agent": "curl/7.68.0"
}
},
"response": {
"status": 401,
"size": "228",
"latency_seconds": "6.031"
}
}
可以得到如下所⽰的标签列表:
"protocol" => "HTTP/2.0"
"request_time" => "6.032"
"request_method" => "GET"
"request_host" => "afana"
"request_size" => "55"
"response_status" => "401"
"response_size" => "228"
"response_size" => "228"
2. 带有参数的。在你的管道中使⽤|json label="expression", another="expression"将只提取指定的 json 字段为标签,你可以⽤这种⽅式指定⼀个或多个表达式,与label_format相同,所有表达式必须加引号。
当前仅⽀持字段访问(my.field, my["field"])和数组访问(list[0]),以及任何级别嵌套中的这些组合(my.list[0]["field"])。
例如,|json first_server="servers[0]", ua="request.headers[\"User-Agent\"]将从以下⽇志⽂件中提取标签:
{
"protocol": "HTTP/2.0",
"servers": ["129.0.1.1", "10.2.1.3"],
"request": {
"time": "6.032",
"method": "GET",
"host": "afana",
"size": "55",
"headers": {
"Accept": "*/*",
"User-Agent": "curl/7.68.0"
}
},
"response": {
"status": 401,
"size": "228",
"latency_seconds": "6.031"
}
}
提取的标签列表为:
"first_server" => "129.0.1.1"
"ua" => "curl/7.68.0"
如果表达式返回⼀个数组或对象,它将以 json 格式分配给标签。例如,|json server_list="services", headers="request.headers将提取到如下标签:
"server_list" => `["129.0.1.1","10.2.1.3"]`
"headers" => `{"Accept": "*/*", "User-Agent": "curl/7.68.0"}`
logfmt
logfmt解析器可以通过使⽤|logfmt来添加,它将从 logfmt 格式的⽇志⾏中提前所有的键和值。
例如,下⾯的⽇志⾏数据:
at=info method=GET path=/ host=grafana fwd="124.133.124.161" service=8ms status=200
将提取得到如下所⽰的标签:
"at" => "info"
"method" => "GET"
"method" => "GET"
"path" => "/"
"host" => "grafana"
"fwd" => "124.133.124.161"
"service" => "8ms"
"status" => "200"
regexp
与logfmt和json(它们隐式提取所有值且不需要参数)不同,regexp解析器采⽤单个参数| regexp "<re>"的格式,其参数是使⽤ Golang 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
unpack解析器将解析 json ⽇志⾏,并通过打包阶段解开所有嵌⼊的标签,⼀个特殊的属性_entry也将被⽤来替换原来的⽇志⾏。
例如,使⽤| unpack解析器,可以得到如下所⽰的标签:
{
"container": "myapp",
"pod": "pod-3223f",
"_entry": "original log message"
}
允许提取container和pod标签以及原始⽇志信息作为新的⽇志⾏。
如果原始嵌⼊的⽇志⾏是特定的格式,你可以将 unpack 与 json 解析器(或其他解析器)相结合使⽤。
标签过滤表达式
标签过滤表达式允许使⽤其原始和提取的标签来过滤⽇志⾏,它可以包含多个谓词。
⼀个谓词包含⼀个标签标识符、操作符和⽤于⽐较标签的值。
例如cluster="namespace"其中的cluster是标签标识符,操作符是=,值是"namespace"。
LogQL ⽀持从查询输⼊中⾃动推断出的多种值类型:
String(字符串)⽤双引号或反引号引起来,例如"200"或`us-central1`。
Duration(时间)是⼀串⼗进制数字,每个数字都有可选的数和单位后缀,如"300ms"、"1.5h"或"2h45m",有效的时间单位是"ns"、"us"(或"µs")、"ms"、"s"、"m"、"h"。
Number(数字)是浮点数(64 位),如 250、89.923。
Bytes(字节)是⼀串⼗进制数字,每个数字都有可选的数和单位后缀,如"42MB"、"1.5Kib"或"20b",有效的字节单位是"b"、"kib"、"kb"、"mib"、"mb"、"gib"、"gb"、"tib"、"tb"、"pib"、"bb"、"eb"。
字符串类型的⼯作⽅式与 Prometheus 标签匹配器在⽇志流选择器中使⽤的⽅式完全⼀样,这意味着你可以使⽤同样的操作符(=、!=、=~、!~)。
使⽤ Duration、Number 和 Bytes 将在⽐较前转换标签值,并⽀持以下⽐较器。
==或=相等⽐较
!=不等于⽐较
>和>=⽤于⼤于或⼤于等于⽐较
<;和<=⽤于⼩于或⼩于等于⽐较
例如logfmt | duration > 1m and bytes_consumed > 20MB过滤表达式。
如果标签值的转换失败,⽇志⾏就不会被过滤,⽽会添加⼀个__error__标签,要过滤这些错误,请看管道错误部分。
你可以使⽤and和or来连接多个谓词,它们分别表⽰且和或的⼆进制操作,and可以⽤逗号、空格或其他管道来表⽰,标签过滤器可以放在⽇志管道的任何地⽅。
以下所有的表达式都是等价的:
| 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 method!~"2.."
默认情况下,多个谓词的优先级是从右到左,你可以⽤圆括号包装谓词,强制使⽤从左到右的不同优先级。
例如,以下内容是等价的:
| duration >= 20ms or method="GET" and size <= 20KB
| ((duration >= 20ms or method="GET") and size <= 20KB)
它将⾸先评估duration>=20ms or method="GET",要⾸先评估method="GET" and size<=20KB,请确保使⽤适当的括号,如下所⽰。
| duration >= 20ms or (method="GET" and size <= 20KB)
⽇志⾏格式表达式
⽇志⾏格式化表达式可以通过使⽤ Golang 的text/template模板格式重写⽇志⾏的内容,它需要⼀个字符串参数| line_format "{{.label_name}}"作为模板格式,所有的标签都是注⼊模板的变量,可以⽤{{.label_name}}例如,下⾯的表达式:
{container="frontend"} | logfmt | line_format "{{.query}} {{.duration}}"
将提取并重写⽇志⾏,只包含query和请求的duration。你可以为模板使⽤双引号字符串或反引号 `{{.label_name}}` 来避免转义特殊字符。
此外line_format也⽀持数学函数,例如:
如果我们有以下标签ip=1.1.1.1, status=200和duration=3000(ms), 我们可以⽤duration除以 1000 得到以秒为单位的值:
{container="frontend"} | logfmt | line_format "{{.ip}} {{.status}} {{div .duration 1000}}"
上⾯的查询将得到的⽇志⾏内容为1.1.1.1 200 3。
标签格式表达式
| label_format表达式可以重命名、修改或添加标签,它以逗号分隔的操作列表作为参数,可以同时进⾏多个操作。
当两边都是标签标识符时,例如dst=src,该操作将把src标签重命名为dst。
左边也可以是⼀个模板字符串,例如dst="{{.status}} {{.query}}",在这种情况下,dst标签值会被 Golang 模板执⾏结果所取代,这与| line_format表达式是同⼀个模板引擎,这意味着标签可以作为变量使⽤,也可以使⽤同样
在上⾯两种情况下,如果⽬标标签不存在,那么就会创建⼀个新的标签。
重命名形式dst=src会在将src标签重新映射到dst标签后将其删除,然⽽,模板形式将保留引⽤的标签,例如dst="{{.src}}"的结果是dst和src都有相同的值。
⼀个标签名称在每个表达式中只能出现⼀次,这意味着| label_format foo=bar,foo="new"是不允许的,但你可以使⽤两个表达式来达到预期效果,⽐如| label_format foo=bar | label_format foo="new"。
4查询⽰例
多重过滤
过滤应该⾸先使⽤标签匹配器,然后是⾏过滤器,最后使⽤标签过滤器:
{cluster="ops-tools1", namespace="loki-dev", job="loki-dev/query-frontend"} |= "" !="out of order" | logfmt | duration > 30s or status_code!="200"
多解析器
⽐如要提取以下格式⽇志⾏的⽅法和路径:
level=debug ts=2020-10-02T10:10:42.092268913Z :66 traceID=a9d4d8a928d8db1 msg="POST /api/prom/api/v1/query_range (200) 1.5s"
你可以像下⾯这样使⽤多个解析器:
{job="cortex-ops/query-frontend"} | logfmt | line_format "{{.msg}}" | regexp "(?P<method>\\w+) (?P<path>[\\w|/]+) \\((?P<status>\\d+?)\\) (?P<duration>.*)"`
⾸先通过logfmt解析器提取⽇志中的数据,然后使⽤| line_format重新将⽇志格式化为POST /api/prom/api/v1/query_range (200) 1.5s,然后紧接着就是⽤regexp解析器通过正则表达式来匹配提前标签了。
格式化
下⾯的查询显⽰了如何重新格式化⽇志⾏,使其更容易阅读。
{cluster="ops-tools1", name="querier", namespace="loki-dev"}
|= ""
|!= "loki-canary"
| logfmt
| query != ""
| label_format query="{{ Replace .query \"\\n\" \"\" -1 }}"
| line_format "{{ .ts}}\t{{.duration}}\ttraceID = {{.traceID}}\t{{ printf \"%-100.100s\" .query }} "
其中的label_format⽤于格式化查询,⽽line_format则⽤于减少信息量并创建⼀个表格化的输出。⽐如对于下⾯的⽇志⾏数据:
level=info ts=2020-10-23T20:32:18.094668233Z :81 org_id=29 traceID=1980d41501b57b68 latency=fast query="{cluster=\"ops-tools1\", job=\"cortex-ops/query-frontend\"} |= \"query_range\"" query_type=filter range_type=range length=15m0s ste level=info ts=2020-10-23T20:32:18.068866235Z :81 org_id=29 traceID=1980d41501b57b68 latency=fast query="{cluster=\"ops-tools1\", job=\"cortex-ops/query-frontend\"} |= \"query_range\"" query_type=filter range_type=range length=15m0s ste 经过上⾯的查询过后可以得到如下所⽰的结果:
2020-10-23T20:32:18.094668233Z650.22401ms traceID = 1980d41501b57b68{cluster="ops-tools1", job="cortex-ops/query-frontend"} |= "query_range"
mysql 字符串转数组2020-10-23T20:32:18.068866235Z624.008132mstraceID = 1980d41501b57b68{cluster="ops-tools1", j
ob="cortex-ops/query-frontend"} |= "query_range"
5⽇志度量
LogQL 同样⽀持通过函数⽅式将⽇志流进⾏度量,通常我们可以⽤它来计算消息的错误率或者排序⼀段时间内的应⽤⽇志输出 Top N。
区间向量
LogQL 同样也⽀持有限的区间向量度量语句,使⽤⽅式和 PromQL 类似,常⽤函数主要是如下 4 个:
rate: 计算每秒的⽇志条⽬
count_over_time: 对指定范围内的每个⽇志流的条⽬进⾏计数
bytes_rate: 计算⽇志流每秒的字节数
bytes_over_time: 对指定范围内的每个⽇志流的使⽤的字节数
⽐如计算 nginx 的 qps:
rate({filename="/var/log/nginx/access.log"}[5m]))
计算 kernel 过去 5 分钟发⽣ oom 的次数:
count_over_time({filename="/var/log/message"} |~ "oom_kill_process" [5m]))
聚合函数
LogQL 也⽀持聚合运算,我们可⽤它来聚合单个向量内的元素,从⽽产⽣⼀个具有较少元素的新向量,当前⽀持的聚合函数如下:
sum:求和
min:最⼩值
max:最⼤值
avg:平均值
stddev:标准差
stdvar:标准⽅差
count:计数
bottomk:最⼩的 k 个元素
topk:最⼤的 k 个元素
聚合函数我们可以⽤如下表达式描述:
<aggr-op>([parameter,] <vector expression>) [without|by (<label list>)]
对于需要对标签进⾏分组时,我们可以⽤without或者by来区分。⽐如计算 nginx 的 qps,并按照 pod 来分组:
sum(rate({filename="/var/log/nginx/access.log"}[5m])) by (pod)
只有在使⽤bottomk和topk函数时,我们可以对函数输⼊相关的参数。⽐如计算 nginx 的 qps 最⼤的前 5 个,并按照 pod 来分组:
topk(5,sum(rate({filename="/var/log/nginx/access.log"}[5m])) by (pod)))
⼆元运算
数学计算
Loki 存的是⽇志,都是⽂本,怎么计算呢?显然 LogQL 中的数学运算是⾯向区间向量操作的,LogQL 中的⽀持的⼆进制运算符如下:
+:加法
-:减法
*:乘法
/:除法
/:除法
%:求模
^:求幂
⽐如我们要到某个业务⽇志⾥⾯的错误率,就可以按照如下⽅式计算:
sum(rate({app="foo", level="error"}[1m])) / sum(rate({app="foo"}[1m]))
逻辑运算
集合运算仅在区间向量范围内有效,当前⽀持
and:并且
or:或者
unless:排除
⽐如:
rate({app=~"foo|bar"}[1m]) and rate({app="bar"}[1m])
⽐较运算
LogQL ⽀持的⽐较运算符和 PromQL ⼀样,包括:
==:等于
!=:不等于
>:⼤于
>=: ⼤于或等于
<:⼩于
<=: ⼩于或等于
通常我们使⽤区间向量计算后会做⼀个阈值的⽐较,这对应告警是⾮常有⽤的,⽐如统计 5 分钟内 error 级别⽇志条⽬⼤于 10 的情况:count_over_time({app="foo", level="error"}[5m]) > 10
我们也可以通过布尔计算来表达,⽐如统计 5 分钟内 error 级别⽇志条⽬⼤于 10 为真,反正则为假:
count_over_time({app="foo", level="error"}[5m]) > bool 10
注释
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"
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论