Kubernetes 日志收集(二),基于 Logging operator 的日志收集
Logging Operator 介绍
Logging operator 基于 CRD 规定和管理日志收集架构,我们通过相关规定的资源可以在 K8S 中轻松的部署日志采集器、日志转发器与相关的日志路由规则
自定义资源
每次修改完相关 CRD 时,对应的资源需要一段时间之后才会收到影响,需要等待一段时间
Logging
Logging
定义用于收集和传输日志消息的集群的日志记录基础架构
Logging
包含Fluent Bit
日志收集器(基于DemonSet
部署)以及Fluentd
和Syslog-ng
日志转发器(基于StatefulSet
部署)的配置,在新版本中,可以使用FluentbitAgent
代替Fluent Bit
的配置,将其与日志转发器隔离开- 日志收集器(
Fluent Bit
)作为Daemonset
部署在节点上,主要用于收集节点上的日志并传入日志转发器 - 日志转发器可以接收、过滤和转换传入的日志,并将它们传输到一个或多个目标输出,Logging Operator 支持
Fluentd
和Syslog-ng
作为日志转发器,Syslog-ng
支持多线程处理可提供更高的性能,Fluentd
支持丰富的输入输出源以及各种插件,可以根据各种需要选择不同的日志转发器 - 在创建
Logging
时,会建立controlNamespace
,即 Logging Operator 的管理命名空间,Fluentd
|Syslog-ng
和Fluent Bit
部署在此命名空间中,默认情况下,仅在此命名空间中评估诸如ClusterOutput
和ClusterFlow
之类的全局资源(即使它们在任何其他命名空间,除非allowClusterResourcesFromAllNamespaces
设置为 true)
Flow
Flow
将选定的日志消息路由到指定的输出,它包含了Flow
与ClusterFlow
Flow
是一个namespaced
资源,因此仅收集来自相同命名空间的日志。可以指定match
语句根据 Kubernetes labels、容器和主机名来选择或排除日志(匹配语句按照定义和处理的顺序进行评估,直到第一个匹配的 select 或 exclude 规则应用为止)ClusterFlow
定义了一个没有命名空间限制的Flow
。它也只在controlNamespace
中有效。ClusterFlow
从所有命名空间中选择日志Flow
和ClusterFlow
是针对Fluentd Forwarder
的 CRD 资源,如果我们要使用Syslog-ng
作为Forwarder
,需要将对应的名称改为:SyslogNGFlow
和SyslogNGClusterFlow
OutPut
OutPut
是日志转发器将日志消息发送到的目的地,如常用的 Elasticsearch、Loki 或 Kafka 等
Output
也是namespaced
资源,定义了Flow
可以发送日志消息的输出。意味着只有同一命名空间内的Flow
可以访问它。可以在这些定义中使用secrets
,但它们也必须位于同一命名空间中。输出是日志转发的最后阶段ClusterOutput
定义没有命名空间限制的输出- 同
Flow
,如果要使用Syslog-ng
,要将对应的名称改为:SyslogNGFlow
和SyslogNGClusterOutput
官方文档及架构图
Logging Operator 安装
需要提前安装 helm
helm upgrade --install --wait \ |
以上命令除了安装 Logging Operator 外,还会安装一个测试用的 deployment logging-operator-test-receiver
,它侦听 HTTP 端口,接收 JSON 消息,并将它们写入标准输出 (stdout),我们在配置日志转发器时,可以接入这个服务,一边检查我们的日志格式是否有问题。
# 查看服务运行情况 |
配置日志收集器和日志转发器
在本文中,我们使用 Fluentd
作为日志转发器
Fluentd(CRD:Logging)
这里配置了一个三分片的 Fluentd
并且配置了 pod 亲和性使其不调度在一个节点,使用 pvc 对 buffer 数据进行了持久化
apiVersion: logging.banzaicloud.io/v1beta1 |
Fluent-bit(CRD:FluentbitAgent)
这里额外配置了 /data/docker/containers
是因为修改过 docker 的默认 data-root
文件夹,如果默认的 docker 数据保存在 /var/lib/docker
则不需要添加
配置 tolerations
使 daemonset
可以调度到 master 上让我们后续可以收集 kube-system
相关日志
apiVersion: logging.banzaicloud.io/v1beta1 |
当我们 apply
Logging
和FluentbitAgent
时,会启动名为logging-collector-fluentd
的Fluentd
(statefulset)和名为logging-collector-agent-fluentbit
的Fluent-Bit
(daemonset),此时Fluent-Bit
就会采集节点上的日志到Fluentd
中
检查
文件挂载检查
查看 daemonset
的详细信息查看我们配置的额外挂载是否生效
kubectl describe daemonsets.apps -n logging logging-collector-agent-fluentbit |
Volumes:
varlibcontainers:
Type: HostPath (bare host directory volume)
Path: /var/lib/docker/containers
HostPathType:
varlogs:
Type: HostPath (bare host directory volume)
Path: /var/log
HostPathType:
extravolumemount0:
Type: HostPath (bare host directory volume)
Path: /data/docker/containers/
HostPathType:
config:
Type: Secret (a volume populated by a Secret)
SecretName: logging-collector-agent-fluentbit
Optional: false
positiondb:
Type: EmptyDir (a temporary directory that shares a pod's lifetime)
Medium:
SizeLimit: <unset>
buffers:
Type: EmptyDir (a temporary directory that shares a pod's lifetime)
Medium:
SizeLimit: <unset>
配置检查
kubectl get secret logging-collector-agent-fluentbit -n logging -o jsonpath='{.data.fluent-bit\.conf}'|base64 --decode |
[SERVICE] |
日志等级(可选)
修改日志级别可以查看采集是否有问题,如下所示将 logLevel
配置为 trace
apiVersion: logging.banzaicloud.io/v1beta1 |
Fluentd
同上
apiVersion: logging.banzaicloud.io/v1beta1 |
Flow 和 OutPut
部署打印测试日志的容器
通过 golang 编写一个一直打印多行错误日志的 Deployment 用于测试,日志格式如下:
2024-03-20 06:31:44.934 ERROR log_test/main.go:19 main.main 日志测试 2024-03-20 06:31:44.934
main.main
log_test/main.go:19
runtime.main
runtime/proc.go:271
代码如下:
package main |
apiVersion: apps/v1 |
配置 Flow 和 OutPut (使用logging-operator-test-receiver测试)
apiVersion: logging.banzaicloud.io/v1beta1 |
创建完毕后,会在
default
命名空间创建Flow:log-generator
和OutPut:test-receiver
,并将kubernetes label
为logging=golang
的日志传输到logging-operator-test-receiver
打印
日志的格式化
多行日志的初步合并
在使用 docker 作为 kubernetes 的容器运行时时,容器日志会将每一行打印的日志拆分开
实际日志为:
2024-03-20 06:31:44.934 ERROR log_test/main.go:19 main.main 日志测试 2024-03-20 06:31:44.934
main.main
log_test/main.go:19
runtime.main
runtime/proc.go:271
会被拆分成:
{"log":"2024-03-20 06:31:44.934 ERROR log_test/main.go:19 main.main 日志测试 2024-03-20 06:31:44.934\n","stream":"stdout","time":"2024-03-20T06:29:59.935462228Z"}
{"log":"main.main\n","stream":"stdout","time":"2024-03-20T06:29:59.935581674Z"}
{"log":"\u0009log_test/main.go:19\n","stream":"stdout","time":"2024-03-20T06:29:59.935610097Z"}
{"log":"runtime.main\n","stream":"stdout","time":"2024-03-20T06:29:59.935633418Z"}
{"log":"\u0009runtime/proc.go:271\n","stream":"stdout","time":"2024-03-20T06:29:59.93565774Z"}
需要在日志转发器中先合并相同的 log,再将对应的 log 进行格式化,这里仍然使用 logging-operator-test-receiver
测试
apiVersion: logging.banzaicloud.io/v1beta1 |
运行之后发现,日志已经被合并到了 key
为 log
的部分
[0] http.0: [[1710923000.932884258, {}], {"log"=>"2024-03-20 08:23:14.937 ERROR log_test/main.go:19 main.main 日志测试 2024-03-20 08:23:14.937
main.main
D:/GolandProjects/log_test/main.go:19
runtime.main
C:/Program Files/Go/src/runtime/proc.go:271
", "stream"=>"stdout", "time"=>"2024-03-20T08:23:14.937900447Z", "kubernetes"=>{"pod_name"=>"print-logs-64fb98db85-c4zdz", "namespace_name"=>"default", "pod_id"=>"d72c4042-cb45-4fe1-a684-eb4e56861049", "labels"=>{"app"=>"print-logs", "logging"=>"golang", "pod-template-hash"=>"64fb98db85"}, "annotations"=>{"cni.projectcalico.org/containerID"=>"24c125288decf36a19b5d333569258a45b2f41b2dfbbef407854145234ec7323", "cni.projectcalico.org/podIP"=>"10.244.166.182/32", "cni.projectcalico.org/podIPs"=>"10.244.166.182/32"}, "host"=>"node1", "container_name"=>"print-logs", "docker_id"=>"ffb6313adb1347f828348f128852ba110f608124f3a8aca68b400f238bfde71a","container_hash"=>"print-test-log@sha256:ad1a3e5bb60d81a6b13e8085c618244055c35807e7a05caabf50f77adc7a11e0", "container_image"=>"print-test-log"}}]
如果使用的容器运行时为 containerd,则需要对应的日志为:
2024-04-10T02:15:17.527436711Z stdout F 2024-04-10 02:15:17.527 ERROR log_test/main.go:19 main.main 日志测试 2024-04-10 02:15:17.527
2024-04-10T02:15:17.527496993Z stdout F main.main
2024-04-10T02:15:17.527501224Z stdout F D:/GolandProjects/log_test/main.go:19
2024-04-10T02:15:17.527503316Z stdout F runtime.main
2024-04-10T02:15:17.527505153Z stdout F C:/Program Files/Go/src/runtime/proc.go:271
这时,fluentbit 会将后面的部分收集到 key 为 message 中,需要修改 Flow 的合并配置合并的 key 为 message
filters: |
日志的字段格式化
2024-03-20 06:31:44.934 ERROR log_test/main.go:19 main.main 日志测试 2024-03-20 06:31:44.934
main.main
log_test/main.go:19
runtime.main
runtime/proc.go:271
使用上述日志格式的作为参考编写正则表达式,将我们的数据使用正则命令将其拆分为如下部分
- time:2024-03-20 08:23:14.937
- loglevel:ERROR
- line:log_test/main.go:19
- func:main.main
- log部分保持不变
正则表达式如下:(正则测试网站:https://regex101.com/)
/^(?<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3})(\s+)(?<loglevel>\w+)(\s+)(?<line>[\w\.\:\/\-]+)(\s+)(?<func>[\w\.\:\/\-]+)/
配置日志字段格式化
apiVersion: logging.banzaicloud.io/v1beta1 |
运行效果:
[0] http.0: [[1710928830.558128299, {}], {"log"=>"2024-03-20 10:00:24.938 ERROR log_test/main.go:19 main.main 日志测试 2024-03-20 10:00:24.938
main.main
D:/GolandProjects/log_test/main.go:19
runtime.main
C:/Program Files/Go/src/runtime/proc.go:271
", "stream"=>"stdout", "time"=>"2024-03-20T10:00:24.939195916Z", "kubernetes"=>{"pod_name"=>"print-logs-64fb98db85-c4zdz", "namespace_name"=>"default", "pod_id"=>"d72c4042-cb45-4fe1-a684-eb4e56861049", "labels"=>{"app"=>"print-logs", "logging"=>"golang", "pod-template-hash"=>"64fb98db85"}, "annotations"=>{"cni.projectcalico.org/containerID"=>"24c125288decf36a19b5d333569258a45b2f41b2dfbbef407854145234ec7323", "cni.projectcalico.org/podIP"=>"10.244.166.182/32", "cni.projectcalico.org/podIPs"=>"10.244.166.182/32"}, "host"=>"node1", "container_name"=>"print-logs", "docker_id"=>"ffb6313adb1347f828348f128852ba110f608124f3a8aca68b400f238bfde71a", "container_hash"=>"print-test-log@sha256:ad1a3e5bb60d81a6b13e8085c618244055c35807e7a05caabf50f77adc7a11e0", "container_image"=>"print-test-log"}, "loglevel"=>"ERROR", "line"=>"log_test/main.go:19", "func"=>"main.main"}]
删除不需要的字段
看上述日志发现其中有很多数据我们都不需要,可以使用 record_transformer
删除不需要的字段
apiVersion: logging.banzaicloud.io/v1beta1 |
日志输出到 ElasticSearch
ElasticSearch OutPut 配置
在 default 命名空间创建 elastic 密码的 secret
kubectl create secret generic elastic-password --from-literal=password='密码' |
apiVersion: logging.banzaicloud.io/v1beta1 |
使用动态的索引名称
如果需要配置动态索引名称,需要在 buffer 中添加对应的 key 值,如我们需要在索引名称中添加命名空间与容器名称,配置如下:
apiVersion: logging.banzaicloud.io/v1beta1 |
ElasticSearch 使用数据流模式
apiVersion: logging.banzaicloud.io/v1beta1 |