访问日志
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 响应。留空或不设置则禁用 |
唯一接受的值是 all 和 error。设置无法识别的值会记录一条警告并禁用访问日志。
日志格式
每条访问日志条目都是写入 stdout 的一行 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 会与标准字段一同包含在内:
{
"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 会让服务器安静下来,而访问日志照常运行,无需额外配置。
# 诊断日志安静,访问日志完整保留
LOG_LEVEL=warn
ACCESS_LOG=all在 RUST_LOG 中写出完整目标名的指令对该目标具有更高优先级,包括高于 OxPHP 添加的那条指令。单独的级别不会覆盖它,就像 LOG_LEVEL 也不会一样(RUST_LOG=off 会让服务器自身的诊断日志安静下来,而访问日志照常运行)。缩写的名称(如 access=off)同样不行:它是两条指令中不那么具体的一条,因此会落败。要单独关闭访问日志,请使用写出完整名称的指令:
# 关闭访问日志,警告和错误仍会上报
ACCESS_LOG=all
RUST_LOG=warn,access_log=off # 这里的 `warn` 就是原本 LOG_LEVEL 的值RUST_LOG 在已设置且可解析时会取代 LOG_LEVEL,而不是在其基础上叠加,因此请把你的级别也写进去。此外,上面的组合本身是自相矛盾的(先开启访问日志,再把它过滤掉),因此服务器会在启动时报告一次,并给出这两个值。更好的做法是取消设置 ACCESS_LOG。
access_log 目标用于 RUST_LOG 过滤,但不会包含在 JSON 输出中。要在下游系统中识别访问日志条目,请使用 "message": "request completed" 字段以及其特有的字段集合(method、path、status、duration_us)。
故障排查
没有出现任何访问日志条目
当 ACCESS_LOG 未设置或为空时,访问日志处于禁用状态。
修复方法: 将变量设置为 all 或 error:
ACCESS_LOG=all如果它已经设置,剩下的可能就是 RUST_LOG 指定了 access_log 目标并将其关闭;运维人员自己写的指令优先于 OxPHP 添加的那条。服务器会在启动时指出这一点:
{"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,因此这种组合总会被报告出来:
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。
检查方法: 确认当前生效的设置:
curl -s http://localhost:9090/config | jq '.access_log'日志条目出现,但没有 `trace_id` 和 `span_id`
只有在启用 W3C Trace Context 传播时才会存在追踪上下文字段。
修复方法: 通过以下方式启用它:
TRACE_CONTEXT=true并确保你的上游客户端或负载均衡器在请求上发送 traceparent 头。
Docker 示例
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 服务器——需要哪些页面它会自己去读。
把 OxPHP 的访问日志接进我的日志聚合工具。通过 OxPHP 文档 MCP 服务器读取 `features/access-logging` 页面。
选好这个项目该用哪种模式,检查我的 `LOG_LEVEL` 或 `RUST_LOG` 设置没有把 `access_log` 目标过滤掉,并告诉我聚合工具该怎样解析这些 JSON 字段,让它们和这个仓库已经在写的应用日志并列。