跳到主要内容
版本:3.10.x

在 Kubernetes 上采集网关日志

在 Kubernetes 上,可以通过两种方式采集 API7 网关的访问日志和错误日志:

  • 从容器输出采集。 网关写入标准输出和标准错误,容器运行时将该数据流持久化到节点上,再由节点级采集器读取。这是默认方式,无需修改网关配置。
  • 从 Pod 内的日志文件采集。 网关写入真实文件,通过卷将文件暴露出来,再由采集器读取文件。这需要修改网关配置,但每类日志各自独立成文件。

本文分别用 OpenTelemetry Collector 和 Filebeat 介绍这两种方式。

关于网关生成的日志类型以及控制这些日志的配置项,请参阅配置集中式日志记录

选择采集方式

对比项容器输出Pod 内的日志文件
网关配置无需修改,这是默认方式。需要重定向访问日志和错误日志、开启轮转、挂载卷。
访问日志与错误日志交织在同一个数据流中,通过 stdoutstderr 标记区分。各自独立的文件。
轮转与保留由容器运行时和 kubelet 处理。由网关的 log-rotate 插件处理。
kubectl logs能看到网关日志。几乎看不到内容。
超长日志行可能被运行时切分,再由采集器重组。完整写入。
并发写入单行日志超过管道缓冲区大小时,可能与另一个 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

使用 CRI 的容器运行时可能把一个超长的逻辑行切分成多个物理行,并把除最后一行以外的所有行标记为 P 而不是 F。例如 containerd 使用一个可配置的上限,默认值为 16 KiB;其它运行时实现和集群配置可能使用不同的阈值。下文介绍的两种采集器都会识别该格式、剥离其前缀并重组被切分的行。

在 containerd 的默认节点权限下,这些文件属主为 root、权限为 0640。请让采集器具备读取节点运行时日志的权限,通常意味着以 root 身份运行。如果使用其它运行时或安全策略,请自行确认节点上的权限设置。

使用 OpenTelemetry Collector 采集容器输出

以 DaemonSet 方式运行 Collector,并以只读方式挂载 /var/log/pods。一个 container 算子即可处理运行时格式:

otel-collector-config.yaml
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.namek8s.pod.uidk8s.namespace.name文件路径
k8s.container.namek8s.container.restart_count文件路径
log.iostream运行时前缀,取值为 stdoutstderr

由于 Pod UID 来自路径,k8sattributes 处理器可以据此关联到运行中的 Pod,补充上面配置的节点名称和 Deployment 名称。

在 DaemonSet 中挂载宿主机路径:

otel-collector-daemonset.yaml
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 卷而不是 emptyDirfile_storage 扩展记录了每个文件已读取到的位置,而 DaemonSet 的 Pod 在每次升级时都会被重建,emptyDir 会随之删除,导致 Collector 在每次重启后重新读取节点上的所有日志文件。

k8sattributes 处理器还需要读取用于补充属性的 Kubernetes 资源的权限。请把下列权限绑定到 Collector DaemonSet 使用的 ServiceAccount,并把 spec.template.spec.serviceAccountName 设为 otel-collector

otel-collector-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: otel-collector
namespace: <collector-namespace>
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: otel-collector-k8sattributes
rules:
- apiGroups: [""]
resources: ["pods", "namespaces"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["replicasets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: otel-collector-k8sattributes
subjects:
- kind: ServiceAccount
name: otel-collector
namespace: <collector-namespace>
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: otel-collector-k8sattributes
备注

OpenTelemetry Collector 没有与 Filebeat autodiscover hints 等价的机制;后者可以根据注解为每个 Pod 应用解析规则。由于 include 模式是静态的,如需按工作负载解析,可以为每个工作负载配置一个接收器并使用范围更窄的 glob 模式,也可以在处理管道中根据 k8s.container.name 等属性进行分流。

使用 Filebeat 采集容器输出

Filebeat 读取的是同一批文件。使用 filestream 输入配合 container 解析器:

filebeat.yml
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

filebeat-daemonset.yaml
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 和命名空间才能解析元数据。请为它授予对 podsnamespacesnodes 以及 apps API 组中 replicasetsgetwatchlist 权限,并让容器以 root 身份运行,以便读取运行时日志文件。

在 Pod 内写入日志文件

配置网关日志文件

在官方容器镜像中,/usr/local/apisix/logs/access.log/usr/local/apisix/logs/error.log 是指向 /dev/stdout/dev/stderr 的符号链接。因此把配置改回这两个路径,日志仍然会写到容器输出。请改用其他目录,并在该目录上挂载卷:

dp-values.yaml
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: 16Gi

extraVolumeMounts:
- name: api7-gateway-logs
mountPath: /var/log/apisix

不要把卷挂载到 /usr/local/apisix/logs 上,该目录同时存放网关的运行时文件,包括 nginx.pid 和 worker 事件套接字。

网关容器以 UID 636 运行。emptyDir 创建时权限较为宽松,但设置 fsGroup 可以让属主关系更明确:

dp-values.yaml
apisix:
podSecurityContext:
fsGroup: 636

对可能为空的字段(例如 $upstream_status)加引号,这样在没有上游时该行仍然是合法 JSON。$status$request_time 等数值字段可以不加引号。

警告

四层访问日志不在轮转范围内。如果开启 logs.stream.enableAccessLog 并将其指向文件,该文件会无限增长。除非另有控制体积的方案,否则请保持关闭。

配置日志轮转

网关一旦开始写文件,容器运行时就不再轮转任何内容。没有轮转,卷会一直增长直到 Pod 被驱逐。请开启 log-rotate 插件,控制面下发给数据面的插件列表中已经包含该插件:

dp-values.yaml
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 关闭。开启压缩后,插件会在轮转后不久归档并删除原文件,这可能与尚未读完该文件的采集器发生竞争。

max_kept 对每种日志类型分别生效。按上面的取值,网关最多会保留约 12 GiB 的轮转后访问日志和错误日志(2 × 24 × 256 MiB)。两个活动文件以及下一次容量检查之前写入的数据还需要额外空间,因此本示例使用 16 GiB 的卷。调整这些取值时,请按每种会被轮转的日志类型、活动文件和写入超额一并核算卷及其所在节点的容量。

选择采集器的部署位置

对比项边车(Sidecar)采集器节点级 DaemonSet
采集器实例数每个网关 Pod 一个每个节点一个
Pod 内共享的 emptyDir同一个 emptyDir,从节点上读取
宿主机挂载以只读方式挂载 /var/lib/kubelet
资源开销较高较低
停机在网关停止之后才退出,可以排空Pod 被删除时可能丢失末尾数据

emptyDir 并不局限在 Pod 自身的文件系统内,kubelet 会用节点上的一个目录来承载它:

/var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~empty-dir/<volume-name>/

正是这个路径让节点级采集 Pod 内文件成为可能,同时卷仍然绑定 Pod 生命周期,Pod 删除时 kubelet 会一并回收。

写入容器自身文件系统而非卷的文件,在节点上没有稳定路径。它位于容器可写层的快照目录下,该目录的标识符在容器重建时会变化,也无法通过 Pod 名称反查。任何采集器都无法发现它,所以通过卷暴露文件是必需的,而不是可选的。Filebeat 也受同样的限制。

使用边车 OpenTelemetry Collector 采集日志文件

API7 网关 Helm Chart 没有提供 extraContainers 配置项,但 Kubernetes 1.29 及以后的版本支持以 restartPolicy: Always 的 init 容器形式声明原生边车。在 Kubernetes 1.29 到 1.32 上,请确认集群管理员没有关闭 SidecarContainers 特性门控;在 Kubernetes 1.33 及以后的版本中,原生边车已 GA 且无法关闭。这类容器先于网关启动、后于网关停止,因此既能覆盖启动日志,也能覆盖停机时的排空。

dp-values.yaml
extraVolumes:
- name: api7-gateway-logs
emptyDir:
sizeLimit: 16Gi
- name: otelcol-config
configMap:
name: gateway-otelcol-config
- name: otelcol-storage
emptyDir: {}

extraVolumeMounts:
- name: api7-gateway-logs
mountPath: /var/log/apisix

extraInitContainers:
- name: otel-collector
image: otel/opentelemetry-collector-contrib:0.119.0
restartPolicy: Always
args: ["--config=/etc/otelcol/config.yaml"]
env:
- name: K8S_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: K8S_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: K8S_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
volumeMounts:
- name: api7-gateway-logs
mountPath: /var/log/apisix
readOnly: true
- name: otelcol-config
mountPath: /etc/otelcol
- name: otelcol-storage
mountPath: /var/lib/otelcol/storage
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi

Collector 直接读取这两个文件,并通过 Downward API 获取 Pod 身份:

gateway-otelcol-config.yaml
extensions:
file_storage/checkpoints:
directory: /var/lib/otelcol/storage

receivers:
filelog/access:
include: [/var/log/apisix/access.log]
exclude: [/var/log/apisix/*__*.log, /var/log/apisix/*.tar.gz]
start_at: beginning
include_file_path: true
storage: file_storage/checkpoints
operators:
- type: json_parser
timestamp:
parse_from: attributes.time
layout_type: gotime
layout: '2006-01-02T15:04:05Z07:00'

filelog/error:
include: [/var/log/apisix/error.log]
exclude: [/var/log/apisix/*__*.log, /var/log/apisix/*.tar.gz]
start_at: beginning
include_file_path: true
storage: file_storage/checkpoints
multiline:
line_start_pattern: '^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}'
operators:
- type: regex_parser
regex: '(?s)^(?P<ts>\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[(?P<sev>\w+)\] (?P<rest>.*)$'
timestamp:
parse_from: attributes.ts
layout_type: strptime
layout: '%Y/%m/%d %H:%M:%S'
location: UTC
severity:
parse_from: attributes.sev
mapping:
debug: debug
info: info
warn: [warn, notice]
error: [error, crit]
fatal: [alert, emerg]

processors:
resource:
attributes:
- key: k8s.namespace.name
value: ${env:K8S_NAMESPACE}
action: upsert
- key: k8s.pod.name
value: ${env:K8S_POD_NAME}
action: upsert
- key: k8s.node.name
value: ${env:K8S_NODE_NAME}
action: upsert
batch:
timeout: 2s

exporters:
otlp:
endpoint: <your-backend>:4317

service:
extensions: [file_storage/checkpoints]
pipelines:
logs:
receivers: [filelog/access, filelog/error]
processors: [resource, batch]
exporters: [otlp]

使用节点级 OpenTelemetry Collector 采集日志文件

若要从 DaemonSet 读取同一批文件,保留配置网关日志文件中的配置但不部署边车。在 Collector 中以只读方式挂载 /var/lib/kubelet,并把 include 匹配规则改为 kubelet 路径。请给卷取一个足够独特的名称,避免匹配到其他工作负载的 emptyDir

otel-collector-config.yaml
receivers:
filelog/gateway-access:
include:
- /var/lib/kubelet/pods/*/volumes/kubernetes.io~empty-dir/api7-gateway-logs/access.log
exclude:
- /var/lib/kubelet/pods/*/volumes/kubernetes.io~empty-dir/api7-gateway-logs/*__*.log
start_at: beginning
include_file_path: true
storage: file_storage/checkpoints
operators:
- type: regex_parser
parse_from: 'attributes["log.file.path"]'
parse_to: attributes.k8s
regex: '^/var/lib/kubelet/pods/(?P<uid>[0-9a-f]{8}-[0-9a-f-]{27})/volumes/'
- type: move
from: attributes.k8s.uid
to: 'resource["k8s.pod.uid"]'
- type: remove
field: attributes.k8s
- type: json_parser
parse_from: body
timestamp:
parse_from: attributes.time
layout_type: gotime
layout: '2006-01-02T15:04:05Z07:00'

与容器输出示例一样,搭配 k8sattributes 处理器并在 pod_association 中使用 k8s.pod.uid,即可解析出 Pod 名称、命名空间、节点名称和 Deployment 名称。

请给 'attributes["log.file.path"]''resource["k8s.pod.uid"]' 这类字段引用加引号。YAML 流式映射中未加引号的方括号属于语法错误,而 Collector 报出的是 retrieved value (type=string) cannot be used as a Conf,并不指向真正的问题所在。

otel-collector-daemonset.yaml
volumeMounts:
- name: varlibkubelet
mountPath: /var/lib/kubelet
readOnly: true
mountPropagation: HostToContainer
volumes:
- name: varlibkubelet
hostPath:
path: /var/lib/kubelet
警告

/var/lib/kubelet 是 kubelet 的工作目录,可以通过它的 --root-dir 参数改到别处。挂载该目录意味着能看到节点上所有 Pod 的卷,而且 restricted Pod 安全标准完全禁止 hostPath 卷。存在这些约束时,请改用边车方式。

使用 Filebeat 采集日志文件

Filebeat 通过同样的卷挂载读取 kubelet 路径。使用 pod_uid 索引器和限定在 /var/lib/kubelet/pods/logs_path 匹配器来解析 Pod 元数据:

filebeat.yml
filebeat.inputs:
- type: filestream
id: api7-gateway-access
paths:
- /var/lib/kubelet/pods/*/volumes/kubernetes.io~empty-dir/api7-gateway-logs/access.log
prospector.scanner.exclude_files: ['__access\.log$', '\.tar\.gz$']
parsers:
- ndjson:
target: ""
add_error_key: true
processors:
- add_kubernetes_metadata:
host: ${NODE_NAME}
indexers:
- pod_uid: ~
matchers:
- logs_path:
logs_path: "/var/lib/kubelet/pods/"
resource_type: "pod"

- type: filestream
id: api7-gateway-error
paths:
- /var/lib/kubelet/pods/*/volumes/kubernetes.io~empty-dir/api7-gateway-logs/error.log
prospector.scanner.exclude_files: ['__error\.log$', '\.tar\.gz$']
parsers:
- multiline:
type: pattern
pattern: '^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}'
negate: true
match: after
processors:
- add_kubernetes_metadata:
host: ${NODE_NAME}
indexers:
- pod_uid: ~
matchers:
- logs_path:
logs_path: "/var/lib/kubelet/pods/"
resource_type: "pod"

ndjson 解析器在 target 为空时,会把 JSON 访问日志的字段提升到事件顶层。multiline 解析器会把 Lua 调用栈的后续行归并到起始行所在的条目中。

若要改为以边车方式运行 Filebeat,保持输入配置不变,把路径换成 /var/log/apisix/access.log/var/log/apisix/error.log,并将 add_kubernetes_metadata 替换为通过 Downward API 填充的 add_fields

备注

add_kubernetes_metadata 的元数据监视器会延迟初始化。在元数据监视器完成同步之前 Filebeat 发出的事件不会带有 kubernetes.* 字段。这一点在首次启动且存在大量积压时最明显,因为 Filebeat 读取积压的速度快于元数据监视器就绪的速度。稳定运行后的事件不受影响。

验证日志采集

通过网关发送一个路径足够特殊的请求,便于后续检索:

curl "http://<gateway-address>:9080/collection-probe"

确认网关已经写入该条目。对于容器输出:

kubectl logs -n <namespace> <gateway-pod> -c gateway | grep collection-probe

对于文件输出:

kubectl exec -n <namespace> <gateway-pod> -c gateway -- grep collection-probe /var/log/apisix/access.log

然后在日志后端中检索同一字符串。如果条目到达了网关但没有到达后端,问题出在采集器;如果它根本没有到达网关,问题出在网关的日志配置。

故障排查

采集器提示没有匹配到文件。 请对照节点上的真实路径检查匹配规则。Pod 目录名在命名空间、Pod 名称和 UID 之间使用下划线分隔,而裸 Pod 没有自动生成的名称后缀。如果 Filebeat 读取的是 /var/log/containers/,请确认已设置 prospector.scanner.symlinks: true

采集器启动了但没有任何输出。 请检查节点上运行时日志的权限,并确认采集器能够读取。在 containerd 的默认配置下,/var/log/pods 下的文件权限为 0640、属主为 root,因此采集器通常需要以 root 身份运行。

时间戳不正确或回退成了摄入时间。 访问日志使用 $time_iso8601,它输出的 UTC 偏移量带冒号,例如 +08:00,而 strptime%z 指令不接受这种形式,因此请改用 layout_type: gotime 配合 2006-01-02T15:04:05Z07:00。错误日志完全不带偏移量,其时间戳按配置的时区解释。如果给网关 Pod 设置了时区,请在 location 中填写相同的值,否则解析出的时间会发生偏移。

错误日志条目丢失了级别或时间戳。 Lua 调用栈跨越多行。这些行被合并之后,记录正文中包含换行符,而 Go 正则表达式中的 . 默认不匹配换行符。请在表达式中加上 (?s) 标志。

记录中没有 Kubernetes 元数据。 对于 OpenTelemetry Collector,请确认 pod_association 使用的属性确实由处理管道设置。对于 Filebeat,请检查受影响的事件是否是在启动期间、元数据监视器完成同步之前发出的。

卷被写满。 请确认已开启 log-rotate 插件。保留的轮转数据量可按「会被轮转的日志类型数 × max_kept × max_size」估算,再加上活动文件以及两次容量检查之间写入的余量。卷的 sizeLimit 和节点的可用存储都应大于该总量。同时注意四层访问日志不会被轮转。

后续步骤