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
139 lines
4.0 KiB
<?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 [];
|
|
}
|
|
}
|