访问日志

OxPHP 为每个 HTTP 请求输出结构化的 JSON 访问日志,写入 stdout。日志记录是异步的,因此绝不会阻塞请求处理。

工作原理

设置 ACCESS_LOG 后,OxPHP 会在每个请求完成后向 stdout 写入一行 JSON。这些写入操作会在一个后台写入线程中缓冲,因此日志记录绝不会阻塞请求流水线。

模式决定了记录哪些内容。ACCESS_LOG=all 记录每个请求;ACCESS_LOG=error 只记录状态码为 400 或更高的响应。

是否写入这些日志行只由 ACCESS_LOG 决定;LOG_LEVEL 对它们不起作用。即使服务器的诊断日志级别设为 warn,仍会写出 ACCESS_LOG 所启用的访问日志。唯一的例外是显式指定 access_log 目标的 RUST_LOG 指令;参见细粒度过滤。

每一行日志都带有一个 request_id 字段,用于将访问日志条目与你的应用日志关联起来。当启用 W3C Trace Context 传播时,日志条目还会包含 trace_id 和 span_id 字段。

配置

变量 默认值 说明
ACCESS_LOG (未设置) 访问日志模式。all 记录每个请求;error 只记录 4xx 和 5xx 响应。留空或不设置则禁用
Note

唯一接受的值是 all 和 error。设置无法识别的值会记录一条警告并禁用访问日志。

日志格式

每条访问日志条目都是写入 stdout 的一行 JSON:

json
{ "timestamp": "2026-02-11T12:34:56.789012Z", "level": "INFO", "fields": { "request_id": "67890abc12341a2b0042", "method": "GET", "path": "/api/users", "status": 200, "duration_us": 1234, "remote_ip": "10.0.0.1", "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0", "message": "request completed" } }

当 W3C Trace Context 处于活动状态时,trace_id 和 span_id 会与标准字段一同包含在内:

json
{ "timestamp": "2026-02-11T12:34:56.789012Z", "level": "INFO", "fields": { "request_id": "67890abc12341a2b0042", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "span_id": "00f067aa0ba902b7", "method": "POST", "path": "/api/orders", "status": 201, "duration_us": 8421, "remote_ip": "10.0.0.1", "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0", "message": "request completed" } }

字段

字段 类型 说明
request_id string 唯一请求标识符。参见 请求 ID
method string HTTP 方法(GET、POST 等)
path string 请求 URI 路径
status number HTTP 响应状态码
duration_us number 请求处理总时间,单位为微秒
remote_ip string 客户端 IP 地址(不含端口)。当配置了 TRUSTED_PROXIES 时,显示从转发头中提取的真实客户端 IP;只有当转发头中得不到任何地址时,才显示代理的 IP(参见可信代理)
user_agent string 请求的 User-Agent 头(如果发送了多个,取第一个),超过 512 字节时截断到 512 字节(包括一个 …(truncated) 标记)。不是有效 UTF-8 的字节会被替换为 U+FFFD。请求未发送该头时不存在
trace_id string W3C 追踪 ID(仅当 TRACE_CONTEXT=true 时存在)
span_id string W3C span ID(仅当 TRACE_CONTEXT=true 时存在)

细粒度过滤

OxPHP 以内部的 access_log 日志目标输出访问日志条目,并且只要 ACCESS_LOG 要求记录访问日志,就会在自己的日志过滤器中为该目标添加一条指令。这让两个设置互不干扰:LOG_LEVEL=warn 会让服务器安静下来,而访问日志照常运行,无需额外配置。

bash
# 诊断日志安静,访问日志完整保留 LOG_LEVEL=warn ACCESS_LOG=all

在 RUST_LOG 中写出完整目标名的指令对该目标具有更高优先级,包括高于 OxPHP 添加的那条指令。单独的级别不会覆盖它,就像 LOG_LEVEL 也不会一样(RUST_LOG=off 会让服务器自身的诊断日志安静下来,而访问日志照常运行)。缩写的名称(如 access=off)同样不行:它是两条指令中不那么具体的一条,因此会落败。要单独关闭访问日志,请使用写出完整名称的指令:

bash
# 关闭访问日志,警告和错误仍会上报 ACCESS_LOG=all RUST_LOG=warn,access_log=off # 这里的 `warn` 就是原本 LOG_LEVEL 的值
Warning

RUST_LOG 在已设置且可解析时会取代 LOG_LEVEL,而不是在其基础上叠加,因此请把你的级别也写进去。此外,上面的组合本身是自相矛盾的(先开启访问日志,再把它过滤掉),因此服务器会在启动时报告一次,并给出这两个值。更好的做法是取消设置 ACCESS_LOG。

Note

access_log 目标用于 RUST_LOG 过滤,但不会包含在 JSON 输出中。要在下游系统中识别访问日志条目,请使用 "message": "request completed" 字段以及其特有的字段集合(method、path、status、duration_us)。

故障排查

没有出现任何访问日志条目

当 ACCESS_LOG 未设置或为空时,访问日志处于禁用状态。

修复方法: 将变量设置为 all 或 error:

bash
ACCESS_LOG=all

如果它已经设置,剩下的可能就是 RUST_LOG 指定了 access_log 目标并将其关闭;运维人员自己写的指令优先于 OxPHP 添加的那条。服务器会在启动时指出这一点:

json
{"timestamp":"…","level":"WARN","fields":{"message":"ACCESS_LOG is set but the log filter drops the access_log target — no access log entries will be written","access_log":"all","filter":"warn,access_log=off"}}

filter 字段显示当前生效的指令,在这里正好就是你设置的那些:凡是被你自己的指令覆盖之处,OxPHP 为该目标添加的指令都会被丢弃。从 RUST_LOG 中删掉你写的 access_log=…,条目就会重新写出。

如果过滤器安静到连 WARN 也会丢弃,同样的报告会以纯文本行而非 JSON 的形式写入 stderr,因此这种组合总会被报告出来:

text
WARN ACCESS_LOG is set but the log filter drops the access_log target — no access log entries will be written (access_log=all, filter=error,access_log=off)

单纯的 LOG_LEVEL(trace、debug、info、warn、error)永远不会导致上述情况:单独的级别会同时作用于所有目标,而指定该目标的那条指令是两者中更具体的一条。

访问日志显示为 `error` 模式,但缺少成功的请求

ACCESS_LOG=error 只记录状态码为 400 或更高的响应。这是设计使然——请检查该值,如果你需要记录所有请求,请切换为 all。

检查方法: 确认当前生效的设置:

bash
curl -s http://localhost:9090/config | jq '.access_log'
日志条目出现,但没有 `trace_id` 和 `span_id`

只有在启用 W3C Trace Context 传播时才会存在追踪上下文字段。

修复方法: 通过以下方式启用它:

bash
TRACE_CONTEXT=true

并确保你的上游客户端或负载均衡器在请求上发送 traceparent 头。

Docker 示例

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.13.0 ports: - "80:80" - "9090:9090" volumes: - ./src:/var/www/html:ro environment: ACCESS_LOG: "all" ENTRY_FILE: "index.php" INTERNAL_ADDR: "0.0.0.0:9090"

最佳实践

  • 在生产环境中使用 ACCESS_LOG=error,以减少日志量,同时仍能捕获所有失败的请求。成功的请求不会被记录,但任何状态码为 400+ 的错误始终会被捕获。
  • 通过 oxphp_request_id() 在你的应用日志中包含请求 ID,这样你就可以将 PHP 层面的日志条目与访问日志条目关联起来。
  • 使用诸如 Elasticsearch、Loki 或 Datadog 之类的结构化日志聚合工具,以高效地查询和过滤 JSON 日志行。

集成

由于日志是 stdout 上的 JSON 行,它们可以直接与容器日志驱动和聚合工具集成:

  • Docker —— 通过容器日志驱动自动收集(json-file、fluentd 等)
  • Kubernetes —— 由节点的日志代理拾取(Fluentd、Fluent Bit、Filebeat 等)
  • systemd —— 当作为 systemd 服务运行并通过 journald 记录 stdout 时被捕获

无需 sidecar 或基于文件的日志转发。

给你的智能体的提示词

这些提示词假定你的智能体已连接 OxPHP 文档 MCP 服务器——需要哪些页面它会自己去读。

prompt
把 OxPHP 的访问日志接进我的日志聚合工具。通过 OxPHP 文档 MCP 服务器读取 `features/access-logging` 页面。 选好这个项目该用哪种模式,检查我的 `LOG_LEVEL` 或 `RUST_LOG` 设置没有把 `access_log` 目标过滤掉,并告诉我聚合工具该怎样解析这些 JSON 字段,让它们和这个仓库已经在写的应用日志并列。

另请参阅

  • 请求 ID —— 每条日志条目都包含一个用于追踪的 request_id
  • 配置参考 —— 完整的环境变量参考
发现错误?报告问题 →