不能只靠 app_end 钩子捕获全部异常,必须在 app_init→action_begin→action_end 三阶段埋点,并通过中间件+事件监听覆盖路由失败、中间件异常、400+状态码等全场景。

直接上结论:不能只靠 app_end 钩子或全局异常处理器捕获全部异常请求,必须在 app_init → action_begin → action_end 三阶段中埋点,且对 400+ 状态码、未进控制器的路由失败、中间件抛出异常等场景分别处理。
为什么 app_end 拦不住真正的异常请求
很多项目只在 app_end 里写日志,结果线上突然出现大量 404/401/500 却查不到记录——因为这些请求根本没走到控制器,app_end 不会执行。比如:
- 路由未匹配(
thinkexceptionRouteNotFoundException) - 中间件
handle()抛出HttpException或直接return response()->code(401) - 前置验证(如 Token 解析)失败并提前终止
这些都在 action_begin 之前就结束了生命周期,app_end 完全不触发。
用中间件 + 三阶段钩子完整捕获异常请求
在 app/middleware/ApiExceptionLogger.php 中实现统一拦截:
namespace appmiddleware;
<p>use thinkacadeLog;
use thinkacadeRequest;
use thinkResponse;</p><p>class ApiExceptionLogger
{
protected $startTime;</p><pre class="brush:php;toolbar:false;">public function handle($request, Closure $next)
{
$this->startTime = microtime(true);
// app_init 阶段:记录原始请求头、IP、UA、trace_id
$traceId = 't_' . uniqid('', true);
$request->setAttr('trace_id', $traceId);
Log::channel('api_exception')->info('request_start', [
'trace_id' => $traceId,
'ip' => $request->ip(),
'url' => $request->url(true),
'method' => $request->method(),
'ua' => $request->header('user-agent', ''),
'referer' => $request->header('referer', ''),
]);
try {
$response = $next($request);
// action_end 阶段:检查响应码
$code = $response->getCode();
if ($code >= 400) {
Log::channel('api_exception')->warning('http_status_abnormal', [
'trace_id' => $traceId,
'code' => $code,
'url' => $request->url(true),
'method' => $request->method(),
'cost_ms' => round((microtime(true) - $this->startTime) * 1000),
'response_type' => $response->getHeader('content-type')[0] ?? '',
]);
}
return $response;
} catch (Throwable $e) {
// action_begin 之后、action_end 之前崩溃:控制器内异常
$code = $e instanceof hinkException ? 500 : 500;
Log::channel('api_exception')->error('controller_exception', [
'trace_id' => $traceId,
'code' => $code,
'url' => $request->url(true),
'method' => $request->method(),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
}
关键点:
- 日志通道
api_exception需在config/log.php中单独配置为文件驱动,避免和业务日志混在一起 - 所有 400+ 响应都走
warning级别,便于 ELK 或日志平台按 level 聚合告警 - 不依赖
$response->getData(),防止 JSON 渲染失败导致二次异常 - 耗时计算从
handle()开始,覆盖中间件执行时间,真实反映端到端异常上下文
补漏:如何捕获 app_init 阶段就失败的请求(如路由错、跨域拦截)
这类请求连中间件都不进,只能靠框架底层事件监听。在 app/event.php 中注册:
return [
'bind' => [
// 绑定自定义事件
],
'listen' => [
'AppInit' => [
'app\listener\LogEarlyFailure',
],
'HttpRun' => [
'app\listener\LogRouteFailure',
],
],
];
然后创建 app/listener/LogRouteFailure.php:
namespace applistener;
<p>use thinkacadeLog;
use thinkacadeRequest;</p><p>class LogRouteFailure
{
public function handle()
{
// 此处无法获取 Response,但可确定是路由层失败
Log::channel('api_exception')->alert('route_failure', [
'url' => Request::instance()->url(true),
'method'=> Request::instance()->method(),
'ip' => Request::instance()->ip(),
]);
}
}
</p>
注意:AppInit 和 HttpRun 是 ThinkPHP 内置事件,比任何中间件都早,能捕获 404 Not Found、CORS preflight 失败 等最前端异常。
线上环境必须关掉调试信息,但日志要保留足够上下文
很多人开了 app_debug => false 就以为安全了,结果日志里只有 "SQLSTATE[HY000]",找不到具体哪条 SQL、哪个参数触发的。实操要点:
- 在
app/exception/Handler.php的report()方法中,对$exception主动提取getTrace()并过滤敏感字段(如 password、token),再写入api_exception日志 - 数据库异常必加
Db::getLastSql(),哪怕事务已回滚——它仍保留在 PDOStatement 缓存中 - 不要在日志里 dump 整个
$_POST,而应记录array_keys(input())+ 各字段长度,既可识别字段注入又不泄露内容
最易被忽略的是:日志文件权限、磁盘满导致写入静默失败、以及多进程下文件锁冲突造成日志丢失——上线前务必用 ab -n 1000 -c 100 压测日志链路是否完整。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











