一、容器化日志的“小麻烦”:格式不兼容问题
容器化环境现在几乎是开发部署的标配,不管是用Docker还是K8s,大家都习惯把服务打包成容器跑。但跑起来之后有个绕不开的需求:得看日志啊,不然服务出问题都不知道咋回事。
Loki是现在很火的日志采集工具,和Prometheus同一家公司做的,主打轻量化、存得省,还能和Prometheus的监控体系打通。但很多人用Loki的时候会碰到一个头疼的问题:容器日志的格式和Loki不兼容,导致采集到的日志要么乱码、要么关键信息丢了、要么没法搜索筛选。
举个最常见的例子:很多Java服务的容器日志是“JSON格式”的,比如每一行日志是{"level":"ERROR","time":"2024-05-20T10:00:00Z","msg":"连接数据库失败","traceId":"abc123"},但Loki默认采集容器日志的时候,会把这一整行当成“纯文本”存,搜索的时候搜“level:ERROR”根本搜不到,因为Loki不知道这是个JSON字段。还有些服务的日志是自定义的分隔格式,比如“[时间] [级别] [模块] 内容”,Loki默认也没法把这些部分拆成单独的标签或者字段,用起来特别麻烦。
这一章我们先把这个问题的本质说清楚:容器日志的格式是服务自己定的,Loki默认是“通用采集”,只会原封不动拉过来,不会做格式适配,所以才会出现兼容问题。
二、Loki日志采集的基础:先搞懂Loki怎么拿容器日志
要解决格式兼容问题,首先得知道Loki是怎么拿到容器日志的。这里我们统一用K8s环境来做示例,因为现在大部分容器化部署都是K8s,技术栈明确:Kubernetes + Docker + Loki(用官方的Helm包部署)。
Loki采集K8s容器日志,核心是靠一个叫“Promtail”的组件,Promtail是Loki的日志采集客户端,相当于“快递员”,负责把容器里的日志打包好,送到Loki的“仓库”里存起来。
Promtail拿K8s容器日志的逻辑很简单:K8s会把每个容器的日志存在宿主机的/var/log/containers/目录下,每个容器对应一个日志文件,文件名格式是“<容器名><命名空间><Pod名>-<容器ID>.log”。Promtail会盯着这个目录,一旦有新的日志行产生,就把这行日志拿过来,然后给它加一些“标签”(比如这个日志属于哪个Pod、哪个命名空间、哪个服务),最后发给Loki。
这里有个关键点:Promtail拿到原始日志行之后,默认只会做两件事:加标签、发给Loki,不会对原始日志的内容做任何处理。所以如果原始日志是JSON,它就存JSON;如果是纯文本,就存纯文本。这就是格式兼容问题的根源。
我们先看一个默认的Promtail配置示例,技术栈:Kubernetes + Docker + Loki + Promtail:
# Promtail默认配置(简化版)
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
# 给日志加标签:命名空间、Pod名、容器名
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
- source_labels: [__meta_kubernetes_pod_container_name]
target_label: container
这个配置里,Promtail只会把K8s的元数据转成标签,不会碰日志内容。
三、解决格式兼容的核心:用Promtail的“Pipeline”做日志加工
Promtail有个非常强大的功能叫“Pipeline”(管道),就像一条流水线,拿到原始日志行之后,可以对它做各种加工:拆分、提取、重命名、过滤、转换格式……加工完之后再发给Loki。这就是我们解决格式兼容问题的核心工具。
Pipeline的加工步骤可以有很多,我们这里针对最常见的两种格式兼容问题来演示:一是JSON格式日志的解析,二是自定义分隔格式日志的解析。
3.1 场景1:JSON格式日志的解析
还是拿之前的Java服务JSON日志来举例:每一行是{"level":"ERROR","time":"2024-05-20T10:00:00Z","msg":"连接数据库失败","traceId":"abc123"}。我们要做的是让Promtail把这个JSON解析开,把level、time、traceId这些字段变成可以搜索的标签或者字段。
Promtail里用来解析JSON的步骤叫“json”,还有一个步骤叫“labels”,用来把解析出来的字段转成标签。我们先给大家看完整的配置,然后逐行解释:
# Promtail加工JSON日志的配置(技术栈:Kubernetes + Docker + Loki + Promtail)
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
# 给日志加基础标签:命名空间、Pod名、容器名
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
- source_labels: [__meta_kubernetes_pod_container_name]
target_label: container
# 新增Pipeline加工步骤
pipeline_stages:
# 第一步:解析原始日志的JSON
- json:
# 定义要提取的JSON字段,key是我们给字段起的名字,value是JSON里的键名
fields:
level: level # 提取JSON里的level字段,命名为level
log_time: time # 提取JSON里的time字段,命名为log_time
trace_id: traceId # 提取JSON里的traceId字段,命名为trace_id
msg: msg # 提取JSON里的msg字段,命名为msg
# 第二步:把提取出来的字段转成标签(标签可以用来搜索和筛选)
- labels:
level: level # 把提取的level字段转成标签,标签名也叫level
trace_id: trace_id # 把提取的trace_id转成标签,标签名也叫trace_id
# 第三步:把日志时间改成JSON里的时间(Loki默认用采集时间,这里改成日志实际产生时间)
- timestamp:
source: log_time # 用提取的log_time作为日志的时间
format: RFC3339Nano # 时间格式,对应JSON里的ISO8601格式
这个配置的作用是:Promtail拿到一行JSON日志后,先把它拆成level、log_time、trace_id、msg四个字段,然后把level和trace_id变成标签,这样我们在Loki的查询界面里,就可以用{namespace="default", level="ERROR"}来搜索所有默认命名空间下的错误日志,还可以用{trace_id="abc123"}搜索整个请求链的所有日志,比之前方便太多。
这里要注意一个小细节:如果JSON里的某个字段不存在,Promtail会跳过这个字段,不会报错,所以不用担心服务偶尔输出不标准的JSON导致整个采集失败。
3.2 场景2:自定义分隔格式日志的解析
有些服务的日志不是JSON,是自定义的分隔格式,比如Nginx的访问日志,或者自己写的服务日志,格式是“[2024-05-20T10:00:00Z] [ERROR] [user-service] 连接数据库失败,traceId=abc123”。这种格式是用“[]”把各个部分包起来,然后空格分隔的。
针对这种格式,Promtail有个步骤叫“regex”(正则表达式),可以用正则把各个部分提取出来。同样先给大家看完整配置,再解释:
# Promtail加工自定义分隔日志的配置(技术栈:Kubernetes + Docker + Loki + Promtail)
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
# 基础标签配置和之前一样
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
- source_labels: [__meta_kubernetes_pod_container_name]
target_label: container
# 新增Pipeline加工步骤
pipeline_stages:
# 第一步:用正则提取各个部分
- regex:
# 正则表达式解释:
# \[(.*?)\] 匹配第一个[]里的内容(时间)
# \s+\[(.*?)\]\s+ 匹配第二个[]里的内容(级别)
# \[(.*?)\]\s+ 匹配第三个[]里的内容(模块)
# (.*) 匹配剩下的所有内容(日志内容)
expression: '^\[(.*?)\]\s+\[(.*?)\]\s+\[(.*?)\]\s+(.*)$'
# 定义提取的字段,对应正则里的分组
fields:
log_time: 1 # 第一个分组是时间,命名为log_time
level: 2 # 第二个分组是级别,命名为level
module: 3 # 第三个分组是模块,命名为module
content: 4 # 第四个分组是内容,命名为content
# 第二步:从内容里提取traceId(因为traceId在内容里)
- regex:
expression: 'traceId=([a-zA-Z0-9]+)'
source: content # 从之前提取的content字段里匹配
fields:
trace_id: 1 # 提取traceId
# 第三步:把字段转成标签
- labels:
level: level
module: module
trace_id: trace_id
# 第四步:设置日志时间
- timestamp:
source: log_time
format: RFC3339Nano
这个配置的作用是:先把整行日志拆成时间、级别、模块、内容四个部分,然后再从内容里把traceId提取出来,最后把级别、模块、traceId变成标签。这样我们就可以用{module="user-service", level="ERROR"}来搜索用户服务的错误日志,非常灵活。
正则表达式是这里的关键,很多人会觉得正则难,但其实不用自己写复杂的正则,比如我们这个例子里的正则,是针对“[]包裹”的格式写的,只要日志的格式符合这个规律,就可以直接用。如果你的日志格式不一样,只要改正则表达式就行。
四、应用场景、优缺点与注意事项
4.1 应用场景
我们刚才讲的两种格式适配方法,适合哪些场景呢? 第一,多服务混合部署的场景:比如一个K8s集群里,既有Java服务的JSON日志,又有Go服务的自定义分隔日志,还有Nginx的访问日志,这时候可以给不同的服务配置不同的Pipeline,让Loki能统一采集所有格式的日志,不用为不同的日志类型换不同的采集工具。 第二,需要精准搜索日志的场景:比如排查线上问题的时候,要快速找到某个级别、某个模块、某个请求链的日志,把日志里的关键信息转成标签后,搜索速度会快很多,而且结果更精准。 第三,日志审计的场景:比如需要统计某个时间段内的错误日志数量、某个服务的请求量,把日志字段转成标签后,就可以用Loki的查询语言来做统计分析,非常方便。
4.2 技术优缺点
用Promtail的Pipeline来解决格式兼容问题,有什么好处和坏处呢? 优点: 第一,成本低:不用改服务的日志格式,不用额外部署其他工具,只要修改Promtail的配置就能解决问题,对现有系统的影响很小。 第二,灵活性高:Pipeline可以支持各种复杂的加工逻辑,除了解析JSON和正则,还可以做过滤、重命名、甚至调用外部接口来加工日志,能适配几乎所有的日志格式。 第三,统一管理:所有的日志加工逻辑都在Promtail里配置,不用分散到各个服务里,管理起来更方便。 缺点: 第一,对正则的要求高:如果你的日志格式很复杂,需要写很复杂的正则表达式,对不熟悉正则的人来说有一定的门槛。 第二,性能损耗:Pipeline的加工步骤越多,Promtail的性能损耗就越大,比如一行日志要经过好几层正则解析,会增加CPU的占用,日志量很大的时候要注意测试性能。 第三,调试麻烦:如果配置的Pipeline有问题,比如正则写错了,日志解析失败,排查问题比较麻烦,需要专门的工具来调试Pipeline。
4.3 注意事项
在实际使用的时候,有几个地方要特别注意: 第一,日志格式要尽量规范:虽然Pipeline可以适配各种格式,但如果你的日志格式太乱,比如有时候输出JSON,有时候输出纯文本,或者JSON的字段不固定,会导致解析失败,所以尽量让服务的日志格式保持统一。 第二,配置要做灰度测试:修改Promtail的配置之前,最好先在测试环境测试,或者用K8s的标签选择器,先给一部分服务应用新的配置,确认没问题之后再全量推广,避免影响生产环境的日志采集。 第三,不要过度加工:Pipeline的步骤不要太多,尽量简化加工逻辑,减少性能损耗,比如只把需要搜索的字段转成标签,不用把所有字段都转成标签。 第四,注意时间格式的匹配:解析日志时间的时候,一定要确保format参数和日志里的时间格式一致,比如日志里的时间是“2024-05-20 10:00:00”,就要用“2006-01-02 15:04:05”作为format,Loki的时间格式是用Go语言的时间格式来定义的,这个要注意。
五、文章总结
容器日志格式和Loki不兼容的问题,本质上是Loki默认只做通用采集,不会对日志内容做加工,而解决这个问题的核心就是利用Promtail的Pipeline功能,对日志进行自定义加工。
我们通过两个具体的场景,演示了如何用Pipeline解析JSON格式和自定义分隔格式的日志,把关键信息转成标签,让Loki能正常采集和搜索。这种方法不用改服务、不用换工具,成本低、灵活性高,适合大部分容器化环境的日志采集需求。
最后要提醒大家,日志采集的适配没有“银弹”,具体的配置要根据自己的日志格式来调整,在配置的时候要注意规范日志格式、做灰度测试、优化性能,这样才能让Loki更好地为你的服务保驾护航。
评论
围绕“Loki在容器化环境中的日志采集适配:解决容器日志格式兼容问题”参与讨论