diff --git a/Dockerfile b/Dockerfile index ba2d2c5..f751f4c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/Makefile b/Makefile index 6702a25..189ef59 100644 --- a/Makefile +++ b/Makefile @@ -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) + + diff --git a/README.md b/README.md index bf9065c..631cb50 100644 --- a/README.md +++ b/README.md @@ -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` - Логи группы сервисов ## Конфигурация diff --git a/app/scripts/substitute_env.py b/app/scripts/substitute_env.py new file mode 100644 index 0000000..814c148 --- /dev/null +++ b/app/scripts/substitute_env.py @@ -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 \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()) + + diff --git a/docker-compose-prod.yaml b/docker-compose-prod.yaml new file mode 100644 index 0000000..35bdcac --- /dev/null +++ b/docker-compose-prod.yaml @@ -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 + diff --git a/docs/api.md b/docs/api.md index 5a5616f..b7e7d70 100644 --- a/docs/api.md +++ b/docs/api.md @@ -16,7 +16,7 @@ LogBoard+ предоставляет REST API и WebSocket API для работы с логами Docker контейнеров. API разработан для интеграции с системами мониторинга и автоматизации, а также для создания собственных клиентов. -### 🎯 **Применение API** +### Применение API - **Интеграция с CI/CD** - автоматический мониторинг логов в пайплайнах - **Собственные дашборды** - создание кастомных интерфейсов мониторинга @@ -108,7 +108,7 @@ Authorization: Bearer ### Контейнеры и сервисы -#### GET /api/services +#### GET /api/containers/services Получение списка всех контейнеров. @@ -122,7 +122,7 @@ Authorization: Bearer **Пример запроса:** ```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') diff --git a/docs/configuration.md b/docs/configuration.md index 86651a8..1117f10 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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"]' diff --git a/docs/index.md b/docs/index.md index f7e23d3..f69d36d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 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` - Логи группы сервисов ## Конфигурация diff --git a/docs/installation.md b/docs/installation.md index 3ad9096..a62311b 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -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" ``` diff --git a/docs/security.md b/docs/security.md index a6f4dd3..1f2939b 100644 --- a/docs/security.md +++ b/docs/security.md @@ -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 "" diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e5c7d24..c9f467f 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -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' ``` diff --git a/docs/websocket.md b/docs/websocket.md index 1e67dee..d28803c 100644 --- a/docs/websocket.md +++ b/docs/websocket.md @@ -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); diff --git a/release/docker-compose-prod.tmpl.yaml b/release/docker-compose-prod.tmpl.yaml new file mode 100644 index 0000000..09bafd3 --- /dev/null +++ b/release/docker-compose-prod.tmpl.yaml @@ -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 + diff --git a/release/generate_compose.py b/release/generate_compose.py new file mode 100644 index 0000000..1d15e26 --- /dev/null +++ b/release/generate_compose.py @@ -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()) + + diff --git a/release/publish_image.py b/release/publish_image.py new file mode 100644 index 0000000..e00e42a --- /dev/null +++ b/release/publish_image.py @@ -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()) + +