You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

139 lines
4.0 KiB

1 week ago
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SystemLogController extends Controller
{
private const MAX_LINES = 5000;
private const DEFAULT_LINES = 400;
/** 仅允许 laravel.log 与 laravel-YYYY-MM-DD.log */
private function isAllowedBasename(string $name): bool
{
return (bool) preg_match('/^laravel(-\d{4}-\d{2}-\d{2})?\.log$/', $name);
}
public function index(Request $request): JsonResponse
{
abort_unless($request->user()?->isSuperAdmin(), 403, '仅超级管理员可查看系统日志');
$logsDir = storage_path('logs');
$files = $this->listLogFiles($logsDir);
$requested = basename((string) $request->query('file', ''));
$selected = '';
if ($requested !== '' && $requested !== '.' && $this->isAllowedBasename($requested)) {
$candidate = $logsDir.DIRECTORY_SEPARATOR.$requested;
if (is_file($candidate)) {
$selected = $requested;
}
}
if ($selected === '' && $files !== []) {
$selected = $files[0]['name'];
}
$linesParam = (int) $request->query('lines', self::DEFAULT_LINES);
$lines = max(50, min(self::MAX_LINES, $linesParam));
$linesPayload = [];
$error = null;
if ($selected !== '') {
$fullPath = $logsDir.DIRECTORY_SEPARATOR.$selected;
if (! is_file($fullPath) || ! is_readable($fullPath)) {
$error = '日志文件不可读';
} else {
$linesPayload = $this->tailLines($fullPath, $lines);
}
}
return response()->json([
'files' => $files,
'file' => $selected,
'lines' => $linesPayload,
'lines_requested' => $lines,
'error' => $error,
]);
}
/**
* @return array<int, array{name: string, size_bytes: int, modified_at: string}>
*/
private function listLogFiles(string $logsDir): array
{
if (! is_dir($logsDir)) {
return [];
}
$pattern = $logsDir.DIRECTORY_SEPARATOR.'laravel*.log';
$paths = glob($pattern) ?: [];
$files = [];
foreach ($paths as $full) {
$base = basename($full);
if (! $this->isAllowedBasename($base)) {
continue;
}
$mtime = @filemtime($full);
$size = @filesize($full);
$files[] = [
'name' => $base,
'size_bytes' => $size !== false ? $size : 0,
'modified_at' => $mtime ? date('c', $mtime) : '',
];
}
usort($files, static function (array $a, array $b): int {
return strcmp($b['modified_at'], $a['modified_at']);
});
return array_values($files);
}
/**
* @return array<int, string>
*/
private function tailLines(string $path, int $maxLines): array
{
$size = filesize($path);
if ($size === false || $size === 0) {
return [];
}
$maxLines = max(1, min(self::MAX_LINES, $maxLines));
$chunk = min($size, 262144);
while ($chunk <= $size) {
$start = $size - $chunk;
$data = file_get_contents($path, false, null, $start, $chunk);
if ($data === false) {
return [];
}
if ($start > 0) {
$firstNl = strpos($data, "\n");
if ($firstNl !== false) {
$data = substr($data, $firstNl + 1);
}
}
$parts = preg_split('/\r\n|\r|\n/', $data);
if ($parts === false) {
$parts = [];
}
if (count($parts) >= $maxLines || $chunk >= $size) {
return array_values(array_slice($parts, -$maxLines));
}
$chunk = min($size, $chunk * 2);
}
return [];
}
}