一、loki简介

Loki是 Grafana Labs 团队最新的开源项目,是一个水平可扩展,高可用性,多租户的日志聚合系统。它的设计非常经济高效且易于操作,因为它不会为日志内容编制索引,而是为每个日志流编制一组标签。项目受 Prometheus 启发,官方的介绍就是: Like Prometheus, but for logs ,类似于 Prometheus 的日志系统。

二、Loki 快速上手

Loki 作为日志系统的后起之秀,设计上可以说非常优秀,设计的理念就是为了让日志聚合更简单,它被设计为非常经济高效且易于操作。它不索引日志的内容,而是为每个日志流设置一组标签。它主要由三部分组成。

与其他日志聚合系统相比, Loki 具有下面的一些特性:

1
2
3
4
5
不对日志进行全文索引。通过存储压缩非结构化日志和仅索引元数据, Loki 操作起来会更简
单,更省成本。
通过使用与 Prometheus 相同的标签记录流对日志进行索引和分组,这使得日志的扩展和操
作效率更高。
特别适合储存 Kubernetes Pod 日志 ; 诸如 Pod 标签之类的元数据会被自动删除和编入索引

各日志收集组件简单对比

名称 安装的组件 优点
ELK/EFK elasticsearch、logstash、kibana 、filebeat、kafka、redis 支持自定义 grok 正则解析复杂日志内容;dashboard 支持主富的可视化展示
Loki grafana 、 loki 、 promtail 占用资源小; grafana 原生支持;查询速度快

官方地址:https://grafana.com/oss/loki/
文档地址: https://grafana.com/docs/grafana/latest/features/datasources/loki/
git 地址: https://github.com/grafana/loki/blob/master/docs/README.md
下载地址: Releases · grafana/loki (github.com)

第一步安装 Loki

简单介绍下 Loki :
Grafana Loki 是一个日志聚合工具,它是功能齐全的日志堆栈的核心。
Loki 是一个为有效保存日志数据而优化的数据存储。日志数据的高效索引将 Loki 与其他日志系统区分开来,
与其他日志系统不同, Loki 索引是根据标签构建的,原始日志消息未编入索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
yum install -y https://github.com/grafana/loki/releases/download/v2.9.8/loki-2.9.8.x86_64.rpm
# 配置文件详解
[getui@39-210 loki]$ cat config.yaml
auth_enabled: false
server:
http_listen_port: 3200
querier:
# 控制一个 Loki 实例上的最大并发查询数
max_concurrent: 500
query_scheduler:
#控制每个 Loki 租户(tenant)在集群范围内允许的最大查询请求数
max_outstanding_requests_per_tenant: 4096
ingester:
lifecycler:
address: 172.16.39.210
ring:
kvstore:
store: inmemory
replication_factor: 1
final_sleep: 0s
chunk_target_size: 104857600
chunk_idle_period: 5m
chunk_retain_period: 30s

#配置索引信息
schema_config:
configs:
- from: 2023-01-01
store: boltdb
object_store: filesystem
schema: v11
index:
prefix: index_ #索引前缀
period: 168h #每张表的时间范围7天

storage_config:
boltdb:
directory: /app/new/loki/data/index #索引文件存储地址
filesystem:
directory: /app/new/loki/data/chunks #块存储地址

compactor:
working_directory: /app/new/loki/data/compactor
shared_store: filesystem
compaction_interval: 5m
retention_enabled: true
retention_delete_delay: 5m
retention_delete_worker_count: 150

limits_config:
enforce_metric_name: false
reject_old_samples: true
reject_old_samples_max_age: 168h
# for big logs tune
cardinality_limit: 200000
ingestion_burst_size_mb: 1000
ingestion_rate_mb: 10000
max_entries_limit_per_query: 100000000000
max_label_value_length: 20480
max_label_name_length: 10240
max_label_names_per_series: 300
split_queries_by_interval: 24h
retention_period: 1344h
max_query_series: 2000

chunk_store_config:
# 最大可查询历史日期 28天,这个时间必须是schema_config中的period的倍数,否则报错。
max_look_back_period: 672h
# 表的保留期28天
table_manager:
retention_deletes_enabled: true
retention_period: 672h

[getui@39-210 loki]$ cat run.sh
#!/bin/bash
nohup ./loki-linux-amd64 -config.file=./config.yaml > /dev/null 2>&1 &

第二步安装 Promtail(采集插件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
yum install -y https://github.com/grafana/loki/releases/download/v2.9.8/promtail-2.9.8.x86_64.rpm
# 配置文件详解
[getui@bjmjq-service-13-93 current]$ cat promtail.yaml
server:
http_listen_port: 9081
grpc_listen_port: 19081

positions:
filename: ./positions.yaml #用于记录每次读取日志文件的索引行数,如:promtail重启后从该配置中恢复日志文件的读取位置

clients:
- url: http://172.16.39.210:3100/loki/api/v1/push # 推到Loki

scrape_configs:
- job_name: ras_exception #采集模块name
static_configs:
- targets:
- 10.10.13.93 #采集机器ip
- labels:
job: ras_exception
host: 10.10.13.93
__path__: /app/new/ras/logs/rp-exception*.log #采集路径
pipeline_stages:
- match:
selector: '{job="ras_exception"}'
stages:
- regex: #正则匹配
expression: "^(?P<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\|(?P<brief>\\w+)\\|(?P<class>\\w+(\\.\\w+|\\$\\w+)*)\\|(?P<reason>.*)\\|(?P<jsonData>.*)\\|(?P<content>.*)\\|(?P<base64>.*)$"
- labels:
level:
- timestamp:
format: RFC3339
location: 'Asia/Shanghai'
source: timestamp
- template:
source: content
template: '{{.timestamp}}|{{.brief}}|{{.class}}|{{.reason}}|{{.content}}'
- output:
source: content
- job_name: ras_auth_exception
static_configs:
- targets:
- 10.10.13.93
- labels:
job: ras_auth_exception
host: 10.10.13.93
__path__: /app/new/ras/logs/rp-message-*.log
pipeline_stages:
- match:
selector: '{job="ras_auth_exception"}'
stages:
- multiline:
firstline: "^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})"
negate: true
match: after
- drop:
expression: "(no_user|ok|no_valid_push|invalid_param|other_error|ResetMasterSecret|query_cid|push_single|get_tag|no_appid|sign_error|set_tag|get-pushmsg-result|appid_notmatch|bind_alias)"

[getui@13-93 current]$ cat run.sh
#!/bin/bash
nohup ./promtail-linux-amd64 -config.file=./promtail.yaml &

Promtail Pipeline 日志处理配置

采集nginx日志

1、尝试搜集nginx日志

所以首先对nginx默认的日志进行改造,让他以json的方式进行输出到目录,然后用Promtail对其进行读取。
读取使用LogQL的json方式去读取,这个LogQL内容填写在grafana中。

2、nginx的部分配置改造
1
2
3
4
5
6
7
8
server {
server_name loki.test.com; # 域名设置
listen 8888;
access_log /var/log/nginx/loki_access.log promtail_json;
location / {
return 200 "It's ok!";
}
}

promtail_json日志格式配置(nginx.conf)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
log_format promtail_json '{"@timestamp":"$time_iso8601",'
'"@version":"Promtail json",'
'"server_addr":"$server_addr",'
'"remote_addr":"$remote_addr",'
'"host":"$host",'
'"uri":"$uri",'
'"body_bytes_sent":$body_bytes_sent,'
'"bytes_sent":$body_bytes_sent,'
'"request":"$request",'
'"request_length":$request_length,'
'"request_time":$request_time,'
'"status":"$status",'
'"http_referer":"$http_referer",'
'"http_user_agent":"$http_user_agent"'
'}';

访问127.0.0.1:8888,观察日志已经正常输出为json格式,请保证该json格式正确。

1
2
root@test:/etc/nginx/conf.d$tail -f /var/log/nginx/loki_access.log 
{"@timestamp":"2024-09-06T01:54:42-05:00","@version":"Promtail json","server_addr":"127.0.0.1","remote_addr":"192.168.65.130","host":"127.0.0.1","uri":"/","body_bytes_sent":8,"bytes_sent":8,"request":"GET / HTTP/1.1","request_length":78,"request_time":0.000,"status":"200","http_referer":"-","http_user_agent":"curl/7.29.0"}
3、Promtail配置文件修改

搜集日志是Promtail处理,所以自然而然是需要根据自己需求来配置Promtail的配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
http_listen_port: 9080
grpc_listen_port: 0

positions:
filename: /tmp/loki-positions.yaml # 记录pos点
sync_period: 5s # 5s一次将当前读取到的pos点同步至filename配置的文件内

clients:
- url: http://localhost:3100/loki/api/v1/push

scrape_configs:
- job_name: Loki
static_configs:
- labels: # 设定的部分标签
job: Loki-nginx
host: localhost
app: nginx
__path__: /var/log/nginx/loki_access.log # 待读取的nginx日志
4、LogQL json部分文档理解

json的提取分为两种方式,带参数和不带参数

不带参数的方式:

使用|json来提取日志的json内容,前提是json内容为有效json格式。
嵌套的字段会用”_”将内外层的key进行拼接。
忽略数组。
官网中不带参数方式的样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"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"
}
}

被json解后,得到如下:

1
2
3
4
5
6
7
8
"protocol" => "HTTP/2.0"
"request_time" => "6.032"
"request_method" => "GET"
"request_host" => "foo.grafana.net"
"request_size" => "55"
"response_status" => "401"
"response_size" => "228"
"response_size" => "228"

从输出能看到,原本request字段内容为嵌套,所以request里面的内容的key验证了如上第二点,使用”_”进行了拼接。同时servers由于是个数组,所以在解析后直接丢弃了servers这个key,验证了第三点。

带参数的方式:

带参数的方式,json只会根据参数来解开需要的部分(当单条json数据比较大的时候应该能省很多资源)。
使用| json label=”expression”, another=”expression”的方式来编写该方法。可以存在多个参数

看一下官网中带参数方式的样例
使用| json first_server=”servers[0]”, ua=”request.headers[“User-Agent”]进行提取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"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"
}
}

输出结果为:

1
2
"first_server" => "129.0.1.1"
"ua" => "curl/7.68.0"

first_server和ua都为原先参数中指定的key

如果要提取整个对象,可以使用| json server_list=”servers”, headers=”request.headers 这样就能得到如下输出:

1
2
"server_list" => `["129.0.1.1","10.2.1.3"]`
"headers" => `{"Accept": "*/*", "User-Agent": "curl/7.68.0"}`
5、尝试写一条LogQL表达式

一条完整的LogQL表达式由两部分构成:

a log stream selector,可以理解为,通过设定的label去匹配要抓取哪些日志。
a log pipeline,可以理解为表达式。比如json的提取。

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

编写一个简单的nginx日志需求
Loki-nginx日志中状态码为200的条数。
根据当前选定时间范围,自动调整。

思考:

如何指定Loki-nginx,可以使用log stream selector的表达式来选定。
nginx日志已经转变为了json,所以可以用|json来提取。
如何获取status字段的信息? |json后面直接跟随|status即可,即|json|status。
如何根据当前选定的时间范围?使用内置变量[$__interval]。
条数该得用什么方法获得?LogQL有内置函数count_over_time配合sum,这边需要注意的是count_over_time是根据指定时间范围返回日志条目的具体内容,所以还需要配合sum获得时间段内的总数。
编写:

首先选定Loki-nginx的日志,{job=”Loki-nginx”}。
使用count_over_time函数配合[$__interval]来获取总共的条数。count_over_time({job=”Loki-nginx”}[$__interval])
过滤status字段,让其等于200,表达式count_over_time({job=”Loki-nginx”} | json | status = 200 [$__interval]),此时会报错,因为status为字符串,可以添加__error__=””让其忽略转换出现的异常。得到count_over_time({job=”Loki-nginx”} | json | status = 200 __error__=”” [$__interval])
此时在grafana上显示为多条数据,配合sum得到单独一个数值。
最终的表达式为:sum(count_over_time({job=”Loki-nginx”} | json | status = 200 error=”” [$__interval]))