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

在 Kubernetes 上采集网关日志

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

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

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

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

选择采集方式

容器输出Pod 内的日志文件
网关配置无需修改,这是默认方式。需要重定向访问日志和错误日志、开启轮转、挂载卷。
访问日志与错误日志交织在同一个数据流中,通过 stdoutstderr 标记区分。各自独立的文件。
轮转与保留由容器运行时和 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 算子即可处理运行时格式:

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,补充路径中不包含的属性,例如节点名称、工作负载名称和 Pod 标签。

在 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 在每次重启后重新读取节点上的所有日志文件。

备注

OpenTelemetry Collector 没有与 Filebeat autodiscover hints 对应的机制,后者可以根据注解为不同 Pod 应用不同的解析规则。include 匹配规则是静态的。要按工作负载区分解析,可以为每个工作负载配置一个 receiver 并使用更精确的匹配规则,或者在 pipeline 中根据 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 内写入日志文件

步骤 1:将访问日志和错误日志重定向到文件

在官方容器镜像中,/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: 10Gi

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 并将其指向文件,该文件会无限增长。除非另有控制体积的方案,否则请保持关闭。

步骤 2:开启日志轮转

网关一旦开始写文件,容器运行时就不再轮转任何内容。没有轮转,卷会一直增长直到 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 关闭。开启压缩后,插件会在轮转后不久归档并删除原文件,这可能与尚未读完该文件的采集器发生竞争。

步骤 3:选择采集器的部署位置

边车采集器节点级 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 采集日志文件

网关 Chart 没有提供 extraContainers 配置项,但 Kubernetes 1.29 及以后的版本支持以 restartPolicy: Always 的 init 容器形式声明边车。这类容器先于网关启动、后于网关停止,因此既能覆盖启动日志,也能覆盖停机时的排空。

dp-values.yaml
extraVolumes:
- name: api7-gateway-logs
emptyDir:
sizeLimit: 10Gi
- 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 读取同一批文件,保留步骤 1 中的网关配置但不部署边车,在 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 名称、命名空间、节点和标签。

请给 '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 indexer 和限定在 /var/lib/kubelet/pods/logs_path matcher 来解析 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 的 watcher 是延迟初始化的。在 watcher 完成同步之前 Filebeat 发出的事件不会带有 kubernetes.* 字段。这一点在首次启动且存在大量积压时最明显,因为 Filebeat 读取积压的速度快于 watcher 就绪的速度。稳定运行后的事件不受影响。

验证日志采集

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

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

采集器启动了但没有任何输出。 请确认它以 root 身份运行。/var/log/pods 下的运行时日志文件权限为 0640、属主为 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 使用的属性确实由 pipeline 设置。对于 Filebeat,请检查受影响的事件是否是在启动期间、元数据 watcher 完成同步之前发出的。

卷被写满。 请确认已开启 log-rotate 插件,且 max_keptmax_size 能把总量限制在卷的 sizeLimit 以内。同时注意四层访问日志不会被轮转。

后续步骤