docs: update README and docs with strict Quick Start (docker-compose-prod), fix WebSocket paths, enforce strict tone

This commit is contained in:
Sergey Antropoff 2025-09-04 13:43:10 +03:00
parent afa2829872
commit 7ccdf75bab
15 changed files with 825 additions and 131 deletions

View File

@ -3,8 +3,20 @@ FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1 ENV PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
WORKDIR /app WORKDIR /app
# Устанавливаем системные зависимости для сборки некоторых Python пакетов
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
gcc \
libffi-dev \
libssl-dev \
cargo \
rustc && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt /app/requirements.txt COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt RUN python -m pip install --upgrade pip setuptools wheel && \
pip install --no-cache-dir -r requirements.txt
COPY ./app /app COPY ./app /app

View File

@ -1,12 +1,15 @@
.ONESHELL:
# Makefile для LogBoard+ # Makefile для LogBoard+
# Автор: Сергей Антропов # Автор: Сергей Антропов
# Сайт: https://devops.org.ru # Сайт: https://devops.org.ru
.PHONY: help setup build up down restart logs clean status ps shell .PHONY: help setup build up down restart logs clean status ps shell release release-compose
# Переменные # Переменные
COMPOSE_FILE = docker-compose.yml COMPOSE_FILE = docker-compose.yml
SERVICE_NAME = logboard SERVICE_NAME = logboard
COMPOSE_PROD_FILE = docker-compose-prod.yaml
PLATFORMS ?= linux/amd64,linux/arm64
# Цвета для вывода # Цвета для вывода
GREEN = \033[0;32m GREEN = \033[0;32m
@ -165,3 +168,25 @@ debug-status: ## Показать статус режима отладки
fi fi
# -----------------------------------------------------------------------------
# release: Сборка и публикация образа в Docker Registry + генерация compose для продакшена
# Описание:
# - Запрашивает реестр, логин и пароль
# - Собирает образ, тегирует и пушит
# - Генерирует файл $(COMPOSE_PROD_FILE) для запуска в продакшене
# Пример:
# make release
# -----------------------------------------------------------------------------
release: ## Собрать, залогиниться, запушить образ и сгенерировать docker-compose-prod.yaml
@echo "$(GREEN)Release: сборка и публикация образа$(NC)"
@python3 release/publish_image.py --platforms "$(PLATFORMS)"
# -----------------------------------------------------------------------------
# release-compose: Генерация docker-compose-prod.yaml с подстановкой переменных
# -----------------------------------------------------------------------------
release-compose: ## Сгенерировать docker-compose-prod.yaml с переменными
@echo "$(YELLOW)Генерация файла $(COMPOSE_PROD_FILE) для продакшена...$(NC)"
@python3 release/generate_compose.py --env env.example --template release/docker-compose-prod.tmpl.yaml --output $(COMPOSE_PROD_FILE)

135
README.md
View File

@ -15,7 +15,7 @@
LogBoard+ - это современная веб-панель для мониторинга и просмотра логов Docker контейнеров в реальном времени. Приложение идеально подходит для локальной разработки, позволяя разработчикам всегда держать логи микросервисов перед глазами на втором мониторе. LogBoard+ - это современная веб-панель для мониторинга и просмотра логов Docker контейнеров в реальном времени. Приложение идеально подходит для локальной разработки, позволяя разработчикам всегда держать логи микросервисов перед глазами на втором мониторе.
### 🎯 **Идеально для локальной разработки** ### Идеально для локальной разработки
LogBoard+ особенно полезен для разработчиков, работающих с микросервисной архитектурой: LogBoard+ особенно полезен для разработчиков, работающих с микросервисной архитектурой:
@ -24,7 +24,7 @@ LogBoard+ особенно полезен для разработчиков, р
- **Мониторинг в реальном времени** - Видите проблемы сразу, как они возникают - **Мониторинг в реальном времени** - Видите проблемы сразу, как они возникают
- **Централизованный просмотр** - Все логи в одном месте, а не в десятках терминалов - **Централизованный просмотр** - Все логи в одном месте, а не в десятках терминалов
### 🐳 **Оптимизирован для Docker и Docker Compose** ### Оптимизирован для Docker и Docker Compose
Если ваша инфраструктура основана на Docker и Docker Compose, LogBoard+ станет незаменимым инструментом: Если ваша инфраструктура основана на Docker и Docker Compose, LogBoard+ станет незаменимым инструментом:
@ -34,7 +34,7 @@ LogBoard+ особенно полезен для разработчиков, р
- **Multi-view режим** - одновременный просмотр логов нескольких контейнеров - **Multi-view режим** - одновременный просмотр логов нескольких контейнеров
- **Интеграция с Docker API** - прямая работа с контейнерами - **Интеграция с Docker API** - прямая работа с контейнерами
### 🚀 **Производительность и удобство** ### Производительность и удобство
Приложение предоставляет удобный веб-интерфейс для работы с логами микросервисов, поддерживает множественные проекты Docker Compose и включает в себя функции безопасности. Приложение предоставляет удобный веб-интерфейс для работы с логами микросервисов, поддерживает множественные проекты Docker Compose и включает в себя функции безопасности.
@ -51,35 +51,35 @@ LogBoard+ особенно полезен для разработчиков, р
## Скриншоты ## Скриншоты
### 🔐 Страница входа ### Страница входа
| Светлая тема | Темная тема | | Светлая тема | Темная тема |
|--------------|-------------| |--------------|-------------|
| ![Вход - светлая тема](screenshots/login-white.png) | ![Вход - темная тема](screenshots/login-dark.png) | | ![Вход - светлая тема](screenshots/login-white.png) | ![Вход - темная тема](screenshots/login-dark.png) |
### 📊 Основной интерфейс ### Основной интерфейс
| Светлая тема | Темная тема | | Светлая тема | Темная тема |
|--------------|-------------| |--------------|-------------|
| ![Single View - светлая тема](screenshots/single-view-white.png) | ![Single View - темная тема](screenshots/single-view-dark.png) | | ![Single View - светлая тема](screenshots/single-view-white.png) | ![Single View - темная тема](screenshots/single-view-dark.png) |
### 🖥️ Multi-view режим ### Multi-view режим
![Multi-view режим](screenshots/multi-view.png) ![Multi-view режим](screenshots/multi-view.png)
### 📋 Карточки контейнеров ### Карточки контейнеров
![Карточки контейнеров](screenshots/container-cards.png) ![Карточки контейнеров](screenshots/container-cards.png)
### 📁 Проекты ### Проекты
![Список проектов](screenshots/projects.png) ![Список проектов](screenshots/projects.png)
### ⚙️ Настройки ### Настройки
![Панель настроек](screenshots/options.png) ![Панель настроек](screenshots/options.png)
### 🔧 Сворачиваемая боковая панель ### Сворачиваемая боковая панель
![Сворачиваемая боковая панель](screenshots/collapse-sidebar.png) ![Сворачиваемая боковая панель](screenshots/collapse-sidebar.png)
### Справка ### Справка
![Окно справки](screenshots/help.png) ![Окно справки](screenshots/help.png)
### 🚨 Страницы ошибок ### Страницы ошибок
![Страницы ошибок](screenshots/error%20pages.png) ![Страницы ошибок](screenshots/error%20pages.png)
## Быстрый старт ## Быстрый старт
@ -91,7 +91,7 @@ LogBoard+ особенно полезен для разработчиков, р
- 1 GB RAM - 1 GB RAM
- 1 CPU core - 1 CPU core
### Установка и запуск ### Вариант A: Локальный запуск (разработка)
1. **Клонирование репозитория** 1. **Клонирование репозитория**
```bash ```bash
@ -99,17 +99,6 @@ LogBoard+ особенно полезен для разработчиков, р
cd logboard cd logboard
``` ```
2. **Настройка переменных окружения**
```bash
cp env.example .env
# Отредактируйте .env файл при необходимости
```
3. **Запуск приложения**
```bash
docker compose up --build -d
```
2. **Настройка переменных окружения** 2. **Настройка переменных окружения**
```bash ```bash
make setup make setup
@ -126,6 +115,88 @@ LogBoard+ особенно полезен для разработчиков, р
http://localhost:9001 http://localhost:9001
``` ```
### Вариант B: Продакшен через docker-compose-prod.yaml
Ниже приведен пример конфигурации из `docker-compose-prod.yaml` для запуска готового образа в продакшене:
```yaml
services:
logboard:
image: docker.io/inecs/logboard:v1
container_name: logboard
environment:
# ОСНОВНЫЕ НАСТРОЙКИ ПРИЛОЖЕНИЯ
LOGBOARD_PORT: "9001"
LOGBOARD_TAIL: "500"
LOGBOARD_USER: "admin"
LOGBOARD_PASS: "admin"
LOGBOARD_SNAPSHOT_DIR: "/app/snapshots"
LOGBOARD_STATIC_DIR: "/app/static"
LOGBOARD_INDEX_HTML: "./app/templates/index.html"
TZ_TS: "Europe/Moscow"
# НАСТРОЙКИ DOCKER
DOCKER_HOST: "unix:///var/run/docker.sock"
DOCKER_TLS_VERIFY: ""
DOCKER_CERT_PATH: ""
DOCKER_NETWORKS: "iaas,infrastructure_iaas"
# БЕЗОПАСНОСТЬ
SECRET_KEY: "your-secret-key-here"
ENCRYPTION_KEY: "your-encryption-key-here"
# ЛОГИРОВАНИЕ
LOG_LEVEL: "INFO"
LOG_FORMAT: "json"
# ВЕБ-ИНТЕРФЕЙС
WEB_TITLE: "LogBoard+"
WEB_DESCRIPTION: "Веб-панель для просмотра логов микросервисов"
WEB_VERSION: "1.0.0"
# РЕЖИМ РАЗРАБОТКИ
DEBUG_MODE: "false"
# ПРОИЗВОДИТЕЛЬНОСТЬ
MAX_CONNECTIONS: "100"
CONNECTION_TIMEOUT: "30"
READ_TIMEOUT: "60"
# ФИЛЬТРАЦИЯ КОНТЕЙНЕРОВ
LOGBOARD_SKIP_UNHEALTHY: "true"
LOGBOARD_CONTAINER_LIST_TIMEOUT: "10"
LOGBOARD_CONTAINER_INFO_TIMEOUT: "3"
LOGBOARD_HEALTH_CHECK_TIMEOUT: "2"
# АУТЕНТИФИКАЦИЯ
AUTH_ENABLED: "true"
AUTH_METHOD: "jwt"
SESSION_TIMEOUT: "3600"
# УВЕДОМЛЕНИЯ
NOTIFICATIONS_ENABLED: "false"
SMTP_HOST: ""
SMTP_PORT: "587"
SMTP_USER: ""
SMTP_PASS: ""
SMTP_FROM: ""
# AJAX ОБНОВЛЕНИЕ
LOGBOARD_AJAX_UPDATE_INTERVAL: "2000"
ports:
- "9001:9001"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./snapshots:/app/snapshots
restart: unless-stopped
user: 0:0
```
Рекомендации для продакшена:
- Используйте файл `.env` для значений чувствительных переменных (`SECRET_KEY`, `ENCRYPTION_KEY`, `LOGBOARD_PASS`) и не храните их в открытом виде в compose-файле.
- Настройте reverse proxy (Nginx/Traefik) и включите HTTPS.
### Учетные данные по умолчанию ### Учетные данные по умолчанию
- **Пользователь:** `admin` - **Пользователь:** `admin`
@ -231,23 +302,23 @@ logboard/
#### Контейнеры и сервисы #### Контейнеры и сервисы
- `GET /api/services` - Список контейнеров - `GET /api/containers/services` - Список контейнеров
- `GET /api/projects` - Список проектов Docker Compose - `GET /api/containers/projects` - Список проектов Docker Compose
- `GET /api/logs/{container_id}` - Логи контейнера - `GET /api/logs/{container_id}` - Логи контейнера
- `GET /api/logs/stats/{container_id}` - Статистика логов - `GET /api/logs/stats/{container_id}` - Статистика логов
#### Управление #### Управление
- `GET /api/settings` - Настройки приложения - `GET /api/settings` - Настройки приложения
- `GET /api/excluded-containers` - Список исключенных контейнеров - `GET /api/containers/excluded` - Список исключенных контейнеров
- `POST /api/excluded-containers` - Обновление исключенных контейнеров - `POST /api/containers/excluded` - Обновление исключенных контейнеров
- `POST /api/snapshot` - Создание снимка логов - `POST /api/logs/snapshot` - Создание снимка логов
### WebSocket API ### WebSocket API
- `ws://host:port/ws/logs/{container_id}` - Логи отдельного контейнера - `ws://host:port/api/websocket/logs/{container_id}` - Логи отдельного контейнера
- `ws://host:port/ws/fan/{service_name}` - Логи сервиса (все реплики) - `ws://host:port/api/websocket/fan/{service_name}` - Логи сервиса (все реплики)
- `ws://host:port/ws/fan_group` - Логи группы сервисов - `ws://host:port/api/websocket/fan_group` - Логи группы сервисов
## Конфигурация ## Конфигурация

View File

@ -0,0 +1,84 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Скрипт подстановки переменных окружения из .env/env.example в файл шаблона.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
Использование:
python3 app/scripts/substitute_env.py PATH_TO_ENV PATH_TO_COMPOSE_FILE
Логика:
- Читает пары KEY=VALUE из указанного env-файла (игнорирует комментарии и пустые строки)
- Не выполняет файл как shell (безопасно парсит)
- Заменяет плейсхолдеры вида ${KEY} в целевом файле
"""
from __future__ import annotations
import io
import os
import re
import sys
from typing import Dict
def parse_env_file(env_path: str) -> Dict[str, str]:
"""Простой парсер .env файла: KEY=VALUE, без исполнения shell."""
variables: Dict[str, str] = {}
with io.open(env_path, "r", encoding="utf-8") as fh:
for raw in fh:
line = raw.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
key, val = line.split("=", 1)
key = key.strip()
val = val.strip()
# Снимаем внешние кавычки, если присутствуют
if (val.startswith('"') and val.endswith('"')) or (
val.startswith("'") and val.endswith("'")
):
val = val[1:-1]
variables[key] = val
return variables
def substitute_placeholders(text: str, env: Dict[str, str]) -> str:
"""Заменяет ${VAR} из словаря env, оставляет плейсхолдер, если VAR не найден."""
pattern = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
return pattern.sub(lambda m: env.get(m.group(1), m.group(0)), text)
def main() -> int:
if len(sys.argv) != 3:
sys.stderr.write(
"Usage: substitute_env.py <env_path> <compose_path>\n"
)
return 2
env_path, compose_path = sys.argv[1], sys.argv[2]
if not os.path.exists(env_path):
sys.stderr.write(f"Env file not found: {env_path}\n")
return 3
if not os.path.exists(compose_path):
sys.stderr.write(f"Compose file not found: {compose_path}\n")
return 4
env = parse_env_file(env_path)
with io.open(compose_path, "r", encoding="utf-8") as fh:
content = fh.read()
new_content = substitute_placeholders(content, env)
with io.open(compose_path, "w", encoding="utf-8") as fh:
fh.write(new_content)
return 0
if __name__ == "__main__":
raise SystemExit(main())

113
docker-compose-prod.yaml Normal file
View File

@ -0,0 +1,113 @@
services:
logboard:
image: docker.io/inecs/logboard:v1
container_name: logboard
environment:
# ОСНОВНЫЕ НАСТРОЙКИ ПРИЛОЖЕНИЯ
# Порт веб-интерфейса LogBoard+
LOGBOARD_PORT: "9001"
# Количество строк логов по умолчанию (tail)
LOGBOARD_TAIL: "500"
# Имя пользователя для входа
LOGBOARD_USER: "admin"
# Пароль для входа (обязательно поменять в продакшене)
LOGBOARD_PASS: "admin"
# Директория для снимков логов (в контейнере)
LOGBOARD_SNAPSHOT_DIR: "/app/snapshots"
# Директория для статических файлов
LOGBOARD_STATIC_DIR: "/app/static"
# Путь к HTML шаблону главной страницы
LOGBOARD_INDEX_HTML: "./app/templates/index.html"
# Таймзона для временных меток (например: Europe/Moscow, UTC)
TZ_TS: "Europe/Moscow"
# НАСТРОЙКИ DOCKER
# Фильтр по проекту Docker Compose (опционально)
# COMPOSE_PROJECT_NAME: "${COMPOSE_PROJECT_NAME}"
# Перечень проектов для отображения (через запятую)
# LOGBOARD_PROJECTS: "${LOGBOARD_PROJECTS}"
# Путь к Docker socket / удалённому хосту
DOCKER_HOST: "unix:///var/run/docker.sock"
# Включить проверку TLS для Docker (удалённые хосты)
DOCKER_TLS_VERIFY: ""
# Путь к TLS сертификатам Docker
DOCKER_CERT_PATH: ""
# Внешние сети Docker (через запятую)
DOCKER_NETWORKS: "iaas,infrastructure_iaas"
# БЕЗОПАСНОСТЬ
# Секретный ключ для JWT (обязательно поменять в продакшене)
SECRET_KEY: "your-secret-key-here"
# Ключ шифрования для чувствительных данных (обязательно поменять)
ENCRYPTION_KEY: "your-encryption-key-here"
# ЛОГИРОВАНИЕ
# Уровень логирования (DEBUG, INFO, WARNING, ERROR)
LOG_LEVEL: "INFO"
# Формат логов (json, text)
LOG_FORMAT: "json"
# ВЕБ-ИНТЕРФЕЙС
# Заголовок веб-интерфейса
WEB_TITLE: "LogBoard+"
# Описание веб-интерфейса
WEB_DESCRIPTION: "Веб-панель для просмотра логов микросервисов"
# Версия веб-интерфейса
WEB_VERSION: "1.0.0"
# РЕЖИМ РАЗРАБОТКИ
# Режим отладки (true/false)
DEBUG_MODE: "false"
# ПРОИЗВОДИТЕЛЬНОСТЬ
# Максимум одновременных подключений
MAX_CONNECTIONS: "100"
# Таймаут подключения (сек)
CONNECTION_TIMEOUT: "30"
# Таймаут чтения (сек)
READ_TIMEOUT: "60"
# ФИЛЬТРАЦИЯ КОНТЕЙНЕРОВ
# Пропускать контейнеры с проблемным health check (true/false)
LOGBOARD_SKIP_UNHEALTHY: "true"
# Таймаут получения списка контейнеров (сек)
LOGBOARD_CONTAINER_LIST_TIMEOUT: "10"
# Таймаут получения информации о контейнере (сек)
LOGBOARD_CONTAINER_INFO_TIMEOUT: "3"
# Таймаут health check контейнера (сек)
LOGBOARD_HEALTH_CHECK_TIMEOUT: "2"
# АУТЕНТИФИКАЦИЯ
# Включить аутентификацию (true/false)
AUTH_ENABLED: "true"
# Метод аутентификации (jwt)
AUTH_METHOD: "jwt"
# Время жизни сессии (сек)
SESSION_TIMEOUT: "3600"
# УВЕДОМЛЕНИЯ
# Включить уведомления по email (true/false)
NOTIFICATIONS_ENABLED: "false"
# SMTP сервер
SMTP_HOST: ""
# Порт SMTP
SMTP_PORT: "587"
# Пользователь SMTP
SMTP_USER: ""
# Пароль SMTP
SMTP_PASS: ""
# Email отправителя
SMTP_FROM: ""
# AJAX ОБНОВЛЕНИЕ
# Интервал AJAX обновления логов (мс)
LOGBOARD_AJAX_UPDATE_INTERVAL: "2000"
ports:
- "9001:9001"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./snapshots:/app/snapshots
restart: unless-stopped
user: 0:0

View File

@ -16,7 +16,7 @@
LogBoard+ предоставляет REST API и WebSocket API для работы с логами Docker контейнеров. API разработан для интеграции с системами мониторинга и автоматизации, а также для создания собственных клиентов. LogBoard+ предоставляет REST API и WebSocket API для работы с логами Docker контейнеров. API разработан для интеграции с системами мониторинга и автоматизации, а также для создания собственных клиентов.
### 🎯 **Применение API** ### Применение API
- **Интеграция с CI/CD** - автоматический мониторинг логов в пайплайнах - **Интеграция с CI/CD** - автоматический мониторинг логов в пайплайнах
- **Собственные дашборды** - создание кастомных интерфейсов мониторинга - **Собственные дашборды** - создание кастомных интерфейсов мониторинга
@ -108,7 +108,7 @@ Authorization: Bearer <token>
### Контейнеры и сервисы ### Контейнеры и сервисы
#### GET /api/services #### GET /api/containers/services
Получение списка всех контейнеров. Получение списка всех контейнеров.
@ -122,7 +122,7 @@ Authorization: Bearer <token>
**Пример запроса:** **Пример запроса:**
```bash ```bash
curl -X GET "http://localhost:9001/api/services?projects=myproject&include_stopped=true" \ curl -X GET "http://localhost:9001/api/containers/services?projects=myproject&include_stopped=true" \
-H "Authorization: Bearer YOUR_TOKEN" -H "Authorization: Bearer YOUR_TOKEN"
``` ```
@ -157,7 +157,7 @@ curl -X GET "http://localhost:9001/api/services?projects=myproject&include_stopp
] ]
``` ```
#### GET /api/projects #### GET /api/containers/projects
Получение списка всех проектов Docker Compose. Получение списка всех проектов Docker Compose.
@ -268,7 +268,7 @@ curl -X GET "http://localhost:9001/api/logs/abc123def456?tail=100&since=2024-01-
} }
``` ```
#### GET /api/excluded-containers #### GET /api/containers/excluded
Получение списка исключенных контейнеров. Получение списка исключенных контейнеров.
@ -283,7 +283,7 @@ curl -X GET "http://localhost:9001/api/logs/abc123def456?tail=100&since=2024-01-
} }
``` ```
#### POST /api/excluded-containers #### POST /api/containers/excluded
Обновление списка исключенных контейнеров. Обновление списка исключенных контейнеров.
@ -306,7 +306,7 @@ curl -X GET "http://localhost:9001/api/logs/abc123def456?tail=100&since=2024-01-
} }
``` ```
#### POST /api/snapshot #### POST /api/logs/snapshot
Создание снимка логов. Создание снимка логов.
@ -374,7 +374,7 @@ ok
Все WebSocket endpoints требуют JWT токен в параметре `token`. Все WebSocket endpoints требуют JWT токен в параметре `token`.
### ws://host:port/ws/logs/{container_id} ### ws://host:port/api/websocket/logs/{container_id}
Получение логов отдельного контейнера. Получение логов отдельного контейнера.
@ -390,7 +390,7 @@ ok
```javascript ```javascript
const token = "your-jwt-token"; const token = "your-jwt-token";
const containerId = "abc123def456"; const containerId = "abc123def456";
const ws = new WebSocket(`ws://localhost:9001/ws/logs/${containerId}?token=${token}&tail=100`); const ws = new WebSocket(`ws://localhost:9001/api/websocket/logs/${containerId}?token=${token}&tail=100`);
ws.onmessage = function(event) { ws.onmessage = function(event) {
console.log('Получены логи:', event.data); console.log('Получены логи:', event.data);
@ -401,7 +401,7 @@ ws.onerror = function(error) {
}; };
``` ```
### ws://host:port/ws/fan/{service_name} ### ws://host:port/api/websocket/fan/{service_name}
Получение логов сервиса (все реплики). Получение логов сервиса (все реплики).
@ -416,14 +416,14 @@ ws.onerror = function(error) {
```javascript ```javascript
const token = "your-jwt-token"; const token = "your-jwt-token";
const serviceName = "web"; const serviceName = "web";
const ws = new WebSocket(`ws://localhost:9001/ws/fan/${serviceName}?token=${token}&tail=100`); const ws = new WebSocket(`ws://localhost:9001/api/websocket/fan/${serviceName}?token=${token}&tail=100`);
ws.onmessage = function(event) { ws.onmessage = function(event) {
console.log('Логи сервиса:', event.data); console.log('Логи сервиса:', event.data);
}; };
``` ```
### ws://host:port/ws/fan_group ### ws://host:port/api/websocket/fan_group
Получение логов группы сервисов. Получение логов группы сервисов.
@ -438,7 +438,7 @@ ws.onmessage = function(event) {
```javascript ```javascript
const token = "your-jwt-token"; const token = "your-jwt-token";
const services = "web,db,redis"; const services = "web,db,redis";
const ws = new WebSocket(`ws://localhost:9001/ws/fan_group?services=${services}&token=${token}&tail=100`); const ws = new WebSocket(`ws://localhost:9001/api/websocket/fan_group?services=${services}&token=${token}&tail=100`);
ws.onmessage = function(event) { ws.onmessage = function(event) {
console.log('Логи группы сервисов:', event.data); console.log('Логи группы сервисов:', event.data);
@ -522,7 +522,7 @@ def login(username, password):
# Получение списка контейнеров # Получение списка контейнеров
def get_containers(token): def get_containers(token):
headers = {"Authorization": f"Bearer {token}"} headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{BASE_URL}/api/services", headers=headers) response = requests.get(f"{BASE_URL}/api/containers/services", headers=headers)
return response.json() return response.json()
# Получение логов контейнера # Получение логов контейнера
@ -563,7 +563,7 @@ class LogBoardAPI {
if (projects) params.append('projects', projects); if (projects) params.append('projects', projects);
if (includeStopped) params.append('include_stopped', 'true'); if (includeStopped) params.append('include_stopped', 'true');
const response = await fetch(`${this.baseUrl}/api/services?${params}`, { const response = await fetch(`${this.baseUrl}/api/containers/services?${params}`, {
headers: this.headers headers: this.headers
}); });
return await response.json(); return await response.json();
@ -592,7 +592,7 @@ class LogBoardAPI {
// WebSocket для live-логов // WebSocket для live-логов
createLogsWebSocket(containerId, tail = 100) { createLogsWebSocket(containerId, tail = 100) {
const ws = new WebSocket( const ws = new WebSocket(
`ws://${this.baseUrl.replace('http://', '')}/ws/logs/${containerId}?token=${this.token}&tail=${tail}` `ws://${this.baseUrl.replace('http://', '')}/api/websocket/logs/${containerId}?token=${this.token}&tail=${tail}`
); );
return ws; return ws;
} }
@ -654,12 +654,12 @@ echo "Токен получен: ${TOKEN:0:20}..."
# Получение списка контейнеров # Получение списка контейнеров
echo "Получение списка контейнеров..." echo "Получение списка контейнеров..."
curl -s -X GET "$BASE_URL/api/services" \ curl -s -X GET "$BASE_URL/api/containers/services" \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
| jq '.[] | {name: .name, status: .status, project: .project}' | jq '.[] | {name: .name, status: .status, project: .project}'
# Получение логов первого контейнера # Получение логов первого контейнера
CONTAINER_ID=$(curl -s -X GET "$BASE_URL/api/services" \ CONTAINER_ID=$(curl -s -X GET "$BASE_URL/api/containers/services" \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
| jq -r '.[0].id') | jq -r '.[0].id')

View File

@ -109,7 +109,7 @@ LogBoard+ использует переменные окружения для н
- **ReDoc** - альтернативная документация по адресу `/redoc` - **ReDoc** - альтернативная документация по адресу `/redoc`
- **Подробное логирование** - детальные логи для отладки - **Подробное логирование** - детальные логи для отладки
**⚠️ Важно:** В продакшене обязательно установите `DEBUG_MODE=false`! Важно: в продакшене обязательно установите `DEBUG_MODE=false`.
### Настройки уведомлений ### Настройки уведомлений
@ -311,11 +311,11 @@ volumes:
```bash ```bash
# Получение списка исключенных контейнеров # Получение списка исключенных контейнеров
curl -X GET "http://localhost:9001/api/excluded-containers" \ curl -X GET "http://localhost:9001/api/containers/excluded" \
-H "Authorization: Bearer YOUR_TOKEN" -H "Authorization: Bearer YOUR_TOKEN"
# Обновление списка # Обновление списка
curl -X POST "http://localhost:9001/api/excluded-containers" \ curl -X POST "http://localhost:9001/api/containers/excluded" \
-H "Authorization: Bearer YOUR_TOKEN" \ -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '["container1", "container2"]' -d '["container1", "container2"]'

View File

@ -7,7 +7,7 @@
LogBoard+ - это современная веб-панель для мониторинга и просмотра логов Docker контейнеров в реальном времени. Приложение идеально подходит для локальной разработки, позволяя разработчикам всегда держать логи микросервисов перед глазами на втором мониторе. LogBoard+ - это современная веб-панель для мониторинга и просмотра логов Docker контейнеров в реальном времени. Приложение идеально подходит для локальной разработки, позволяя разработчикам всегда держать логи микросервисов перед глазами на втором мониторе.
### 🎯 **Идеально для локальной разработки** ### Идеально для локальной разработки
LogBoard+ особенно полезен для разработчиков, работающих с микросервисной архитектурой: LogBoard+ особенно полезен для разработчиков, работающих с микросервисной архитектурой:
@ -16,7 +16,7 @@ LogBoard+ особенно полезен для разработчиков, р
- **Мониторинг в реальном времени** - Видите проблемы сразу, как они возникают - **Мониторинг в реальном времени** - Видите проблемы сразу, как они возникают
- **Централизованный просмотр** - Все логи в одном месте, а не в десятках терминалов - **Централизованный просмотр** - Все логи в одном месте, а не в десятках терминалов
### 🐳 **Оптимизирован для Docker и Docker Compose** ### Оптимизирован для Docker и Docker Compose
Если ваша инфраструктура основана на Docker и Docker Compose, LogBoard+ станет незаменимым инструментом: Если ваша инфраструктура основана на Docker и Docker Compose, LogBoard+ станет незаменимым инструментом:
@ -26,13 +26,13 @@ LogBoard+ особенно полезен для разработчиков, р
- **Multi-view режим** - одновременный просмотр логов нескольких контейнеров - **Multi-view режим** - одновременный просмотр логов нескольких контейнеров
- **Интеграция с Docker API** - прямая работа с контейнерами - **Интеграция с Docker API** - прямая работа с контейнерами
### 🚀 **Производительность и удобство** ### Производительность и удобство
Приложение предоставляет удобный веб-интерфейс для работы с логами микросервисов, поддерживает множественные проекты Docker Compose и включает в себя функции безопасности. Приложение предоставляет удобный веб-интерфейс для работы с логами микросервисов, поддерживает множественные проекты Docker Compose и включает в себя функции безопасности.
## Быстрый старт ## Быстрый старт
### Установка ### Вариант A: Разработка
```bash ```bash
# Клонирование репозитория # Клонирование репозитория
@ -40,17 +40,50 @@ git clone <repository-url>
cd logboard cd logboard
# Настройка переменных окружения # Настройка переменных окружения
cp env.example .env make setup
# Запуск приложения # Запуск приложения
docker compose up --build -d make up
``` ```
### Доступ ### Вариант B: Продакшен (пример docker-compose-prod.yaml)
- **URL:** http://localhost:9001 ```yaml
- **Пользователь:** `admin` services:
- **Пароль:** `admin` (обязательно измените в продакшене!) logboard:
image: docker.io/inecs/logboard:v1
container_name: logboard
environment:
LOGBOARD_PORT: "9001"
LOGBOARD_TAIL: "500"
LOGBOARD_USER: "admin"
LOGBOARD_PASS: "admin"
LOGBOARD_SNAPSHOT_DIR: "/app/snapshots"
LOGBOARD_STATIC_DIR: "/app/static"
LOGBOARD_INDEX_HTML: "./app/templates/index.html"
TZ_TS: "Europe/Moscow"
DOCKER_HOST: "unix:///var/run/docker.sock"
SECRET_KEY: "your-secret-key-here"
ENCRYPTION_KEY: "your-encryption-key-here"
LOG_LEVEL: "INFO"
LOG_FORMAT: "json"
DEBUG_MODE: "false"
AUTH_ENABLED: "true"
SESSION_TIMEOUT: "3600"
ports:
- "9001:9001"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./snapshots:/app/snapshots
restart: unless-stopped
user: 0:0
```
После запуска интерфейс доступен по адресу: http://localhost:9001
Учетные данные по умолчанию:
- Пользователь: `admin`
- Пароль: `admin` (обязательно измените в продакшене)
## Документация ## Документация
@ -106,35 +139,35 @@ docker compose up --build -d
## Скриншоты ## Скриншоты
### 🔐 Страница входа ### Страница входа
| Светлая тема | Темная тема | | Светлая тема | Темная тема |
|--------------|-------------| |--------------|-------------|
| ![Вход - светлая тема](../screenshots/login-white.png) | ![Вход - темная тема](../screenshots/login-dark.png) | | ![Вход - светлая тема](../screenshots/login-white.png) | ![Вход - темная тема](../screenshots/login-dark.png) |
### 📊 Основной интерфейс ### Основной интерфейс
| Светлая тема | Темная тема | | Светлая тема | Темная тема |
|--------------|-------------| |--------------|-------------|
| ![Single View - светлая тема](../screenshots/single-view-white.png) | ![Single View - темная тема](../screenshots/single-view-dark.png) | | ![Single View - светлая тема](../screenshots/single-view-white.png) | ![Single View - темная тема](../screenshots/single-view-dark.png) |
### 🖥️ Multi-view режим ### Multi-view режим
![Multi-view режим](../screenshots/multi-view.png) ![Multi-view режим](../screenshots/multi-view.png)
### 📋 Карточки контейнеров ### Карточки контейнеров
![Карточки контейнеров](../screenshots/container-cards.png) ![Карточки контейнеров](../screenshots/container-cards.png)
### 📁 Проекты ### Проекты
![Список проектов](../screenshots/projects.png) ![Список проектов](../screenshots/projects.png)
### ⚙️ Настройки ### Настройки
![Панель настроек](../screenshots/options.png) ![Панель настроек](../screenshots/options.png)
### 🔧 Сворачиваемая боковая панель ### Сворачиваемая боковая панель
![Сворачиваемая боковая панель](../screenshots/collapse-sidebar.png) ![Сворачиваемая боковая панель](../screenshots/collapse-sidebar.png)
### Справка ### Справка
![Окно справки](../screenshots/help.png) ![Окно справки](../screenshots/help.png)
### 🚨 Страницы ошибок ### Страницы ошибок
![Страницы ошибок](../screenshots/error%20pages.png) ![Страницы ошибок](../screenshots/error%20pages.png)
## Архитектура ## Архитектура
@ -169,22 +202,22 @@ docker compose up --build -d
- `GET /api/auth/me` - Информация о текущем пользователе - `GET /api/auth/me` - Информация о текущем пользователе
#### Контейнеры и сервисы #### Контейнеры и сервисы
- `GET /api/services` - Список контейнеров - `GET /api/containers/services` - Список контейнеров
- `GET /api/projects` - Список проектов Docker Compose - `GET /api/containers/projects` - Список проектов Docker Compose
- `GET /api/logs/{container_id}` - Логи контейнера - `GET /api/logs/{container_id}` - Логи контейнера
- `GET /api/logs/stats/{container_id}` - Статистика логов - `GET /api/logs/stats/{container_id}` - Статистика логов
#### Управление #### Управление
- `GET /api/settings` - Настройки приложения - `GET /api/settings` - Настройки приложения
- `GET /api/excluded-containers` - Список исключенных контейнеров - `GET /api/containers/excluded` - Список исключенных контейнеров
- `POST /api/excluded-containers` - Обновление исключенных контейнеров - `POST /api/containers/excluded` - Обновление исключенных контейнеров
- `POST /api/snapshot` - Создание снимка логов - `POST /api/logs/snapshot` - Создание снимка логов
### WebSocket API ### WebSocket API
- `ws://host:port/ws/logs/{container_id}` - Логи отдельного контейнера - `ws://host:port/api/websocket/logs/{container_id}` - Логи отдельного контейнера
- `ws://host:port/ws/fan/{service_name}` - Логи сервиса (все реплики) - `ws://host:port/api/websocket/fan/{service_name}` - Логи сервиса (все реплики)
- `ws://host:port/ws/fan_group` - Логи группы сервисов - `ws://host:port/api/websocket/fan_group` - Логи группы сервисов
## Конфигурация ## Конфигурация

View File

@ -15,7 +15,7 @@
## Обзор и применение ## Обзор и применение
### 🎯 **Идеально для локальной разработки** ### Идеально для локальной разработки
LogBoard+ создан специально для разработчиков, работающих с микросервисной архитектурой. Если вы используете Docker и Docker Compose для локальной разработки, этот инструмент станет незаменимым помощником: LogBoard+ создан специально для разработчиков, работающих с микросервисной архитектурой. Если вы используете Docker и Docker Compose для локальной разработки, этот инструмент станет незаменимым помощником:
@ -24,7 +24,7 @@ LogBoard+ создан специально для разработчиков,
- **Мониторинг в реальном времени** - Видите проблемы сразу, как они возникают - **Мониторинг в реальном времени** - Видите проблемы сразу, как они возникают
- **Централизованный просмотр** - Все логи в одном месте, а не в десятках терминалов - **Централизованный просмотр** - Все логи в одном месте, а не в десятках терминалов
### 🐳 **Оптимизирован для Docker и Docker Compose** ### Оптимизирован для Docker и Docker Compose
Если ваша инфраструктура основана на Docker и Docker Compose, LogBoard+ предоставляет: Если ваша инфраструктура основана на Docker и Docker Compose, LogBoard+ предоставляет:
@ -34,7 +34,7 @@ LogBoard+ создан специально для разработчиков,
- **Multi-view режим** - одновременный просмотр логов нескольких контейнеров - **Multi-view режим** - одновременный просмотр логов нескольких контейнеров
- **Интеграция с Docker API** - прямая работа с контейнерами - **Интеграция с Docker API** - прямая работа с контейнерами
### 💡 **Сценарии использования** ### Сценарии использования
- **Локальная разработка** - мониторинг логов микросервисов на втором мониторе - **Локальная разработка** - мониторинг логов микросервисов на втором мониторе
- **Отладка проблем** - быстрый поиск ошибок в логах - **Отладка проблем** - быстрый поиск ошибок в логах
@ -212,11 +212,11 @@ curl -X POST "http://localhost:9001/api/auth/login" \
```bash ```bash
# Проверка списка контейнеров # Проверка списка контейнеров
curl -X GET "http://localhost:9001/api/services" \ curl -X GET "http://localhost:9001/api/containers/services" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Authorization: Bearer YOUR_TOKEN_HERE"
# Проверка списка проектов # Проверка списка проектов
curl -X GET "http://localhost:9001/api/projects" \ curl -X GET "http://localhost:9001/api/containers/projects" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Authorization: Bearer YOUR_TOKEN_HERE"
``` ```

View File

@ -129,39 +129,39 @@ echo ""
# Проверка наличия OpenSSL # Проверка наличия OpenSSL
if ! command -v openssl &> /dev/null; then if ! command -v openssl &> /dev/null; then
echo "OpenSSL не найден. Установите OpenSSL для генерации ключей." echo "OpenSSL не найден. Установите OpenSSL для генерации ключей."
exit 1 exit 1
fi fi
echo "🔐 Генерация SECRET_KEY..." echo "Генерация SECRET_KEY..."
SECRET_KEY=$(openssl rand -hex 32) SECRET_KEY=$(openssl rand -hex 32)
echo "SECRET_KEY=$SECRET_KEY" echo "SECRET_KEY=$SECRET_KEY"
echo "" echo ""
echo "🔐 Генерация ENCRYPTION_KEY..." echo "Генерация ENCRYPTION_KEY..."
ENCRYPTION_KEY=$(openssl rand -hex 32) ENCRYPTION_KEY=$(openssl rand -hex 32)
echo "ENCRYPTION_KEY=$ENCRYPTION_KEY" echo "ENCRYPTION_KEY=$ENCRYPTION_KEY"
echo "" echo ""
echo "🔐 Генерация пароля пользователя..." echo "Генерация пароля пользователя..."
USER_PASSWORD=$(openssl rand -base64 16 | tr -d "=+/" | cut -c1-16) USER_PASSWORD=$(openssl rand -base64 16 | tr -d "=+/" | cut -c1-16)
echo "LOGBOARD_PASS=$USER_PASSWORD" echo "LOGBOARD_PASS=$USER_PASSWORD"
echo "" echo ""
echo "📝 Обновление .env файла..." echo "Обновление .env файла..."
if [ -f .env ]; then if [ -f .env ]; then
# Создание резервной копии # Создание резервной копии
cp .env .env.backup.$(date +%Y%m%d_%H%M%S) cp .env .env.backup.$(date +%Y%m%d_%H%M%S)
echo "Создана резервная копия .env" echo "Создана резервная копия .env"
# Обновление ключей # Обновление ключей
sed -i "s/^SECRET_KEY=.*/SECRET_KEY=$SECRET_KEY/" .env sed -i "s/^SECRET_KEY=.*/SECRET_KEY=$SECRET_KEY/" .env
sed -i "s/^ENCRYPTION_KEY=.*/ENCRYPTION_KEY=$ENCRYPTION_KEY/" .env sed -i "s/^ENCRYPTION_KEY=.*/ENCRYPTION_KEY=$ENCRYPTION_KEY/" .env
sed -i "s/^LOGBOARD_PASS=.*/LOGBOARD_PASS=$USER_PASSWORD/" .env sed -i "s/^LOGBOARD_PASS=.*/LOGBOARD_PASS=$USER_PASSWORD/" .env
echo "Ключи обновлены в .env" echo "Ключи обновлены в .env"
else else
echo "⚠️ Файл .env не найден. Создайте его из env.example" echo "Файл .env не найден. Создайте его из env.example"
fi fi
echo "" echo ""
@ -171,7 +171,7 @@ echo "2. Не передавайте ключи через незащищенн
echo "3. Регулярно обновляйте ключи" echo "3. Регулярно обновляйте ключи"
echo "4. Используйте разные ключи для разных окружений" echo "4. Используйте разные ключи для разных окружений"
echo "" echo ""
echo "Генерация ключей завершена!" echo "Генерация ключей завершена!"
``` ```
```bash ```bash
@ -213,9 +213,9 @@ def check_entropy(key_hex):
print(f'Энтропия: {entropy:.2%}') print(f'Энтропия: {entropy:.2%}')
if entropy > 0.8: if entropy > 0.8:
print('Ключ имеет хорошую энтропию') print('Ключ имеет хорошую энтропию')
else: else:
print('⚠️ Ключ может иметь низкую энтропию') print('Ключ может иметь низкую энтропию')
# Проверка вашего ключа # Проверка вашего ключа
key = '8a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6' key = '8a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6'
@ -660,43 +660,43 @@ echo ""
# Проверка паролей по умолчанию # Проверка паролей по умолчанию
if grep -q "LOGBOARD_PASS=admin" .env; then if grep -q "LOGBOARD_PASS=admin" .env; then
echo "Пароль по умолчанию не изменен!" echo "Пароль по умолчанию не изменен"
else else
echo "Пароль изменен" echo "Пароль изменен"
fi fi
# Проверка секретных ключей # Проверка секретных ключей
if grep -q "SECRET_KEY=your-secret-key-here" .env; then if grep -q "SECRET_KEY=your-secret-key-here" .env; then
echo "SECRET_KEY не изменен!" echo "SECRET_KEY не изменен"
else else
echo "SECRET_KEY изменен" echo "SECRET_KEY изменен"
fi fi
if grep -q "ENCRYPTION_KEY=your-encryption-key-here" .env; then if grep -q "ENCRYPTION_KEY=your-encryption-key-here" .env; then
echo "ENCRYPTION_KEY не изменен!" echo "ENCRYPTION_KEY не изменен"
else else
echo "ENCRYPTION_KEY изменен" echo "ENCRYPTION_KEY изменен"
fi fi
# Проверка HTTPS # Проверка HTTPS
if curl -s -I https://localhost:9001 > /dev/null 2>&1; then if curl -s -I https://localhost:9001 > /dev/null 2>&1; then
echo "HTTPS доступен" echo "HTTPS доступен"
else else
echo "⚠️ HTTPS недоступен" echo "HTTPS недоступен"
fi fi
# Проверка прав Docker # Проверка прав Docker
if ls -la /var/run/docker.sock | grep -q "srw-rw-rw-"; then if ls -la /var/run/docker.sock | grep -q "srw-rw-rw-"; then
echo "Docker socket доступен" echo "Docker socket доступен"
else else
echo "Проблемы с доступом к Docker socket" echo "Проблемы с доступом к Docker socket"
fi fi
# Проверка файрвола # Проверка файрвола
if command -v ufw > /dev/null && sudo ufw status | grep -q "Status: active"; then if command -v ufw > /dev/null && sudo ufw status | grep -q "Status: active"; then
echo "Файрвол активен" echo "Файрвол активен"
else else
echo "⚠️ Файрвол не настроен" echo "Файрвол не настроен"
fi fi
echo "" echo ""

View File

@ -363,7 +363,7 @@ echo $SESSION_TIMEOUT
```bash ```bash
# Проверка WebSocket endpoint # Проверка WebSocket endpoint
curl -I "http://localhost:9001/ws/logs/test" curl -I "http://localhost:9001/api/websocket/logs/test"
# Проверка логов приложения # Проверка логов приложения
make logs | grep -i websocket make logs | grep -i websocket
@ -376,8 +376,8 @@ make logs | grep -i websocket
1. **Проблема с прокси** 1. **Проблема с прокси**
```nginx ```nginx
# Настройка Nginx для WebSocket # Настройка Nginx для WebSocket с корректными путями API
location /ws/ { location /api/websocket/ {
proxy_pass http://localhost:9001; proxy_pass http://localhost:9001;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
@ -390,7 +390,7 @@ make logs | grep -i websocket
```javascript ```javascript
// Проверка токена в WebSocket URL // Проверка токена в WebSocket URL
const token = "your-valid-token"; const token = "your-valid-token";
const ws = new WebSocket(`ws://localhost:9001/ws/logs/container-id?token=${token}`); const ws = new WebSocket(`ws://localhost:9001/api/websocket/logs/container-id?token=${token}`);
``` ```
3. **Проблема с CORS** 3. **Проблема с CORS**
@ -703,7 +703,7 @@ curl -X POST "http://localhost:9001/api/auth/login" \
# Проверка API с токеном # Проверка API с токеном
TOKEN="your-token" TOKEN="your-token"
curl -X GET "http://localhost:9001/api/services" \ curl -X GET "http://localhost:9001/api/containers/services" \
-H "Authorization: Bearer $TOKEN" -H "Authorization: Bearer $TOKEN"
``` ```
@ -762,6 +762,6 @@ curl -s -X POST "http://localhost:9001/api/auth/login" \
TOKEN=$(curl -s -X POST "http://localhost:9001/api/auth/login" \ TOKEN=$(curl -s -X POST "http://localhost:9001/api/auth/login" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin"}' | jq -r '.access_token') -d '{"username":"admin","password":"admin"}' | jq -r '.access_token')
curl -s -X GET "http://localhost:9001/api/services" \ curl -s -X GET "http://localhost:9001/api/containers/services" \
-H "Authorization: Bearer $TOKEN" | jq 'length' -H "Authorization: Bearer $TOKEN" | jq 'length'
``` ```

View File

@ -45,12 +45,12 @@ curl -X POST "http://localhost:9001/api/auth/login" \
```javascript ```javascript
const token = "your-jwt-token"; const token = "your-jwt-token";
const ws = new WebSocket(`ws://localhost:9001/ws/logs/container-id?token=${token}`); const ws = new WebSocket(`ws://localhost:9001/api/websocket/logs/container-id?token=${token}`);
``` ```
## Endpoints ## Endpoints
### ws://host:port/ws/logs/{container_id} ### ws://host:port/api/websocket/logs/{container_id}
Получение логов отдельного контейнера в реальном времени. Получение логов отдельного контейнера в реальном времени.
@ -70,7 +70,7 @@ const ws = new WebSocket(`ws://localhost:9001/ws/logs/container-id?token=${token
const containerId = "abc123def456"; const containerId = "abc123def456";
const token = "your-jwt-token"; const token = "your-jwt-token";
const ws = new WebSocket(`ws://localhost:9001/ws/logs/${containerId}?token=${token}&tail=100`); const ws = new WebSocket(`ws://localhost:9001/api/websocket/logs/${containerId}?token=${token}&tail=100`);
ws.onopen = function() { ws.onopen = function() {
console.log('WebSocket соединение установлено'); console.log('WebSocket соединение установлено');
@ -104,7 +104,7 @@ Connected to container: myproject_web_1
2024-01-15T10:30:15.123456789Z 2024/01/15 10:30:15 [notice] 1#1: start worker process 1234 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} ### ws://host:port/api/websocket/fan/{service_name}
Получение логов всех реплик сервиса Docker Compose (fan-in). Получение логов всех реплик сервиса Docker Compose (fan-in).
@ -123,7 +123,7 @@ Connected to container: myproject_web_1
const serviceName = "web"; const serviceName = "web";
const token = "your-jwt-token"; const token = "your-jwt-token";
const ws = new WebSocket(`ws://localhost:9001/ws/fan/${serviceName}?token=${token}&tail=100`); const ws = new WebSocket(`ws://localhost:9001/api/websocket/fan/${serviceName}?token=${token}&tail=100`);
ws.onmessage = function(event) { ws.onmessage = function(event) {
// Логи приходят с префиксом ID контейнера // Логи приходят с префиксом ID контейнера
@ -140,7 +140,7 @@ ws.onmessage = function(event) {
[def456gh] 2024-01-15T10:30:16.123456789Z 2024/01/15 10:30:16 [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 ### ws://host:port/api/websocket/fan_group
Получение логов группы сервисов одновременно. Получение логов группы сервисов одновременно.
@ -159,7 +159,7 @@ ws.onmessage = function(event) {
const services = "web,db,redis"; const services = "web,db,redis";
const token = "your-jwt-token"; const token = "your-jwt-token";
const ws = new WebSocket(`ws://localhost:9001/ws/fan_group?services=${services}&token=${token}&tail=100`); const ws = new WebSocket(`ws://localhost:9001/api/websocket/fan_group?services=${services}&token=${token}&tail=100`);
ws.onmessage = function(event) { ws.onmessage = function(event) {
// Логи приходят с префиксом ID контейнера и имени сервиса // Логи приходят с префиксом ID контейнера и имени сервиса
@ -193,7 +193,7 @@ class LogBoardWebSocket {
connectToContainer(containerId, options = {}) { connectToContainer(containerId, options = {}) {
const { tail = 100, onMessage, onError, onClose } = options; const { tail = 100, onMessage, onError, onClose } = options;
const url = `ws://${this.baseUrl.replace('http://', '')}/ws/logs/${containerId}?token=${this.token}&tail=${tail}`; const url = `ws://${this.baseUrl.replace('http://', '')}/api/websocket/logs/${containerId}?token=${this.token}&tail=${tail}`;
const ws = new WebSocket(url); const ws = new WebSocket(url);
ws.onopen = () => { ws.onopen = () => {
@ -222,7 +222,7 @@ class LogBoardWebSocket {
connectToService(serviceName, options = {}) { connectToService(serviceName, options = {}) {
const { tail = 100, project, onMessage, onError, onClose } = options; const { tail = 100, project, onMessage, onError, onClose } = options;
let url = `ws://${this.baseUrl.replace('http://', '')}/ws/fan/${serviceName}?token=${this.token}&tail=${tail}`; let url = `ws://${this.baseUrl.replace('http://', '')}/api/websocket/fan/${serviceName}?token=${this.token}&tail=${tail}`;
if (project) url += `&project=${project}`; if (project) url += `&project=${project}`;
const ws = new WebSocket(url); const ws = new WebSocket(url);
@ -253,7 +253,7 @@ class LogBoardWebSocket {
connectToServiceGroup(services, options = {}) { connectToServiceGroup(services, options = {}) {
const { tail = 100, project, onMessage, onError, onClose } = 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}`; let url = `ws://${this.baseUrl.replace('http://', '')}/api/websocket/fan_group?services=${services}&token=${this.token}&tail=${tail}`;
if (project) url += `&project=${project}`; if (project) url += `&project=${project}`;
const ws = new WebSocket(url); const ws = new WebSocket(url);
@ -393,7 +393,7 @@ class LogBoardWebSocket:
async def connect_to_container(self, container_id, tail=100, callback=None): 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}" uri = f"{self.base_url}/api/websocket/logs/{container_id}?token={self.token}&tail={tail}"
try: try:
async with websockets.connect(uri) as websocket: async with websockets.connect(uri) as websocket:
@ -412,7 +412,7 @@ class LogBoardWebSocket:
async def connect_to_service(self, service_name, tail=100, project=None, callback=None): 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}" uri = f"{self.base_url}/api/websocket/fan/{service_name}?token={self.token}&tail={tail}"
if project: if project:
uri += f"&project={project}" uri += f"&project={project}"
@ -433,7 +433,7 @@ class LogBoardWebSocket:
async def connect_to_service_group(self, services, tail=100, project=None, callback=None): 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}" uri = f"{self.base_url}/api/websocket/fan_group?services={services}&token={self.token}&tail={tail}"
if project: if project:
uri += f"&project={project}" uri += f"&project={project}"
@ -507,7 +507,7 @@ class LogBoardWebSocket {
connectToContainer(containerId, options = {}) { connectToContainer(containerId, options = {}) {
const { tail = 100, onMessage, onError, onClose } = options; const { tail = 100, onMessage, onError, onClose } = options;
const url = `${this.baseUrl}/ws/logs/${containerId}?token=${this.token}&tail=${tail}`; const url = `${this.baseUrl}/api/websocket/logs/${containerId}?token=${this.token}&tail=${tail}`;
const ws = new WebSocket(url); const ws = new WebSocket(url);
ws.on('open', () => { ws.on('open', () => {
@ -538,7 +538,7 @@ class LogBoardWebSocket {
connectToService(serviceName, options = {}) { connectToService(serviceName, options = {}) {
const { tail = 100, project, onMessage, onError, onClose } = options; const { tail = 100, project, onMessage, onError, onClose } = options;
let url = `${this.baseUrl}/ws/fan/${serviceName}?token=${this.token}&tail=${tail}`; let url = `${this.baseUrl}/api/websocket/fan/${serviceName}?token=${this.token}&tail=${tail}`;
if (project) url += `&project=${project}`; if (project) url += `&project=${project}`;
const ws = new WebSocket(url); const ws = new WebSocket(url);

View File

@ -0,0 +1,113 @@
services:
logboard:
image: REGISTRY_PLACEHOLDER/IMAGE_NAME_PLACEHOLDER:IMAGE_TAG_PLACEHOLDER
container_name: logboard
environment:
# ОСНОВНЫЕ НАСТРОЙКИ ПРИЛОЖЕНИЯ
# Порт веб-интерфейса LogBoard+
LOGBOARD_PORT: "${LOGBOARD_PORT}"
# Количество строк логов по умолчанию (tail)
LOGBOARD_TAIL: "${LOGBOARD_TAIL}"
# Имя пользователя для входа
LOGBOARD_USER: "${LOGBOARD_USER}"
# Пароль для входа (обязательно поменять в продакшене)
LOGBOARD_PASS: "${LOGBOARD_PASS}"
# Директория для снимков логов (в контейнере)
LOGBOARD_SNAPSHOT_DIR: "${LOGBOARD_SNAPSHOT_DIR}"
# Директория для статических файлов
LOGBOARD_STATIC_DIR: "${LOGBOARD_STATIC_DIR}"
# Путь к HTML шаблону главной страницы
LOGBOARD_INDEX_HTML: "${LOGBOARD_INDEX_HTML}"
# Таймзона для временных меток (например: Europe/Moscow, UTC)
TZ_TS: "${TZ_TS}"
# НАСТРОЙКИ DOCKER
# Фильтр по проекту Docker Compose (опционально)
COMPOSE_PROJECT_NAME: "${COMPOSE_PROJECT_NAME}"
# Перечень проектов для отображения (через запятую)
LOGBOARD_PROJECTS: "${LOGBOARD_PROJECTS}"
# Путь к Docker socket / удалённому хосту
DOCKER_HOST: "${DOCKER_HOST}"
# Включить проверку TLS для Docker (удалённые хосты)
DOCKER_TLS_VERIFY: "${DOCKER_TLS_VERIFY}"
# Путь к TLS сертификатам Docker
DOCKER_CERT_PATH: "${DOCKER_CERT_PATH}"
# Внешние сети Docker (через запятую)
DOCKER_NETWORKS: "${DOCKER_NETWORKS}"
# БЕЗОПАСНОСТЬ
# Секретный ключ для JWT (обязательно поменять в продакшене)
SECRET_KEY: "${SECRET_KEY}"
# Ключ шифрования для чувствительных данных (обязательно поменять)
ENCRYPTION_KEY: "${ENCRYPTION_KEY}"
# ЛОГИРОВАНИЕ
# Уровень логирования (DEBUG, INFO, WARNING, ERROR)
LOG_LEVEL: "${LOG_LEVEL}"
# Формат логов (json, text)
LOG_FORMAT: "${LOG_FORMAT}"
# ВЕБ-ИНТЕРФЕЙС
# Заголовок веб-интерфейса
WEB_TITLE: "${WEB_TITLE}"
# Описание веб-интерфейса
WEB_DESCRIPTION: "${WEB_DESCRIPTION}"
# Версия веб-интерфейса
WEB_VERSION: "${WEB_VERSION}"
# РЕЖИМ РАЗРАБОТКИ
# Режим отладки (true/false)
DEBUG_MODE: "${DEBUG_MODE}"
# ПРОИЗВОДИТЕЛЬНОСТЬ
# Максимум одновременных подключений
MAX_CONNECTIONS: "${MAX_CONNECTIONS}"
# Таймаут подключения (сек)
CONNECTION_TIMEOUT: "${CONNECTION_TIMEOUT}"
# Таймаут чтения (сек)
READ_TIMEOUT: "${READ_TIMEOUT}"
# ФИЛЬТРАЦИЯ КОНТЕЙНЕРОВ
# Пропускать контейнеры с проблемным health check (true/false)
LOGBOARD_SKIP_UNHEALTHY: "${LOGBOARD_SKIP_UNHEALTHY}"
# Таймаут получения списка контейнеров (сек)
LOGBOARD_CONTAINER_LIST_TIMEOUT: "${LOGBOARD_CONTAINER_LIST_TIMEOUT}"
# Таймаут получения информации о контейнере (сек)
LOGBOARD_CONTAINER_INFO_TIMEOUT: "${LOGBOARD_CONTAINER_INFO_TIMEOUT}"
# Таймаут health check контейнера (сек)
LOGBOARD_HEALTH_CHECK_TIMEOUT: "${LOGBOARD_HEALTH_CHECK_TIMEOUT}"
# АУТЕНТИФИКАЦИЯ
# Включить аутентификацию (true/false)
AUTH_ENABLED: "${AUTH_ENABLED}"
# Метод аутентификации (jwt)
AUTH_METHOD: "${AUTH_METHOD}"
# Время жизни сессии (сек)
SESSION_TIMEOUT: "${SESSION_TIMEOUT}"
# УВЕДОМЛЕНИЯ
# Включить уведомления по email (true/false)
NOTIFICATIONS_ENABLED: "${NOTIFICATIONS_ENABLED}"
# SMTP сервер
SMTP_HOST: "${SMTP_HOST}"
# Порт SMTP
SMTP_PORT: "${SMTP_PORT}"
# Пользователь SMTP
SMTP_USER: "${SMTP_USER}"
# Пароль SMTP
SMTP_PASS: "${SMTP_PASS}"
# Email отправителя
SMTP_FROM: "${SMTP_FROM}"
# AJAX ОБНОВЛЕНИЕ
# Интервал AJAX обновления логов (мс)
LOGBOARD_AJAX_UPDATE_INTERVAL: "${LOGBOARD_AJAX_UPDATE_INTERVAL}"
ports:
- "${LOGBOARD_PORT}:${LOGBOARD_PORT}"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./snapshots:/app/snapshots
restart: unless-stopped
user: 0:0

101
release/generate_compose.py Normal file
View File

@ -0,0 +1,101 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Генерация docker-compose-prod.yaml из шаблона и env.example.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
Поведение:
- Читает env.example и подставляет значения в docker-compose-prod.tmpl.yaml
- Запрашивает (или берет из ENV/CLI) REGISTRY_HOST, IMAGE_NAME_FULL, IMAGE_TAG
- Заменяет плейсхолдеры REGISTRY_PLACEHOLDER/IMAGE_NAME_PLACEHOLDER/IMAGE_TAG_PLACEHOLDER
"""
from __future__ import annotations
import argparse
import io
import os
import re
from typing import Dict
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
def parse_env_file(env_path: str) -> Dict[str, str]:
variables: Dict[str, str] = {}
with io.open(env_path, "r", encoding="utf-8") as fh:
for raw in fh:
line = raw.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
key, val = line.split("=", 1)
key = key.strip()
val = val.strip()
if (val.startswith('"') and val.endswith('"')) or (
val.startswith("'") and val.endswith("'")
):
val = val[1:-1]
variables[key] = val
return variables
def substitute_placeholders(text: str, env: Dict[str, str]) -> str:
pattern = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
return pattern.sub(lambda m: env.get(m.group(1), m.group(0)), text)
def main() -> int:
parser = argparse.ArgumentParser(description="Генерация docker-compose-prod.yaml")
parser.add_argument("--template", default=os.path.join(ROOT, "release", "docker-compose-prod.tmpl.yaml"))
parser.add_argument("--output", default=os.path.join(ROOT, "docker-compose-prod.yaml"))
parser.add_argument("--env", dest="env_path", default=os.path.join(ROOT, "env.example"))
parser.add_argument("--registry", default=os.getenv("REGISTRY_HOST", ""))
parser.add_argument("--image", dest="image", default=os.getenv("IMAGE_NAME_FULL", ""))
parser.add_argument("--tag", dest="tag", default=os.getenv("IMAGE_TAG", ""))
args = parser.parse_args()
env = parse_env_file(args.env_path)
# Подстановка ${VAR} из env.example
with io.open(args.template, "r", encoding="utf-8") as fh:
content = fh.read()
content = substitute_placeholders(content, env)
# Комментирование строк, где остались неразрешенные ${VAR}
# Ищем строки формата 'KEY: "${VAR}"' или 'KEY: ${VAR}' и комментируем их
lines = content.splitlines()
commented: list[str] = []
unresolved_pattern = re.compile(r"^([ \t-]*[^:#\n]+:\s*)(\"?\$\{[A-Za-z_][A-Za-z0-9_]*\}\"?)\s*$")
for line in lines:
if unresolved_pattern.match(line):
commented.append("# " + line)
else:
commented.append(line)
content = "\n".join(commented) + ("\n" if content.endswith("\n") else "")
# Плейсхолдеры реестра/имени/тега
registry = args.registry.strip() or input("Введите Docker Registry (например, ghcr.io или docker.io): ").strip() or "docker.io"
if registry == "registry.hub.docker.com":
registry = "docker.io"
image = args.image.strip() or input(f"Введите имя образа (например, inecs/logboard) (по умолчанию: logboard): ").strip() or "logboard"
tag = args.tag.strip() or input("Введите тег образа (по умолчанию: latest): ").strip() or "latest"
content = content.replace("REGISTRY_PLACEHOLDER", registry)
content = content.replace("IMAGE_NAME_PLACEHOLDER", image)
content = content.replace("IMAGE_TAG_PLACEHOLDER", tag)
with io.open(args.output, "w", encoding="utf-8") as fh:
fh.write(content)
print(f"Файл {os.path.relpath(args.output, ROOT)} сгенерирован")
return 0
if __name__ == "__main__":
raise SystemExit(main())

142
release/publish_image.py Normal file
View File

@ -0,0 +1,142 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Скрипт публикации Docker-образа (multi-arch) в реестр.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
Поведение:
- Собирает multi-arch образ с использованием docker buildx
- Логинится в реестр (пароль можно передать через аргумент/ENV, либо будет запрошен интерактивно)
- Публикует образ
Параметры через аргументы/ENV:
- --registry / $REGISTRY_HOST (например, docker.io, ghcr.io)
- --image / $IMAGE_NAME_FULL (например, inecs/logboard)
- --tag / $IMAGE_TAG (например, v1 или короткий sha)
- --user / $REG_USER
- --password / $REG_PASS
- --platforms / $PLATFORMS (по умолчанию: linux/amd64,linux/arm64)
"""
from __future__ import annotations
import argparse
import getpass
import os
import subprocess
import sys
from typing import List
def run(cmd: List[str], check: bool = True, quiet: bool = False) -> subprocess.CompletedProcess:
stdout = subprocess.DEVNULL if quiet else None
stderr = subprocess.STDOUT if quiet else None
return subprocess.run(cmd, check=check, stdout=stdout, stderr=stderr)
def git_short_sha_default() -> str:
try:
out = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"], stderr=subprocess.DEVNULL)
return out.decode("utf-8").strip() or "latest"
except Exception:
return "latest"
def ensure_buildx_builder(builder_name: str) -> None:
try:
run(["docker", "buildx", "inspect", builder_name], check=True, quiet=True)
except subprocess.CalledProcessError:
run(["docker", "buildx", "create", "--name", builder_name, "--use"], check=True)
run(["docker", "buildx", "use", builder_name], check=True)
def ensure_binfmt() -> None:
try:
run(["docker", "run", "--privileged", "--rm", "tonistiigi/binfmt", "--install", "all"], check=False, quiet=True)
except Exception:
pass
def main() -> int:
parser = argparse.ArgumentParser(description="Публикация Docker-образа в реестр (multi-arch)")
parser.add_argument("--registry", default=os.getenv("REGISTRY_HOST", ""))
parser.add_argument("--image", dest="image", default=os.getenv("IMAGE_NAME_FULL", ""))
parser.add_argument("--tag", dest="tag", default=os.getenv("IMAGE_TAG", ""))
parser.add_argument("--user", dest="user", default=os.getenv("REG_USER", ""))
parser.add_argument("--password", dest="password", default=os.getenv("REG_PASS", ""))
parser.add_argument("--platforms", default=os.getenv("PLATFORMS", "linux/amd64,linux/arm64"))
parser.add_argument("--builder", default=os.getenv("BUILDX_BUILDER", "logboard_builder"))
args = parser.parse_args()
# 1) Определяем реестр, поддерживая ввод вида "host" или "host/namespace"
registry_input = (args.registry or os.getenv("REGISTRY_HOST", "")).strip()
if not registry_input:
registry_input = input(
"Введите Docker Registry (например, ghcr.io, docker.io, registry.hub.docker.com/namespace): "
).strip()
registry_host = registry_input.split("/", 1)[0] if registry_input else "docker.io"
registry_path = ""
if "/" in registry_input:
registry_path = registry_input.split("/", 1)[1]
if not registry_host or registry_host == "registry.hub.docker.com":
registry_host = "docker.io"
# 2) Имя образа. Если не задано, спрашиваем пользователя (по умолчанию logboard)
image_input = (args.image or os.getenv("IMAGE_NAME_FULL", "")).strip()
if not image_input:
image_input = input(
"Введите имя образа (по умолчанию: logboard): "
).strip() or "logboard"
# Если пользователь ввёл только имя без namespace, но registry_path задан, добавим префикс
if "/" not in image_input and registry_path:
image_full = f"{registry_path}/{image_input}"
else:
image_full = image_input
# 3) Тег: если не задан, спросим. По умолчанию короткий SHA
tag_input = (args.tag or os.getenv("IMAGE_TAG", "")).strip()
if not tag_input:
default_tag = git_short_sha_default()
tag_input = input(f"Введите тег образа (по умолчанию: {default_tag}): ").strip() or default_tag
user = (args.user or os.getenv("REG_USER", "")).strip()
password = args.password
if not user:
user = input("Введите имя пользователя реестра: ").strip()
if not password:
password = getpass.getpass("Введите пароль реестра: ")
image_remote = f"{registry_host}/{image_full}:{tag_input}"
print(f"Логин в реестр: {registry_host}")
p = subprocess.Popen(["docker", "login", registry_host, "-u", user, "--password-stdin"], stdin=subprocess.PIPE)
p.communicate(input=password.encode("utf-8"))
if p.returncode != 0:
print("Ошибка логина в реестр", file=sys.stderr)
return 1
print("Установка binfmt (для multi-arch)...")
ensure_binfmt()
print(f"Подготовка buildx builder: {args.builder}")
ensure_buildx_builder(args.builder)
print(f"Сборка и публикация образа: {image_remote}")
run([
"docker", "buildx", "build",
"--platform", args.platforms,
"-t", image_remote,
"--push",
".",
], check=True)
print("Образ опубликован успешно!")
print("Готово. Для генерации compose выполните: make release-compose")
return 0
if __name__ == "__main__":
raise SystemExit(main())