feat(admin): refactor logs viewer with table display and detailed dialog
Backend changes:
- Update LogsHandler.php to parse Monolog logs using regex
- Add parseLogLines() method to extract structured data from logs
- Support ISO 8601 format with microseconds and timezone
- Parse JSON context with nested objects and escaped characters support
- Add formatDateTime() method for readable date formatting
- Add findJsonEnd() method for correct JSON object extraction
- Return data in JSON format instead of plain string
Frontend changes:
- Update logs.js store: change data structure from string to array of objects
- Add loading flag for loading indicator
- Remove caching check for data freshness
- Completely refactor LogsViewer.vue component:
* Replace textarea with PrimeVue DataTable with pagination and sorting
* Add "Actions" column with view button (eye icon)
* Use Badge component for log levels with color indicators
* Remove "Context" column from table (unreadable in table view)
* Add dialog with detailed log information:
- Date and time (formatted and raw)
- Level with color indicator
- Channel
- Message
- Context (formatted JSON)
- Raw string
* Add word wrap for all text fields in dialog
* Dialog closes on outside click (dismissableMask)
* Configure pagination: 15 records per page by default
UX improvements:
- Improved log readability with structured display
- Easy navigation through large number of records via pagination
- Quick access to detailed information through dialog
- Color-coded levels for quick visual assessment
This commit is contained in:
@@ -19,17 +19,154 @@ class LogsHandler
|
||||
|
||||
public function getLogs(): JsonResponse
|
||||
{
|
||||
$data = [];
|
||||
$parsedLogs = [];
|
||||
|
||||
$logsPath = $this->findLastLogsFileInDir(
|
||||
$this->settings->get('logs.path')
|
||||
);
|
||||
|
||||
if ($logsPath) {
|
||||
$data = implode(PHP_EOL, $this->readLastLogsRows($logsPath, 100));
|
||||
$lines = $this->readLastLogsRows($logsPath, 100);
|
||||
$parsedLogs = $this->parseLogLines($lines);
|
||||
}
|
||||
|
||||
return new JsonResponse(compact('data'));
|
||||
return new JsonResponse(['data' => $parsedLogs]);
|
||||
}
|
||||
|
||||
private function parseLogLines(array $lines): array
|
||||
{
|
||||
$parsed = [];
|
||||
|
||||
// Регулярка для формата Monolog с ISO 8601 датой: [YYYY-MM-DDTHH:MM:SS.microseconds+timezone] channel.LEVEL: message {context} [extra]
|
||||
// Пример: [2025-11-23T14:28:21.772518+00:00] TeleCart.ERROR: Invalid Telegram Signature. {"exception":"..."} []
|
||||
// Поддерживает также формат без контекста и extra: [2025-11-23T14:28:21.772518+00:00] TeleCart.INFO: Message text
|
||||
$pattern = '/^\[([^\]]+)\]\s+([^.]+)\.(\w+):\s+(.+)$/s';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match($pattern, $line, $matches)) {
|
||||
$datetime = $matches[1] ?? '';
|
||||
$channel = $matches[2] ?? '';
|
||||
$level = $matches[3] ?? '';
|
||||
$rest = $matches[4] ?? '';
|
||||
|
||||
// Извлекаем сообщение и контекст
|
||||
// Контекст начинается с { и заканчивается соответствующим }
|
||||
$message = $rest;
|
||||
$context = null;
|
||||
|
||||
// Ищем JSON контекст (начинается с {, может быть после пробела или сразу)
|
||||
$jsonStart = strpos($rest, ' {');
|
||||
if ($jsonStart === false) {
|
||||
$jsonStart = strpos($rest, '{');
|
||||
} else {
|
||||
$jsonStart++; // Пропускаем пробел перед {
|
||||
}
|
||||
|
||||
if ($jsonStart !== false) {
|
||||
$message = trim(substr($rest, 0, $jsonStart));
|
||||
$jsonPart = substr($rest, $jsonStart);
|
||||
|
||||
// Находим конец JSON объекта, учитывая вложенность
|
||||
$jsonEnd = $this->findJsonEnd($jsonPart);
|
||||
if ($jsonEnd !== false) {
|
||||
$contextJson = substr($jsonPart, 0, $jsonEnd + 1);
|
||||
$decoded = json_decode($contextJson, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$context = $decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Форматируем дату для отображения (убираем микросекунды и временную зону для читаемости)
|
||||
$formattedDatetime = $this->formatDateTime($datetime);
|
||||
|
||||
$parsed[] = [
|
||||
'datetime' => $formattedDatetime,
|
||||
'datetime_raw' => $datetime,
|
||||
'channel' => $channel,
|
||||
'level' => $level,
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
'raw' => $line,
|
||||
];
|
||||
} else {
|
||||
// Если строка не соответствует формату, сохраняем как есть
|
||||
$parsed[] = [
|
||||
'datetime' => '',
|
||||
'datetime_raw' => '',
|
||||
'channel' => '',
|
||||
'level' => '',
|
||||
'message' => $line,
|
||||
'context' => null,
|
||||
'raw' => $line,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит позицию конца JSON объекта, учитывая вложенность
|
||||
* @param string $json JSON строка, начинающаяся с {
|
||||
* @return int|false Позиция закрывающей скобки или false, если не найдено
|
||||
*/
|
||||
private function findJsonEnd(string $json)
|
||||
{
|
||||
$depth = 0;
|
||||
$inString = false;
|
||||
$escape = false;
|
||||
$len = strlen($json);
|
||||
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$char = $json[$i];
|
||||
|
||||
if ($escape) {
|
||||
$escape = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '\\') {
|
||||
$escape = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '"') {
|
||||
$inString = !$inString;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($inString) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '{') {
|
||||
$depth++;
|
||||
} elseif ($char === '}') {
|
||||
$depth--;
|
||||
if ($depth === 0) {
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function formatDateTime(string $datetime): string
|
||||
{
|
||||
// Парсим ISO 8601 формат: 2025-11-23T14:28:21.772518+00:00
|
||||
// Преобразуем в более читаемый формат: 2025-11-23 14:28:21
|
||||
if (preg_match('/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/', $datetime, $dateMatches)) {
|
||||
return $dateMatches[1] . ' ' . $dateMatches[2];
|
||||
}
|
||||
|
||||
return $datetime;
|
||||
}
|
||||
|
||||
private function readLastLogsRows(string $path, int $lines = 1000, int $buffer = 4096): array
|
||||
|
||||
Reference in New Issue
Block a user