refactor: replace database logging with file logging and admin audit log

This commit is contained in:
xboard
2026-03-11 06:50:07 +08:00
parent 6efedcebd4
commit ec20847f31
12 changed files with 121 additions and 368 deletions

View File

@@ -1,53 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class ExportV2Log extends Command
{
protected $signature = 'log:export {days=1 : The number of days to export logs for}';
protected $description = 'Export v2_log table records of the specified number of days to a file';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$days = $this->argument('days');
$date = Carbon::now()->subDays((float) $days)->startOfDay();
$logs = DB::table('v2_log')
->where('created_at', '>=', $date->timestamp)
->get();
$fileName = "v2_logs_" . Carbon::now()->format('Y_m_d_His') . ".csv";
$handle = fopen(storage_path("logs/$fileName"), 'w');
// 根据您的表结构
fputcsv($handle, ['Level', 'ID', 'Title', 'Host', 'URI', 'Method', 'Data', 'IP', 'Context', 'Created At', 'Updated At']);
foreach ($logs as $log) {
fputcsv($handle, [
$log->level,
$log->id,
$log->title,
$log->host,
$log->uri,
$log->method,
$log->data,
$log->ip,
$log->context,
Carbon::createFromTimestamp($log->created_at)->toDateTimeString(),
Carbon::createFromTimestamp($log->updated_at)->toDateTimeString()
]);
}
fclose($handle);
$this->info("日志成功导出到: " . storage_path("logs/$fileName"));
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Console\Commands;
use App\Models\Log;
use App\Models\AdminAuditLog;
use App\Models\StatServer;
use App\Models\StatUser;
use Illuminate\Console\Command;
@@ -43,6 +43,6 @@ class ResetLog extends Command
{
StatUser::where('record_at', '<', strtotime('-2 month', time()))->delete();
StatServer::where('record_at', '<', strtotime('-2 month', time()))->delete();
Log::where('created_at', '<', strtotime('-1 month', time()))->delete();
AdminAuditLog::where('created_at', '<', strtotime('-3 month', time()))->delete();
}
}

View File

@@ -3,7 +3,7 @@
namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Models\Log as LogModel;
use App\Models\AdminAuditLog;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
@@ -23,37 +23,10 @@ class SystemController extends Controller
'schedule' => $this->getScheduleStatus(),
'horizon' => $this->getHorizonStatus(),
'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null)),
'logs' => $this->getLogStatistics()
];
return $this->success($data);
}
/**
* 获取日志统计信息
*
* @return array 各级别日志的数量统计
*/
protected function getLogStatistics(): array
{
// 初始化日志统计数组
$statistics = [
'info' => 0,
'warning' => 0,
'error' => 0,
'total' => 0
];
if (class_exists(LogModel::class) && LogModel::count() > 0) {
$statistics['info'] = LogModel::where('level', 'INFO')->count();
$statistics['warning'] = LogModel::where('level', 'WARNING')->count();
$statistics['error'] = LogModel::where('level', 'ERROR')->count();
$statistics['total'] = LogModel::count();
return $statistics;
}
return $statistics;
}
public function getQueueWorkload(WorkloadRepository $workload)
{
return $this->success(collect($workload->get())->sortBy('name')->values()->toArray());
@@ -125,34 +98,26 @@ class SystemController extends Controller
})->count();
}
public function getSystemLog(Request $request)
public function getAuditLog(Request $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10;
$level = $request->input('level');
$keyword = $request->input('keyword');
$current = max(1, (int) $request->input('current', 1));
$pageSize = max(10, (int) $request->input('page_size', 10));
$builder = LogModel::orderBy('created_at', 'DESC')
->when($level, function ($query) use ($level) {
return $query->where('level', strtoupper($level));
})
->when($keyword, function ($query) use ($keyword) {
return $query->where(function ($q) use ($keyword) {
$q->where('data', 'like', '%' . $keyword . '%')
->orWhere('context', 'like', '%' . $keyword . '%')
->orWhere('title', 'like', '%' . $keyword . '%')
->orWhere('uri', 'like', '%' . $keyword . '%');
$builder = AdminAuditLog::with('admin:id,email')
->orderBy('id', 'DESC')
->when($request->input('action'), fn($q, $v) => $q->where('action', $v))
->when($request->input('admin_id'), fn($q, $v) => $q->where('admin_id', $v))
->when($request->input('keyword'), function ($q, $keyword) {
$q->where(function ($q) use ($keyword) {
$q->where('uri', 'like', '%' . $keyword . '%')
->orWhere('request_data', 'like', '%' . $keyword . '%');
});
});
$total = $builder->count();
$res = $builder->forPage($current, $pageSize)
->get();
$res = $builder->forPage($current, $pageSize)->get();
return response([
'data' => $res,
'total' => $total
]);
return response(['data' => $res, 'total' => $total]);
}
public function getHorizonFailedJobs(Request $request, JobRepository $jobRepository)
@@ -176,125 +141,4 @@ class SystemController extends Controller
]);
}
/**
* 清除系统日志
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function clearSystemLog(Request $request)
{
$request->validate([
'days' => 'integer|min:0|max:365',
'level' => 'string|in:info,warning,error,all',
'limit' => 'integer|min:100|max:10000'
], [
'days.required' => '请指定要清除多少天前的日志',
'days.integer' => '天数必须为整数',
'days.min' => '天数不能少于1天',
'days.max' => '天数不能超过365天',
'level.in' => '日志级别只能是info、warning、error、all',
'limit.min' => '单次清除数量不能少于100条',
'limit.max' => '单次清除数量不能超过10000条'
]);
$days = $request->input('days', 30); // 默认清除30天前的日志
$level = $request->input('level', 'all'); // 默认清除所有级别
$limit = $request->input('limit', 1000); // 默认单次清除1000条
try {
$cutoffDate = now()->subDays($days);
// 构建查询条件
$query = LogModel::where('created_at', '<', $cutoffDate->timestamp);
if ($level !== 'all') {
$query->where('level', strtoupper($level));
}
// 获取要删除的记录数量
$totalCount = $query->count();
if ($totalCount === 0) {
return $this->success([
'message' => '没有找到符合条件的日志记录',
'deleted_count' => 0,
'total_count' => $totalCount
]);
}
// 分批删除,避免单次删除过多数据
$deletedCount = 0;
$batchSize = min($limit, 1000); // 每批最多1000条
while ($deletedCount < $limit && $deletedCount < $totalCount) {
$remainingLimit = min($batchSize, $limit - $deletedCount);
$batchQuery = LogModel::where('created_at', '<', $cutoffDate->timestamp);
if ($level !== 'all') {
$batchQuery->where('level', strtoupper($level));
}
$idsToDelete = $batchQuery->limit($remainingLimit)->pluck('id');
if ($idsToDelete->isEmpty()) {
break;
}
$batchDeleted = LogModel::whereIn('id', $idsToDelete)->delete();
$deletedCount += $batchDeleted;
// 避免长时间占用数据库连接
if ($deletedCount < $limit && $deletedCount < $totalCount) {
usleep(100000); // 暂停0.1秒
}
}
return $this->success([
'message' => '日志清除完成',
'deleted_count' => $deletedCount,
'total_count' => $totalCount,
'remaining_count' => max(0, $totalCount - $deletedCount)
]);
} catch (\Exception $e) {
return $this->fail(ResponseEnum::HTTP_ERROR, null, '清除日志失败:' . $e->getMessage());
}
}
/**
* 获取日志清除统计信息
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getLogClearStats(Request $request)
{
$days = $request->input('days', 30);
$level = $request->input('level', 'all');
try {
$cutoffDate = now()->subDays($days);
$query = LogModel::where('created_at', '<', $cutoffDate->timestamp);
if ($level !== 'all') {
$query->where('level', strtoupper($level));
}
$stats = [
'days' => $days,
'level' => $level,
'cutoff_date' => $cutoffDate->format(format: 'Y-m-d H:i:s'),
'total_logs' => LogModel::count(),
'logs_to_clear' => $query->count(),
'oldest_log' => LogModel::orderBy('created_at', 'asc')->first(),
'newest_log' => LogModel::orderBy('created_at', 'desc')->first(),
];
return $this->success($stats);
} catch (\Exception $e) {
return $this->fail(ResponseEnum::HTTP_ERROR, null, '获取统计信息失败:' . $e->getMessage());
}
}
}

View File

@@ -2,23 +2,59 @@
namespace App\Http\Middleware;
use App\Models\AdminAuditLog;
use Closure;
class RequestLog
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
private const SENSITIVE_KEYS = ['password', 'token', 'secret', 'key', 'api_key'];
public function handle($request, Closure $next)
{
if ($request->method() === 'POST') {
$path = $request->path();
info("POST {$path}");
};
return $next($request);
if ($request->method() !== 'POST') {
return $next($request);
}
$response = $next($request);
try {
$admin = $request->user();
if (!$admin || !$admin->is_admin) {
return $response;
}
$action = $this->resolveAction($request->path());
$data = collect($request->all())->except(self::SENSITIVE_KEYS)->toArray();
AdminAuditLog::insert([
'admin_id' => $admin->id,
'action' => $action,
'method' => $request->method(),
'uri' => $request->getRequestUri(),
'request_data' => json_encode($data, JSON_UNESCAPED_UNICODE),
'ip' => $request->getClientIp(),
'created_at' => time(),
'updated_at' => time(),
]);
} catch (\Throwable $e) {
\Log::warning('Audit log write failed: ' . $e->getMessage());
}
return $response;
}
private function resolveAction(string $path): string
{
// api/v2/{secure_path}/user/update → user.update
$path = preg_replace('#^api/v[12]/[^/]+/#', '', $path);
// gift-card/create-template → gift_card.create_template
$path = str_replace('-', '_', $path);
// user/update → user.update, server/manage/sort → server_manage.sort
$segments = explode('/', $path);
$method = array_pop($segments);
$resource = implode('_', $segments);
return $resource . '.' . $method;
}
}

View File

@@ -218,10 +218,8 @@ class AdminRoute
$router->get('/getQueueStats', [SystemController::class, 'getQueueStats']);
$router->get('/getQueueWorkload', [SystemController::class, 'getQueueWorkload']);
$router->get('/getQueueMasters', '\\Laravel\\Horizon\\Http\\Controllers\\MasterSupervisorController@index');
$router->get('/getSystemLog', [SystemController::class, 'getSystemLog']);
$router->get('/getHorizonFailedJobs', [SystemController::class, 'getHorizonFailedJobs']);
$router->post('/clearSystemLog', [SystemController::class, 'clearSystemLog']);
$router->get('/getLogClearStats', [SystemController::class, 'getLogClearStats']);
$router->any('/getAuditLog', [SystemController::class, 'getAuditLog']);
});
// Update

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Logging;
class MysqlLogger
{
public function __invoke(array $config){
return tap(new \Monolog\Logger('mysql'), function ($logger) {
$logger->pushHandler(new MysqlLoggerHandler());
});
}
}

View File

@@ -1,45 +0,0 @@
<?php
namespace App\Logging;
use Illuminate\Support\Facades\Log;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;
use App\Models\Log as LogModel;
use Monolog\LogRecord;
class MysqlLoggerHandler extends AbstractProcessingHandler
{
public function __construct($level = Logger::DEBUG, bool $bubble = true)
{
parent::__construct($level, $bubble);
}
protected function write(LogRecord $record): void
{
$record = $record->toArray();
try {
if (isset($record['context']['exception']) && is_object($record['context']['exception'])) {
$record['context']['exception'] = (array)$record['context']['exception'];
}
$record['request_data'] = request()->all();
$log = [
'title' => $record['message'],
'level' => $record['level_name'],
'host' => $record['extra']['request_host'] ?? request()->getSchemeAndHttpHost(),
'uri' => $record['extra']['request_uri'] ?? request()->getRequestUri(),
'method' => $record['extra']['request_method'] ?? request()->getMethod(),
'ip' => request()->getClientIp(),
'data' => json_encode($record['request_data']),
'context' => json_encode($record['context']),
'created_at' => $record['datetime']->getTimestamp(),
'updated_at' => $record['datetime']->getTimestamp(),
];
LogModel::insert($log);
} catch (\Exception $e) {
// Log::channel('daily')->error($e->getMessage().$e->getFile().$e->getTraceAsString());
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AdminAuditLog extends Model
{
protected $table = 'v2_admin_audit_log';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
];
public function admin()
{
return $this->belongsTo(User::class, 'admin_id');
}
}

View File

@@ -1,17 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Log extends Model
{
use \App\Scope\FilterScope;
protected $table = 'v2_log';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}