本文目录导读:

- 核心:服务标识(Service Identity)
- 异常捕获与标记(在入口或中间件)
- 告警数据路由与存储(区分的关键)
- 告警发送与通知(按服务配置不同通道)
- 聚合与去重:按服务+异常类型分组
- 高级:分布式追踪(区分调用链中的服务)
- 最小可行方案清单
在 PHP 项目中,要区分 不同服务 的异常告警,核心思路是:为每个服务/模块打上唯一的标识(标签/元数据),并在告警系统中基于这些标签进行路由、分组和展示。
以下是具体的实现方案,分为几个关键层面:
核心:服务标识(Service Identity)
你的PHP应用必须知道“我是谁”,这需要在应用启动时初始化一个全局上下文。
方案 A:环境变量(推荐用于K8s/Docker)
// config/app.php 或 bootstrap.php
define('SERVICE_NAME', getenv('SERVICE_NAME') ?: 'unknown-service');
define('SERVICE_ENV', getenv('APP_ENV') ?: 'production'); // dev, staging, prod
define('SERVICE_INSTANCE', gethostname()); // 或容器ID
方案 B:配置文件(适用于传统服务器集群)
// config/services.php
return [
'user_service' => [
'name' => 'user-service',
'hosts' => ['192.168.1.10', '192.168.1.11'],
],
'order_service' => [
'name' => 'order-service',
'hosts' => ['192.168.2.20'],
],
];
// 启动时根据IP或主机名匹配
$host = gethostname();
// 遍历配置匹配
异常捕获与标记(在入口或中间件)
在 PHP 的异常处理中心(如 Laravel 的 App\Exceptions\Handler、框架中间件或全局 set_exception_handler 中),将 服务标识 附加到告警数据包中。
// Laravel 异常处理 Handler
public function report(Throwable $e)
{
// 构造告警上下文
$alertData = [
'title' => get_class($e) . ': ' . $e->getMessage(),
'service' => SERVICE_NAME, // 关键字段:服务名
'environment' => SERVICE_ENV,
'instance' => SERVICE_INSTANCE,
'exception' => [
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
],
'request' => [
'url' => request()->fullUrl(),
'method' => request()->method(),
'ip' => request()->ip(),
],
'timestamp' => now()->toIso8601String(),
];
// 发送到告警系统(下文详述)
AlertService::send($alertData);
}
告警数据路由与存储(区分的关键)
告警不能只存成一条字符串,必须结构化存储,你需要一个能够 按字段查询 的存储后端,而不是简单写入日志文件。
方案 A:使用专业的 APM/错误跟踪平台(推荐)
这些平台天然支持按服务名、环境、主机等标签过滤。
-
Sentry:设置
release或tags\Sentry\configureScope(function (\Sentry\State\Scope $scope): void { $scope->setTag('service', SERVICE_NAME); $scope->setTag('env', SERVICE_ENV); $scope->setUser(['id' => auth()->id() ?? 'anonymous']); });结果:在 Sentry Dashboard 中可以直接选择 “service: user-service” 查看其所有异常。
-
Laravel Telescope / Flare:通过上下文数据区分。
方案 B:自建告警系统(使用 Elasticsearch 或类似)。
将上述 $alertData 存储到 Elasticsearch(ES),索引按天分(如 alerts-2023-10-01)。
查询示例:
// 查询 user-service 的错误
GET /alerts-*/_search
{
"query": {
"bool": {
"filter": [
{ "term": { "service": "user-service" } },
{ "term": { "environment": "production" } }
]
}
}
}
方案 C:使用消息队列 + 日志中心
发送到 Kafka/RabbitMQ,然后由 Logstash/Fluentd 消费写入 ES 或 Grafana Loki。
告警发送与通知(按服务配置不同通道)
在告警规则层面,你可以根据 service 字段做路由:
- 规则 1:
service = "payment-service"ANDlevel = "critical"→ 发送到 PagerDuty + 电话通知 - 规则 2:
service = "user-service"ANDlevel = "warning"→ 发送到 企业微信/钉钉群 2 - 规则 3:
service = "cron-job-service"ANDstatusCode > 500→ 发送到 飞书机器人
实现工具:
- Prometheus + Alertmanager:虽然 Prometheus 通常拉取指标,但你也可以通过
pushgateway发送告警事件,并利用 Alertmanager 的group_by: ['service']和路由规则。 - Grafana OnCall:支持丰富的标签路由。
- 阿里云/腾讯云监控:自定义监控项,上报时带上维度(service, host)。
聚合与去重:按服务+异常类型分组
在展示汇总时(如每日邮件或群日报),可以按 service + exception_class 聚合。
SQL/ES 聚合伪代码:
SELECT
service,
exception_class,
COUNT(*) as count,
MAX(created_at) as last_occurrence
FROM alerts
WHERE created_at > NOW() - INTERVAL 24 HOUR
GROUP BY service, exception_class;
结果输出到群消息:
【用户服务 - UserService】
- 500 Error (10次) | 最后触发: 10:23
- PDOException (3次) | 最后触发: 09:15
【订单服务 - OrderService】
- LogicException (7次) | 最后触发: 11:00
- RuntimeException (2次) | 最后触发: 08:30
高级:分布式追踪(区分调用链中的服务)
如果你的服务是微服务架构(A调用B,B调用C),异常可能发生在B,但影响的是A,此时建议集成 OpenTelemetry:
$span = OpenTelemetry::getCurrentSpan();
$alertData['trace_id'] = $span->getContext()->getTraceId();
$alertData['span_id'] = $span->getContext()->getSpanId();
$alertData['parent_service'] = $span->getAttributes()->get('service.name');
这样你不仅知道异常发生在哪个服务,还能知道是哪个上游调用导致的。
最小可行方案清单
如果你不想引入复杂系统,可以按照以下步骤快速实现:
- 定义常量:每个服务器/容器设置
SERVICE_NAME环境变量。 - 改写异常处理:在捕捉异常时,构造一个包含
service、env、trace的 JSON 对象。 - 写入统一日志文件:例如每天一个
alert-{date}.log,每行一个 JSON。 - 使用
tail+jq+grep查看:// 只看 user-service 的5xx错误 tail -f /var/log/alert-$(date +%F).log | jq -c 'select(.service == "user-service" and .http_code >= 500)'
- 发送到群聊:写一个简单的脚本,读取日志文件,按
service分组,每10分钟发一次汇总消息到企业微信。
核心思想就是:给每一行告警打上 SERVICE 标签,所有的过滤、分组、通知都基于这个标签来做。