feat: улучшения UI/UX LogBoard+

- Добавлена кнопка Update для управления AJAX auto-update
- AJAX auto-update включен по умолчанию
- Улучшено управление видимостью кнопки Refresh
- Переупорядочены кнопки в header (Update, Refresh)
- Унифицированы стили кнопок (высота, шрифт, границы)
- Добавлен hover эффект для кнопки options с цветом warning
- Позиционирование help-btn в свернутом sidebar
- Уменьшена ширина свернутого sidebar на 30%
- Добавлена логика разворачивания sidebar при клике на options
- Отображение внешнего порта в статусе контейнера
- Показ 'standalone' для контейнеров без проекта
- Обновлена документация

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
2025-08-18 20:32:46 +03:00
parent 6e51f00791
commit 749b40a494
20 changed files with 1162 additions and 1649 deletions

View File

@@ -1,184 +1,172 @@
# AJAX Обновление Логов
# AJAX Auto-update для LogBoard+
## Описание
Функциональность AJAX обновления логов позволяет периодически получать новые логи контейнеров без необходимости обновления всей страницы. Это особенно полезно для мониторинга логов в реальном времени с минимальной нагрузкой на сервер.
AJAX Auto-update - это система автоматического обновления логов через AJAX запросы, которая позволяет получать новые логи без перезагрузки страницы.
## Принцип работы
## Основные возможности
1. **Загрузка истории**: История логов загружается через WebSocket при открытии контейнера
2. **Периодические запросы**: Система отправляет AJAX запросы к API эндпоинту `/api/logs/{container_id}` с заданным интервалом
3. **Умное сравнение**: Новые логи сравниваются с уже отображенными, добавляются только новые записи
4. **Автоматическое обновление**: Счетчики логов и фильтры применяются автоматически к новым данным
5. **Управление состоянием**: AJAX обновление автоматически останавливается при смене контейнера или переключении режимов
6. **Multi-view поддержка**: В режиме multi-view обновляются логи всех выбранных контейнеров одновременно
- **Автоматическое обновление**: Логи обновляются с заданным интервалом
- **Умное управление кнопкой Refresh**: Кнопка refresh автоматически скрывается при включенном AJAX autoupdate и показывается при выключенном
- **Поддержка Multi-view**: Работает как в single-view, так и в multi-view режимах
- **Настраиваемый интервал**: Интервал обновления настраивается через API
- **Эффективное обновление**: Обновляются только новые логи с момента последнего запроса
### Преимущества:
## Управление кнопками
- **Полнота данных**: История логов загружается при открытии контейнера
- **Реальное время**: Новые логи обновляются через AJAX
- **Гибкость**: Работает как с WebSocket для истории, так и с AJAX для обновлений
### Кнопка Refresh
## API Эндпоинты
Кнопка refresh в header автоматически управляется в зависимости от состояния AJAX autoupdate:
### GET /api/logs/{container_id}
- **AJAX autoupdate включен** → Кнопка refresh **скрыта**
- **AJAX autoupdate выключен** → Кнопка refresh **показана**
Получает логи контейнера через AJAX.
### Кнопка Update
### GET /api/settings
Кнопка update в header показывает состояние AJAX autoupdate и позволяет переключать его:
Получает настройки приложения.
**Пример ответа:**
```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
- **AJAX autoupdate включен** → Кнопка update **зеленая**
- **AJAX autoupdate выключен** → Кнопка update **красная**
- **Клик по кнопке** → Переключает состояние AJAX autoupdate
### Функции управления
```javascript
// Включить AJAX обновление
enableAjaxLogUpdate(intervalMs);
// Отключить AJAX обновление
disableAjaxLogUpdate();
// Переключить состояние
toggleAjaxLogUpdate();
// Выполнить одно обновление
performAjaxLogUpdate();
/**
* Обновить видимость кнопки refresh в header
*/
function updateRefreshButtonVisibility() {
const refreshButtons = document.querySelectorAll('.log-refresh-btn');
refreshButtons.forEach(btn => {
if (ajaxUpdateEnabled) {
// Если ajax autoupdate включен, скрываем кнопку refresh
btn.style.display = 'none';
} else {
// Если ajax autoupdate выключен, показываем кнопку refresh
btn.style.display = 'inline-flex';
}
});
}
```
### Глобальные переменные
### Автоматическое обновление видимости
Видимость кнопки refresh автоматически обновляется в следующих случаях:
1. **При инициализации AJAX update**
2. **При изменении состояния чекбокса "Auto-update logs"**
3. **При программном включении/выключении AJAX update**
4. **При переключении состояния через функцию toggleAjaxLogUpdate**
## Настройки
### Интервал обновления
Интервал обновления настраивается через API endpoint `/api/settings`:
```json
{
"ajax_update_interval": 2000
}
```
По умолчанию используется интервал 2000ms (2 секунды).
### Чекбокс управления
В sidebar есть чекбокс "Auto-update logs", который позволяет пользователю:
- Включить автоматическое обновление
- Выключить автоматическое обновление
- Автоматически управляет видимостью кнопки refresh
## API Endpoints
### Получение настроек
```
GET /api/settings
Authorization: Bearer <token>
```
Ответ:
```json
{
"ajax_update_interval": 2000
}
```
### Получение логов с поддержкой AJAX
```
GET /api/logs/{container_id}?tail={lines}&since={timestamp}
Authorization: Bearer <token>
```
Параметры:
- `tail`: количество строк для получения (или "all")
- `since`: временная метка последнего обновления (опционально)
## Переменные состояния
```javascript
// Интервал обновления
ajaxUpdateIntervalMs
// Состояние активности
ajaxUpdateEnabled
// Состояние для каждого контейнера (для multi-view)
containerStates // Map: containerId -> {lastTimestamp, lastSecondCount}
let ajaxUpdateEnabled = true; // Состояние AJAX обновления (по умолчанию включен)
let ajaxUpdateIntervalMs = 2000; // Интервал обновления в миллисекундах
let ajaxUpdateInterval = null; // ID интервала
const containerStates = new Map(); // Состояние контейнеров для отслеживания обновлений
```
## Особенности реализации
## Функции управления
### Обработка новых логов
### enableAjaxLogUpdate(intervalMs)
Включает AJAX обновление логов с заданным интервалом.
1. **Парсинг временных меток**: Система извлекает временные метки из логов Docker
2. **Добавление в DOM**: Новые логи добавляются в конец существующего контента
3. **Применение фильтров**: Автоматически применяются активные фильтры
4. **Обновление счетчиков**: Пересчитываются счетчики уровней логов
5. **Очистка дубликатов**: Удаляются дублированные строки
### disableAjaxLogUpdate()
Отключает AJAX обновление логов.
### Управление состоянием
### toggleAjaxLogUpdate()
Переключает состояние AJAX обновления.
- AJAX обновление автоматически останавливается при смене контейнера
- При переключении в multi-view режим обновление также останавливается
- Состояние контейнеров сбрасывается при смене режимов просмотра
- В multi-view режиме состояние отслеживается отдельно для каждого контейнера
### performAjaxLogUpdate()
Выполняет одно обновление логов через AJAX.
### Обработка ошибок
### updateContainerLogs(containerId, tailLines, token)
Обновляет логи для конкретного контейнера.
- Ошибки сети не останавливают обновление
- Все ошибки логируются в консоль
- При отсутствии токена авторизации обновление пропускается
### updateRefreshButtonVisibility()
Обновляет видимость кнопки refresh и состояние кнопки update в зависимости от состояния AJAX autoupdate.
## Преимущества
### setAjaxUpdateState(enabled)
Обновляет визуальное состояние кнопки update (зеленая/красная) в зависимости от состояния AJAX autoupdate.
1. **Низкая нагрузка**: Только новые логи передаются по сети
2. **Надежность**: Простая HTTP архитектура без WebSocket сложностей
3. **Гибкость**: Настраиваемый интервал обновления
4. **Совместимость**: Работает с существующими фильтрами и счетчиками
5. **Производительность**: Минимальное влияние на производительность браузера
## Интеграция с существующим кодом
## Ограничения
AJAX update интегрируется с существующими функциями:
1. **Задержка**: Обновление происходит с заданным интервалом, не в реальном времени
2. **Ограничения браузера**: Может быть ограничено политиками CORS
3. **Нагрузка на сервер**: При большом количестве контейнеров может создавать нагрузку
- **switchToSingle**: Останавливает AJAX обновление при смене контейнера
- **switchToMultiView**: Останавливает AJAX обновление при переключении в multi-view
- **refreshLogsAndCounters**: Ручное обновление логов (кнопка refresh)
## Автор
## Логирование
Сергей Антропов
Сайт: https://devops.org.ru
Все операции AJAX update логируются в консоль браузера:
```javascript
console.log('AJAX обновление логов включено с интервалом 2000ms');
console.log('AJAX обновление логов отключено');
console.log('AJAX Update: Обновляем 2 контейнеров: ["container1", "container2"]');
```
## Обработка ошибок
При ошибках AJAX запросов:
- Обновление не останавливается автоматически
- Ошибки логируются в консоль
- Пользователь может вручную отключить обновление через чекбокс
## Совместимость
- Работает с существующими WebSocket соединениями
- Поддерживает все режимы просмотра (single-view, multi-view)
- Совместимо с фильтрацией и настройками уровней логирования

View File

@@ -1,163 +0,0 @@
#!/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)

View File

@@ -1,259 +0,0 @@
#!/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)

View File

@@ -1,148 +0,0 @@
#!/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)

View File

@@ -1,166 +0,0 @@
#!/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)

View File

@@ -1,285 +0,0 @@
#!/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)

View File

@@ -1,154 +0,0 @@
#!/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)