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
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
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

View File

@ -1,12 +1,15 @@
.ONESHELL:
# Makefile для LogBoard+
# Автор: Сергей Антропов
# Сайт: 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
SERVICE_NAME = logboard
COMPOSE_PROD_FILE = docker-compose-prod.yaml
PLATFORMS ?= linux/amd64,linux/arm64
# Цвета для вывода
GREEN = \033[0;32m
@ -165,3 +168,25 @@ debug-status: ## Показать статус режима отладки
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+ особенно полезен для разработчиков, работающих с микросервисной архитектурой:
@ -24,7 +24,7 @@ LogBoard+ особенно полезен для разработчиков, р
- **Мониторинг в реальном времени** - Видите проблемы сразу, как они возникают
- **Централизованный просмотр** - Все логи в одном месте, а не в десятках терминалов
### 🐳 **Оптимизирован для Docker и Docker Compose**
### Оптимизирован для Docker и Docker Compose
Если ваша инфраструктура основана на Docker и Docker Compose, LogBoard+ станет незаменимым инструментом:
@ -34,7 +34,7 @@ LogBoard+ особенно полезен для разработчиков, р
- **Multi-view режим** - одновременный просмотр логов нескольких контейнеров
- **Интеграция с Docker API** - прямая работа с контейнерами
### 🚀 **Производительность и удобство**
### Производительность и удобство
Приложение предоставляет удобный веб-интерфейс для работы с логами микросервисов, поддерживает множественные проекты Docker Compose и включает в себя функции безопасности.
@ -51,35 +51,35 @@ LogBoard+ особенно полезен для разработчиков, р
## Скриншоты
### 🔐 Страница входа
### Страница входа
| Светлая тема | Темная тема |
|--------------|-------------|
| ![Вход - светлая тема](screenshots/login-white.png) | ![Вход - темная тема](screenshots/login-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)
### 📋 Карточки контейнеров
### Карточки контейнеров
![Карточки контейнеров](screenshots/container-cards.png)
### 📁 Проекты
### Проекты
![Список проектов](screenshots/projects.png)
### ⚙️ Настройки
### Настройки
![Панель настроек](screenshots/options.png)
### 🔧 Сворачиваемая боковая панель
### Сворачиваемая боковая панель
![Сворачиваемая боковая панель](screenshots/collapse-sidebar.png)
### Справка
### Справка
![Окно справки](screenshots/help.png)
### 🚨 Страницы ошибок
### Страницы ошибок
![Страницы ошибок](screenshots/error%20pages.png)
## Быстрый старт
@ -91,7 +91,7 @@ LogBoard+ особенно полезен для разработчиков, р
- 1 GB RAM
- 1 CPU core
### Установка и запуск
### Вариант A: Локальный запуск (разработка)
1. **Клонирование репозитория**
```bash
@ -99,17 +99,6 @@ LogBoard+ особенно полезен для разработчиков, р
cd logboard
```
2. **Настройка переменных окружения**
```bash
cp env.example .env
# Отредактируйте .env файл при необходимости
```
3. **Запуск приложения**
```bash
docker compose up --build -d
```
2. **Настройка переменных окружения**
```bash
make setup
@ -126,6 +115,88 @@ LogBoard+ особенно полезен для разработчиков, р
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`
@ -231,23 +302,23 @@ logboard/
#### Контейнеры и сервисы
- `GET /api/services` - Список контейнеров
- `GET /api/projects` - Список проектов Docker Compose
- `GET /api/containers/services` - Список контейнеров
- `GET /api/containers/projects` - Список проектов Docker Compose
- `GET /api/logs/{container_id}` - Логи контейнера
- `GET /api/logs/stats/{container_id}` - Статистика логов
#### Управление
- `GET /api/settings` - Настройки приложения
- `GET /api/excluded-containers` - Список исключенных контейнеров
- `POST /api/excluded-containers` - Обновление исключенных контейнеров
- `POST /api/snapshot` - Создание снимка логов
- `GET /api/containers/excluded` - Список исключенных контейнеров
- `POST /api/containers/excluded` - Обновление исключенных контейнеров
- `POST /api/logs/snapshot` - Создание снимка логов
### WebSocket API
- `ws://host:port/ws/logs/{container_id}` - Логи отдельного контейнера
- `ws://host:port/ws/fan/{service_name}` - Логи сервиса (все реплики)
- `ws://host:port/ws/fan_group` - Логи группы сервисов
- `ws://host:port/api/websocket/logs/{container_id}` - Логи отдельного контейнера
- `ws://host:port/api/websocket/fan/{service_name}` - Логи сервиса (все реплики)
- `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 разработан для интеграции с системами мониторинга и автоматизации, а также для создания собственных клиентов.
### 🎯 **Применение API**
### Применение API
- **Интеграция с CI/CD** - автоматический мониторинг логов в пайплайнах
- **Собственные дашборды** - создание кастомных интерфейсов мониторинга
@ -108,7 +108,7 @@ Authorization: Bearer <token>
### Контейнеры и сервисы
#### GET /api/services
#### GET /api/containers/services
Получение списка всех контейнеров.
@ -122,7 +122,7 @@ Authorization: Bearer <token>
**Пример запроса:**
```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"
```
@ -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.
@ -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`.
### ws://host:port/ws/logs/{container_id}
### ws://host:port/api/websocket/logs/{container_id}
Получение логов отдельного контейнера.
@ -390,7 +390,7 @@ ok
```javascript
const token = "your-jwt-token";
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) {
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
const token = "your-jwt-token";
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) {
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
const token = "your-jwt-token";
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) {
console.log('Логи группы сервисов:', event.data);
@ -522,7 +522,7 @@ def login(username, password):
# Получение списка контейнеров
def get_containers(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()
# Получение логов контейнера
@ -563,7 +563,7 @@ class LogBoardAPI {
if (projects) params.append('projects', projects);
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
});
return await response.json();
@ -592,7 +592,7 @@ class LogBoardAPI {
// WebSocket для live-логов
createLogsWebSocket(containerId, tail = 100) {
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;
}
@ -654,12 +654,12 @@ echo "Токен получен: ${TOKEN:0:20}..."
# Получение списка контейнеров
echo "Получение списка контейнеров..."
curl -s -X GET "$BASE_URL/api/services" \
curl -s -X GET "$BASE_URL/api/containers/services" \
-H "Authorization: Bearer $TOKEN" \
| 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" \
| jq -r '.[0].id')

View File

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

View File

@ -7,7 +7,7 @@
LogBoard+ - это современная веб-панель для мониторинга и просмотра логов Docker контейнеров в реальном времени. Приложение идеально подходит для локальной разработки, позволяя разработчикам всегда держать логи микросервисов перед глазами на втором мониторе.
### 🎯 **Идеально для локальной разработки**
### Идеально для локальной разработки
LogBoard+ особенно полезен для разработчиков, работающих с микросервисной архитектурой:
@ -16,7 +16,7 @@ LogBoard+ особенно полезен для разработчиков, р
- **Мониторинг в реальном времени** - Видите проблемы сразу, как они возникают
- **Централизованный просмотр** - Все логи в одном месте, а не в десятках терминалов
### 🐳 **Оптимизирован для Docker и Docker Compose**
### Оптимизирован для Docker и Docker Compose
Если ваша инфраструктура основана на Docker и Docker Compose, LogBoard+ станет незаменимым инструментом:
@ -26,13 +26,13 @@ LogBoard+ особенно полезен для разработчиков, р
- **Multi-view режим** - одновременный просмотр логов нескольких контейнеров
- **Интеграция с Docker API** - прямая работа с контейнерами
### 🚀 **Производительность и удобство**
### Производительность и удобство
Приложение предоставляет удобный веб-интерфейс для работы с логами микросервисов, поддерживает множественные проекты Docker Compose и включает в себя функции безопасности.
## Быстрый старт
### Установка
### Вариант A: Разработка
```bash
# Клонирование репозитория
@ -40,17 +40,50 @@ git clone <repository-url>
cd logboard
# Настройка переменных окружения
cp env.example .env
make setup
# Запуск приложения
docker compose up --build -d
make up
```
### Доступ
### Вариант B: Продакшен (пример docker-compose-prod.yaml)
- **URL:** http://localhost:9001
- **Пользователь:** `admin`
- **Пароль:** `admin` (обязательно измените в продакшене!)
```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_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) |
### 📊 Основной интерфейс
### Основной интерфейс
| Светлая тема | Темная тема |
|--------------|-------------|
| ![Single View - светлая тема](../screenshots/single-view-white.png) | ![Single View - темная тема](../screenshots/single-view-dark.png) |
### 🖥️ Multi-view режим
### Multi-view режим
![Multi-view режим](../screenshots/multi-view.png)
### 📋 Карточки контейнеров
### Карточки контейнеров
![Карточки контейнеров](../screenshots/container-cards.png)
### 📁 Проекты
### Проекты
![Список проектов](../screenshots/projects.png)
### ⚙️ Настройки
### Настройки
![Панель настроек](../screenshots/options.png)
### 🔧 Сворачиваемая боковая панель
### Сворачиваемая боковая панель
![Сворачиваемая боковая панель](../screenshots/collapse-sidebar.png)
### Справка
### Справка
![Окно справки](../screenshots/help.png)
### 🚨 Страницы ошибок
### Страницы ошибок
![Страницы ошибок](../screenshots/error%20pages.png)
## Архитектура
@ -169,22 +202,22 @@ docker compose up --build -d
- `GET /api/auth/me` - Информация о текущем пользователе
#### Контейнеры и сервисы
- `GET /api/services` - Список контейнеров
- `GET /api/projects` - Список проектов Docker Compose
- `GET /api/containers/services` - Список контейнеров
- `GET /api/containers/projects` - Список проектов Docker Compose
- `GET /api/logs/{container_id}` - Логи контейнера
- `GET /api/logs/stats/{container_id}` - Статистика логов
#### Управление
- `GET /api/settings` - Настройки приложения
- `GET /api/excluded-containers` - Список исключенных контейнеров
- `POST /api/excluded-containers` - Обновление исключенных контейнеров
- `POST /api/snapshot` - Создание снимка логов
- `GET /api/containers/excluded` - Список исключенных контейнеров
- `POST /api/containers/excluded` - Обновление исключенных контейнеров
- `POST /api/logs/snapshot` - Создание снимка логов
### WebSocket API
- `ws://host:port/ws/logs/{container_id}` - Логи отдельного контейнера
- `ws://host:port/ws/fan/{service_name}` - Логи сервиса (все реплики)
- `ws://host:port/ws/fan_group` - Логи группы сервисов
- `ws://host:port/api/websocket/logs/{container_id}` - Логи отдельного контейнера
- `ws://host:port/api/websocket/fan/{service_name}` - Логи сервиса (все реплики)
- `ws://host:port/api/websocket/fan_group` - Логи группы сервисов
## Конфигурация

View File

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

View File

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

View File

@ -363,7 +363,7 @@ echo $SESSION_TIMEOUT
```bash
# Проверка WebSocket endpoint
curl -I "http://localhost:9001/ws/logs/test"
curl -I "http://localhost:9001/api/websocket/logs/test"
# Проверка логов приложения
make logs | grep -i websocket
@ -376,8 +376,8 @@ make logs | grep -i websocket
1. **Проблема с прокси**
```nginx
# Настройка Nginx для WebSocket
location /ws/ {
# Настройка Nginx для WebSocket с корректными путями API
location /api/websocket/ {
proxy_pass http://localhost:9001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
@ -390,7 +390,7 @@ make logs | grep -i websocket
```javascript
// Проверка токена в WebSocket URL
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**
@ -703,7 +703,7 @@ curl -X POST "http://localhost:9001/api/auth/login" \
# Проверка API с токеном
TOKEN="your-token"
curl -X GET "http://localhost:9001/api/services" \
curl -X GET "http://localhost:9001/api/containers/services" \
-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" \
-H "Content-Type: application/json" \
-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'
```

View File

@ -45,12 +45,12 @@ curl -X POST "http://localhost:9001/api/auth/login" \
```javascript
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
### 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 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() {
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
```
### ws://host:port/ws/fan/{service_name}
### ws://host:port/api/websocket/fan/{service_name}
Получение логов всех реплик сервиса Docker Compose (fan-in).
@ -123,7 +123,7 @@ Connected to container: myproject_web_1
const serviceName = "web";
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) {
// Логи приходят с префиксом 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
```
### 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 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) {
// Логи приходят с префиксом ID контейнера и имени сервиса
@ -193,7 +193,7 @@ class LogBoardWebSocket {
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 url = `ws://${this.baseUrl.replace('http://', '')}/api/websocket/logs/${containerId}?token=${this.token}&tail=${tail}`;
const ws = new WebSocket(url);
ws.onopen = () => {
@ -222,7 +222,7 @@ class LogBoardWebSocket {
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}`;
let url = `ws://${this.baseUrl.replace('http://', '')}/api/websocket/fan/${serviceName}?token=${this.token}&tail=${tail}`;
if (project) url += `&project=${project}`;
const ws = new WebSocket(url);
@ -253,7 +253,7 @@ class LogBoardWebSocket {
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}`;
let url = `ws://${this.baseUrl.replace('http://', '')}/api/websocket/fan_group?services=${services}&token=${this.token}&tail=${tail}`;
if (project) url += `&project=${project}`;
const ws = new WebSocket(url);
@ -393,7 +393,7 @@ class LogBoardWebSocket:
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:
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):
"""Подключение к логам сервиса"""
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:
uri += f"&project={project}"
@ -433,7 +433,7 @@ class LogBoardWebSocket:
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:
uri += f"&project={project}"
@ -507,7 +507,7 @@ class LogBoardWebSocket {
connectToContainer(containerId, 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);
ws.on('open', () => {
@ -538,7 +538,7 @@ class LogBoardWebSocket {
connectToService(serviceName, 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}`;
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())