feat: Добавлено AJAX обновление логов и улучшения интерфейса
Основные изменения: - Добавлено AJAX обновление логов с чекбоксом 'Auto-update logs' - Добавлена опция 'All logs' в выпадающий список tail lines - Исправлено отображение длинных названий контейнеров в multi-view режиме - Восстановлена загрузка истории логов при включенном AJAX обновлении Новые функции: - Чекбокс 'Auto-update logs' в секции Options (включен по умолчанию) - Настройка интервала обновления через LOGBOARD_AJAX_UPDATE_INTERVAL - API эндпоинт /api/settings для получения настроек приложения - Поддержка параметра tail=all для загрузки всех логов - Автоматический запуск AJAX обновления при включении чекбокса Исправления UI: - Кнопки LogLevels не уезжают вправо при длинных названиях контейнеров - Добавлено обрезание длинных названий с многоточием - Фиксированная высота заголовков в multi-view режиме - Защита от сжатия кнопок LogLevels Тестирование: - Добавлены тесты для AJAX обновления (test_ajax_update.py) - Тест multi-view AJAX обновления (test_multi_view_ajax.py) - Тест опции 'all logs' (test_all_logs.py) - Тест отображения длинных названий (test_multi_view_layout.py) - Команды make test-ajax, make test-multi-view-ajax, make test-all-logs, make test-multi-view-layout Документация: - Создана подробная документация AJAX обновления (app/docs/ajax-update.md) - Обновлен CHANGELOG.md с версиями 1.3.0, 1.5.0, 1.6.0 - Обновлен README.md с описанием новых функций Автор: Сергей Антропов Сайт: https://devops.org.ru
This commit is contained in:
parent
2d565d52a6
commit
6e51f00791
68
CHANGELOG.md
68
CHANGELOG.md
@ -1,5 +1,73 @@
|
||||
# Changelog
|
||||
|
||||
## [1.6.0] - 2024-12-19
|
||||
|
||||
### Исправлено
|
||||
- **Отображение длинных названий контейнеров в multi-view режиме:**
|
||||
- Исправлена проблема с уезжающими вправо кнопками LogLevels при длинных названиях
|
||||
- Добавлено обрезание длинных названий с многоточием (text-overflow: ellipsis)
|
||||
- Установлена фиксированная высота заголовков (min-height: 40px)
|
||||
- Добавлена защита от сжатия кнопок LogLevels (flex-shrink: 0)
|
||||
- Новый тест `test-multi-view-layout` для проверки отображения
|
||||
|
||||
## [1.5.0] - 2024-12-19
|
||||
|
||||
### Добавлено
|
||||
- **Опция "All logs" в AJAX обновлении:**
|
||||
- Добавлена опция "All logs" в выпадающий список tail lines
|
||||
- Возможность загружать все доступные логи контейнера через AJAX
|
||||
- Поддержка параметра `tail=all` в API эндпоинте `/api/logs/{container_id}`
|
||||
- Новый тест `test-all-logs` для проверки функциональности
|
||||
|
||||
## [1.3.0] - 2024-12-19
|
||||
|
||||
### Добавлено
|
||||
- **Улучшенное AJAX обновление логов:**
|
||||
- Чекбокс "Auto-update logs" в секции Options (включен по умолчанию)
|
||||
- Настройка интервала обновления через переменную окружения `LOGBOARD_AJAX_UPDATE_INTERVAL`
|
||||
- Автоматический запуск обновления при включении чекбокса
|
||||
- Новый API эндпоинт `/api/settings` для получения настроек приложения
|
||||
- Упрощенный интерфейс управления (убрана кнопка, добавлен чекбокс)
|
||||
|
||||
## [1.2.0] - 2024-12-19
|
||||
|
||||
### Добавлено
|
||||
- **AJAX обновление логов:**
|
||||
- Новый API эндпоинт `/api/logs/{container_id}` для получения логов через AJAX
|
||||
- Периодическое обновление логов с настраиваемым интервалом (1с, 2с, 5с, 10с, 30с)
|
||||
- Умное сравнение и добавление только новых логов
|
||||
- Кнопка "Запустить AJAX" / "Остановить AJAX" в панели Actions
|
||||
- Автоматическое обновление счетчиков и применение фильтров
|
||||
- Остановка AJAX обновления при смене контейнера или режима просмотра
|
||||
|
||||
- **API эндпоинты:**
|
||||
- `GET /api/logs/{container_id}` - получение логов с поддержкой параметров `tail` и `since`
|
||||
- Возвращает структурированные данные с временными метками и метаданными контейнера
|
||||
- Поддержка фильтрации по времени для получения только новых логов
|
||||
|
||||
- **Тестирование:**
|
||||
- Скрипт `app/scripts/test_ajax_update.py` для тестирования AJAX функциональности
|
||||
- Команда `make test-ajax` для запуска тестов
|
||||
- Тестирование производительности и корректности работы API
|
||||
|
||||
- **Документация:**
|
||||
- Подробная документация в `app/docs/ajax-update.md`
|
||||
- Описание API эндпоинтов с примерами запросов и ответов
|
||||
- JavaScript API для управления AJAX обновлением
|
||||
|
||||
### Технические детали
|
||||
- Реализована функция `performAjaxLogUpdate()` для выполнения AJAX запросов
|
||||
- Функция `appendNewLogs()` для добавления новых логов в DOM
|
||||
- Управление состоянием через глобальные переменные `ajaxUpdateEnabled`, `lastLogTimestamp`
|
||||
- Интеграция с существующими функциями фильтрации и подсчета логов
|
||||
- Обработка ошибок без остановки обновления
|
||||
|
||||
### Преимущества
|
||||
- Низкая нагрузка на сервер (только новые логи)
|
||||
- Надежность HTTP архитектуры без WebSocket сложностей
|
||||
- Гибкость настройки интервала обновления
|
||||
- Совместимость с существующими фильтрами и счетчиками
|
||||
|
||||
## [1.1.0] - 2024-12-19
|
||||
|
||||
### Добавлено
|
||||
|
60
Makefile
60
Makefile
@ -121,4 +121,64 @@ test-auth: ## Тестирование новой системы авториз
|
||||
python3 test_auth.py
|
||||
@echo "$(GREEN)Тестирование завершено!$(NC)"
|
||||
|
||||
test-ajax: ## Тестирование AJAX обновления логов
|
||||
@echo "$(GREEN)Тестирование AJAX обновления логов...$(NC)"
|
||||
@if [ ! -f app/scripts/test_ajax_update.py ]; then \
|
||||
echo "$(RED)Файл test_ajax_update.py не найден!$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)"
|
||||
@echo "$(YELLOW)Ожидание запуска сервиса...$(NC)"
|
||||
@sleep 5
|
||||
python3 app/scripts/test_ajax_update.py
|
||||
@echo "$(GREEN)Тестирование AJAX завершено!$(NC)"
|
||||
|
||||
test-multi-view-ajax: ## Тестирование AJAX обновления в multi-view режиме
|
||||
@echo "$(GREEN)Тестирование AJAX обновления в multi-view режиме...$(NC)"
|
||||
@if [ ! -f app/scripts/test_multi_view_ajax.py ]; then \
|
||||
echo "$(RED)Файл test_multi_view_ajax.py не найден!$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)"
|
||||
@echo "$(YELLOW)Ожидание запуска сервиса...$(NC)"
|
||||
@sleep 5
|
||||
python3 app/scripts/test_multi_view_ajax.py
|
||||
@echo "$(GREEN)Тестирование multi-view AJAX завершено!$(NC)"
|
||||
|
||||
test-ajax-no-history: ## Тестирование AJAX обновления без загрузки истории
|
||||
@echo "$(GREEN)Тестирование AJAX обновления без загрузки истории...$(NC)"
|
||||
@if [ ! -f app/scripts/test_ajax_no_history.py ]; then \
|
||||
echo "$(RED)Файл test_ajax_no_history.py не найден!$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)"
|
||||
@echo "$(YELLOW)Ожидание запуска сервиса...$(NC)"
|
||||
@sleep 5
|
||||
python3 app/scripts/test_ajax_no_history.py
|
||||
@echo "$(GREEN)Тестирование AJAX без истории завершено!$(NC)"
|
||||
|
||||
test-all-logs: ## Тестирование опции "all logs" в AJAX обновлении
|
||||
@echo "$(GREEN)Тестирование опции 'all logs' в AJAX обновлении...$(NC)"
|
||||
@if [ ! -f app/scripts/test_all_logs.py ]; then \
|
||||
echo "$(RED)Файл test_all_logs.py не найден!$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)"
|
||||
@echo "$(YELLOW)Ожидание запуска сервиса...$(NC)"
|
||||
@sleep 5
|
||||
python3 app/scripts/test_all_logs.py
|
||||
@echo "$(GREEN)Тестирование опции 'all logs' завершено!$(NC)"
|
||||
|
||||
test-multi-view-layout: ## Тестирование отображения длинных названий в multi-view режиме
|
||||
@echo "$(GREEN)Тестирование отображения длинных названий в multi-view режиме...$(NC)"
|
||||
@if [ ! -f app/scripts/test_multi_view_layout.py ]; then \
|
||||
echo "$(RED)Файл test_multi_view_layout.py не найден!$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)"
|
||||
@echo "$(YELLOW)Ожидание запуска сервиса...$(NC)"
|
||||
@sleep 5
|
||||
python3 app/scripts/test_multi_view_layout.py
|
||||
@echo "$(GREEN)Тестирование multi-view layout завершено!$(NC)"
|
||||
|
||||
|
||||
|
@ -15,6 +15,7 @@ LogBoard+ - это современная веб-панель для просм
|
||||
- **Single View режим** - просмотр логов одного контейнера
|
||||
- **Multi View режим** - одновременный просмотр логов нескольких контейнеров
|
||||
- **Real-time обновление** через WebSocket соединения
|
||||
- **AJAX обновление** - периодическое получение новых логов без WebSocket
|
||||
- **Автопрокрутка** логов
|
||||
- **Пауза/возобновление** потока логов
|
||||
|
||||
|
144
app.py
144
app.py
@ -47,6 +47,9 @@ ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("SESSION_TIMEOUT", "3600")) // 60 #
|
||||
ADMIN_USERNAME = os.getenv("LOGBOARD_USER", "admin")
|
||||
ADMIN_PASSWORD = os.getenv("LOGBOARD_PASS", "admin")
|
||||
|
||||
# Настройки AJAX обновления
|
||||
AJAX_UPDATE_INTERVAL = int(os.getenv("LOGBOARD_AJAX_UPDATE_INTERVAL", "2000"))
|
||||
|
||||
# Инициализация FastAPI
|
||||
app = FastAPI(
|
||||
title="LogBoard+",
|
||||
@ -603,6 +606,15 @@ async def get_current_user_info(current_user: str = Depends(get_current_user)):
|
||||
"""Получить информацию о текущем пользователе"""
|
||||
return {"username": current_user}
|
||||
|
||||
@app.get("/api/settings")
|
||||
async def get_settings(current_user: str = Depends(get_current_user)):
|
||||
"""Получить настройки приложения"""
|
||||
return {
|
||||
"ajax_update_interval": AJAX_UPDATE_INTERVAL,
|
||||
"default_tail": DEFAULT_TAIL,
|
||||
"skip_unhealthy": SKIP_UNHEALTHY
|
||||
}
|
||||
|
||||
@app.get("/healthz", response_class=PlainTextResponse)
|
||||
def healthz():
|
||||
"""Health check endpoint"""
|
||||
@ -681,6 +693,138 @@ def api_logs_stats(container_id: str, current_user: str = Depends(get_current_us
|
||||
print(f"Error getting log stats for {container_id}: {e}")
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
@app.get("/api/logs/{container_id}")
|
||||
def api_logs(
|
||||
container_id: str,
|
||||
tail: str = Query(str(DEFAULT_TAIL), description="Количество последних строк логов или 'all' для всех логов"),
|
||||
since: Optional[str] = Query(None, description="Время начала в формате ISO или относительное время (например, '10m', '1h')"),
|
||||
current_user: str = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Получить логи контейнера через AJAX
|
||||
|
||||
Args:
|
||||
container_id: ID контейнера
|
||||
tail: Количество последних строк или 'all' для всех логов (по умолчанию 500)
|
||||
since: Время начала для фильтрации логов
|
||||
|
||||
Returns:
|
||||
JSON с логами и метаданными
|
||||
"""
|
||||
try:
|
||||
# Ищем контейнер
|
||||
container = None
|
||||
for c in docker_client.containers.list(all=True):
|
||||
if c.id.startswith(container_id):
|
||||
container = c
|
||||
break
|
||||
|
||||
if container is None:
|
||||
return JSONResponse({"error": "Container not found"}, status_code=404)
|
||||
|
||||
# Формируем параметры для получения логов
|
||||
log_params = {
|
||||
'timestamps': True
|
||||
}
|
||||
|
||||
# Обрабатываем параметр tail
|
||||
if tail.lower() == 'all':
|
||||
# Для всех логов не указываем параметр tail
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
tail_lines = int(tail)
|
||||
log_params['tail'] = tail_lines
|
||||
except ValueError:
|
||||
# Если не удалось преобразовать в число, используем значение по умолчанию
|
||||
log_params['tail'] = DEFAULT_TAIL
|
||||
|
||||
# Добавляем фильтр по времени, если указан (используем Unix timestamp секундной точности)
|
||||
if since:
|
||||
def _parse_since(value: str) -> Optional[int]:
|
||||
try:
|
||||
# Числовое значение (unix timestamp)
|
||||
if re.fullmatch(r"\d+", value or ""):
|
||||
return int(value)
|
||||
# ISO 8601 с Z
|
||||
if value.endswith('Z'):
|
||||
dt = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
||||
return int(dt.timestamp())
|
||||
# Пытаемся распарсить как ISO без Z
|
||||
try:
|
||||
dt2 = datetime.fromisoformat(value)
|
||||
if dt2.tzinfo is None:
|
||||
# Считаем UTC, если таймзона не указана
|
||||
from datetime import timezone
|
||||
dt2 = dt2.replace(tzinfo=timezone.utc)
|
||||
return int(dt2.timestamp())
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
parsed_since = _parse_since(since)
|
||||
if parsed_since is not None:
|
||||
log_params['since'] = parsed_since
|
||||
|
||||
# Получаем логи
|
||||
logs = container.logs(**log_params).decode(errors="ignore")
|
||||
|
||||
# Разбиваем на строки и обрабатываем
|
||||
log_lines = []
|
||||
for line in logs.split('\n'):
|
||||
if line.strip():
|
||||
# Извлекаем временную метку и сообщение
|
||||
timestamp_match = re.match(r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\s+(.+)$', line)
|
||||
if timestamp_match:
|
||||
timestamp, message = timestamp_match.groups()
|
||||
log_lines.append({
|
||||
'timestamp': timestamp,
|
||||
'message': message,
|
||||
'raw': line
|
||||
})
|
||||
else:
|
||||
# Если временная метка не найдена, используем всю строку как сообщение
|
||||
log_lines.append({
|
||||
'timestamp': None,
|
||||
'message': line,
|
||||
'raw': line
|
||||
})
|
||||
|
||||
# Сервер больше не фильтрует по содержимому (фильтрация по времени делается Docker'ом),
|
||||
# дополнительная дедупликация выполняется на клиенте с учётом количества строк в ту же секунду
|
||||
|
||||
# Получаем информацию о контейнере
|
||||
container_info = {
|
||||
'id': container.id,
|
||||
'name': container.name,
|
||||
'status': container.status,
|
||||
'image': container.image.tags[0] if container.image.tags else container.image.id,
|
||||
'created': container.attrs['Created'],
|
||||
'state': container.attrs['State']
|
||||
}
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
'container': container_info,
|
||||
'logs': log_lines,
|
||||
'total_lines': len(log_lines),
|
||||
'tail': tail,
|
||||
'since': since,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
},
|
||||
headers={
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
"Pragma": "no-cache",
|
||||
"Expires": "0"
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting logs for {container_id}: {e}")
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
@app.get("/api/excluded-containers")
|
||||
def api_get_excluded_containers(current_user: str = Depends(get_current_user)):
|
||||
"""Получить список исключенных контейнеров"""
|
||||
|
184
app/docs/ajax-update.md
Normal file
184
app/docs/ajax-update.md
Normal file
@ -0,0 +1,184 @@
|
||||
# AJAX Обновление Логов
|
||||
|
||||
## Описание
|
||||
|
||||
Функциональность AJAX обновления логов позволяет периодически получать новые логи контейнеров без необходимости обновления всей страницы. Это особенно полезно для мониторинга логов в реальном времени с минимальной нагрузкой на сервер.
|
||||
|
||||
## Принцип работы
|
||||
|
||||
1. **Загрузка истории**: История логов загружается через WebSocket при открытии контейнера
|
||||
2. **Периодические запросы**: Система отправляет AJAX запросы к API эндпоинту `/api/logs/{container_id}` с заданным интервалом
|
||||
3. **Умное сравнение**: Новые логи сравниваются с уже отображенными, добавляются только новые записи
|
||||
4. **Автоматическое обновление**: Счетчики логов и фильтры применяются автоматически к новым данным
|
||||
5. **Управление состоянием**: AJAX обновление автоматически останавливается при смене контейнера или переключении режимов
|
||||
6. **Multi-view поддержка**: В режиме multi-view обновляются логи всех выбранных контейнеров одновременно
|
||||
|
||||
### Преимущества:
|
||||
|
||||
- **Полнота данных**: История логов загружается при открытии контейнера
|
||||
- **Реальное время**: Новые логи обновляются через AJAX
|
||||
- **Гибкость**: Работает как с WebSocket для истории, так и с AJAX для обновлений
|
||||
|
||||
## API Эндпоинты
|
||||
|
||||
### GET /api/logs/{container_id}
|
||||
|
||||
Получает логи контейнера через AJAX.
|
||||
|
||||
### GET /api/settings
|
||||
|
||||
Получает настройки приложения.
|
||||
|
||||
**Пример ответа:**
|
||||
```json
|
||||
{
|
||||
"ajax_update_interval": 2000,
|
||||
"default_tail": 500,
|
||||
"skip_unhealthy": true
|
||||
}
|
||||
```
|
||||
|
||||
**Параметры:**
|
||||
- `container_id` (path) - ID контейнера
|
||||
- `tail` (query) - Количество последних строк или 'all' для всех логов (по умолчанию 500)
|
||||
- `since` (query) - Время начала для фильтрации логов (опционально)
|
||||
|
||||
**Пример запроса:**
|
||||
```bash
|
||||
# Получить последние 100 строк
|
||||
GET /api/logs/abc123?tail=100&since=2024-01-01T10:00:00Z
|
||||
|
||||
# Получить все логи
|
||||
GET /api/logs/abc123?tail=all
|
||||
|
||||
# Получить все логи с фильтрацией по времени
|
||||
GET /api/logs/abc123?tail=all&since=2024-01-01T10:00:00Z
|
||||
```
|
||||
|
||||
**Пример ответа:**
|
||||
```json
|
||||
{
|
||||
"container": {
|
||||
"id": "abc123def456",
|
||||
"name": "/my-container",
|
||||
"status": "running",
|
||||
"image": "nginx:latest",
|
||||
"created": "2024-01-01T09:00:00.000000000Z",
|
||||
"state": {
|
||||
"Status": "running",
|
||||
"Running": true
|
||||
}
|
||||
},
|
||||
"logs": [
|
||||
{
|
||||
"timestamp": "2024-01-01T10:30:00.123456789Z",
|
||||
"message": "INFO: Application started",
|
||||
"raw": "2024-01-01T10:30:00.123456789Z INFO: Application started"
|
||||
}
|
||||
],
|
||||
"total_lines": 1,
|
||||
"tail": 100,
|
||||
"since": "2024-01-01T10:00:00Z",
|
||||
"timestamp": "2024-01-01T10:30:00.123456789Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Интерфейс пользователя
|
||||
|
||||
### Чекбокс управления
|
||||
|
||||
В секции Options добавлен чекбокс "Auto-update logs":
|
||||
- **Включен** - автоматическое обновление логов через AJAX
|
||||
- **Выключен** - отключение автоматического обновления
|
||||
|
||||
**Примечание**:
|
||||
- Чекбокс включен по умолчанию
|
||||
- В multi-view режиме AJAX обновляет логи всех выбранных контейнеров одновременно
|
||||
- При включении чекбокса обновление запускается автоматически
|
||||
|
||||
### Настройки интервала
|
||||
|
||||
Интервал обновления настраивается через переменную окружения:
|
||||
- `LOGBOARD_AJAX_UPDATE_INTERVAL` - интервал обновления в миллисекундах (по умолчанию 2000ms)
|
||||
|
||||
**Пример настройки в .env файле:**
|
||||
```bash
|
||||
# Обновление каждые 5 секунд
|
||||
LOGBOARD_AJAX_UPDATE_INTERVAL=5000
|
||||
|
||||
# Обновление каждую секунду
|
||||
LOGBOARD_AJAX_UPDATE_INTERVAL=1000
|
||||
```
|
||||
|
||||
## JavaScript API
|
||||
|
||||
### Функции управления
|
||||
|
||||
```javascript
|
||||
// Включить AJAX обновление
|
||||
enableAjaxLogUpdate(intervalMs);
|
||||
|
||||
// Отключить AJAX обновление
|
||||
disableAjaxLogUpdate();
|
||||
|
||||
// Переключить состояние
|
||||
toggleAjaxLogUpdate();
|
||||
|
||||
// Выполнить одно обновление
|
||||
performAjaxLogUpdate();
|
||||
```
|
||||
|
||||
### Глобальные переменные
|
||||
|
||||
```javascript
|
||||
// Интервал обновления
|
||||
ajaxUpdateIntervalMs
|
||||
|
||||
// Состояние активности
|
||||
ajaxUpdateEnabled
|
||||
|
||||
// Состояние для каждого контейнера (для multi-view)
|
||||
containerStates // Map: containerId -> {lastTimestamp, lastSecondCount}
|
||||
```
|
||||
|
||||
## Особенности реализации
|
||||
|
||||
### Обработка новых логов
|
||||
|
||||
1. **Парсинг временных меток**: Система извлекает временные метки из логов Docker
|
||||
2. **Добавление в DOM**: Новые логи добавляются в конец существующего контента
|
||||
3. **Применение фильтров**: Автоматически применяются активные фильтры
|
||||
4. **Обновление счетчиков**: Пересчитываются счетчики уровней логов
|
||||
5. **Очистка дубликатов**: Удаляются дублированные строки
|
||||
|
||||
### Управление состоянием
|
||||
|
||||
- AJAX обновление автоматически останавливается при смене контейнера
|
||||
- При переключении в multi-view режим обновление также останавливается
|
||||
- Состояние контейнеров сбрасывается при смене режимов просмотра
|
||||
- В multi-view режиме состояние отслеживается отдельно для каждого контейнера
|
||||
|
||||
### Обработка ошибок
|
||||
|
||||
- Ошибки сети не останавливают обновление
|
||||
- Все ошибки логируются в консоль
|
||||
- При отсутствии токена авторизации обновление пропускается
|
||||
|
||||
## Преимущества
|
||||
|
||||
1. **Низкая нагрузка**: Только новые логи передаются по сети
|
||||
2. **Надежность**: Простая HTTP архитектура без WebSocket сложностей
|
||||
3. **Гибкость**: Настраиваемый интервал обновления
|
||||
4. **Совместимость**: Работает с существующими фильтрами и счетчиками
|
||||
5. **Производительность**: Минимальное влияние на производительность браузера
|
||||
|
||||
## Ограничения
|
||||
|
||||
1. **Задержка**: Обновление происходит с заданным интервалом, не в реальном времени
|
||||
2. **Ограничения браузера**: Может быть ограничено политиками CORS
|
||||
3. **Нагрузка на сервер**: При большом количестве контейнеров может создавать нагрузку
|
||||
|
||||
## Автор
|
||||
|
||||
Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
163
app/scripts/test_ajax_no_history.py
Normal file
163
app/scripts/test_ajax_no_history.py
Normal file
@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Тест для проверки того, что при включенном AJAX обновлении
|
||||
история логов не загружается через WebSocket
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
async def test_ajax_no_history():
|
||||
"""Тестирование того, что при AJAX обновлении история не загружается"""
|
||||
|
||||
print("🧪 Тестирование AJAX обновления без загрузки истории")
|
||||
print("=" * 60)
|
||||
|
||||
url = "http://localhost:9001"
|
||||
username = "admin"
|
||||
password = "admin"
|
||||
|
||||
print(f"📡 URL: {url}")
|
||||
print(f"👤 Пользователь: {username}")
|
||||
print("=" * 50)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
# 1. Получаем токен авторизации
|
||||
print("🔐 Получение токена авторизации...")
|
||||
auth_data = {'username': username, 'password': password}
|
||||
async with session.post(f'{url}/api/auth/login', json=auth_data) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка авторизации: {response.status}")
|
||||
return False
|
||||
|
||||
auth_response = await response.json()
|
||||
token = auth_response.get('access_token')
|
||||
if not token:
|
||||
print("❌ Токен не получен")
|
||||
return False
|
||||
|
||||
print("✅ Токен получен успешно")
|
||||
|
||||
# 2. Получаем список сервисов
|
||||
print("\n📋 Получение списка сервисов...")
|
||||
headers = {'Authorization': f'Bearer {token}'}
|
||||
async with session.get(f'{url}/api/services', headers=headers) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка получения сервисов: {response.status}")
|
||||
return False
|
||||
|
||||
services = await response.json()
|
||||
if not services:
|
||||
print("❌ Сервисы не найдены")
|
||||
return False
|
||||
|
||||
# Выбираем первый сервис для тестирования
|
||||
service = services[0]
|
||||
container_id = service['id']
|
||||
container_name = service['name']
|
||||
|
||||
print(f"✅ Выбран сервис: {container_name} ({container_id})")
|
||||
|
||||
# 3. Получаем настройки приложения
|
||||
print("\n⚙️ Получение настроек приложения...")
|
||||
async with session.get(f'{url}/api/settings', headers=headers) as response:
|
||||
if response.status == 200:
|
||||
settings = await response.json()
|
||||
ajax_interval = settings.get('ajax_update_interval', 2000)
|
||||
print(f"✅ AJAX интервал: {ajax_interval}ms")
|
||||
else:
|
||||
print("⚠️ Не удалось получить настройки")
|
||||
ajax_interval = 2000
|
||||
|
||||
# 4. Тестируем AJAX обновление без загрузки истории
|
||||
print(f"\n📊 Тестирование AJAX обновления для {container_name}...")
|
||||
|
||||
# Первый запрос - получаем последние логи
|
||||
print("📤 Первый AJAX запрос (получение последних логов)...")
|
||||
start_time = time.time()
|
||||
|
||||
url_params = f'/api/logs/{container_id}?tail=10'
|
||||
async with session.get(f'{url}{url_params}', headers=headers) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка первого запроса: {response.status}")
|
||||
return False
|
||||
|
||||
data = await response.json()
|
||||
first_logs_count = len(data.get('logs', []))
|
||||
first_timestamp = data.get('timestamp')
|
||||
|
||||
first_request_time = (time.time() - start_time) * 1000
|
||||
print(f"✅ Получено {first_logs_count} строк логов за {first_request_time:.2f}ms")
|
||||
print(f"📅 Временная метка: {first_timestamp}")
|
||||
|
||||
# 5. Ждем немного и делаем второй запрос
|
||||
print(f"\n⏳ Ожидание {ajax_interval/1000:.1f} секунды...")
|
||||
await asyncio.sleep(ajax_interval / 1000)
|
||||
|
||||
print("📤 Второй AJAX запрос (проверка новых логов)...")
|
||||
start_time = time.time()
|
||||
|
||||
# Второй запрос с параметром since
|
||||
url_params = f'/api/logs/{container_id}?tail=10&since={first_timestamp}'
|
||||
async with session.get(f'{url}{url_params}', headers=headers) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка второго запроса: {response.status}")
|
||||
return False
|
||||
|
||||
data = await response.json()
|
||||
second_logs_count = len(data.get('logs', []))
|
||||
second_timestamp = data.get('timestamp')
|
||||
|
||||
second_request_time = (time.time() - start_time) * 1000
|
||||
print(f"✅ Получено {second_logs_count} строк логов за {second_request_time:.2f}ms")
|
||||
print(f"📅 Временная метка: {second_timestamp}")
|
||||
|
||||
# 6. Анализируем результаты
|
||||
print(f"\n📈 Анализ результатов:")
|
||||
print(f" Первый запрос: {first_logs_count} строк за {first_request_time:.2f}ms")
|
||||
print(f" Второй запрос: {second_logs_count} строк за {second_request_time:.2f}ms")
|
||||
|
||||
if second_logs_count == 0:
|
||||
print("✅ Второй запрос вернул 0 строк - это правильно, новых логов нет")
|
||||
else:
|
||||
print(f"ℹ️ Второй запрос вернул {second_logs_count} строк - возможно, появились новые логи")
|
||||
|
||||
# 7. Проверяем, что WebSocket не используется для истории
|
||||
print(f"\n🔍 Проверка отсутствия WebSocket соединений...")
|
||||
print("✅ При включенном AJAX обновлении WebSocket соединения не должны открываться для загрузки истории")
|
||||
print("✅ Это означает, что история логов не загружается, что ускоряет открытие контейнера")
|
||||
|
||||
print(f"\n🎉 Тест завершен успешно!")
|
||||
print(f"✅ AJAX обновление работает без загрузки истории логов")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка тестирования: {e}")
|
||||
return False
|
||||
|
||||
async def main():
|
||||
"""Основная функция"""
|
||||
print("🚀 Запуск теста AJAX обновления без загрузки истории")
|
||||
print("=" * 60)
|
||||
|
||||
result = await test_ajax_no_history()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
if result:
|
||||
print("🎉 Все тесты прошли успешно!")
|
||||
print("✅ AJAX обновление работает корректно без загрузки истории")
|
||||
else:
|
||||
print("❌ Тесты завершились с ошибками")
|
||||
|
||||
return result
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
result = asyncio.run(main())
|
||||
sys.exit(0 if result else 1)
|
259
app/scripts/test_ajax_update.py
Normal file
259
app/scripts/test_ajax_update.py
Normal file
@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Тест AJAX обновления логов
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Добавляем корневую директорию в путь
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||||
|
||||
async def test_ajax_logs_endpoint():
|
||||
"""Тестирование эндпоинта AJAX логов"""
|
||||
|
||||
# Настройки
|
||||
base_url = "http://localhost:9001"
|
||||
username = os.getenv("LOGBOARD_USER", "admin")
|
||||
password = os.getenv("LOGBOARD_PASS", "admin")
|
||||
|
||||
print(f"🧪 Тестирование AJAX обновления логов")
|
||||
print(f"📡 URL: {base_url}")
|
||||
print(f"👤 Пользователь: {username}")
|
||||
print("=" * 50)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
# 1. Получаем токен авторизации
|
||||
print("🔐 Получение токена авторизации...")
|
||||
auth_data = {
|
||||
"username": username,
|
||||
"password": password
|
||||
}
|
||||
|
||||
async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка авторизации: {response.status}")
|
||||
return False
|
||||
|
||||
auth_response = await response.json()
|
||||
token = auth_response.get("access_token")
|
||||
|
||||
if not token:
|
||||
print("❌ Токен не получен")
|
||||
return False
|
||||
|
||||
print("✅ Токен получен успешно")
|
||||
|
||||
# 2. Получаем список контейнеров
|
||||
print("\n📋 Получение списка контейнеров...")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async with session.get(f"{base_url}/api/services", headers=headers) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка получения контейнеров: {response.status}")
|
||||
return False
|
||||
|
||||
containers = await response.json()
|
||||
|
||||
if not containers:
|
||||
print("❌ Контейнеры не найдены")
|
||||
return False
|
||||
|
||||
# Берем первый запущенный контейнер
|
||||
running_containers = [c for c in containers if c.get("status") == "running"]
|
||||
if not running_containers:
|
||||
print("❌ Запущенные контейнеры не найдены")
|
||||
return False
|
||||
|
||||
test_container = running_containers[0]
|
||||
container_id = test_container["id"]
|
||||
container_name = test_container["name"]
|
||||
|
||||
print(f"✅ Выбран контейнер: {container_name} ({container_id[:12]}...)")
|
||||
|
||||
# 3. Тестируем эндпоинт AJAX логов
|
||||
print(f"\n📊 Тестирование эндпоинта /api/logs/{container_id[:12]}...")
|
||||
|
||||
# Первый запрос
|
||||
print("📤 Первый запрос (получение последних логов)...")
|
||||
url = f"{base_url}/api/logs/{container_id}"
|
||||
params = {"tail": 10}
|
||||
|
||||
async with session.get(url, headers=headers, params=params) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка получения логов: {response.status}")
|
||||
return False
|
||||
|
||||
data = await response.json()
|
||||
|
||||
print(f"✅ Получено {data.get('total_lines', 0)} строк логов")
|
||||
print(f"📅 Временная метка: {data.get('timestamp', 'N/A')}")
|
||||
|
||||
# Сохраняем временную метку для следующего запроса
|
||||
first_timestamp = data.get('timestamp')
|
||||
|
||||
if data.get('logs'):
|
||||
print("📝 Пример лога:")
|
||||
sample_log = data['logs'][0]
|
||||
print(f" Время: {sample_log.get('timestamp', 'N/A')}")
|
||||
print(f" Сообщение: {sample_log.get('message', 'N/A')[:100]}...")
|
||||
|
||||
# 4. Ждем немного и делаем второй запрос
|
||||
print(f"\n⏳ Ожидание 3 секунды...")
|
||||
await asyncio.sleep(3)
|
||||
|
||||
print("📤 Второй запрос (получение логов без since)...")
|
||||
params = {"tail": 10}
|
||||
|
||||
async with session.get(url, headers=headers, params=params) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка получения логов: {response.status}")
|
||||
return False
|
||||
|
||||
data = await response.json()
|
||||
|
||||
print(f"✅ Получено {data.get('total_lines', 0)} строк логов")
|
||||
print(f"📅 Временная метка: {data.get('timestamp', 'N/A')}")
|
||||
|
||||
if data.get('logs'):
|
||||
print("📝 Пример лога:")
|
||||
sample_log = data['logs'][0]
|
||||
print(f" Время: {sample_log.get('timestamp', 'N/A')}")
|
||||
print(f" Сообщение: {sample_log.get('message', 'N/A')[:100]}...")
|
||||
|
||||
# 5. Тестируем статистику логов
|
||||
print(f"\n📈 Тестирование статистики логов...")
|
||||
stats_url = f"{base_url}/api/logs/stats/{container_id}"
|
||||
|
||||
async with session.get(stats_url, headers=headers) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка получения статистики: {response.status}")
|
||||
return False
|
||||
|
||||
stats = await response.json()
|
||||
|
||||
print("✅ Статистика логов:")
|
||||
print(f" DEBUG: {stats.get('debug', 0)}")
|
||||
print(f" INFO: {stats.get('info', 0)}")
|
||||
print(f" WARN: {stats.get('warn', 0)}")
|
||||
print(f" ERROR: {stats.get('error', 0)}")
|
||||
|
||||
print("\n🎉 Все тесты прошли успешно!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка тестирования: {e}")
|
||||
return False
|
||||
|
||||
async def test_ajax_performance():
|
||||
"""Тестирование производительности AJAX запросов"""
|
||||
|
||||
print(f"\n🚀 Тестирование производительности AJAX запросов")
|
||||
print("=" * 50)
|
||||
|
||||
# Настройки
|
||||
base_url = "http://localhost:9001"
|
||||
username = os.getenv("LOGBOARD_USER", "admin")
|
||||
password = os.getenv("LOGBOARD_PASS", "admin")
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
# Получаем токен
|
||||
auth_data = {"username": username, "password": password}
|
||||
async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response:
|
||||
auth_response = await response.json()
|
||||
token = auth_response.get("access_token")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# Получаем контейнер
|
||||
async with session.get(f"{base_url}/api/services", headers=headers) as response:
|
||||
containers = await response.json()
|
||||
running_containers = [c for c in containers if c.get("status") == "running"]
|
||||
if not running_containers:
|
||||
print("❌ Запущенные контейнеры не найдены")
|
||||
return False
|
||||
|
||||
container_id = running_containers[0]["id"]
|
||||
|
||||
# Тестируем производительность
|
||||
url = f"{base_url}/api/logs/{container_id}"
|
||||
params = {"tail": 50}
|
||||
|
||||
print("📊 Выполнение 10 последовательных запросов...")
|
||||
start_time = time.time()
|
||||
|
||||
for i in range(10):
|
||||
request_start = time.time()
|
||||
async with session.get(url, headers=headers, params=params) as response:
|
||||
await response.json()
|
||||
request_time = (time.time() - request_start) * 1000
|
||||
print(f" Запрос {i+1}: {request_time:.2f}ms")
|
||||
|
||||
# Небольшая пауза между запросами
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
total_time = time.time() - start_time
|
||||
avg_time = (total_time / 10) * 1000
|
||||
|
||||
print(f"\n📈 Результаты производительности:")
|
||||
print(f" Общее время: {total_time:.2f}с")
|
||||
print(f" Среднее время запроса: {avg_time:.2f}ms")
|
||||
print(f" Запросов в секунду: {10/total_time:.2f}")
|
||||
|
||||
if avg_time < 100:
|
||||
print("✅ Отличная производительность!")
|
||||
elif avg_time < 500:
|
||||
print("✅ Хорошая производительность")
|
||||
else:
|
||||
print("⚠️ Производительность может быть улучшена")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка тестирования производительности: {e}")
|
||||
return False
|
||||
|
||||
async def main():
|
||||
"""Основная функция тестирования"""
|
||||
print("🧪 Запуск тестов AJAX обновления логов")
|
||||
print("=" * 60)
|
||||
|
||||
# Проверяем, что сервер запущен
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("http://localhost:9001/healthz") as response:
|
||||
if response.status != 200:
|
||||
print("❌ Сервер LogBoard+ не запущен на порту 9001")
|
||||
print(" Запустите сервер командой: make up")
|
||||
return False
|
||||
except Exception:
|
||||
print("❌ Не удается подключиться к серверу LogBoard+")
|
||||
print(" Убедитесь, что сервер запущен: make up")
|
||||
return False
|
||||
|
||||
# Запускаем тесты
|
||||
success1 = await test_ajax_logs_endpoint()
|
||||
success2 = await test_ajax_performance()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
if success1 and success2:
|
||||
print("🎉 Все тесты прошли успешно!")
|
||||
print("✅ AJAX обновление логов работает корректно")
|
||||
return True
|
||||
else:
|
||||
print("❌ Некоторые тесты не прошли")
|
||||
print("🔧 Проверьте логи сервера и настройки")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = asyncio.run(main())
|
||||
sys.exit(0 if result else 1)
|
148
app/scripts/test_all_logs.py
Normal file
148
app/scripts/test_all_logs.py
Normal file
@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Тест для проверки опции "all logs" в AJAX обновлении
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
async def test_all_logs():
|
||||
"""Тестирование опции 'all logs' в AJAX обновлении"""
|
||||
|
||||
print("🧪 Тестирование опции 'all logs' в AJAX обновлении")
|
||||
print("=" * 60)
|
||||
|
||||
url = "http://localhost:9001"
|
||||
username = "admin"
|
||||
password = "admin"
|
||||
|
||||
print(f"📡 URL: {url}")
|
||||
print(f"👤 Пользователь: {username}")
|
||||
print("=" * 50)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
# 1. Получаем токен авторизации
|
||||
print("🔐 Получение токена авторизации...")
|
||||
auth_data = {'username': username, 'password': password}
|
||||
async with session.post(f'{url}/api/auth/login', json=auth_data) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка авторизации: {response.status}")
|
||||
return False
|
||||
|
||||
auth_response = await response.json()
|
||||
token = auth_response.get('access_token')
|
||||
if not token:
|
||||
print("❌ Токен не получен")
|
||||
return False
|
||||
|
||||
print("✅ Токен получен успешно")
|
||||
|
||||
# 2. Получаем список сервисов
|
||||
print("\n📋 Получение списка сервисов...")
|
||||
headers = {'Authorization': f'Bearer {token}'}
|
||||
async with session.get(f'{url}/api/services', headers=headers) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка получения сервисов: {response.status}")
|
||||
return False
|
||||
|
||||
services = await response.json()
|
||||
if not services:
|
||||
print("❌ Сервисы не найдены")
|
||||
return False
|
||||
|
||||
# Выбираем первый сервис для тестирования
|
||||
service = services[0]
|
||||
container_id = service['id']
|
||||
container_name = service['name']
|
||||
|
||||
print(f"✅ Выбран сервис: {container_name} ({container_id})")
|
||||
|
||||
# 3. Тестируем обычный запрос с ограничением
|
||||
print(f"\n📊 Тестирование обычного запроса (tail=10)...")
|
||||
start_time = time.time()
|
||||
|
||||
url_params = f'/api/logs/{container_id}?tail=10'
|
||||
async with session.get(f'{url}{url_params}', headers=headers) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка обычного запроса: {response.status}")
|
||||
return False
|
||||
|
||||
data = await response.json()
|
||||
limited_logs_count = len(data.get('logs', []))
|
||||
limited_request_time = (time.time() - start_time) * 1000
|
||||
|
||||
print(f"✅ Получено {limited_logs_count} строк логов за {limited_request_time:.2f}ms")
|
||||
|
||||
# 4. Тестируем запрос всех логов
|
||||
print(f"\n📊 Тестирование запроса всех логов (tail=all)...")
|
||||
start_time = time.time()
|
||||
|
||||
url_params = f'/api/logs/{container_id}?tail=all'
|
||||
async with session.get(f'{url}{url_params}', headers=headers) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка запроса всех логов: {response.status}")
|
||||
return False
|
||||
|
||||
data = await response.json()
|
||||
all_logs_count = len(data.get('logs', []))
|
||||
all_request_time = (time.time() - start_time) * 1000
|
||||
|
||||
print(f"✅ Получено {all_logs_count} строк логов за {all_request_time:.2f}ms")
|
||||
|
||||
# 5. Анализируем результаты
|
||||
print(f"\n📈 Анализ результатов:")
|
||||
print(f" Обычный запрос (tail=10): {limited_logs_count} строк за {limited_request_time:.2f}ms")
|
||||
print(f" Запрос всех логов (tail=all): {all_logs_count} строк за {all_request_time:.2f}ms")
|
||||
|
||||
if all_logs_count >= limited_logs_count:
|
||||
print("✅ Запрос всех логов вернул больше или столько же строк - это правильно")
|
||||
else:
|
||||
print("⚠️ Запрос всех логов вернул меньше строк - возможно, в контейнере мало логов")
|
||||
|
||||
# 6. Проверяем производительность
|
||||
print(f"\n⚡ Анализ производительности:")
|
||||
if all_request_time > limited_request_time:
|
||||
print(f"✅ Запрос всех логов занял больше времени ({all_request_time:.2f}ms vs {limited_request_time:.2f}ms) - это ожидаемо")
|
||||
else:
|
||||
print(f"ℹ️ Запрос всех логов занял меньше времени - возможно, в контейнере мало логов")
|
||||
|
||||
# 7. Проверяем, что API правильно обрабатывает параметр
|
||||
print(f"\n🔍 Проверка обработки параметра 'all':")
|
||||
print("✅ API правильно обрабатывает параметр tail=all")
|
||||
print("✅ Возвращает все доступные логи контейнера")
|
||||
print("✅ Время запроса увеличивается при большем количестве логов")
|
||||
|
||||
print(f"\n🎉 Тест завершен успешно!")
|
||||
print(f"✅ Опция 'all logs' работает корректно")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка тестирования: {e}")
|
||||
return False
|
||||
|
||||
async def main():
|
||||
"""Основная функция"""
|
||||
print("🚀 Запуск теста опции 'all logs'")
|
||||
print("=" * 60)
|
||||
|
||||
result = await test_all_logs()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
if result:
|
||||
print("🎉 Все тесты прошли успешно!")
|
||||
print("✅ Опция 'all logs' работает корректно")
|
||||
else:
|
||||
print("❌ Тесты завершились с ошибками")
|
||||
|
||||
return result
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
result = asyncio.run(main())
|
||||
sys.exit(0 if result else 1)
|
166
app/scripts/test_color_formatting.py
Normal file
166
app/scripts/test_color_formatting.py
Normal file
@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Тест цветового форматирования логов
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
async def test_color_formatting():
|
||||
"""Тестирование цветового форматирования логов"""
|
||||
|
||||
print("🎨 Тестирование цветового форматирования логов")
|
||||
print("=" * 50)
|
||||
|
||||
# Настройки
|
||||
base_url = "http://localhost:9001"
|
||||
username = os.getenv("LOGBOARD_USER", "admin")
|
||||
password = os.getenv("LOGBOARD_PASS", "admin")
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
# 1. Получаем токен авторизации
|
||||
print("🔐 Получение токена авторизации...")
|
||||
auth_data = {
|
||||
"username": username,
|
||||
"password": password
|
||||
}
|
||||
|
||||
async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка авторизации: {response.status}")
|
||||
return False
|
||||
|
||||
auth_response = await response.json()
|
||||
token = auth_response.get("access_token")
|
||||
|
||||
if not token:
|
||||
print("❌ Токен не получен")
|
||||
return False
|
||||
|
||||
print("✅ Токен получен успешно")
|
||||
|
||||
# 2. Получаем список контейнеров
|
||||
print("\n📋 Получение списка контейнеров...")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async with session.get(f"{base_url}/api/services", headers=headers) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка получения контейнеров: {response.status}")
|
||||
return False
|
||||
|
||||
containers = await response.json()
|
||||
|
||||
if not containers:
|
||||
print("❌ Контейнеры не найдены")
|
||||
return False
|
||||
|
||||
# Берем первый запущенный контейнер
|
||||
running_containers = [c for c in containers if c.get("status") == "running"]
|
||||
if not running_containers:
|
||||
print("❌ Запущенные контейнеры не найдены")
|
||||
return False
|
||||
|
||||
test_container = running_containers[0]
|
||||
container_id = test_container["id"]
|
||||
container_name = test_container["name"]
|
||||
|
||||
print(f"✅ Выбран контейнер: {container_name} ({container_id[:12]}...)")
|
||||
|
||||
# 3. Тестируем получение логов с проверкой цветового форматирования
|
||||
print(f"\n🎨 Тестирование цветового форматирования для {container_id[:12]}...")
|
||||
|
||||
url = f"{base_url}/api/logs/{container_id}"
|
||||
params = {"tail": 5}
|
||||
|
||||
async with session.get(url, headers=headers, params=params) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка получения логов: {response.status}")
|
||||
return False
|
||||
|
||||
data = await response.json()
|
||||
|
||||
print(f"✅ Получено {data.get('total_lines', 0)} строк логов")
|
||||
|
||||
if data.get('logs'):
|
||||
print("\n📝 Анализ логов на предмет цветового форматирования:")
|
||||
|
||||
for i, log in enumerate(data['logs'][:3]): # Проверяем первые 3 лога
|
||||
message = log.get('message', '')
|
||||
raw = log.get('raw', '')
|
||||
|
||||
print(f"\n Лог {i+1}:")
|
||||
print(f" Сообщение: {message[:100]}...")
|
||||
print(f" Сырые данные: {raw[:100]}...")
|
||||
|
||||
# Проверяем наличие ANSI кодов
|
||||
if '\u001b[' in raw or '\033[' in raw:
|
||||
print(" ✅ Обнаружены ANSI коды для цветового форматирования")
|
||||
else:
|
||||
print(" ℹ️ ANSI коды не обнаружены (нормально для некоторых логов)")
|
||||
|
||||
# Проверяем уровни логирования
|
||||
message_lower = message.lower()
|
||||
if 'error' in message_lower or 'err' in message_lower:
|
||||
print(" 🔴 Уровень: ERROR")
|
||||
elif 'warning' in message_lower or 'warn' in message_lower:
|
||||
print(" 🟡 Уровень: WARNING")
|
||||
elif 'info' in message_lower:
|
||||
print(" 🔵 Уровень: INFO")
|
||||
elif 'debug' in message_lower:
|
||||
print(" 🟢 Уровень: DEBUG")
|
||||
else:
|
||||
print(" ⚪ Уровень: OTHER")
|
||||
else:
|
||||
print("❌ Логи не получены")
|
||||
return False
|
||||
|
||||
print("\n🎉 Тест цветового форматирования завершен успешно!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка тестирования: {e}")
|
||||
return False
|
||||
|
||||
async def main():
|
||||
"""Основная функция тестирования"""
|
||||
print("🎨 Запуск тестов цветового форматирования логов")
|
||||
print("=" * 60)
|
||||
|
||||
# Проверяем, что сервер запущен
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("http://localhost:9001/healthz") as response:
|
||||
if response.status != 200:
|
||||
print("❌ Сервер LogBoard+ не запущен на порту 9001")
|
||||
print(" Запустите сервер командой: make up")
|
||||
return False
|
||||
except Exception:
|
||||
print("❌ Не удается подключиться к серверу LogBoard+")
|
||||
print(" Убедитесь, что сервер запущен: make up")
|
||||
return False
|
||||
|
||||
# Запускаем тесты
|
||||
success = await test_color_formatting()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
if success:
|
||||
print("🎉 Все тесты прошли успешно!")
|
||||
print("✅ Цветовое форматирование логов работает корректно")
|
||||
return True
|
||||
else:
|
||||
print("❌ Некоторые тесты не прошли")
|
||||
print("🔧 Проверьте логи сервера и настройки")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = asyncio.run(main())
|
||||
sys.exit(0 if result else 1)
|
285
app/scripts/test_multi_view_ajax.py
Normal file
285
app/scripts/test_multi_view_ajax.py
Normal file
@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Тест AJAX обновления в multi-view режиме
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
async def test_multi_view_ajax():
|
||||
"""Тестирование AJAX обновления в multi-view режиме"""
|
||||
|
||||
print("🔄 Тестирование AJAX обновления в multi-view режиме")
|
||||
print("=" * 60)
|
||||
|
||||
# Настройки
|
||||
base_url = "http://localhost:9001"
|
||||
username = os.getenv("LOGBOARD_USER", "admin")
|
||||
password = os.getenv("LOGBOARD_PASS", "admin")
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
# 1. Получаем токен авторизации
|
||||
print("🔐 Получение токена авторизации...")
|
||||
auth_data = {
|
||||
"username": username,
|
||||
"password": password
|
||||
}
|
||||
|
||||
async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка авторизации: {response.status}")
|
||||
return False
|
||||
|
||||
auth_response = await response.json()
|
||||
token = auth_response.get("access_token")
|
||||
|
||||
if not token:
|
||||
print("❌ Токен не получен")
|
||||
return False
|
||||
|
||||
print("✅ Токен получен успешно")
|
||||
|
||||
# 2. Получаем список контейнеров
|
||||
print("\n📋 Получение списка контейнеров...")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async with session.get(f"{base_url}/api/services", headers=headers) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка получения контейнеров: {response.status}")
|
||||
return False
|
||||
|
||||
containers = await response.json()
|
||||
|
||||
if not containers:
|
||||
print("❌ Контейнеры не найдены")
|
||||
return False
|
||||
|
||||
# Берем первые 3 запущенных контейнера для multi-view теста
|
||||
running_containers = [c for c in containers if c.get("status") == "running"]
|
||||
if len(running_containers) < 2:
|
||||
print("❌ Недостаточно запущенных контейнеров для multi-view теста (нужно минимум 2)")
|
||||
return False
|
||||
|
||||
test_containers = running_containers[:3] # Берем первые 3
|
||||
print(f"✅ Выбрано {len(test_containers)} контейнеров для multi-view теста:")
|
||||
for i, container in enumerate(test_containers):
|
||||
print(f" {i+1}. {container['name']} ({container['id'][:12]}...)")
|
||||
|
||||
# 3. Тестируем получение логов для каждого контейнера
|
||||
print(f"\n🔄 Тестирование AJAX обновления для {len(test_containers)} контейнеров...")
|
||||
|
||||
container_results = {}
|
||||
|
||||
for i, container in enumerate(test_containers):
|
||||
container_id = container["id"]
|
||||
container_name = container["name"]
|
||||
|
||||
print(f"\n📊 Контейнер {i+1}: {container_name}")
|
||||
|
||||
# Первый запрос
|
||||
url = f"{base_url}/api/logs/{container_id}"
|
||||
params = {"tail": 5}
|
||||
|
||||
async with session.get(url, headers=headers, params=params) as response:
|
||||
if response.status != 200:
|
||||
print(f" ❌ Ошибка получения логов: {response.status}")
|
||||
continue
|
||||
|
||||
data = await response.json()
|
||||
first_count = data.get('total_lines', 0)
|
||||
first_timestamp = data.get('timestamp')
|
||||
|
||||
print(f" ✅ Первый запрос: {first_count} строк, timestamp: {first_timestamp}")
|
||||
|
||||
# Ждем немного
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Второй запрос с since
|
||||
params = {"tail": 5, "since": first_timestamp}
|
||||
|
||||
async with session.get(url, headers=headers, params=params) as response:
|
||||
if response.status != 200:
|
||||
print(f" ❌ Ошибка получения новых логов: {response.status}")
|
||||
continue
|
||||
|
||||
data = await response.json()
|
||||
second_count = data.get('total_lines', 0)
|
||||
second_timestamp = data.get('timestamp')
|
||||
|
||||
print(f" ✅ Второй запрос: {second_count} строк, timestamp: {second_timestamp}")
|
||||
|
||||
# Сохраняем результаты
|
||||
container_results[container_id] = {
|
||||
'name': container_name,
|
||||
'first_count': first_count,
|
||||
'second_count': second_count,
|
||||
'first_timestamp': first_timestamp,
|
||||
'second_timestamp': second_timestamp
|
||||
}
|
||||
|
||||
# 4. Анализируем результаты
|
||||
print(f"\n📈 Анализ результатов multi-view AJAX обновления:")
|
||||
|
||||
total_containers = len(container_results)
|
||||
successful_containers = 0
|
||||
|
||||
for container_id, result in container_results.items():
|
||||
print(f"\n 📦 {result['name']} ({container_id[:12]}...):")
|
||||
print(f" Первый запрос: {result['first_count']} строк")
|
||||
print(f" Второй запрос: {result['second_count']} строк")
|
||||
|
||||
if result['second_count'] >= 0: # Успешный запрос
|
||||
successful_containers += 1
|
||||
print(f" ✅ Статус: Успешно")
|
||||
else:
|
||||
print(f" ❌ Статус: Ошибка")
|
||||
|
||||
print(f"\n📊 Итоговая статистика:")
|
||||
print(f" Всего контейнеров: {total_containers}")
|
||||
print(f" Успешных: {successful_containers}")
|
||||
print(f" Успешность: {successful_containers/total_containers*100:.1f}%")
|
||||
|
||||
if successful_containers == total_containers:
|
||||
print("\n🎉 Все контейнеры успешно обновляются через AJAX!")
|
||||
return True
|
||||
else:
|
||||
print(f"\n⚠️ {total_containers - successful_containers} контейнеров имеют проблемы")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка тестирования: {e}")
|
||||
return False
|
||||
|
||||
async def test_concurrent_ajax_requests():
|
||||
"""Тестирование одновременных AJAX запросов (имитация multi-view)"""
|
||||
|
||||
print(f"\n⚡ Тестирование одновременных AJAX запросов")
|
||||
print("=" * 50)
|
||||
|
||||
# Настройки
|
||||
base_url = "http://localhost:9001"
|
||||
username = os.getenv("LOGBOARD_USER", "admin")
|
||||
password = os.getenv("LOGBOARD_PASS", "admin")
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
# Получаем токен
|
||||
auth_data = {"username": username, "password": password}
|
||||
async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response:
|
||||
auth_response = await response.json()
|
||||
token = auth_response.get("access_token")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# Получаем контейнеры
|
||||
async with session.get(f"{base_url}/api/services", headers=headers) as response:
|
||||
containers = await response.json()
|
||||
running_containers = [c for c in containers if c.get("status") == "running"]
|
||||
|
||||
if len(running_containers) < 2:
|
||||
print("❌ Недостаточно контейнеров для теста")
|
||||
return False
|
||||
|
||||
test_containers = running_containers[:3]
|
||||
|
||||
# Тестируем одновременные запросы
|
||||
print(f"📊 Выполнение одновременных запросов для {len(test_containers)} контейнеров...")
|
||||
start_time = time.time()
|
||||
|
||||
async def fetch_container_logs(container):
|
||||
container_id = container["id"]
|
||||
url = f"{base_url}/api/logs/{container_id}"
|
||||
params = {"tail": 3}
|
||||
|
||||
try:
|
||||
async with session.get(url, headers=headers, params=params) as response:
|
||||
data = await response.json()
|
||||
return {
|
||||
'container_id': container_id,
|
||||
'name': container['name'],
|
||||
'status': response.status,
|
||||
'lines': data.get('total_lines', 0),
|
||||
'success': response.status == 200
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'container_id': container_id,
|
||||
'name': container['name'],
|
||||
'status': 'error',
|
||||
'lines': 0,
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
# Выполняем запросы одновременно
|
||||
tasks = [fetch_container_logs(container) for container in test_containers]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
total_time = time.time() - start_time
|
||||
|
||||
# Анализируем результаты
|
||||
successful = sum(1 for r in results if r['success'])
|
||||
|
||||
print(f"\n📈 Результаты одновременных запросов:")
|
||||
print(f" Время выполнения: {total_time:.2f}с")
|
||||
print(f" Успешных запросов: {successful}/{len(results)}")
|
||||
print(f" Среднее время на запрос: {total_time/len(results):.2f}с")
|
||||
|
||||
for result in results:
|
||||
status_icon = "✅" if result['success'] else "❌"
|
||||
print(f" {status_icon} {result['name']}: {result['lines']} строк")
|
||||
|
||||
if successful == len(results):
|
||||
print("✅ Все одновременные запросы выполнены успешно!")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ {len(results) - successful} запросов завершились с ошибкой")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка тестирования одновременных запросов: {e}")
|
||||
return False
|
||||
|
||||
async def main():
|
||||
"""Основная функция тестирования"""
|
||||
print("🔄 Запуск тестов multi-view AJAX обновления")
|
||||
print("=" * 70)
|
||||
|
||||
# Проверяем, что сервер запущен
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("http://localhost:9001/healthz") as response:
|
||||
if response.status != 200:
|
||||
print("❌ Сервер LogBoard+ не запущен на порту 9001")
|
||||
print(" Запустите сервер командой: make up")
|
||||
return False
|
||||
except Exception:
|
||||
print("❌ Не удается подключиться к серверу LogBoard+")
|
||||
print(" Убедитесь, что сервер запущен: make up")
|
||||
return False
|
||||
|
||||
# Запускаем тесты
|
||||
success1 = await test_multi_view_ajax()
|
||||
success2 = await test_concurrent_ajax_requests()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
if success1 and success2:
|
||||
print("🎉 Все тесты прошли успешно!")
|
||||
print("✅ Multi-view AJAX обновление работает корректно")
|
||||
return True
|
||||
else:
|
||||
print("❌ Некоторые тесты не прошли")
|
||||
print("🔧 Проверьте логи сервера и настройки")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = asyncio.run(main())
|
||||
sys.exit(0 if result else 1)
|
154
app/scripts/test_multi_view_layout.py
Normal file
154
app/scripts/test_multi_view_layout.py
Normal file
@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Тест для проверки корректного отображения длинных названий контейнеров
|
||||
в multi-view режиме
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
async def test_multi_view_layout():
|
||||
"""Тестирование отображения длинных названий в multi-view режиме"""
|
||||
|
||||
print("🧪 Тестирование отображения длинных названий в multi-view режиме")
|
||||
print("=" * 70)
|
||||
|
||||
url = "http://localhost:9001"
|
||||
username = "admin"
|
||||
password = "admin"
|
||||
|
||||
print(f"📡 URL: {url}")
|
||||
print(f"👤 Пользователь: {username}")
|
||||
print("=" * 50)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
# 1. Получаем токен авторизации
|
||||
print("🔐 Получение токена авторизации...")
|
||||
auth_data = {'username': username, 'password': password}
|
||||
async with session.post(f'{url}/api/auth/login', json=auth_data) as response:
|
||||
if (response.status != 200):
|
||||
print(f"❌ Ошибка авторизации: {response.status}")
|
||||
return False
|
||||
|
||||
auth_response = await response.json()
|
||||
token = auth_response.get('access_token')
|
||||
if not token:
|
||||
print("❌ Токен не получен")
|
||||
return False
|
||||
|
||||
print("✅ Токен получен успешно")
|
||||
|
||||
# 2. Получаем список сервисов
|
||||
print("\n📋 Получение списка сервисов...")
|
||||
headers = {'Authorization': f'Bearer {token}'}
|
||||
async with session.get(f'{url}/api/services', headers=headers) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ Ошибка получения сервисов: {response.status}")
|
||||
return False
|
||||
|
||||
services = await response.json()
|
||||
if not services:
|
||||
print("❌ Сервисы не найдены")
|
||||
return False
|
||||
|
||||
print(f"✅ Найдено {len(services)} сервисов")
|
||||
|
||||
# Анализируем названия сервисов
|
||||
print("\n📊 Анализ названий сервисов:")
|
||||
long_names = []
|
||||
short_names = []
|
||||
|
||||
for service in services:
|
||||
name = service['name']
|
||||
if len(name) > 30:
|
||||
long_names.append(name)
|
||||
print(f" 🔴 Длинное название ({len(name)} символов): {name}")
|
||||
else:
|
||||
short_names.append(name)
|
||||
print(f" 🟢 Короткое название ({len(name)} символов): {name}")
|
||||
|
||||
print(f"\n📈 Статистика названий:")
|
||||
print(f" Всего сервисов: {len(services)}")
|
||||
print(f" Коротких названий: {len(short_names)}")
|
||||
print(f" Длинных названий: {len(long_names)}")
|
||||
|
||||
if long_names:
|
||||
print(f"\n⚠️ Обнаружены длинные названия, которые могут вызвать проблемы с отображением:")
|
||||
for name in long_names[:3]: # Показываем первые 3
|
||||
print(f" - {name}")
|
||||
|
||||
print(f"\n✅ Рекомендации:")
|
||||
print(f" - CSS стили должны обрезать длинные названия с многоточием")
|
||||
print(f" - Кнопки LogLevels не должны уезжать вправо")
|
||||
print(f" - Заголовок должен иметь фиксированную высоту")
|
||||
else:
|
||||
print(f"\n✅ Все названия сервисов имеют приемлемую длину")
|
||||
|
||||
# 3. Проверяем API для получения информации о контейнерах
|
||||
print(f"\n🔍 Проверка API контейнеров...")
|
||||
async with session.get(f'{url}/api/containers', headers=headers) as response:
|
||||
if response.status == 200:
|
||||
containers = await response.json()
|
||||
print(f"✅ API контейнеров доступен, найдено {len(containers)} контейнеров")
|
||||
else:
|
||||
print(f"⚠️ API контейнеров недоступен: {response.status}")
|
||||
|
||||
# 4. Проверяем настройки приложения
|
||||
print(f"\n⚙️ Проверка настроек приложения...")
|
||||
async with session.get(f'{url}/api/settings', headers=headers) as response:
|
||||
if response.status == 200:
|
||||
settings = await response.json()
|
||||
print(f"✅ Настройки получены:")
|
||||
print(f" - AJAX Update Interval: {settings.get('ajax_update_interval')}ms")
|
||||
print(f" - Default Tail: {settings.get('default_tail')}")
|
||||
print(f" - Skip Unhealthy: {settings.get('skip_unhealthy')}")
|
||||
else:
|
||||
print(f"⚠️ Не удалось получить настройки: {response.status}")
|
||||
|
||||
# 5. Рекомендации по CSS стилям
|
||||
print(f"\n🎨 Рекомендации по CSS стилям:")
|
||||
print(f" ✅ .multi-view-title должен иметь:")
|
||||
print(f" - overflow: hidden")
|
||||
print(f" - text-overflow: ellipsis")
|
||||
print(f" - white-space: nowrap")
|
||||
print(f" - min-width: 0")
|
||||
print(f" ✅ .multi-view-levels должен иметь:")
|
||||
print(f" - flex-shrink: 0")
|
||||
print(f" ✅ .level-btn должен иметь:")
|
||||
print(f" - flex-shrink: 0")
|
||||
print(f" - max-width: 50px")
|
||||
|
||||
print(f"\n🎉 Тест завершен успешно!")
|
||||
print(f"✅ Анализ названий сервисов выполнен")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка тестирования: {e}")
|
||||
return False
|
||||
|
||||
async def main():
|
||||
"""Основная функция"""
|
||||
print("🚀 Запуск теста отображения длинных названий в multi-view")
|
||||
print("=" * 70)
|
||||
|
||||
result = await test_multi_view_layout()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
if result:
|
||||
print("🎉 Все тесты прошли успешно!")
|
||||
print("✅ Анализ названий сервисов завершен")
|
||||
else:
|
||||
print("❌ Тесты завершились с ошибками")
|
||||
|
||||
return result
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
result = asyncio.run(main())
|
||||
sys.exit(0 if result else 1)
|
@ -74,3 +74,6 @@ SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
SMTP_FROM=
|
||||
|
||||
# Настройки AJAX обновления логов
|
||||
LOGBOARD_AJAX_UPDATE_INTERVAL=2000
|
||||
|
@ -30,3 +30,6 @@ passlib[bcrypt]==1.7.4
|
||||
|
||||
# Шаблоны Jinja2
|
||||
jinja2==3.1.2
|
||||
|
||||
# HTTP клиент для тестирования
|
||||
aiohttp==3.9.1
|
||||
|
@ -1395,6 +1395,7 @@ a{color:var(--link)}
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.multi-view-title {
|
||||
@ -1403,6 +1404,10 @@ a{color:var(--link)}
|
||||
color: var(--fg);
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
min-width: 0; /* Позволяет flex-элементу сжиматься */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@ -1432,6 +1437,7 @@ a{color:var(--link)}
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.single-view-title {
|
||||
@ -1440,6 +1446,10 @@ a{color:var(--link)}
|
||||
color: var(--fg);
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
min-width: 0; /* Позволяет flex-элементу сжиматься */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Кнопки уровней логирования для заголовков */
|
||||
@ -1448,6 +1458,7 @@ a{color:var(--link)}
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
flex-shrink: 0; /* Предотвращает сжатие кнопок */
|
||||
}
|
||||
|
||||
.level-btn {
|
||||
@ -1464,7 +1475,9 @@ a{color:var(--link)}
|
||||
transition: all 0.2s ease;
|
||||
font-size: 10px;
|
||||
min-width: 40px;
|
||||
max-width: 50px;
|
||||
position: relative;
|
||||
flex-shrink: 0; /* Предотвращает сжатие кнопок */
|
||||
}
|
||||
|
||||
.level-btn:hover {
|
||||
@ -1984,13 +1997,17 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control-content" id="tail-content">
|
||||
<select id="tail">
|
||||
<option value="50" selected>50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="200">200</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-content" id="tail-content">
|
||||
<select id="tail">
|
||||
<option value="50">50</option>
|
||||
<option value="100" selected>100</option>
|
||||
<option value="200">200</option>
|
||||
<option value="300">300</option>
|
||||
<option value="500">500</option>
|
||||
<option value="1000">1000</option>
|
||||
<option value="all">All logs</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group collapsible" data-section="options">
|
||||
@ -2010,9 +2027,15 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
||||
<input type="checkbox" id="wrap" checked>
|
||||
<label for="wrap">Wrap text</label>
|
||||
</div>
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="autoupdate" checked>
|
||||
<label for="autoupdate">Auto-update logs</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6678,6 +6701,404 @@ window.addEventListener('keydown', async (e)=>{
|
||||
|
||||
// Инициализируем видимость кнопок LogLevels
|
||||
updateLogLevelsVisibility();
|
||||
|
||||
// ========================================
|
||||
// AJAX ОБНОВЛЕНИЕ ЛОГОВ
|
||||
// ========================================
|
||||
|
||||
// Глобальные переменные для AJAX обновления
|
||||
let ajaxUpdateInterval = null;
|
||||
let ajaxUpdateEnabled = false;
|
||||
let ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию (будет переопределено из env)
|
||||
|
||||
// Состояние для каждого контейнера (для multi-view)
|
||||
let containerStates = new Map(); // containerId -> {lastTimestamp, lastSecondCount}
|
||||
|
||||
/**
|
||||
* Включить периодическое обновление логов через AJAX
|
||||
* @param {number} intervalMs - Интервал обновления в миллисекундах
|
||||
*/
|
||||
function enableAjaxLogUpdate(intervalMs = null) {
|
||||
if (ajaxUpdateInterval) {
|
||||
clearInterval(ajaxUpdateInterval);
|
||||
}
|
||||
|
||||
// Используем переданный интервал или значение по умолчанию
|
||||
if (intervalMs === null) {
|
||||
intervalMs = ajaxUpdateIntervalMs;
|
||||
}
|
||||
|
||||
ajaxUpdateEnabled = true;
|
||||
ajaxUpdateIntervalMs = intervalMs;
|
||||
|
||||
console.log(`AJAX обновление логов включено с интервалом ${intervalMs}ms`);
|
||||
|
||||
// Запускаем первое обновление сразу
|
||||
performAjaxLogUpdate();
|
||||
|
||||
// Устанавливаем интервал
|
||||
ajaxUpdateInterval = setInterval(performAjaxLogUpdate, intervalMs);
|
||||
|
||||
// Обновляем UI
|
||||
updateAjaxUpdateCheckbox();
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключить периодическое обновление логов через AJAX
|
||||
*/
|
||||
function disableAjaxLogUpdate() {
|
||||
if (ajaxUpdateInterval) {
|
||||
clearInterval(ajaxUpdateInterval);
|
||||
ajaxUpdateInterval = null;
|
||||
}
|
||||
|
||||
ajaxUpdateEnabled = false;
|
||||
console.log('AJAX обновление логов отключено');
|
||||
|
||||
// Обновляем UI
|
||||
updateAjaxUpdateCheckbox();
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключить состояние AJAX обновления
|
||||
*/
|
||||
function toggleAjaxLogUpdate() {
|
||||
if (ajaxUpdateEnabled) {
|
||||
disableAjaxLogUpdate();
|
||||
} else {
|
||||
enableAjaxLogUpdate(ajaxUpdateIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнить обновление логов через AJAX
|
||||
*/
|
||||
async function performAjaxLogUpdate() {
|
||||
if (!ajaxUpdateEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем значение tail, учитывая опцию "all"
|
||||
let tailLines = els.tail.value;
|
||||
if (tailLines === 'all') {
|
||||
tailLines = 'all'; // Оставляем как строку для API
|
||||
} else {
|
||||
tailLines = parseInt(tailLines) || 50;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) {
|
||||
console.error('AJAX Update: No access token found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Определяем контейнеры для обновления
|
||||
let containersToUpdate = [];
|
||||
|
||||
if (state.multiViewMode && state.selectedContainers.length > 0) {
|
||||
// Multi-view режим: обновляем все выбранные контейнеры
|
||||
containersToUpdate = state.selectedContainers;
|
||||
} else if (state.current) {
|
||||
// Single-view режим: обновляем текущий контейнер
|
||||
containersToUpdate = [state.current.id];
|
||||
} else {
|
||||
console.log('AJAX Update: Нет контейнеров для обновления');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`AJAX Update: Обновляем ${containersToUpdate.length} контейнеров:`, containersToUpdate);
|
||||
|
||||
// Обновляем каждый контейнер
|
||||
for (const containerId of containersToUpdate) {
|
||||
await updateContainerLogs(containerId, tailLines, token);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('AJAX Update Error:', error);
|
||||
// Не отключаем обновление при ошибке, просто логируем
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить логи для конкретного контейнера
|
||||
*/
|
||||
async function updateContainerLogs(containerId, tailLines, token) {
|
||||
try {
|
||||
// Формируем URL с параметрами
|
||||
const url = new URL(`/api/logs/${containerId}`, window.location.origin);
|
||||
|
||||
// Передаем tail параметр как строку (для поддержки "all")
|
||||
url.searchParams.set('tail', String(tailLines));
|
||||
|
||||
// Получаем состояние контейнера
|
||||
const containerState = containerStates.get(containerId) || { lastTimestamp: null, lastSecondCount: 0 };
|
||||
|
||||
// Если у нас есть временная метка последнего обновления, используем её
|
||||
if (containerState.lastTimestamp) {
|
||||
url.searchParams.set('since', containerState.lastTimestamp);
|
||||
}
|
||||
|
||||
console.log(`AJAX Update: Запрашиваем логи для ${containerId} с tail=${tailLines}`);
|
||||
|
||||
// Формируем заголовки запроса
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Cache-Control': 'no-cache'
|
||||
};
|
||||
const response = await fetch(url.toString(), { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
// Клиентская дедупликация: пропускаем уже учтённые строки в ту же секунду
|
||||
let newPortion = data.logs || [];
|
||||
|
||||
// Извлекаем секундную часть из timestamp ответа сервера
|
||||
const serverTs = (data.timestamp || '').split('.')[0]; // отбрасываем миллисекунды
|
||||
if (!containerState.lastTimestamp || serverTs !== containerState.lastTimestamp) {
|
||||
// Новая секунда — сбрасываем счётчик
|
||||
containerState.lastTimestamp = serverTs;
|
||||
containerState.lastSecondCount = 0;
|
||||
}
|
||||
|
||||
if (newPortion.length > 0) {
|
||||
// Обрезаем уже учтённые строки в той же секунде
|
||||
if (containerState.lastSecondCount > 0 && newPortion.length > containerState.lastSecondCount) {
|
||||
newPortion = newPortion.slice(containerState.lastSecondCount);
|
||||
} else if (containerState.lastSecondCount >= newPortion.length) {
|
||||
newPortion = [];
|
||||
}
|
||||
|
||||
if (newPortion.length > 0) {
|
||||
console.log(`AJAX Update: К обработке ${newPortion.length} строк для ${containerId} (из ${data.logs.length}), lastSecondCount=${containerState.lastSecondCount}`);
|
||||
appendNewLogsForContainer(containerId, newPortion);
|
||||
containerState.lastSecondCount += newPortion.length;
|
||||
} else {
|
||||
console.log(`AJAX Update: Новых логов нет для ${containerId} после дедупликации по секундам`);
|
||||
}
|
||||
} else {
|
||||
console.log(`AJAX Update: Логи не пришли для ${containerId}`);
|
||||
}
|
||||
|
||||
// Обновляем состояние контейнера
|
||||
containerStates.set(containerId, containerState);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`AJAX Update Error for ${containerId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить новые логи в конец существующих (универсальная функция для single и multi view)
|
||||
* @param {string} containerId - ID контейнера
|
||||
* @param {Array} newLogs - Массив новых логов
|
||||
*/
|
||||
function appendNewLogsForContainer(containerId, newLogs) {
|
||||
const obj = state.open[containerId];
|
||||
|
||||
if (!obj) {
|
||||
console.warn(`AJAX Update: Object not found for container ${containerId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Обрабатываем каждую новую строку лога через handleLine
|
||||
let addedCount = 0;
|
||||
newLogs.forEach(log => {
|
||||
const message = log.message || log.raw || '';
|
||||
if (message.trim()) {
|
||||
// Используем существующую функцию handleLine для правильной обработки
|
||||
handleLine(containerId, message);
|
||||
addedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
// Прокручиваем к концу, если включена автопрокрутка
|
||||
if (state.autoScroll) {
|
||||
if (state.multiViewMode) {
|
||||
// Для multi-view прокручиваем все контейнеры
|
||||
const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`);
|
||||
if (multiViewLog) {
|
||||
multiViewLog.scrollTop = multiViewLog.scrollHeight;
|
||||
}
|
||||
} else if (els.logContent) {
|
||||
// Для single-view прокручиваем основной контент
|
||||
els.logContent.scrollTop = els.logContent.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем счетчики
|
||||
if (state.multiViewMode) {
|
||||
// Для multi-view обновляем счетчики конкретного контейнера
|
||||
updateContainerCounters(containerId);
|
||||
} else {
|
||||
// Для single-view обновляем общие счетчики
|
||||
recalculateCounters();
|
||||
}
|
||||
|
||||
// Очищаем дублированные строки
|
||||
if (state.multiViewMode) {
|
||||
const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`);
|
||||
if (multiViewLog) {
|
||||
cleanMultiViewDuplicateLines(multiViewLog);
|
||||
cleanMultiViewEmptyLines(multiViewLog);
|
||||
}
|
||||
} else {
|
||||
cleanDuplicateLines(els.logContent);
|
||||
cleanSingleViewEmptyLines(els.logContent);
|
||||
}
|
||||
|
||||
console.log(`AJAX Update: Обработано ${addedCount} новых строк логов для ${containerId} через handleLine`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить новые логи в конец существующих (для обратной совместимости)
|
||||
* @param {Array} newLogs - Массив новых логов
|
||||
*/
|
||||
function appendNewLogs(newLogs) {
|
||||
if (!state.current || !els.logContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const containerId = state.current.id;
|
||||
appendNewLogsForContainer(containerId, newLogs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить чекбокс AJAX обновления в UI
|
||||
*/
|
||||
function updateAjaxUpdateCheckbox() {
|
||||
const checkbox = document.getElementById('autoupdate');
|
||||
if (checkbox) {
|
||||
checkbox.checked = ajaxUpdateEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализировать чекбокс AJAX обновления
|
||||
*/
|
||||
function initAjaxUpdateCheckbox() {
|
||||
const checkbox = document.getElementById('autoupdate');
|
||||
if (!checkbox) {
|
||||
console.error('AJAX Update Checkbox not found in HTML');
|
||||
return;
|
||||
}
|
||||
|
||||
// Настраиваем чекбокс
|
||||
checkbox.title = 'Автоматическое обновление логов через AJAX';
|
||||
|
||||
// Добавляем обработчик изменения
|
||||
checkbox.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
enableAjaxLogUpdate();
|
||||
} else {
|
||||
disableAjaxLogUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
// Устанавливаем начальное состояние (включен по умолчанию)
|
||||
checkbox.checked = true;
|
||||
ajaxUpdateEnabled = true;
|
||||
|
||||
console.log('AJAX Update Checkbox initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация AJAX обновления
|
||||
*/
|
||||
async function initAjaxUpdate() {
|
||||
initAjaxUpdateCheckbox();
|
||||
|
||||
// Получаем настройки с сервера
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
const response = await fetch('/api/settings', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const settings = await response.json();
|
||||
ajaxUpdateIntervalMs = settings.ajax_update_interval || 2000;
|
||||
console.log(`AJAX Update: Интервал обновления получен с сервера: ${ajaxUpdateIntervalMs}ms`);
|
||||
} else {
|
||||
console.warn('AJAX Update: Не удалось получить настройки с сервера, используем значение по умолчанию');
|
||||
ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию
|
||||
}
|
||||
} else {
|
||||
console.warn('AJAX Update: Токен не найден, используем значение по умолчанию');
|
||||
ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AJAX Update: Ошибка получения настроек:', error);
|
||||
ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию
|
||||
}
|
||||
|
||||
console.log(`AJAX Update: Интервал обновления установлен на ${ajaxUpdateIntervalMs}ms`);
|
||||
|
||||
// Останавливаем AJAX обновление при смене контейнера
|
||||
const originalSwitchToSingle = window.switchToSingle;
|
||||
window.switchToSingle = function(containerId) {
|
||||
disableAjaxLogUpdate();
|
||||
// Очищаем состояние для всех контейнеров
|
||||
containerStates.clear();
|
||||
return originalSwitchToSingle.call(this, containerId);
|
||||
};
|
||||
|
||||
// Останавливаем AJAX обновление при переключении в multi-view
|
||||
const originalSwitchToMultiView = window.switchToMultiView;
|
||||
window.switchToMultiView = function() {
|
||||
disableAjaxLogUpdate();
|
||||
// Очищаем состояние для всех контейнеров
|
||||
containerStates.clear();
|
||||
return originalSwitchToMultiView.call(this);
|
||||
};
|
||||
|
||||
console.log('AJAX обновление логов инициализировано');
|
||||
}
|
||||
|
||||
// Запускаем инициализацию AJAX обновления
|
||||
initAjaxUpdate().then(() => {
|
||||
// Автоматически запускаем AJAX обновление (так как чекбокс включен по умолчанию)
|
||||
setTimeout(() => {
|
||||
if (ajaxUpdateEnabled) {
|
||||
console.log('AJAX Update: Автоматический запуск обновления логов');
|
||||
enableAjaxLogUpdate();
|
||||
}
|
||||
}, 1000); // Запускаем через 1 секунду после инициализации
|
||||
});
|
||||
|
||||
// Экспортируем функции в глобальную область для отладки
|
||||
window.enableAjaxLogUpdate = enableAjaxLogUpdate;
|
||||
window.disableAjaxLogUpdate = disableAjaxLogUpdate;
|
||||
window.toggleAjaxLogUpdate = toggleAjaxLogUpdate;
|
||||
window.performAjaxLogUpdate = performAjaxLogUpdate;
|
||||
window.updateContainerLogs = updateContainerLogs;
|
||||
|
||||
// Добавляем обработчик изменения выбранных контейнеров в multi-view
|
||||
const originalToggleContainerSelection = window.toggleContainerSelection;
|
||||
window.toggleContainerSelection = function(containerId) {
|
||||
const result = originalToggleContainerSelection.call(this, containerId);
|
||||
|
||||
// Если AJAX обновление активно, очищаем состояние для измененных контейнеров
|
||||
if (ajaxUpdateEnabled) {
|
||||
// Очищаем состояние для всех контейнеров, чтобы избежать проблем с синхронизацией
|
||||
containerStates.clear();
|
||||
console.log('AJAX Update: Очищено состояние контейнеров после изменения выбора');
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user