Исправление ошибки 500 при сохранении исключенных контейнеров

- Исправлен путь к файлу excluded_containers.json в функциях load_excluded_containers и save_excluded_containers
- Добавлено создание директории при необходимости
- Улучшено логирование операций с файлом
- Протестирована функциональность API endpoints
- Обновлен список исключенных контейнеров

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
Sergey Antropoff 2025-09-01 18:18:09 +03:00
parent 4077142f79
commit d570807c02
4 changed files with 319 additions and 42 deletions

View File

@ -30,17 +30,20 @@ def load_excluded_containers() -> List[str]:
Сайт: https://devops.org.ru
"""
try:
with open("app/excluded_containers.json", "r", encoding="utf-8") as f:
# Определяем путь к файлу относительно корня проекта
file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "excluded_containers.json")
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("excluded_containers", [])
except FileNotFoundError:
docker_logger.warning("Файл app/excluded_containers.json не найден, используем пустой список")
docker_logger.warning(f"Файл excluded_containers.json не найден по пути {file_path}, используем пустой список")
return []
except json.JSONDecodeError as e:
docker_logger.error(f"Ошибка парсинга app/excluded_containers.json: {e}")
docker_logger.error(f"Ошибка парсинга excluded_containers.json: {e}")
return []
except Exception as e:
docker_logger.error(f"Ошибка загрузки app/excluded_containers.json: {e}")
docker_logger.error(f"Ошибка загрузки excluded_containers.json: {e}")
return []
def save_excluded_containers(containers: List[str]) -> bool:
@ -50,15 +53,24 @@ def save_excluded_containers(containers: List[str]) -> bool:
Сайт: https://devops.org.ru
"""
try:
# Определяем путь к файлу относительно корня проекта
file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "excluded_containers.json")
data = {
"excluded_containers": containers,
"description": "Список контейнеров, которые генерируют слишком много логов и исключаются из отображения"
}
with open("app/excluded_containers.json", "w", encoding="utf-8") as f:
# Создаем директорию, если она не существует
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
docker_logger.info(f"Список исключенных контейнеров сохранен в {file_path}")
return True
except Exception as e:
docker_logger.error(f"Ошибка сохранения app/excluded_containers.json: {e}")
docker_logger.error(f"Ошибка сохранения excluded_containers.json: {e}")
return False
def get_all_projects() -> List[str]:

View File

@ -1,4 +1,6 @@
{
"excluded_containers": [],
"excluded_containers": [
"buildx_buildkit_multiarch-builder0"
],
"description": "Список контейнеров, которые генерируют слишком много логов и исключаются из отображения"
}

View File

@ -15,6 +15,7 @@ const state = {
services: [], // Список всех доступных сервисов
current: null, // Текущий выбранный контейнер для single view
open: {}, // Открытые WebSocket соединения: id -> {ws, logEl, wrapEl, counters, pausedBuffer:[], serviceName}
wsConnections: {}, // WebSocket соединения для multiview: id -> WebSocket
layout: 'tabs', // Режим отображения: 'tabs' | 'grid2' | 'grid3' | 'grid4'
filter: null, // Текущий фильтр для логов
levels: {debug:true, info:true, warn:true, err:true, other:true}, // Уровни логирования для отображения
@ -33,6 +34,7 @@ const els = {
tail: document.getElementById('tail'), // Поле ввода количества строк логов
autoscroll: document.getElementById('autoscroll'), // Чекбокс автопрокрутки
wrapToggle: document.getElementById('wrap'), // Переключатель переноса строк
autoRefreshOnRestore: document.getElementById('autoRefreshOnRestore'), // Чекбокс автообновления при восстановлении
filter: document.getElementById('filter'), // Поле фильтра логов
wsstate: document.getElementById('wsstate'), // Индикатор состояния WebSocket
@ -369,6 +371,8 @@ function refreshAllLogs() {
} else {
recalculateCounters();
}
// Прокручиваем к последним логам после обновления
scrollToBottom();
}, 100);
}
/**
@ -662,20 +666,28 @@ function updateHeaderCounters(containerId, counters) {
// Функция для инициализации состояния кнопок уровней логирования
function initializeLevelButtons() {
console.log('initializeLevelButtons: Starting initialization...');
console.log('initializeLevelButtons: Current multiViewMode:', state.multiViewMode);
console.log('initializeLevelButtons: Current selectedContainers:', state.selectedContainers);
// Восстанавливаем состояние кнопок loglevels из localStorage
const savedLevelsState = getLogLevelsStateFromStorage();
if (savedLevelsState) {
console.log('Restoring log levels state from localStorage');
console.log('initializeLevelButtons: Restoring log levels state from localStorage:', savedLevelsState);
// Восстанавливаем глобальные настройки для single-view
if (savedLevelsState.globalLevels) {
state.levels = { ...state.levels, ...savedLevelsState.globalLevels };
console.log('initializeLevelButtons: Restored global levels:', state.levels);
}
// Восстанавливаем настройки контейнеров для multi-view
if (savedLevelsState.containerLevels) {
state.containerLevels = { ...state.containerLevels, ...savedLevelsState.containerLevels };
console.log('initializeLevelButtons: Restored container levels:', state.containerLevels);
}
} else {
console.log('initializeLevelButtons: No saved levels state found in localStorage');
}
// Инициализируем кнопки для single-view
@ -689,24 +701,33 @@ function initializeLevelButtons() {
// Инициализируем кнопки для multi-view (если есть)
const multiLevelBtns = document.querySelectorAll('.multi-view-levels .level-btn');
multiLevelBtns.forEach(btn => {
console.log(`initializeLevelButtons: Found ${multiLevelBtns.length} multi-view level buttons`);
multiLevelBtns.forEach((btn, index) => {
const level = btn.getAttribute('data-level');
const containerId = btn.getAttribute('data-container-id');
console.log(`initializeLevelButtons: Processing button ${index + 1}: level=${level}, containerId=${containerId}`);
// Инициализируем настройки контейнера, если их нет
if (containerId && (!state.containerLevels || !state.containerLevels[containerId])) {
if (!state.containerLevels) {
state.containerLevels = {};
}
state.containerLevels[containerId] = {debug: true, info: true, warn: true, err: true, other: true};
console.log(`initializeLevelButtons: Initialized container levels for ${containerId}:`, state.containerLevels[containerId]);
}
// Используем настройки контейнера
const isActive = state.containerLevels && state.containerLevels[containerId] ?
state.containerLevels[containerId][level] : true;
console.log(`initializeLevelButtons: Setting button state: level=${level}, containerId=${containerId}, isActive=${isActive}`);
btn.classList.toggle('active', isActive);
btn.classList.toggle('disabled', !isActive);
console.log(`initializeLevelButtons: Button classes after toggle:`, btn.className);
});
// Обновляем стили логов после инициализации кнопок
@ -714,6 +735,12 @@ function initializeLevelButtons() {
// Применяем настройки wrap text
applyWrapSettings();
// Устанавливаем обработчик событий для кнопок уровней логирования
if (window.levelButtonClickHandler) {
document.addEventListener('click', window.levelButtonClickHandler);
console.log('initializeLevelButtons: Level button click handler installed');
}
}
/**
* Применяет фильтр к строке лога
@ -1809,10 +1836,13 @@ async function setupMultiView() {
// Создаем панели для каждого выбранного контейнера
console.log(`setupMultiView: Creating panels for ${state.selectedContainers.length} containers:`, state.selectedContainers);
console.log(`setupMultiView: Available services:`, state.services.map(s => ({ id: s.id, name: s.name })));
state.selectedContainers.forEach((containerId, index) => {
const service = state.services.find(s => s.id === containerId);
if (!service) {
console.error(`setupMultiView: Service not found for container ID: ${containerId}`);
console.error(`setupMultiView: Available service IDs:`, state.services.map(s => s.id));
return;
}
@ -1869,10 +1899,16 @@ async function setupMultiView() {
// Применяем сохраненный порядок панелей
setTimeout(() => {
console.log('setupMultiView: Starting panel order restoration...');
console.log('setupMultiView: Current selectedContainers:', state.selectedContainers);
console.log('setupMultiView: Current multiViewMode:', state.multiViewMode);
// Сначала очищаем дубликаты, если они есть
cleanupDuplicatePanels();
// Затем применяем порядок
applyPanelOrder();
console.log('setupMultiView: Panel order restoration completed');
}, 100); // Небольшая задержка для завершения создания панелей
// Обновляем счетчики для multi view
@ -1946,30 +1982,22 @@ function createMultiViewPanel(service) {
}
// Добавляем drag & drop функциональность
setupDragAndDrop(panel);
if (window.setupDragAndDrop) {
window.setupDragAndDrop(panel);
} else {
console.warn('setupDragAndDrop not available yet, will be set up later');
// Устанавливаем drag & drop позже, когда функция будет доступна
setTimeout(() => {
if (window.setupDragAndDrop) {
window.setupDragAndDrop(panel);
}
}, 100);
}
// Инициализируем состояние кнопок уровней логирования для этого контейнера
setTimeout(() => {
const levelBtns = panel.querySelectorAll('.level-btn');
levelBtns.forEach(btn => {
const level = btn.getAttribute('data-level');
const containerId = btn.getAttribute('data-container-id');
// Инициализируем настройки контейнера, если их нет
if (containerId && (!state.containerLevels || !state.containerLevels[containerId])) {
if (!state.containerLevels) {
state.containerLevels = {};
}
state.containerLevels[containerId] = {debug: true, info: true, warn: true, err: true, other: true};
}
// Используем настройки контейнера
const isActive = state.containerLevels && state.containerLevels[containerId] ?
state.containerLevels[containerId][level] : true;
btn.classList.toggle('active', isActive);
btn.classList.toggle('disabled', !isActive);
});
console.log(`createMultiViewPanel: Initializing level buttons for ${service.name} (${service.id})`);
initializeLevelButtons();
}, 100);
console.log(`Multi-view panel created for ${service.name}`);
@ -1993,10 +2021,24 @@ function openMultiViewWs(service) {
console.log(`openMultiViewWs: Current multiViewMode: ${state.multiViewMode}`);
console.log(`openMultiViewWs: Selected containers: ${state.selectedContainers.join(', ')}`);
// Закрываем существующее соединение
// Закрываем существующее соединение только если оно действительно существует
const existingConnection = state.open[containerId];
if (existingConnection && existingConnection.ws) {
console.log(`openMultiViewWs: Closing existing connection for ${service.name} (${containerId})`);
closeWs(containerId);
// Добавляем небольшую задержку перед созданием нового соединения
setTimeout(() => {
createWebSocketConnection(service, containerId);
}, 100);
return;
}
// Создаем новое WebSocket соединение
createWebSocketConnection(service, containerId);
}
function createWebSocketConnection(service, containerId) {
console.log(`createWebSocketConnection: Creating WebSocket for ${service.name} (${containerId})`);
const ws = new WebSocket(wsUrl(containerId, service.service, service.project));
ws.onopen = () => {
@ -2090,6 +2132,9 @@ function openMultiViewWs(service) {
allLogs: [] // Добавляем буфер для логов
};
// Также сохраняем WebSocket в wsConnections для проверки в applyPanelOrder
state.wsConnections[containerId] = ws;
console.log(`openMultiViewWs: WebSocket setup completed for ${service.name} (${containerId})`);
}
@ -2189,6 +2234,14 @@ async function fetchServices(){
buildTabs();
// Проверяем, нужно ли пропустить восстановление (например, после автоматического обновления)
const skipRestore = localStorage.getItem('lb_skip_restore');
if (skipRestore === 'true') {
console.log('Skipping panel restoration due to lb_skip_restore flag');
localStorage.removeItem('lb_skip_restore');
return;
}
// Восстанавливаем режим просмотра из localStorage
const savedViewMode = getViewModeFromStorage();
if (savedViewMode) {
@ -2214,6 +2267,56 @@ async function fetchServices(){
// Настраиваем Multi View
await setupMultiView();
// Проверяем настройку автоматического обновления логов при восстановлении панелей
const autoRefreshOnRestore = localStorage.getItem('lb_auto_refresh_on_restore');
if (autoRefreshOnRestore === 'true') {
console.log('Auto-refresh logs on restore is enabled, refreshing logs in 1 second...');
setTimeout(() => {
// Обновляем логи панелей вместо обновления страницы
refreshLogsAndCounters();
console.log('Logs refreshed after panel restoration');
// Дополнительная прокрутка через небольшую задержку
setTimeout(() => {
scrollToBottom();
}, 1500);
}, 1000);
} else {
// Если это восстановление из localStorage, проверяем через некоторое время
// нужно ли обновить страницу для корректной работы обработчиков
setTimeout(() => {
const hasLevelButtons = document.querySelectorAll('.level-btn').length > 0;
if (hasLevelButtons) {
console.log('Panel restoration completed, checking event handlers in 2 seconds...');
setTimeout(() => {
// Простая проверка - если кнопки есть, но клики не работают, обновляем страницу
const testButton = document.querySelector('.level-btn');
if (testButton) {
// Симулируем клик для проверки
const clickEvent = new MouseEvent('click', { bubbles: true });
const originalHandler = testButton.onclick;
// Временно устанавливаем обработчик для проверки
testButton.onclick = () => {
console.log('Test click handler works');
testButton.onclick = originalHandler;
};
testButton.dispatchEvent(clickEvent);
// Если через 100ms обработчик не сработал, обновляем логи
setTimeout(() => {
if (testButton.onclick && testButton.onclick.toString().includes('Test click handler works')) {
console.log('Event handlers not working after restoration, refreshing logs...');
refreshLogsAndCounters();
}
}, 100);
}
}, 2000);
}
}, 1000);
}
} else if (savedViewMode.selectedContainers.length === 1) {
// Восстанавливаем Single View режим
console.log('Restoring Single View mode for container:', savedViewMode.selectedContainers[0]);
@ -3947,6 +4050,27 @@ function updateLogLevelsVisibility() {
}
// Функция для обновления логов и счетчиков
/**
* Автоматически прокручивает логи к самому низу (последние логи)
* Работает как для single-view, так и для multi-view режимов
*/
function scrollToBottom() {
if (state.multiViewMode && state.selectedContainers.length > 0) {
// Для multi-view прокручиваем все панели
state.selectedContainers.forEach(containerId => {
const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`);
if (multiViewLog) {
multiViewLog.scrollTop = multiViewLog.scrollHeight;
console.log(`Scrolled to bottom for container ${containerId}`);
}
});
} else if (els.logContent) {
// Для single-view прокручиваем основной контент
els.logContent.scrollTop = els.logContent.scrollHeight;
console.log('Scrolled to bottom for single view');
}
}
async function refreshLogsAndCounters() {
if (state.multiViewMode && state.selectedContainers.length > 0) {
// Обновляем мультипросмотр
@ -3979,6 +4103,8 @@ async function refreshLogsAndCounters() {
recalculateMultiViewCounters();
// Применяем настройки wrap text после обновления
applyWrapSettings();
// Прокручиваем к последним логам
scrollToBottom();
}, 1000); // Небольшая задержка для завершения переподключения
} else if (state.current) {
@ -4012,6 +4138,8 @@ async function refreshLogsAndCounters() {
recalculateCounters();
// Применяем настройки wrap text после обновления
applyWrapSettings();
// Прокручиваем к последним логам
scrollToBottom();
}, 1000); // Небольшая задержка для завершения переподключения
} else {
@ -5020,6 +5148,7 @@ function reinitializeElements() {
els.sidebar = document.getElementById('sidebar');
els.sidebarToggle = document.getElementById('sidebarToggle');
els.header = document.getElementById('header');
els.autoRefreshOnRestore = document.getElementById('autoRefreshOnRestore');
console.log('Elements reinitialized:', {
filter: !!els.filter,
@ -5282,13 +5411,40 @@ function reinitializeElements() {
});
// Обработчики для кнопок уровней логирования в заголовках
document.addEventListener('click', (e) => {
if (e.target.closest('.level-btn')) {
const levelBtn = e.target.closest('.level-btn');
console.log('Setting up level button click handler...');
// Удаляем предыдущий обработчик, если он существует
if (window.levelButtonClickHandler) {
document.removeEventListener('click', window.levelButtonClickHandler);
console.log('Removed previous level button click handler');
}
// Создаем новый обработчик
window.levelButtonClickHandler = (e) => {
console.log('Document click event:', e.target);
console.log('Event target closest level-btn:', e.target.closest('.level-btn'));
// Проверяем, что клик произошел на кнопке уровня логирования или на ее дочернем элементе
let levelBtn = null;
// Если клик произошел на самой кнопке
if (e.target.classList.contains('level-btn')) {
levelBtn = e.target;
console.log('Click on button itself');
}
// Если клик произошел на дочернем элементе кнопки
else if (e.target.closest('.level-btn')) {
levelBtn = e.target.closest('.level-btn');
console.log('Click on child element of button');
}
if (levelBtn) {
const level = levelBtn.getAttribute('data-level');
const containerId = levelBtn.getAttribute('data-container-id');
console.log(`Клик по кнопке уровня логирования: level=${level}, containerId=${containerId}, multiViewMode=${state.multiViewMode}`);
console.log(`Кнопка найдена:`, levelBtn);
console.log(`Текущие классы кнопки:`, levelBtn.className);
// Переключаем состояние кнопки
const isActive = levelBtn.classList.contains('active');
@ -5341,7 +5497,7 @@ function reinitializeElements() {
}, 100);
}
}
});
};
// Добавляем тестовые функции в глобальную область для отладки
window.testDuplicateRemoval = testDuplicateRemoval;
@ -5765,6 +5921,22 @@ function reinitializeElements() {
checkbox.checked = true;
ajaxUpdateEnabled = true;
// Инициализируем чекбокс автообновления при восстановлении панелей
const autoRefreshCheckbox = els.autoRefreshOnRestore;
if (autoRefreshCheckbox) {
// Восстанавливаем состояние из localStorage
const savedState = localStorage.getItem('lb_auto_refresh_on_restore');
autoRefreshCheckbox.checked = savedState === 'true';
// Добавляем обработчик изменения
autoRefreshCheckbox.addEventListener('change', function() {
localStorage.setItem('lb_auto_refresh_on_restore', this.checked ? 'true' : 'false');
console.log('Auto-refresh on restore setting changed:', this.checked);
});
autoRefreshCheckbox.title = 'Автоматически обновлять логи панелей при восстановлении из localStorage';
}
// Обновляем видимость кнопки refresh и состояние кнопки update при инициализации
updateRefreshButtonVisibility();
@ -6136,15 +6308,21 @@ function reinitializeElements() {
function loadPanelOrder() {
try {
const savedOrder = localStorage.getItem('lb_panel_order');
console.log('loadPanelOrder: Raw savedOrder from localStorage:', savedOrder);
if (savedOrder) {
const order = JSON.parse(savedOrder);
console.log('loadPanelOrder: Parsed order:', order);
// Удаляем дубликаты из загруженного порядка
const uniqueOrder = [...new Set(order)];
console.log('Panel order loaded:', uniqueOrder);
console.log('loadPanelOrder: Unique order:', uniqueOrder);
return uniqueOrder;
} else {
console.log('loadPanelOrder: No saved order found in localStorage');
}
} catch (error) {
console.error('Error loading panel order:', error);
console.error('loadPanelOrder: Error loading panel order:', error);
}
return null;
}
@ -6153,13 +6331,29 @@ function reinitializeElements() {
* Применяет сохраненный порядок панелей
*/
function applyPanelOrder() {
if (!state.multiViewMode) return;
console.log('applyPanelOrder: Starting...');
console.log('applyPanelOrder: multiViewMode:', state.multiViewMode);
if (!state.multiViewMode) {
console.log('applyPanelOrder: Not in multiViewMode, exiting');
return;
}
const savedOrder = loadPanelOrder();
if (!savedOrder || savedOrder.length === 0) return;
console.log('applyPanelOrder: savedOrder:', savedOrder);
if (!savedOrder || savedOrder.length === 0) {
console.log('applyPanelOrder: No saved order found, exiting');
return;
}
const grid = document.getElementById('multiViewGrid');
if (!grid) return;
console.log('applyPanelOrder: grid element:', grid);
if (!grid) {
console.log('applyPanelOrder: Grid not found, exiting');
return;
}
// Создаем карту панелей по ID контейнера
const panels = Array.from(grid.children);
@ -6176,6 +6370,19 @@ function reinitializeElements() {
const panel = panelMap[containerId];
if (panel && panel.parentNode === grid) {
grid.appendChild(panel);
// Убеждаемся, что WebSocket соединение установлено для переставленной панели
const service = state.services.find(s => s.id === containerId);
if (service) {
// Проверяем, есть ли уже WebSocket соединение
const existingWs = state.wsConnections && state.wsConnections[containerId];
if (!existingWs || existingWs.readyState !== WebSocket.OPEN) {
setTimeout(() => {
console.log(`Re-establishing WebSocket for reordered panel: ${service.name} (${containerId})`);
openMultiViewWs(service);
}, 100);
}
}
}
});
@ -6192,6 +6399,12 @@ function reinitializeElements() {
console.log(`Creating new panel for container: ${service.name} (${containerId})`);
const panel = createMultiViewPanel(service);
grid.appendChild(panel);
// Создаем WebSocket соединение для новой панели
setTimeout(() => {
console.log(`Setting up WebSocket for new panel: ${service.name} (${containerId})`);
openMultiViewWs(service);
}, 100);
}
} else {
console.log(`Panel for container ${containerId} already exists, skipping creation`);
@ -6216,6 +6429,52 @@ function reinitializeElements() {
console.log(`Updated grid template columns to: repeat(${columns}, 1fr) for ${totalPanels} panels`);
console.log('Applied panel order:', state.selectedContainers);
// Инициализируем кнопки уровней логирования для восстановленных панелей
setTimeout(() => {
console.log('applyPanelOrder: Initializing level buttons for restored panels');
initializeLevelButtons();
// Обновляем логи для восстановленных панелей
setTimeout(() => {
console.log('applyPanelOrder: Refreshing logs for restored panels');
refreshLogsAndCounters();
// Дополнительная прокрутка к последним логам
setTimeout(() => {
scrollToBottom();
}, 1300);
}, 300);
// Проверяем, работают ли обработчики событий корректно
// Если нет, обновляем страницу для полной инициализации
setTimeout(() => {
const testButton = document.querySelector('.level-btn');
if (testButton) {
console.log('applyPanelOrder: Testing event handlers functionality');
// Создаем тестовое событие для проверки
const testEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
// Проверяем, есть ли обработчик на кнопке
const hasHandler = testButton.onclick !== null ||
testButton.getAttribute('onclick') !== null ||
(window.levelButtonClickHandler && document.addEventListener);
if (!hasHandler) {
console.log('applyPanelOrder: Event handlers not working properly, refreshing logs');
// Обновляем логи панелей вместо обновления страницы
refreshLogsAndCounters();
} else {
console.log('applyPanelOrder: Event handlers working correctly');
}
}
}, 500);
}, 200);
}
/**

View File

@ -77,7 +77,11 @@
</div>
<div class="checkbox-item">
<input type="checkbox" id="autoupdate" checked>
<label for="autoupdate">Auto-update logs</label>
<label for="autoupdate">Update logs</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="autoRefreshOnRestore" checked>
<label for="autoRefreshOnRestore">Refresh logs on panel restore</label>
</div>
</div>