在 Kubernetes 上采集网关日志
在 Kubernetes 上,可以通过两种方式采集 API7 网关的访问日志和错误日志:
- 从容器输出采集。 网关写入标准输出和标准错误,容器运行时将该数据流持久化到节点上,再由节点级采集器读取。这是默认方式,无需修改网关配置。
- 从 Pod 内的日志文件采集。 网关写入真实文件,通过卷将文件暴露出来,再由采集器读取文件。这需要修改网关配置,但每类日志各自独立成文件。
本文分别用 OpenTelemetry Collector 和 Filebeat 介绍这两种方式。
关于网关生成的日志类型以及控制这些日志的配置项,请参阅配置集中式日志记录。
选择采集方式
| 容器输出 | Pod 内的日志文件 | |
|---|---|---|
| 网关配置 | 无需修改,这是 默认方式。 | 需要重定向访问日志和错误日志、开启轮转、挂载卷。 |
| 访问日志与错误日志 | 交织在同一个数据流中,通过 stdout 或 stderr 标记区分。 | 各自独立的文件。 |
| 轮转与保留 | 由容器运行时和 kubelet 处理。 | 由网关的 log-rotate 插件处理。 |
kubectl logs | 能看到网关日志。 | 几乎看不到内容。 |
| 超长日志行 | 由运行时在 16 KB 处切分,再由采集器重组。 | 完整写入。 |
| 并发写入 | 单行日志超过管道缓冲区大小时,可能与另一个 worker 的写入交错。 | 每行作为一个整体写入文件。 |
建议优先使用容器输出方式,它的运维面最小,且两种采集器都能很好地处理。
何时改用日志文件
容器的标准输出是一个管道。在 Linux 上,对管道的写入只有在不超过 PIPE_BUF(4096 字节)时才是原子的。网关运行多个 worker 进程,它们共用同一个管道,因此当单行日志超过该限制时,另一个 worker 的写入可能被插入到这一行的中间。结果是一行被污染的日志同时包含两个不相关请求的片段,并且任何采集器都无法修复它,因为破坏发生在运行时读到数据之前。
出现以下情况时可以考虑改用日志文件:
- 访问日志行经常超过 4096 字节,例如日志格式中包含较大的请求头、请求体或较多上游字段。
- 已经观察到日志条目中混入了不相关请求的片段 。
- 需要让访问日志和错误日志分别落在不同文件中,而不是交织在同一个数据流里。
写文件也有代价:轮转需要自行负责,kubectl logs 对网关容器不再有用,采集器也需要通过卷才能读到文件。本文同时介绍两种方式,方便权衡。
从容器输出采集
Kubernetes 如何存放容器输出
容器运行时会把每个容器的输出写到节点文件系统上,路径中编码了 Pod 身份:
/var/log/pods/<namespace>_<pod-name>_<pod-uid>/<container-name>/<restart-count>.log
/var/log/containers/ 下的文件是指向上述文件的符号链接。
文件中的每一行都带有运行时前缀,包含时间戳、数据流名称,以及标识该行是否被切分的标志:
2026-08-20T11:31:29.123456789Z stdout F 172.18.0.1 - - [20/Aug/2026:11:31:29 +0000] "GET /anything HTTP/1.1" 200
对于超过 16 KB 的日志行,运行时会将其切分成多个物理行,并把除最后一行以外的所有行标记为 P 而不是 F。下文介绍的两种采集器都会剥离该前缀并重组被切分的行,无需自行处理。
这些日志文件属主为 root、权限为 0640,因此采集器必须以 root 身份运行才能读取。
使用 OpenTelemetry Collector 采集容器输出
以 DaemonSet 方式运行 Collector,并以只读方式挂载 /var/log/pods。一个 container 算子即可处理运行时格式:
extensions:
file_storage/checkpoints:
directory: /var/lib/otelcol/storage
receivers:
filelog/gateway:
include:
- /var/log/pods/<namespace>_<gateway-release-name>-*/gateway/*.log
exclude:
- /var/log/pods/*/otel-collector/*.log
start_at: end
include_file_path: true
storage: file_storage/checkpoints
operators:
- type: container
processors:
k8sattributes:
auth_type: serviceAccount
extract:
metadata:
- k8s.node.name
- k8s.deployment.name
pod_association:
- sources:
- from: resource_attribute
name: k8s.pod.uid
batch:
timeout: 2s
exporters:
otlp:
endpoint: <your-backend>:4317
service:
extensions: [file_storage/checkpoints]
pipelines:
logs:
receivers: [filelog/gateway]
processors: [k8sattributes, batch]
exporters: [otlp]
container 算子会自动识别 containerd、CRI-O 或 Docker 格式,剥离运行时前缀,重组被运行时切分的行,并从文件路径中提取资源属性:
| 属性 | 来源 |
|---|---|
k8s.pod.name、k8s.pod.uid、k8s.namespace.name | 文件路径 |
k8s.container.name、k8s.container.restart_count | 文件路径 |
log.iostream | 运行时前缀,取值为 stdout 或 stderr |
由于 Pod UID 来自路径,k8sattributes 处理器可以据此关联到运行中的 Pod,补充路径中不包含的属性,例如节点名称、工作负载名称和 Pod 标签。
在 DaemonSet 中挂载宿主机路径:
volumeMounts:
- name: varlogpods
mountPath: /var/log/pods
readOnly: true
- name: checkpoints
mountPath: /var/lib/otelcol/storage
volumes:
- name: varlogpods
hostPath:
path: /var/log/pods
- name: checkpoints
hostPath:
path: /var/lib/otelcol
type: DirectoryOrCreate
检查点目录应使用 hostPath 卷而不是 emptyDir。file_storage 扩展记录了每个文件已读取到的位置,而 DaemonSet 的 Pod 在每次升级时都会被重建,emptyDir 会随之删除,导致 Collector 在每次重启后重新读取节点上的所有日志文件。
OpenTelemetry Collector 没有与 Filebeat autodiscover hints 对应的机制,后者可以根据注解为不同 Pod 应用不同的解析规则。include 匹配规则是静态的。要按工作负载区分解析,可以为每个工作负载配置一个 receiver 并使用更精确的匹配规则,或者在 pipeline 中根据 k8s.container.name 等属性做分流。
使用 Filebeat 采集容器输出
Filebeat 读取的是同一批文件。使用 filestream 输入配合 container 解析器:
filebeat.inputs:
- type: filestream
id: api7-gateway-container
paths:
- /var/log/containers/<gateway-release-name>-*_<namespace>_gateway-*.log
prospector.scanner.symlinks: true
parsers:
- container:
stream: all
format: auto
processors:
- add_kubernetes_metadata:
host: ${NODE_NAME}
matchers:
- logs_path:
logs_path: "/var/log/containers/"
prospector.scanner.symlinks: true 是必需的。/var/log/containers/ 下的文件是符号链接,而 filestream 输入默认跳过符号链接,不加这一项该输入会静默采集不到任何内容。旧的 type: container 输入默认开启了该行为,因此从它迁移过来的配置会失效。
由于链接指向第二个目录,需要同时以只读方式挂载 /var/log/containers 和 /var/log/pods:
volumeMounts:
- name: varlogcontainers
mountPath: /var/log/containers
readOnly: true
- name: varlogpods
mountPath: /var/log/pods
readOnly: true
- name: data
mountPath: /usr/share/filebeat/data
volumes:
- name: varlogcontainers
hostPath:
path: /var/log/containers
- name: varlogpods
hostPath:
path: /var/log/pods
- name: data
hostPath:
path: /var/lib/filebeat-data
type: DirectoryOrCreate
出于与 OpenTelemetry Collector 需要持久化检查点目录同样的原因,注册表目录(/usr/share/filebeat/data)也应放在 hostPath 卷上。
Filebeat 需要读取 Pod 和命名空间才能解析元数据。请为它授予对 pods、namespaces、nodes 以及 apps API 组中 replicasets 的 get、watch 和 list 权限,并让容器以 root 身份运行,以便读取运行时日志文件。
在 Pod 内写入日志文件
步骤 1:将访问日志和错误日志重定向到文件
在官方容器镜像中,/usr/local/apisix/logs/access.log 和 /usr/local/apisix/logs/error.log 是指向 /dev/stdout 和 /dev/stderr 的符号链接。因此把配置改回这两个路径,日志仍然会写到容器输出。请改用其他目录,并在该目录上挂载卷:
logs:
enableAccessLog: true
accessLog: "/var/log/apisix/access.log"
accessLogFormatEscape: json
accessLogFormat: '{"time":"$time_iso8601","remote_addr":"$remote_addr","host":"$http_host","request":"$request","status":$status,"body_bytes_sent":$body_bytes_sent,"request_time":$request_time,"upstream_addr":"$upstream_addr","upstream_status":"$upstream_status","request_id":"$apisix_request_id"}'
errorLog: "/var/log/apisix/error.log"
errorLogLevel: "warn"
extraVolumes:
- name: api7-gateway-logs
emptyDir:
sizeLimit: 10Gi
extraVolumeMounts:
- name: api7-gateway-logs
mountPath: /var/log/apisix
不要把卷挂载到 /usr/local/apisix/logs 上,该目录同时存放网关的运行时文件,包括 nginx.pid 和 worker 事件套接字。
网关容器以 UID 636 运行。emptyDir 创建时权限较为宽松,但设置 fsGroup 可以让属主关系更明确:
apisix:
podSecurityContext:
fsGroup: 636
对可能为空的字段(例如 $upstream_status)加引号,这样在没有上游时该行仍然是合法 JSON。$status、$request_time 等数值字段可以不加引号。
四层访问日志不在轮转范围内。如果开启 logs.stream.enableAccessLog 并将其指向文件,该文件会无限增长。除非另有控制体积的方案,否则请保持关闭。
步骤 2:开启日志轮转
网关一旦开始写文件,容器运行时就不再轮转任何内容。没有轮转,卷会一直增长直到 Pod 被驱逐。请开启 log-rotate 插件,控制面下发给数据面的插件列表中已经包含该插件:
pluginAttrs:
log-rotate:
enable: true
interval: 3600
max_kept: 24
max_size: 268435456
enable_compression: false
该插件会重命名当前文件,并通知网关重新打开文件。轮转后的文件以时间戳作为文件名前缀:
2026-08-20_19-00-00__access.log
2026-08-20_19-00-00__error.log
请在采集器中排除该模式,避免把轮转后的文件当作新的采集源。两种采集器都会把被重命名的文件读到末尾后再释放,因此排除该模式不会丢失轮转前刚写入的条目。
请保持 enable_compression 关闭。开启压缩后,插件会在轮转后不久归档并删除原文件,这可能与尚未读完该文件的采集器发生竞争。