搭建loki日志聚合
loki
据我不严谨的搜索,loki发布于2020年左右,截至本文落笔记目前仍在不断变化中。所以在开始前非常有必要提醒读者,本文内容用到的loki组件版本:
此前,我没有搭建聚合日志平台的经验。本次我主要是利用 AI 翻译和讲解和反复阅读官方 loki 3.5.x 教程。 如果你阅读过quick-start和tutorial,你会发现这两篇教程并没有手把手教你搭建,更多是界面使用教程。对于期望自己搭建 loki 的新手来说,并不是一个好的开始。
本文的初衷,是给我司的游戏项目多节点部署(非 docker、非 k8s)场景,搭建一个日志聚合平台。
- 可根据项目 app_name、分发渠道 channel、节点ID、日志等级和时间范围查询日志
- 节点分布在多台机器
下文示例是简化后的代码,设计目标
- 过滤日志路径
- 给工程标识、分发渠道、节点打标签
- 得日志对象信息(即loki官方文
log enity)- 给日志等级打标签
- 重写日志时间戳(满足时间范围查询)
- 从日志中捕获时间
- 给非标准时间格式,转换为loki约定的格式
- 处理日志跨行
我必须申明一下,loki文档内容着实太多,我并没有理解完所有内容,所以我的方案可能并不不一定是最佳实践。若读者有更好的建议,非常期望读者反馈。
如果你正在使用的loki各组件版本和我下面提到的不一致,特别是在脚本配置和API相关内容上,务必以对应版本的官方文档为准。
下面用的的知识,我的理解也是源于官方,除非我特别说明。我并不打算再做具体说,请阅者自行阅读官方。我建议先了解以下部分概念
- docker
- docker-compose 的启动和停止
- docker 的目录挂载
- docker 容器加入指定docket网络
- loki
- alloy(重要!!)
alloy 作为日志采集器,关于如何控制它采集日志官方文档已经很详细,我这里不作另外说明。
- grafana
- 当搭建完成后,需要用log explore来查询日志
改造说明
从官方 docker 示例改造
# docker 示例
wget https://raw.githubusercontent.com/grafana/loki/v3.5.7/examples/getting-started/docker-compose.yaml -O docker-compose.yaml
wget https://raw.githubusercontent.com/grafana/loki/v3.5.7/examples/getting-started/alloy-local-config.yaml -O alloy-local-config.yaml
wget https://raw.githubusercontent.com/grafana/loki/v3.5.7/examples/getting-started/loki-config.yaml -O loki-config.yaml
# 启动示例
docker compose -f docker-compose.yaml up
为了便给本示例观察/对比修改。我另起了仓库,main分支就是原官方 docker 示例,new分支是本示例的修改。它们的修改大致如下:
- 将采集端 alloy 将从
docker-compose.yaml抽离到docker-compose-alloy.yaml - 多机部署日志统一放在
var/log(注意权限问题),该目录下以工程名区分各工程日志,如/var/log/my_app_name - 工程名,分放渠道和节点的目录关系
/var/log/my_app_name//var/log/my_app_name/my_channel1/my_node1//var/log/my_app_name/my_channel1/my_node2//var/log/my_app_name/my_channel2/my_node1/
- 日志格式 “[日期]-[级别] 内容” 样例:
[20250923143226869] [INFO] start service - 使用 shell 脚本
start_write_log.sh定期写入日志,模拟输出日志。
改造开始
alloy采集指定目录的日志
在docker-compose-alloy.yaml里,将本机工程my_app_name日志目录挂载给 alloy 容器。
services:
alloy:
image: grafana/alloy:v1.11.3
volumes:
- ./alloy-local-config.yaml:/etc/alloy/config.alloy:ro
- /var/log/my_app_name:/var/log/my_app_name:ro
command: run --server.http.listen-addr=0.0.0.0:12345 --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy
ports:
- 12345:12345
alloy采集脚本配置
alloy 表达式过滤日志文件
local.file_match 用来筛选过滤文件路径。
| Key | Description | Required |
| :— | :— | :— |
| __path__ | doublestar glob pattern specifying which files to discover. | Yes |
| __path_exclude__ | doublestar glob pattern specifying which files to exclude from the __path__ matches. | No |
| additional keys | Any other labels to attach to discovered files. The component preserves these labels in the exported targets. | No |
targets 参数里,除了 __path__ 和 __path_exclude__ 参数外,其他参数都会作为 [labels](上面提到需要阅读的重要概念)。
本脚本配置是 my_app_name 专用的,各个工程日志结构和格式不一样,也不可能通用。但是 /var/log 是系统日志目录,我们只期望采集 /var/log/my_app_name 下的日志文件。my_app_name = "my_app_name" 给这些日志内容打 [labels]。优势类似于索引分片,能加快查询效率。
// 筛选日志文件
local.file_match "my_file_match" {
path_targets = [{
__path__ = "/var/log/my_app_name/*/*/*.log",
my_app_name = "my_app_name",
}]
// 同步间隔(多久扫描一次新文件)
sync_period = "10s"
}
alloy读取日志内容
loki.source.file 用来读取文件内容。它的输入 targets 来自上面 local.file_match 组件筛选的文件路径,会把以 ‘\n’ 结尾的 行 内容 forward_to 到后面提到的内容处理 loki.process.read_line.receiver。请注意这里说的是 行 内容,而不是整个日志文件内容。
// 读取文件内容
loki.source.file "my_file" {
//
targets = local.file_match.my_file_match.targets
// 转发到日志处理管道
forward_to = [loki.process.read_line.receiver]
// 文件读取配置
tail_from_end = true // 默认为false, 设为 true 则只读取新追加的内容
}
处理日志行
loki.process,它是内置组件库中最复杂的组件之一。它的参数只有两个,但需要定义的 stage 却非常多。
loki.process "<LABEL>" {
stage.<STAGENAME> {
...
}
forward_to = <RECEIVER_LIST>
...
}
- stage.***: 日志处理阶段,可以有多个,按编写顺序执行
- forward_to: 下游接收器列表
下面我们将用到以下这些 stage
- stage.regex 表达式捕获关键内容存储到变量
- stage.labels 给当前行日志打标签
- stage.multiline 日志行本身会有跨行,所以要约定如何判断新
日志行 - stage.timestamp 重写行日志时间戳
从日志文件路径中捕获关键信息
为了减少信息冗余,日志行有时并不会输出一些信息,如 my_app_name、my_channel 和 my_node。而它们一般定义在日志路径中。stage.regex 默认是捕获 行日志,若要捕获日志路径中的内容,需要指定内容来源:source = "filename"。
- `/var/log/my_app_name/my_channel1/my_node1/`
- `/var/log/my_app_name/my_channel1/my_node2/`
- `/var/log/my_app_name/my_channel2/my_node1/`
- ...
stage.regex {
// 提取 /var/log/{my_app_name}/{my_channel}/{my_node}/ 中的 instance_name
source = "filename"
expression = "/var/log/(?P<my_app_name>[^/]+)/(?P<my_channel>[^/]+)/(?P<my_node>[^/]+)/.*"
}
stage.labels {
values = {
my_app_name = "my_app_name",
my_channel = "my_channel",
my_node = "my_node",
}
}
处理跨行日志
alloy 默认每行都是一个日志对象,但如果中途出现跨行,会让后续的日志行变得混乱。我们需要告诉 alloy 如何判断新日志行。firstline 参数用于指定匹配新日志行的正则表达式,不匹配的行(比如没有时间戳的报错堆栈)会自动拼接到上一个日志对象。
stage.multiline {
// 匹配以 [yyyyMMddHHmmssSSS] 开头的行(17位数字)
firstline = `^\[\d{17}\]`
}
从日志行中捕获关键信息
- 日志时间戳。alloy 默认以运行时读取日志的时间作为日志对象的产生时间。但是读取日志的时间与日志产生的时间不可能一致,比如将很久之前的日志导入给 alloy 采集,就不可能以开始采集的时间作为日志产生时间。所以这里依据日志行提供的时间戳重新写入。alloy 对时间格式有要求,需要符合 Go 语言 API
time.Parse参数要求。
// 日志格式 "[日期]-[级别] 内容" 样例:`[25/09/23 14:32:26:869] [INFO] start service`
stage.regex {
expression = "\\[(?P<date>[^\\]]+)\\].*?\\[(?P<level>[^\\]]+)\\]"
}
// 重写日志时间戳
stage.timestamp {
source = "date"
format = "06/02/01 15:04:05.000"
location = "Asia/Shanghai"
}
但是,如果老项目的时间格式与 time.Parse 不相符,你必须先用 stage.regex 过滤出时间信息(年月日等),再用 stage.template 重新拼接为符合要求的时间字符串。
stage.regex {
expression = "\\[(?P<year>\\d{4})(?P<month>\\d{2})(?P<day>\\d{2})(?P<hour>\\d{2})(?P<minute>\\d{2})(?P<second>\\d{2})(?P<ms>\\d{3})\\]\\s+\\[(?P<level>[^\\]]+)\\]"
}
// 重组时间以符合go time.parser的格式要求
stage.template {
source = "time"
template = ""
}
stage.timestamp {
source = "time"
format = "2006/02/01 15:04:05.000"
location = "Asia/Shanghai"
}
- 日志等级。grafana ui 针对日志等级有颜色展示。如果你的日志枚举不在 grafana ui 的定义里,将会显示灰色
unknown。所以你项目立项时,务必规范好日志等级。
将处理好的日志对象发送到loki
- 由于在本示例中,alloy 和 nginx 是在两个 docker-compose 中启动的,为了演示本例,这里特意将 alloy 加入到 nginx 所在的 docker 网络。
查看你本地 nginx 所在的网络,我这里输出 loki_simple_demo_loki。
sudo docker inspect -f '' loki_simple_demo-gateway-1
loki_simple_demo_loki
将 alloy 容器加入到网络:
sudo docker network connect loki_simple_demo_loki loki_simple_demo-alloy-1
然后将日志的接收地址修改为 nginx 容器名称 loki_simple_demo-gateway-1,即 http://loki_simple_demo-gateway-1:3100/loki/api/v1/push。
- 如果 nginx 部署在另一台物理机,应该将 “loki_simple_demo-gateway-1:3100” 修改为具体环境的地址和端口,如
http://192.168.2.125:3100/loki/api/v1/push。
loki.process "read_line" {
// 将处理好的日志对象,发给loki
forward_to = [loki.write.write_loki.receiver]
// 这里是以上讨论的各个stage
|
// 写入 Loki
loki.write "write_loki" {
endpoint {
// 将loki_simple_demo-gateway-1:3100,修改具体环境的地址和端口
url = "http://loki_simple_demo-gateway-1:3100/loki/api/v1/push"
}
}
启动
分别在3个bash,依次启动
- bash start_all.sh
- bash start_alloy.sh
- bash start_write_log.sh
从 Grafana 查询日志
http://localhost:3000/explore
关于调试
你会alloy-local-config.yaml看到被我注释掉的这个代码,注释上我附上了链接,看链接吧
// 调式日志
// https://grafana.com/docs/alloy/v1.11/reference/config-blocks/logging/#logging
logging {
level = "debug"
format = "logfmt"
}
// 打印日志到控制台,方便调试
// https://grafana.com/docs/alloy/v1.11/reference/components/loki/loki.echo/#lokiecho
loki.echo "print" { }
// 打alloy ui输出日志
// https://grafana.com/docs/alloy/v1.11/reference/config-blocks/livedebugging/#livedebugging
livedebugging {
enabled = true
}
原文:
https://lizijie.github.io/2025/12/21/%E6%90%AD%E5%BB%BAloki%E6%97%A5%E5%BF%97%E8%81%9A%E5%90%88.html
作者github:
https://github.com/lizijie
</b>