# WebSocket API LogBoard+ **Автор:** Сергей Антропов **Сайт:** https://devops.org.ru ## Содержание 1. [Обзор WebSocket API](#обзор-websocket-api) 2. [Аутентификация](#аутентификация) 3. [Endpoints](#endpoints) 4. [Примеры использования](#примеры-использования) 5. [Обработка ошибок](#обработка-ошибок) 6. [Лучшие практики](#лучшие-практики) ## Обзор WebSocket API WebSocket API LogBoard+ предоставляет возможность получения логов Docker контейнеров в реальном времени. Это позволяет создавать интерактивные приложения для мониторинга и отладки микросервисов. ### Преимущества WebSocket - **Real-time данные** - мгновенное получение новых логов - **Эффективность** - меньше накладных расходов по сравнению с polling - **Масштабируемость** - поддержка множественных соединений - **Фильтрация** - получение логов только нужных контейнеров ### Поддерживаемые протоколы - **ws://** - для HTTP соединений - **wss://** - для HTTPS соединений (рекомендуется для продакшена) ## Аутентификация Все WebSocket endpoints требуют JWT токен для аутентификации. Токен передается как параметр запроса `token`. ### Получение токена ```bash # Получение JWT токена через REST API curl -X POST "http://localhost:9001/api/auth/login" \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"your-password"}' ``` ### Использование токена ```javascript const token = "your-jwt-token"; const ws = new WebSocket(`ws://localhost:9001/ws/logs/container-id?token=${token}`); ``` ## Endpoints ### ws://host:port/ws/logs/{container_id} Получение логов отдельного контейнера в реальном времени. #### Параметры | Параметр | Тип | Обязательный | Описание | |----------|-----|--------------|----------| | `container_id` | string | Да | ID контейнера (первые 12 символов) | | `token` | string | Да | JWT токен аутентификации | | `tail` | number | Нет | Количество начальных строк (по умолчанию 500) | | `service` | string | Нет | Имя сервиса (для информации) | | `project` | string | Нет | Имя проекта (для информации) | #### Пример подключения ```javascript const containerId = "abc123def456"; const token = "your-jwt-token"; const ws = new WebSocket(`ws://localhost:9001/ws/logs/${containerId}?token=${token}&tail=100`); ws.onopen = function() { console.log('WebSocket соединение установлено'); }; ws.onmessage = function(event) { console.log('Получены логи:', event.data); }; ws.onerror = function(error) { console.error('WebSocket ошибка:', error); }; ws.onclose = function(event) { console.log('WebSocket соединение закрыто:', event.code, event.reason); }; ``` #### Формат сообщений При подключении сервер отправляет приветственное сообщение: ``` Connected to container: myproject_web_1 ``` Затем отправляются логи в формате: ``` 2024-01-15T10:30:15.123456789Z 2024/01/15 10:30:15 [notice] 1#1: start worker processes 2024-01-15T10:30:15.123456789Z 2024/01/15 10:30:15 [notice] 1#1: start worker process 1234 ``` ### ws://host:port/ws/fan/{service_name} Получение логов всех реплик сервиса Docker Compose (fan-in). #### Параметры | Параметр | Тип | Обязательный | Описание | |----------|-----|--------------|----------| | `service_name` | string | Да | Имя сервиса Docker Compose | | `token` | string | Да | JWT токен аутентификации | | `tail` | number | Нет | Количество начальных строк (по умолчанию 500) | | `project` | string | Нет | Имя проекта (для фильтрации) | #### Пример подключения ```javascript const serviceName = "web"; const token = "your-jwt-token"; const ws = new WebSocket(`ws://localhost:9001/ws/fan/${serviceName}?token=${token}&tail=100`); ws.onmessage = function(event) { // Логи приходят с префиксом ID контейнера console.log('Логи сервиса:', event.data); }; ``` #### Формат сообщений Логи приходят с префиксом короткого ID контейнера: ``` [abc123de] 2024-01-15T10:30:15.123456789Z 2024/01/15 10:30:15 [notice] 1#1: start worker processes [def456gh] 2024-01-15T10:30:16.123456789Z 2024/01/15 10:30:16 [notice] 1#1: start worker processes ``` ### ws://host:port/ws/fan_group Получение логов группы сервисов одновременно. #### Параметры | Параметр | Тип | Обязательный | Описание | |----------|-----|--------------|----------| | `services` | string | Да | Имена сервисов через запятую | | `token` | string | Да | JWT токен аутентификации | | `tail` | number | Нет | Количество начальных строк (по умолчанию 500) | | `project` | string | Нет | Имя проекта (для фильтрации) | #### Пример подключения ```javascript const services = "web,db,redis"; const token = "your-jwt-token"; const ws = new WebSocket(`ws://localhost:9001/ws/fan_group?services=${services}&token=${token}&tail=100`); ws.onmessage = function(event) { // Логи приходят с префиксом ID контейнера и имени сервиса console.log('Логи группы сервисов:', event.data); }; ``` #### Формат сообщений Логи приходят с префиксом ID контейнера и имени сервиса: ``` [abc123de web] 2024-01-15T10:30:15.123456789Z 2024/01/15 10:30:15 [notice] 1#1: start worker processes [def456gh db] 2024-01-15T10:30:16.123456789Z 2024/01/15 10:30:16 [info] database system is ready to accept connections [ghi789jk redis] 2024-01-15T10:30:17.123456789Z 2024/01/15 10:30:17 [notice] Ready to accept connections ``` ## Примеры использования ### JavaScript (Browser) ```javascript class LogBoardWebSocket { constructor(baseUrl, token) { this.baseUrl = baseUrl; this.token = token; this.connections = new Map(); } // Подключение к логам контейнера connectToContainer(containerId, options = {}) { const { tail = 100, onMessage, onError, onClose } = options; const url = `ws://${this.baseUrl.replace('http://', '')}/ws/logs/${containerId}?token=${this.token}&tail=${tail}`; const ws = new WebSocket(url); ws.onopen = () => { console.log(`Подключение к контейнеру ${containerId} установлено`); }; ws.onmessage = (event) => { if (onMessage) onMessage(event.data); }; ws.onerror = (error) => { console.error(`Ошибка WebSocket для контейнера ${containerId}:`, error); if (onError) onError(error); }; ws.onclose = (event) => { console.log(`Соединение с контейнером ${containerId} закрыто:`, event.code, event.reason); if (onClose) onClose(event); }; this.connections.set(containerId, ws); return ws; } // Подключение к логам сервиса connectToService(serviceName, options = {}) { const { tail = 100, project, onMessage, onError, onClose } = options; let url = `ws://${this.baseUrl.replace('http://', '')}/ws/fan/${serviceName}?token=${this.token}&tail=${tail}`; if (project) url += `&project=${project}`; const ws = new WebSocket(url); ws.onopen = () => { console.log(`Подключение к сервису ${serviceName} установлено`); }; ws.onmessage = (event) => { if (onMessage) onMessage(event.data); }; ws.onerror = (error) => { console.error(`Ошибка WebSocket для сервиса ${serviceName}:`, error); if (onError) onError(error); }; ws.onclose = (event) => { console.log(`Соединение с сервисом ${serviceName} закрыто:`, event.code, event.reason); if (onClose) onClose(event); }; this.connections.set(serviceName, ws); return ws; } // Подключение к группе сервисов connectToServiceGroup(services, options = {}) { const { tail = 100, project, onMessage, onError, onClose } = options; let url = `ws://${this.baseUrl.replace('http://', '')}/ws/fan_group?services=${services}&token=${this.token}&tail=${tail}`; if (project) url += `&project=${project}`; const ws = new WebSocket(url); ws.onopen = () => { console.log(`Подключение к группе сервисов ${services} установлено`); }; ws.onmessage = (event) => { if (onMessage) onMessage(event.data); }; ws.onerror = (error) => { console.error(`Ошибка WebSocket для группы сервисов ${services}:`, error); if (onError) onError(error); }; ws.onclose = (event) => { console.log(`Соединение с группой сервисов ${services} закрыто:`, event.code, event.reason); if (onClose) onClose(event); }; this.connections.set(`group:${services}`, ws); return ws; } // Закрытие соединения disconnect(identifier) { const ws = this.connections.get(identifier); if (ws) { ws.close(); this.connections.delete(identifier); } } // Закрытие всех соединений disconnectAll() { for (const [identifier, ws] of this.connections) { ws.close(); } this.connections.clear(); } } // Использование async function main() { // Получение токена const loginResponse = await fetch('http://localhost:9001/api/auth/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ username: 'admin', password: 'your-password' }) }); const {access_token} = await loginResponse.json(); // Создание WebSocket клиента const wsClient = new LogBoardWebSocket('http://localhost:9001', access_token); // Подключение к контейнеру wsClient.connectToContainer('abc123def456', { tail: 100, onMessage: (data) => { console.log('Логи контейнера:', data); // Добавление в UI appendToLogView(data); }, onError: (error) => { console.error('Ошибка:', error); } }); // Подключение к сервису wsClient.connectToService('web', { tail: 100, onMessage: (data) => { console.log('Логи сервиса:', data); // Парсинг префикса для группировки const match = data.match(/^\[([a-f0-9]+)\]\s+(.+)$/); if (match) { const [, containerId, logMessage] = match; appendToServiceLogView(containerId, logMessage); } } }); // Подключение к группе сервисов wsClient.connectToServiceGroup('web,db,redis', { tail: 100, onMessage: (data) => { console.log('Логи группы:', data); // Парсинг префикса для группировки const match = data.match(/^\[([a-f0-9]+)\s+(\w+)\]\s+(.+)$/); if (match) { const [, containerId, serviceName, logMessage] = match; appendToGroupLogView(serviceName, containerId, logMessage); } } }); } // Функции для обновления UI function appendToLogView(message) { const logContainer = document.getElementById('log-container'); const logLine = document.createElement('div'); logLine.className = 'log-line'; logLine.textContent = message; logContainer.appendChild(logLine); logContainer.scrollTop = logContainer.scrollHeight; } function appendToServiceLogView(containerId, message) { // Реализация для отображения логов сервиса } function appendToGroupLogView(serviceName, containerId, message) { // Реализация для отображения логов группы сервисов } main().catch(console.error); ``` ### Python ```python import asyncio import websockets import json import requests class LogBoardWebSocket: def __init__(self, base_url, token): self.base_url = base_url.replace('http://', 'ws://') self.token = token self.connections = {} async def connect_to_container(self, container_id, tail=100, callback=None): """Подключение к логам контейнера""" uri = f"{self.base_url}/ws/logs/{container_id}?token={self.token}&tail={tail}" try: async with websockets.connect(uri) as websocket: print(f"Подключение к контейнеру {container_id} установлено") async for message in websocket: if callback: await callback(container_id, message) else: print(f"[{container_id}] {message}") except websockets.exceptions.ConnectionClosed: print(f"Соединение с контейнером {container_id} закрыто") except Exception as e: print(f"Ошибка подключения к контейнеру {container_id}: {e}") async def connect_to_service(self, service_name, tail=100, project=None, callback=None): """Подключение к логам сервиса""" uri = f"{self.base_url}/ws/fan/{service_name}?token={self.token}&tail={tail}" if project: uri += f"&project={project}" try: async with websockets.connect(uri) as websocket: print(f"Подключение к сервису {service_name} установлено") async for message in websocket: if callback: await callback(service_name, message) else: print(f"[{service_name}] {message}") except websockets.exceptions.ConnectionClosed: print(f"Соединение с сервисом {service_name} закрыто") except Exception as e: print(f"Ошибка подключения к сервису {service_name}: {e}") async def connect_to_service_group(self, services, tail=100, project=None, callback=None): """Подключение к логам группы сервисов""" uri = f"{self.base_url}/ws/fan_group?services={services}&token={self.token}&tail={tail}" if project: uri += f"&project={project}" try: async with websockets.connect(uri) as websocket: print(f"Подключение к группе сервисов {services} установлено") async for message in websocket: if callback: await callback(services, message) else: print(f"[{services}] {message}") except websockets.exceptions.ConnectionClosed: print(f"Соединение с группой сервисов {services} закрыто") except Exception as e: print(f"Ошибка подключения к группе сервисов {services}: {e}") # Функции обратного вызова async def container_log_callback(container_id, message): """Обработка логов контейнера""" print(f"[{container_id}] {message}") async def service_log_callback(service_name, message): """Обработка логов сервиса""" print(f"[{service_name}] {message}") async def group_log_callback(services, message): """Обработка логов группы сервисов""" print(f"[{services}] {message}") async def main(): # Получение токена response = requests.post('http://localhost:9001/api/auth/login', json={ 'username': 'admin', 'password': 'your-password' }) token = response.json()['access_token'] # Создание WebSocket клиента ws_client = LogBoardWebSocket('http://localhost:9001', token) # Создание задач для одновременного подключения tasks = [ ws_client.connect_to_container('abc123def456', tail=100, callback=container_log_callback), ws_client.connect_to_service('web', tail=100, callback=service_log_callback), ws_client.connect_to_service_group('web,db,redis', tail=100, callback=group_log_callback) ] # Запуск всех подключений одновременно await asyncio.gather(*tasks) if __name__ == "__main__": asyncio.run(main()) ``` ### Node.js ```javascript const WebSocket = require('ws'); const axios = require('axios'); class LogBoardWebSocket { constructor(baseUrl, token) { this.baseUrl = baseUrl.replace('http://', 'ws://'); this.token = token; this.connections = new Map(); } // Подключение к логам контейнера connectToContainer(containerId, options = {}) { const { tail = 100, onMessage, onError, onClose } = options; const url = `${this.baseUrl}/ws/logs/${containerId}?token=${this.token}&tail=${tail}`; const ws = new WebSocket(url); ws.on('open', () => { console.log(`Подключение к контейнеру ${containerId} установлено`); }); ws.on('message', (data) => { const message = data.toString(); if (onMessage) onMessage(message); else console.log(`[${containerId}] ${message}`); }); ws.on('error', (error) => { console.error(`Ошибка WebSocket для контейнера ${containerId}:`, error); if (onError) onError(error); }); ws.on('close', (code, reason) => { console.log(`Соединение с контейнером ${containerId} закрыто:`, code, reason); if (onClose) onClose(code, reason); }); this.connections.set(containerId, ws); return ws; } // Подключение к логам сервиса connectToService(serviceName, options = {}) { const { tail = 100, project, onMessage, onError, onClose } = options; let url = `${this.baseUrl}/ws/fan/${serviceName}?token=${this.token}&tail=${tail}`; if (project) url += `&project=${project}`; const ws = new WebSocket(url); ws.on('open', () => { console.log(`Подключение к сервису ${serviceName} установлено`); }); ws.on('message', (data) => { const message = data.toString(); if (onMessage) onMessage(message); else console.log(`[${serviceName}] ${message}`); }); ws.on('error', (error) => { console.error(`Ошибка WebSocket для сервиса ${serviceName}:`, error); if (onError) onError(error); }); ws.on('close', (code, reason) => { console.log(`Соединение с сервисом ${serviceName} закрыто:`, code, reason); if (onClose) onClose(code, reason); }); this.connections.set(serviceName, ws); return ws; } // Закрытие соединения disconnect(identifier) { const ws = this.connections.get(identifier); if (ws) { ws.close(); this.connections.delete(identifier); } } // Закрытие всех соединений disconnectAll() { for (const [identifier, ws] of this.connections) { ws.close(); } this.connections.clear(); } } async function main() { try { // Получение токена const loginResponse = await axios.post('http://localhost:9001/api/auth/login', { username: 'admin', password: 'your-password' }); const token = loginResponse.data.access_token; // Создание WebSocket клиента const wsClient = new LogBoardWebSocket('http://localhost:9001', token); // Подключение к контейнеру wsClient.connectToContainer('abc123def456', { tail: 100, onMessage: (message) => { console.log('Логи контейнера:', message); } }); // Подключение к сервису wsClient.connectToService('web', { tail: 100, onMessage: (message) => { console.log('Логи сервиса:', message); } }); // Обработка сигналов завершения process.on('SIGINT', () => { console.log('Закрытие соединений...'); wsClient.disconnectAll(); process.exit(0); }); } catch (error) { console.error('Ошибка:', error.message); } } main(); ``` ## Обработка ошибок ### Коды ошибок WebSocket | Код | Описание | |-----|----------| | 1000 | Нормальное закрытие | | 1001 | Удаленная сторона закрыла соединение | | 1002 | Ошибка протокола | | 1003 | Неподдерживаемый тип данных | | 1006 | Аномальное закрытие | | 1011 | Внутренняя ошибка сервера | ### Обработка ошибок аутентификации ```javascript ws.onmessage = function(event) { if (event.data.startsWith('ERROR:')) { const error = event.data.substring(6); console.error('Ошибка аутентификации:', error); if (error.includes('token required') || error.includes('invalid token')) { // Переподключение с новым токеном reconnectWithNewToken(); } } else { // Обработка обычных логов console.log('Логи:', event.data); } }; ``` ### Автоматическое переподключение ```javascript class ReconnectingWebSocket { constructor(url, options = {}) { this.url = url; this.options = options; this.reconnectAttempts = 0; this.maxReconnectAttempts = options.maxReconnectAttempts || 5; this.reconnectInterval = options.reconnectInterval || 1000; this.connect(); } connect() { this.ws = new WebSocket(this.url); this.ws.onopen = () => { console.log('WebSocket соединение установлено'); this.reconnectAttempts = 0; }; this.ws.onclose = (event) => { console.log('WebSocket соединение закрыто:', event.code, event.reason); if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; console.log(`Попытка переподключения ${this.reconnectAttempts}/${this.maxReconnectAttempts}`); setTimeout(() => { this.connect(); }, this.reconnectInterval * this.reconnectAttempts); } else { console.error('Превышено максимальное количество попыток переподключения'); } }; this.ws.onerror = (error) => { console.error('WebSocket ошибка:', error); }; } send(data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(data); } } close() { this.ws.close(); } } ``` ## Лучшие практики ### 1. Управление соединениями ```javascript // Создание пула соединений class WebSocketPool { constructor() { this.connections = new Map(); this.maxConnections = 10; } connect(identifier, url, options = {}) { if (this.connections.size >= this.maxConnections) { console.warn('Достигнут лимит соединений'); return null; } const ws = new WebSocket(url); this.connections.set(identifier, ws); ws.onclose = () => { this.connections.delete(identifier); }; return ws; } disconnect(identifier) { const ws = this.connections.get(identifier); if (ws) { ws.close(); this.connections.delete(identifier); } } } ``` ### 2. Обработка больших объемов данных ```javascript // Буферизация сообщений class LogBuffer { constructor(maxSize = 1000) { this.buffer = []; this.maxSize = maxSize; } add(message) { this.buffer.push(message); if (this.buffer.length > this.maxSize) { this.buffer.shift(); // Удаляем старые сообщения } } getMessages() { return [...this.buffer]; } clear() { this.buffer = []; } } // Использование const logBuffer = new LogBuffer(1000); ws.onmessage = function(event) { logBuffer.add(event.data); // Обновление UI каждые 100ms if (!updateScheduled) { updateScheduled = setTimeout(() => { updateUI(logBuffer.getMessages()); updateScheduled = null; }, 100); } }; ``` ### 3. Фильтрация логов ```javascript // Фильтр по уровню логирования class LogFilter { constructor() { this.filters = { level: 'all', // debug, info, warn, error, all service: null, container: null, text: null }; } setFilter(type, value) { this.filters[type] = value; } shouldDisplay(message) { // Фильтр по уровню if (this.filters.level !== 'all') { const levelMatch = message.toLowerCase().includes(`level=${this.filters.level}`); if (!levelMatch) return false; } // Фильтр по сервису if (this.filters.service) { const serviceMatch = message.includes(`[${this.filters.service}]`); if (!serviceMatch) return false; } // Фильтр по контейнеру if (this.filters.container) { const containerMatch = message.includes(`[${this.filters.container}]`); if (!containerMatch) return false; } // Фильтр по тексту if (this.filters.text) { const textMatch = message.toLowerCase().includes(this.filters.text.toLowerCase()); if (!textMatch) return false; } return true; } } // Использование const logFilter = new LogFilter(); logFilter.setFilter('level', 'error'); ws.onmessage = function(event) { if (logFilter.shouldDisplay(event.data)) { console.log('Отфильтрованные логи:', event.data); } }; ``` ### 4. Мониторинг состояния соединений ```javascript class ConnectionMonitor { constructor() { this.stats = { totalMessages: 0, totalBytes: 0, connectionTime: null, lastMessageTime: null, errors: 0 }; } onConnect() { this.stats.connectionTime = new Date(); this.stats.totalMessages = 0; this.stats.totalBytes = 0; this.stats.errors = 0; } onMessage(message) { this.stats.totalMessages++; this.stats.totalBytes += message.length; this.stats.lastMessageTime = new Date(); } onError() { this.stats.errors++; } getStats() { return { ...this.stats, uptime: this.stats.connectionTime ? Date.now() - this.stats.connectionTime.getTime() : 0, messagesPerSecond: this.stats.totalMessages / (this.stats.uptime / 1000) || 0 }; } } ``` ## Ограничения и рекомендации ### Ограничения - **Максимальное количество соединений:** 100 одновременных WebSocket соединений - **Таймаут соединения:** 60 секунд бездействия - **Размер сообщения:** до 1 MB на сообщение - **Частота сообщений:** до 1000 сообщений в секунду ### Рекомендации 1. **Используйте переподключение** при разрыве соединений 2. **Ограничивайте количество соединений** для одного клиента 3. **Фильтруйте логи** на стороне клиента для снижения нагрузки 4. **Мониторьте состояние соединений** для диагностики проблем 5. **Используйте буферизацию** для больших объемов данных