日志管理
本文介绍 CarefreeCMS 的日志系统配置和使用方法。
日志概述
日志用于记录系统运行信息、错误、用户行为等,是排查问题和监控系统的重要工具。
日志类型
- 应用日志:业务逻辑日志
- 错误日志:系统错误和异常
- 访问日志:HTTP 请求日志
- SQL 日志:数据库查询日志
- 操作日志:用户操作记录
- 安全日志:登录、权限等安全相关
日志配置
基础配置
编辑 config/log.php:
return [
// 默认日志驱动
'default' => 'file',
// 日志通道
'channels' => [
'file' => [
'type' => 'File',
'path' => runtime_path() . 'log/',
'level' => ['error', 'warning', 'info'],
'file_size' => 10 * 1024 * 1024, // 10MB
'max_files' => 30, // 保留30天
],
'daily' => [
'type' => 'File',
'path' => runtime_path() . 'log/',
'level' => ['error'],
'max_files' => 30,
],
'database' => [
'type' => 'Database',
'table' => 'system_logs',
'level' => ['error', 'warning'],
],
],
];
日志级别
'level' => [
'emergency', // 紧急:系统不可用
'alert', // 警报:必须立即采取行动
'critical', // 严重:严重错误
'error', // 错误:运行时错误
'warning', // 警告:警告信息
'notice', // 注意:正常但重要的事件
'info', // 信息:一般信息
'debug', // 调试:详细调试信息
],
多通道配置
'channels' => [
// 错误日志写入文件
'error' => [
'type' => 'File',
'path' => runtime_path() . 'log/error/',
'level' => ['error', 'critical', 'emergency'],
],
// SQL 日志单独记录
'sql' => [
'type' => 'File',
'path' => runtime_path() . 'log/sql/',
'level' => ['info'],
],
// 操作日志写入数据库
'operation' => [
'type' => 'Database',
'table' => 'operation_logs',
'level' => ['info'],
],
],
日志使用
基本用法
use think\facade\Log;
// 不同级别的日志
Log::emergency('系统不可用');
Log::alert('需要立即处理');
Log::critical('严重错误');
Log::error('运行错误');
Log::warning('警告信息');
Log::notice('注意事项');
Log::info('一般信息');
Log::debug('调试信息');
记录上下文
// 记录详细信息
Log::error('文章保存失败', [
'article_id' => $id,
'user_id' => $userId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// 记录请求信息
Log::info('用户登录', [
'user_id' => $user->id,
'username' => $user->username,
'ip' => request()->ip(),
'user_agent' => request()->header('user-agent'),
'timestamp' => time(),
]);
指定通道
// 使用特定通道
Log::channel('error')->error('数据库连接失败');
Log::channel('sql')->info('慢查询', ['sql' => $sql, 'time' => $time]);
Log::channel('operation')->info('删除文章', ['id' => $id]);
应用场景
错误日志
try {
// 业务逻辑
$result = $service->process();
} catch (\Exception $e) {
Log::error('业务处理失败', [
'class' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
SQL 日志
use think\facade\Db;
// 监听 SQL 执行
Db::listen(function($sql, $time, $master) {
// 记录慢查询
if ($time > 1000) {
Log::channel('sql')->warning('慢查询', [
'sql' => $sql,
'time' => $time . 'ms',
'master' => $master,
]);
}
// 调试模式记录所有 SQL
if (app()->isDebug()) {
Log::channel('sql')->info('SQL 执行', [
'sql' => $sql,
'time' => $time . 'ms',
]);
}
});
操作日志
class ArticleController
{
public function save()
{
$article = Article::create(request()->post());
// 记录操作日志
OperationLog::create([
'user_id' => request()->user->id,
'action' => 'create',
'module' => 'article',
'content' => "创建文章:{$article->title}",
'ip' => request()->ip(),
]);
return json(['code' => 200, 'msg' => '保存成功']);
}
public function delete($id)
{
$article = Article::find($id);
// 记录删除操作
OperationLog::create([
'user_id' => request()->user->id,
'action' => 'delete',
'module' => 'article',
'content' => "删除文章:{$article->title}",
'data' => json_encode($article->toArray()),
'ip' => request()->ip(),
]);
$article->delete();
return json(['code' => 200, 'msg' => '删除成功']);
}
}
登录日志
public function login()
{
$username = request()->post('username');
$user = User::where('username', $username)->find();
if ($user && password_verify($password, $user->password)) {
// 登录成功
LoginLog::create([
'user_id' => $user->id,
'username' => $user->username,
'ip' => request()->ip(),
'user_agent' => request()->header('user-agent'),
'status' => 'success',
]);
Log::info('用户登录成功', [
'user_id' => $user->id,
'ip' => request()->ip(),
]);
} else {
// 登录失败
LoginLog::create([
'username' => $username,
'ip' => request()->ip(),
'status' => 'failed',
'reason' => '用户名或密码错误',
]);
Log::warning('登录失败', [
'username' => $username,
'ip' => request()->ip(),
]);
}
}
性能监控
class PerformanceMiddleware
{
public function handle($request, \Closure $next)
{
$startTime = microtime(true);
$startMemory = memory_get_usage();
$response = $next($request);
$endTime = microtime(true);
$endMemory = memory_get_usage();
$duration = ($endTime - $startTime) * 1000; // 毫秒
$memory = ($endMemory - $startMemory) / 1024 / 1024; // MB
// 记录慢请求
if ($duration > 1000) {
Log::warning('慢请求', [
'url' => $request->url(),
'method' => $request->method(),
'duration' => round($duration, 2) . 'ms',
'memory' => round($memory, 2) . 'MB',
]);
}
return $response;
}
}
日志查看
后台查看
class LogController
{
// 日志列表
public function index()
{
$type = request()->param('type', '');
$level = request()->param('level', '');
$date = request()->param('date', date('Y-m-d'));
$query = SystemLog::query();
if ($type) {
$query->where('type', $type);
}
if ($level) {
$query->where('level', $level);
}
$query->whereTime('create_time', $date);
$logs = $query->order('id', 'desc')->paginate(50);
return view('log/index', ['logs' => $logs]);
}
// 日志详情
public function detail($id)
{
$log = SystemLog::find($id);
return view('log/detail', ['log' => $log]);
}
// 清理日志
public function clear()
{
$days = request()->param('days', 30);
SystemLog::where('create_time', '<', time() - $days * 86400)
->delete();
return json(['code' => 200, 'msg' => '清理成功']);
}
}
命令行查看
# 查看实时日志
tail -f runtime/log/202401/15.log
# 查看错误日志
tail -f runtime/log/error.log
# 搜索关键词
grep "ERROR" runtime/log/*.log
# 统计错误数量
grep -c "ERROR" runtime/log/*.log
日志分析
# 分析访问日志
php think log:analyze --type=access --date=2024-01-15
# 分析错误日志
php think log:analyze --type=error --date=2024-01-15
# 生成报表
php think log:report --date=2024-01-15
日志存储
文件存储
// 按日期分割
'daily' => [
'type' => 'File',
'path' => runtime_path() . 'log/',
'apart_level' => ['error', 'warning'],
'max_files' => 30,
],
// 按大小分割
'size' => [
'type' => 'File',
'file_size' => 10 * 1024 * 1024, // 10MB
'max_files' => 10,
],
数据库存储
创建日志表
CREATE TABLE `system_logs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`level` varchar(20) DEFAULT NULL,
`message` text,
`context` text,
`create_time` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `level` (`level`),
KEY `create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
配置
'database' => [
'type' => 'Database',
'table' => 'system_logs',
'level' => ['error', 'warning', 'info'],
],
Redis 存储
'redis' => [
'type' => 'Redis',
'host' => '127.0.0.1',
'port' => 6379,
'key' => 'cms_log',
'expire' => 86400, // 1天
],
日志清理
自动清理
// 定时任务清理
class CronJob
{
public function cleanLogs()
{
$days = 30;
// 清理文件日志
$logPath = runtime_path() . 'log';
$files = glob($logPath . '/*.log');
foreach ($files as $file) {
if (time() - filemtime($file) > $days * 86400) {
unlink($file);
Log::info('清理日志文件', ['file' => basename($file)]);
}
}
// 清理数据库日志
SystemLog::where('create_time', '<', time() - $days * 86400)
->delete();
}
}
手动清理
# 清理30天前的日志
php think log:clear --days=30
# 清理所有日志
php think log:clear --all
# 清理指定类型
php think log:clear --type=error --days=7
日志分析工具
ELK Stack
Logstash 配置
input {
file {
path => "/var/www/cms/runtime/log/*.log"
start_position => "beginning"
}
}
filter {
json {
source => "message"
}
}
output {
elasticsearch {
hosts => ["localhost:9200"]
index => "cms-logs-%{+YYYY.MM.dd}"
}
}
日志统计
class LogAnalyzer
{
public function analyze($date)
{
$logs = SystemLog::whereTime('create_time', $date)->select();
$stats = [
'total' => $logs->count(),
'by_level' => [],
'by_hour' => [],
'top_errors' => [],
];
// 按级别统计
foreach ($logs as $log) {
$level = $log->level;
$stats['by_level'][$level] = ($stats['by_level'][$level] ?? 0) + 1;
}
// 按小时统计
foreach ($logs as $log) {
$hour = date('H', $log->create_time);
$stats['by_hour'][$hour] = ($stats['by_hour'][$hour] ?? 0) + 1;
}
// 高频错误
$errors = $logs->where('level', 'error');
$groupedErrors = [];
foreach ($errors as $error) {
$key = md5($error->message);
if (!isset($groupedErrors[$key])) {
$groupedErrors[$key] = [
'message' => $error->message,
'count' => 0,
];
}
$groupedErrors[$key]['count']++;
}
// 按次数排序
usort($groupedErrors, function($a, $b) {
return $b['count'] - $a['count'];
});
$stats['top_errors'] = array_slice($groupedErrors, 0, 10);
return $stats;
}
}
告警通知
错误告警
class LogHandler
{
public function handle($level, $message, $context)
{
// 严重错误发送通知
if (in_array($level, ['error', 'critical', 'emergency'])) {
$this->sendAlert($level, $message, $context);
}
}
protected function sendAlert($level, $message, $context)
{
// 发送邮件
Queue::push('app\job\SendEmail', [
'email' => config('app.admin_email'),
'subject' => "系统告警:$level",
'content' => $message . "\n\n" . json_encode($context, JSON_PRETTY_PRINT),
]);
// 发送钉钉通知
$this->sendDingTalk($level, $message);
}
protected function sendDingTalk($level, $message)
{
$webhook = config('app.dingtalk_webhook');
$data = [
'msgtype' => 'text',
'text' => [
'content' => "[$level] $message",
],
];
$ch = curl_init($webhook);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);
}
}
阈值告警
class LogMonitor
{
public function checkErrorRate()
{
// 统计最近1小时的错误率
$total = SystemLog::whereTime('create_time', '-1 hour')->count();
$errors = SystemLog::whereTime('create_time', '-1 hour')
->where('level', 'error')
->count();
$errorRate = $total > 0 ? ($errors / $total) * 100 : 0;
// 错误率超过阈值告警
if ($errorRate > 10) {
$this->sendAlert("错误率过高:{$errorRate}%");
}
}
}
最佳实践
日志规范
// ✓ 好的日志
Log::error('文章保存失败', [
'article_id' => $id,
'user_id' => $userId,
'error' => $e->getMessage(),
]);
// ✗ 不好的日志
Log::error('保存失败');
避免敏感信息
// 过滤敏感字段
$data = $user->toArray();
unset($data['password']);
Log::info('用户注册', $data);
性能优化
// 异步写入日志
Queue::push('app\job\WriteLog', [
'level' => 'info',
'message' => '...',
'context' => [...],
]);
// 批量写入
$logs = [];
foreach ($items as $item) {
$logs[] = ['message' => '...'];
}
SystemLog::insertAll($logs);
