本文目录导读:

我来介绍几种 PHP 操作日志的实现方案,从简单到复杂:
基础文件日志方案
简单文件日志
<?php
class SimpleLogger {
private $logPath;
public function __construct($logPath = 'logs/') {
$this->logPath = $logPath;
if (!is_dir($logPath)) {
mkdir($logPath, 0777, true);
}
}
public function write($message, $type = 'INFO') {
$date = date('Y-m-d H:i:s');
$logLine = sprintf(
"[%s] %s: %s %s\n",
$date,
$type,
$message,
$this->getClientInfo()
);
$fileName = $this->logPath . date('Y-m-d') . '.log';
file_put_contents($fileName, $logLine, FILE_APPEND | LOCK_EX);
}
private function getClientInfo() {
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$uri = $_SERVER['REQUEST_URI'] ?? 'unknown';
return "[IP: $ip] [URI: $uri]";
}
}
// 使用示例
$logger = new SimpleLogger();
$logger->write('用户登录成功', 'INFO');
$logger->write('登录失败,密码错误', 'WARNING');
?>
数据库日志方案
日志表结构
CREATE TABLE `operation_logs` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` int(11) DEFAULT NULL, `username` varchar(50) DEFAULT NULL, `module` varchar(50) DEFAULT NULL, `action` varchar(50) DEFAULT NULL, `description` text, `request_data` text, `response_data` text, `ip` varchar(50) DEFAULT NULL, `user_agent` varchar(255) DEFAULT NULL, `execution_time` int(11) DEFAULT NULL, `created_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_module` (`module`), KEY `idx_created_at` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
数据库日志类
<?php
class DbLogger {
private $db;
public function __construct($db) {
$this->db = $db;
}
public function log($module, $action, $description, $requestData = null, $responseData = null) {
$logData = [
'user_id' => $_SESSION['user_id'] ?? null,
'username' => $_SESSION['username'] ?? 'guest',
'module' => $module,
'action' => $action,
'description' => $description,
'request_data' => $requestData ? json_encode($requestData) : null,
'response_data' => $responseData ? json_encode($responseData) : null,
'ip' => $this->getClientIp(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'execution_time' => $this->getExecutionTime(),
'created_at' => date('Y-m-d H:i:s')
];
$sql = "INSERT INTO operation_logs
(user_id, username, module, action, description, request_data,
response_data, ip, user_agent, execution_time, created_at)
VALUES
(:user_id, :username, :module, :action, :description, :request_data,
:response_data, :ip, :user_agent, :execution_time, :created_at)";
$stmt = $this->db->prepare($sql);
return $stmt->execute($logData);
}
private function getClientIp() {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
return $_SERVER['REMOTE_ADDR'] ?? '';
}
}
private function getExecutionTime() {
if (defined('START_TIME')) {
return round((microtime(true) - START_TIME) * 1000);
}
return 0;
}
public function getLogs($filters = [], $page = 1, $perPage = 20) {
// 查询日志的实现
$offset = ($page - 1) * $perPage;
$where = [];
$params = [];
if (!empty($filters['user_id'])) {
$where[] = "user_id = :user_id";
$params[':user_id'] = $filters['user_id'];
}
if (!empty($filters['module'])) {
$where[] = "module = :module";
$params[':module'] = $filters['module'];
}
if (!empty($filters['start_date'])) {
$where[] = "created_at >= :start_date";
$params[':start_date'] = $filters['start_date'];
}
if (!empty($filters['end_date'])) {
$where[] = "created_at <= :end_date";
$params[':end_date'] = $filters['end_date'];
}
$whereSql = $where ? "WHERE " . implode(" AND ", $where) : '';
$countSql = "SELECT COUNT(*) as total FROM operation_logs $whereSql";
$countStmt = $this->db->prepare($countSql);
$countStmt->execute($params);
$total = $countStmt->fetchColumn();
$sql = "SELECT * FROM operation_logs
$whereSql
ORDER BY created_at DESC
LIMIT $perPage OFFSET $offset";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
return [
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'data' => $logs
];
}
}
?>
框架集成方案
Laravel 中间件方案
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
class OperationLogMiddleware
{
public function handle($request, Closure $next)
{
$startTime = microtime(true);
// 记录请求开始
$response = $next($request);
// 记录请求结束
$executionTime = microtime(true) - $startTime;
// 只记录操作型请求
if ($request->method() != 'GET') {
Log::channel('operation')->info('用户操作', [
'user_id' => Auth::id(),
'method' => $request->method(),
'path' => $request->path(),
'data' => $request->all(),
'response_status' => $response->getStatusCode(),
'execution_time' => $executionTime * 1000 . 'ms',
'ip' => $request->ip()
]);
}
return $response;
}
}
?>
ThinkPHP 的行为钩子方案
<?php
namespace app\behavior;
use think\facade\Log;
use think\facade\Request;
class OperationLog
{
public function run()
{
$data = [
'url' => Request::url(),
'method' => Request::method(),
'data' => Request::param(),
'ip' => Request::ip(),
'time' => date('Y-m-d H:i:s')
];
Log::write('操作日志: ' . json_encode($data), 'info');
}
}
?>
完整日志系统示例
<?php
class OperationLogSystem {
private $db;
private $cache;
private $config;
public function __construct($db) {
$this->db = $db;
$this->config = [
'log_methods' => ['POST', 'PUT', 'DELETE'],
'exclude_urls' => ['/login', '/logout'],
'sensitive_data' => ['password', 'token', 'card_number'],
// 日志保留天数
'retention_days' => 30
];
}
public function record($module, $action, $description = '') {
try {
// 过滤敏感字段
$requestData = $this->filterSensitiveData($_POST);
$responseData = $this->getResponseData();
$logData = [
'user_id' => $this->getCurrentUserId(),
'module' => $module,
'action' => $action,
'description' => $description,
'request_data' => json_encode($requestData),
'response_data' => $responseData,
'ip' => $this->getRealIp(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'request_url' => $_SERVER['REQUEST_URI'] ?? '',
'execution_time' => $this->getExecTime(),
'created_at' => date('Y-m-d H:i:s')
];
// 写入日志
$this->writeToDatabase($logData);
// 同时写入文件备份
$this->writeToFile($logData);
} catch (Exception $e) {
// 日志记录失败不影响主流程
Log::error('日志记录失败: ' . $e->getMessage());
}
}
private function filterSensitiveData($data) {
foreach ($this->config['sensitive_data'] as $field) {
if (isset($data[$field])) {
$data[$field] = '***';
}
}
return $data;
}
private function writeToDatabase($logData) {
$sql = "INSERT INTO operation_logs
(user_id, module, action, description, request_data, response_data,
ip, user_agent, request_url, execution_time, created_at)
VALUES (:user_id, :module, :action, :description, :request_data, :response_data,
:ip, :user_agent, :request_url, :execution_time, :created_at)";
$stmt = $this->db->prepare($sql);
$stmt->execute($logData);
}
private function writeToFile($logData) {
$logDir = 'logs/';
if (!is_dir($logDir)) {
mkdir($logDir, 0777, true);
}
$filename = $logDir . date('Y-m-d') . '.json';
$logLine = json_encode($logData, JSON_UNESCAPED_UNICODE) . PHP_EOL;
file_put_contents($filename, $logLine, FILE_APPEND | LOCK_EX);
}
private function getRealIp() {
// IP获取逻辑
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip = trim($ips[0]);
}
return $ip;
}
public function cleanupExpiredLogs() {
$expiredDate = date('Y-m-d', strtotime("-{$this->config['retention_days']} days"));
$sql = "DELETE FROM operation_logs WHERE created_at < ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$expiredDate]);
// 清理文件
$this->cleanupFiles($expiredDate);
}
private function cleanupFiles($expiredDate) {
$files = glob('logs/*.json');
foreach ($files as $file) {
$fileDate = basename($file, '.json');
if ($fileDate < $expiredDate) {
unlink($file);
}
}
}
}
// 使用示例
$logger = new OperationLogSystem($db);
$logger->record('user', 'add', '新增用户');
?>
最佳实践建议
日志管理后台视图
<!-- 日志查询页面 -->
<div class="container">
<h2>操作日志</h2>
<!-- 搜索过滤 -->
<form class="mb-3">
<div class="row">
<div class="col-md-3">
<input type="text" name="username" class="form-control" placeholder="用户名">
</div>
<div class="col-md-2">
<select name="module" class="form-control">
<option value="">全部模块</option>
<option value="user">用户管理</option>
<option value="role">角色管理</option>
<option value="system">系统设置</option>
</select>
</div>
<div class="col-md-3">
<div class="input-group">
<input type="date" name="start_date" class="form-control">
<span>-</span>
<input type="date" name="end_date" class="form-control">
</div>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary">搜索</button>
</div>
</div>
</form>
<!-- 日志列表 -->
<table class="table table-bordered">
<thead>
<tr>
<th>ID</th>
<th>用户</th>
<th>模块</th>
<th>操作</th>
<th>描述</th>
<th>IP</th>
<th>时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($logs as $log): ?>
<tr>
<td><?= $log['id'] ?></td>
<td><?= htmlspecialchars($log['username']) ?></td>
<td><?= htmlspecialchars($log['module']) ?></td>
<td><?= htmlspecialchars($log['action']) ?></td>
<td><?= htmlspecialchars($log['description']) ?></td>
<td><?= $log['ip'] ?></td>
<td><?= $log['created_at'] ?></td>
<td>
<button class="btn btn-sm btn-info"
onclick="viewDetail(<?= $log['id'] ?>)">详情</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- 分页 -->
<?= $pagination ?>
</div>
性能优化建议
// 1. 异步日志处理
public function asyncLog($data) {
$redis = new Redis();
$redis->lpush('log_queue', json_encode($data));
// 后台消费者处理
// 可以定期从队列取出日志写入数据库
}
// 2. 日志缓存
public function cachedLog($data) {
$cache = new Redis();
$cache->sAdd('operation_logs_' . date('Y-m-d'), json_encode($data));
// 定时批量写入
if ($cache->sCard('operation_logs_' . date('Y-m-d')) >= 100) {
$this->batchWriteToDB($cache->sMembers('operation_logs_' . date('Y-m-d')));
$cache->del('operation_logs_' . date('Y-m-d'));
}
}
// 3. 日志级别控制
class LogLevel {
const LOG_DEBUG = 'DEBUG';
const LOG_INFO = 'INFO';
const LOG_WARNING = 'WARNING';
const LOG_ERROR = 'ERROR';
private $minLevel;
public function shouldLog($level) {
$levels = [self::LOG_DEBUG, self::LOG_INFO, self::LOG_WARNING, self::LOG_ERROR];
$currentIndex = array_search($level, $levels);
$minIndex = array_search($this->minLevel, $levels);
return $currentIndex >= $minIndex;
}
}
?>
推荐的日志方案选择:
- 简单项目:使用文件日志,简单易用
- 中大型项目:使用数据库日志,便于查询分析
- 框架项目:使用框架自带的日志功能或中间件
- 高并发项目:使用异步日志(Redis队列 + 批量写入)
实现要点:
- 记录关键信息:用户、模块、操作、时间、IP等
- 敏感信息过滤
- 日志分级和分类
- 定期清理和历史数据处理
- 日志查询和统计功能