在 Laravel 中构建可观测性链路:日志、指标与追踪实战

阿乐
阿乐 管理员 年卡会员
发布于 2026-09-04 09:48 ·3 浏览 ·0 回复

从一把梭到可视化:为什么 Laravel 项目需要可观测性

很多 Laravel 开发者对“可观测性”的第一反应是:打印日志不就行了吗?但当你面对几十个队列任务、第三方 API 调用、定时任务和用户请求时,单一的 `Log::info()` 就像在黑夜里用手电筒找掉落的针——光有,但不够亮。真正的可观测性,是把日志、指标和追踪三条链路打通,让系统的每一次“呼吸”都有迹可循。

Laravel 本身提供了极佳的基础设施(Monolog、Queue、Horizon、Telescope 等),但要让它们变成真正的“链路”,还需要我们主动设计。下面我会结合实战经验,聊聊在 Laravel 中如何一步步落地这三根支柱。

日志:结构化与上下文,告别“裸奔”字符串

Laravel 默认使用 Monolog,但很多项目只写了 `Log::info('user login')` 这种无结构字符串。要支撑链路,必须做到两点:JSON 结构化 + 上下文注入

首先,在 `config/logging.php` 中配置一个按天切分且使用 JSON 格式的 channel:

'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['daily', 'stderr'],
    ],
    'daily' => [
        'driver' => 'daily',
        'path' => storage_path('logs/laravel.log'),
        'level' => 'debug',
        'days' => 14,
        'formatter' => Monolog\Formatter\JsonFormatter::class,
    ],
],

然后,在写日志时把 trace_id、user_id、请求路径等塞进 context 中。这里建议用中间件统一注入请求 ID:

public function handle($request, Closure $next)
{
    $traceId = $request->header('X-Trace-Id') ?: Str::uuid()->toString();
    Log::withContext(['trace_id' => $traceId, 'url' => $request->fullUrl()]);
    // 将 trace_id 放入响应头,方便前端与后端协作排查
    $response = $next($request);
    $response->headers->set('X-Trace-Id', $traceId);
    return $response;
}

这样,每一行日志都自带“身份证”,在 Kibana 或 Loki 里一搜 `trace_id` 就能捞出一整条请求的所有日志,而不是靠猜。

指标:让系统“说话”,从被动看日志到主动看仪表盘

日志是离散的事件,指标是聚合的视图。Laravel 项目中,适合用 Prometheus + `php-prometheus-client` 来暴露指标端点。常见指标有三类:

- 业务指标:订单创建数、支付失败数、队列任务耗时。
- 组件指标:Redis 缓存命中率、MySQL 慢查询数、外部 API 响应时间。
- 运行时指标:内存占用、并发请求数。

以 Prometheus 为例,先安装 `promphp/prometheus_client_php`,然后在 `routes/web.php` 或独立路由中暴露 `/metrics`(建议加鉴权或内网访问):

Route::get('/metrics', function () {
    $registry = Prometheus\CollectorRegistry::getDefault();
    // 在业务逻辑里 $counter = $registry->getOrRegisterCounter(...)
    return response($registry->render())
        ->header('Content-Type', 'text/plain; version=0.0.4');
});

计数器要打在哪里?举一个实际例子——用户注册成功时:

$counter = app('prometheus.counter')->getOrRegisterCounter(
    'app', 'user_registered_total', 'User registration count', ['source']
);
$counter->inc(['email']);

配合 Laravel 的定时任务,你还可以定期记录队列长度、Horizon 的待处理任务数,让 Grafana 图表“甜起来”。指标的价值在于:你不需要盯着日志,告警规则会在 P95 延迟超过阈值时主动通知你。

追踪:用 OpenTelemetry 把一次请求的“旅程”画出来

日志和指标都是“截面”,但一次请求可能跨 Redis、MySQL、队列、HTTP 调用——如果每一步耗时都靠手工打时间戳,那叫考古。真正的追踪应该用 OpenTelemetry (OTel),在 Laravel 中集成 `open-telemetry/opentelemetry` 及对应的 SDK 扩展。

核心思路是:通过中间件为每个请求创建一个根 Span,然后利用 Laravel 的事件系统自动给数据库查询、HTTP client、Redis 操作创建子 Span。示例表意不明,只说代码结构:

public function handle($request, Closure $next)
{
    $tracer = OTel::getTracer('laravel-app');
    $span = $tracer->spanBuilder('HTTP ' . $request->method() . ' ' . $request->path())
        ->startSpan();
    // 将 trace_id 注入日志上下文
    $context = $span->getContext();
    Log::withContext(['trace_id' => $context->getTraceId(), 'span_id' => $context->getSpanId()]);

    try {
        return $next($request);
    } finally {
        $span->end();
    }
}

至于 MySQL 查询,一个取巧的方式是监听 `Illuminate\Database\Events\QueryExecuted` 事件,在其中创建子 Span 并设置属性:

Event::listen(QueryExecuted::class, function ($event) {
    $tracer = OTel::getTracer('laravel-mysql');
    $span = $tracer->spanBuilder('SQL query')->startSpan();
    $span->setAttribute('db.system', 'mysql');
    $span->setAttribute('db.statement', $event->sql);
    $span->end();
});

如果你觉得 OTel 集成成本偏高,可以先用 Laravel Telescope 代替——它能把请求到数据库到队列的关系图展示出来,但 Telescope 只适合开发环境,生产环境还是得靠 Zipkin 或 Jaeger。

三条链路融合:缺了一个角,都是“管中窥豹”

很多团队只做了日志,出了故障先查日志,再从日志里翻时间戳估耗时,效率极低。更合理的链路是:

1. 指标发现异常(如支付成功率下降)。
2. 顺着 trace 找到具体那次失败请求(前后端传递 trace_id)。
3. 拉取该 trace_id 关联的所有日志(中间件已经注入上下文)。
4. 定位根本原因(比如某个 SQL 慢查询或第三方接口超时)。

这套组合拳,我在一个每天亿级请求的 Laravel 项目中验证过:某次促销活动导致 Redis 连接池耗尽,团队不是靠猜,而是通过 Grafana 面板看到 `redis_connected_sockets` 曲线突然崩塌,再用 Trace 定位到是缓存预热逻辑里忘了释放连接,最后看日志确认错误码。整个过程不到十分钟。

最后,别让“可观测”变成“负担”

请注意,可观测性不是工具堆砌。如果项目还在原型阶段,先用 `Log::withContext` 把结构化日志做好,比什么都强。等流量涨了、服务多了,再逐步引入指标和追踪。每个团队都要问自己:我什么时候能从一个数字出发,花五分钟定位到一行代码? 如果答案是否定的,那就从今天开始,把这三根支柱一根一根立起来。

他们都看过 1 人浏览过
一只冷漠的狐狸

全部回复 0

还没有回复,来抢沙发~