本文目录导读:

在PHP API开发中,日志审计(Audit Logging)是安全审计和故障排查的核心手段,它需要记录谁、在什么时间、对什么资源、执行了什么操作、结果如何。
下面我为你梳理一套完整的PHP API日志审计记录方案,包含技术选型、数据结构、代码实现和最佳实践。
审计日志应该包含的核心字段
不要只记录错误,审计日志需要包含上下文。
{
"audit_id": "uuid-xxxx", // 唯一ID,方便关联查询
"timestamp": "2024-05-20T10:30:00Z", // 精确到毫秒
"user_id": 12345, // 操作人ID(匿名用户可为null或"guest")
"user_ip": "192.168.1.100", // 请求来源IP
"user_agent": "Mozilla/5.0 ...", // 客户端信息
"request_id": "req-xxxx", // 请求追踪ID(用于关联微服务链路)
"http_method": "POST",
"api_endpoint": "/api/v1/orders",
"http_status_code": 200,
"action": "CREATE_ORDER", // 业务操作类型,如 ORDER_CREATE, USER_LOGIN
"resource_type": "order", // 操作的对象类型
"resource_id": "ORD-20240520-001",// 操作的对象ID
"target_entity": { // 变更前的状态(关键字段)
"status": "draft",
"total_amount": 100.00
},
"new_entity": { // 变更后的状态(关键字段)
"status": "paid",
"total_amount": 100.00,
"payment_method": "credit_card"
},
"details": "用户支付订单成功", // 人类可读的描述
"severity": "info" // info / warning / critical
}
关键点:审计日志的核心是 "之前是什么,之后是什么"(Before/After Image),这能让你在出现问题时回溯数据变更轨迹。
技术选型
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 文件日志 (Monolog + 自定义Handler) | 简单、无外部依赖、标准化 | 检索困难、不支持高并发写入、日志轮换需自行处理 | 小型项目、开发环境 |
| 数据库表 (MySQL/PostgreSQL) | 结构化、方便查询、支持事务关联 | 写压力大(高频API会锁表)、存储膨胀快 | 中小型项目、对写入QPS要求不高(<500/s) |
| 消息队列 (RabbitMQ/Kafka) + Elasticsearch | 高吞吐、异步、实时搜索、数据持久化 | 架构复杂、需要运维ELK栈 | 中大型生产环境、需要实时检索和可视化 |
| 专用的审计日志服务 (如 Audit Trail 工具) | 开箱即用、有UI、有合规报告 | 额外成本、可能不够灵活 | 企业级、需要满足PCI-DSS/SOC2合规 |
推荐组合(生产环境):
- 高频CRUD API:用 Kafka + 消费者写入Elasticsearch(或S3冷存储)
- 低频管理操作(如管理员配置修改):直接同步写入数据库表(或同时写入数据库+队列)
- 简单项目:用 Monolog + 固定格式文件,配合
grep或tail排查
代码实现(以Laravel为例,但核心逻辑通用)
使用 Middleware 记录请求上下文
// app/Http/Middleware/AuditContextMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class AuditContextMiddleware
{
public function handle($request, Closure $next)
{
// 生成或获取请求ID(如果从网关传入)
$requestId = $request->header('X-Request-Id')
?: (string) Str::uuid();
// 绑定到日志上下文(后续所有日志自动带上这些字段)
Log::withContext([
'request_id' => $requestId,
'client_ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'user_id' => auth()->id() ?? 'guest',
'endpoint' => $request->method() . ' ' . $request->path(),
]);
$response = $next($request);
return $response;
}
}
核心:记录业务操作的 Before/After
方案A:在 Model 的 Observers/Events 里记录
// app/Observers/OrderObserver.php
namespace App\Observers;
use App\Models\Order;
use App\Services\AuditLogger;
class OrderObserver
{
public function updated(Order $order)
{
// 只记录关键字段变化
$changed = $order->getDirty();
$original = $order->getOriginal();
// 过滤掉无关字段(如 updated_at)
$auditableFields = ['status', 'total_amount', 'payment_method'];
$diff = array_intersect_key($changed, array_flip($auditableFields));
if (!empty($diff)) {
AuditLogger::log([
'action' => 'UPDATE_ORDER',
'resource_type' => 'order',
'resource_id' => $order->id,
'target_entity' => array_intersect_key($original, $diff), // 旧值
'new_entity' => $diff, // 新值
'description' => "订单 {$order->order_no} 的状态由 {$original['status']} 变更为 {$order->status}",
]);
}
}
public function created(Order $order)
{
AuditLogger::log([
'action' => 'CREATE_ORDER',
'resource_type' => 'order',
'resource_id' => $order->id,
'target_entity' => null, // 创建操作没有旧值
'new_entity' => $order->toArray(),
'description' => "创建订单 {$order->order_no}",
]);
}
}
方案B:在 Service 层手动记录(推荐)
// app/Services/OrderService.php
use App\Services\AuditLogger;
use Illuminate\Support\Facades\DB;
use App\Exceptions\BusinessException;
class OrderService
{
public function payOrder(int $orderId, array $paymentData)
{
DB::beginTransaction();
try {
$order = Order::findOrFail($orderId);
$beforeStatus = $order->status;
// 执行业务逻辑
$order->status = 'paid';
$order->payment_method = $paymentData['method'];
$order->save();
// 手动记录审计(更清晰)
AuditLogger::log([
'action' => 'ORDER_PAY_SUCCESS',
'resource_type' => 'order',
'resource_id' => $order->id,
'target_entity' => [
'status' => $beforeStatus,
'total' => $order->total_amount,
],
'new_entity' => [
'status' => 'paid',
'payment_method' => $paymentData['method'],
],
'description' => "用户 {$order->user_id} 支付订单 {$order->order_no} 成功",
'severity' => 'info',
]);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
AuditLogger::log([
'action' => 'ORDER_PAY_FAILED',
'resource_type' => 'order',
'resource_id' => $orderId,
'target_entity' => ['error' => $e->getMessage()],
'description' => "订单支付失败: " . $e->getMessage(),
'severity' => 'error',
]);
throw new BusinessException('支付失败');
}
}
}
统一的写入器(支持多后端)
// app/Services/AuditLogger.php
namespace App\Services;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class AuditLogger
{
public static function log(array $data)
{
$auditRecord = [
'audit_id' => (string) Str::uuid(),
'timestamp' => now()->toIso8601String(),
'user_id' => auth()->id() ?? 'system',
'client_ip' => request()->ip() ?? '127.0.0.1',
'request_id' => request()->header('X-Request-Id') ?? Str::uuid(),
'http_method' => request()->method(),
'api_endpoint' => request()->path(),
'http_status' => http_response_code(),
...$data,
];
// 策略1: 本地文件日志(快速开发/调试)
Log::channel('audit')->info(json_encode($auditRecord));
// 策略2: 写入数据库(适用于低频或管理后台)
// DB::table('audit_logs')->insert($auditRecord);
// 策略3: 发送到消息队列(适用于高并发)
// Kafka::publish('audit-topic', $auditRecord);
}
}
配置文件 config/logging.php 增加 audit 频道:
'channels' => [
'audit' => [
'driver' => 'daily', // 按天轮转,保留30天
'path' => storage_path('logs/audit.log'),
'level' => 'info',
'days' => 30,
],
],
常见注意事项与坑
-
不要在日志里记录敏感数据:
- ❌ 记录
password,credit_card_number,access_token - ✅ 记录
payment_method: "credit_card"(不记录卡号) - ✅ 记录
user_id但不记录full_name
- ❌ 记录
-
性能考虑:
- 审计日志写入是 写密集型 操作,高并发场景一定要异步(消息队列)。
- 如果同步写数据库,考虑使用单独的审计日志数据库,或使用批量插入(攒够100条或等1秒再刷入)。
-
存储与保留策略:
- 热数据(最近7天):Elasticsearch 或 MySQL(带索引)
- 温数据(7天~1年):压缩后存到 S3 / 对象存储
- 冷数据(>1年):定期清理或归档到 Glacier 类存储
-
时区与时间同步:
- 全部使用 UTC 时间存储,前端展示时再转本地时间。
- 确保服务器 NTP 同步,否则时间线会乱。
-
不可篡改性的高级需求(合规要求):
- 使用 签名链:每条日志包含前一条日志的哈希,形如区块链。
- 写后立刻转存到只写一次的存储(如 AWS S3 Object Lock)。
- 或使用专门的审计日志 Saas 服务(如 Google Cloud Audit Logs, AWS CloudTrail)。
一句话总结
- 对于小型API:用 Monolog 写入
storage/logs/audit.log,按天轮转,配合grep查找。 - 对于中型API:用 MySQL 表存储审计日志,每条记录包含
user_id, action, resource_id, old_value, new_value, created_at,异步插入。 - 对于大型分布式API:推送到 Kafka,消费后写入 Elasticsearch,配合 Kibana 做可视化和报警。
最后:审计日志的代码实现不难,难在想清楚记录哪些字段以及和业务代码解耦,建议使用 Observer 或 AOP 思想,不要手动在每行业务代码后都写 AuditLogger::log(),否则维护起来会非常痛苦。