Refactor project structure and enhance Docker support
- Removed unnecessary `__init__.py` and `Dockerfile` to streamline project organization. - Updated `.dockerignore` and `.gitignore` to improve exclusion patterns for build artifacts and environment files. - Enhanced `Makefile` with new commands for managing Docker containers and added help documentation. - Introduced `pyproject.toml` for better project metadata management and dependency tracking. - Updated `requirements.txt` to reflect changes in dependencies for metrics and monitoring. - Refactored various handler files to improve code organization and maintainability.
This commit is contained in:
@@ -1,37 +1,73 @@
|
|||||||
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*.pyo
|
*$py.class
|
||||||
*.pyd
|
|
||||||
*.so
|
*.so
|
||||||
*.egg-info/
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
.env
|
.env
|
||||||
.venv
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Git
|
||||||
.git/
|
.git/
|
||||||
.gitignore
|
.gitignore
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Logs
|
||||||
**/__pycache__/
|
logs/*.log
|
||||||
**/*.pyc
|
|
||||||
**/*.pyo
|
|
||||||
**/*.pyd
|
|
||||||
|
|
||||||
# Local settings
|
# Database
|
||||||
settings_example.ini
|
|
||||||
|
|
||||||
# Databases and runtime files
|
|
||||||
*.db
|
*.db
|
||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
logs/
|
|
||||||
|
|
||||||
# Tests and artifacts
|
# Tests
|
||||||
.coverage
|
tests/
|
||||||
|
test_*.py
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
htmlcov/
|
|
||||||
**/tests/
|
|
||||||
|
|
||||||
# Stickers and large assets (if not needed at runtime)
|
# Documentation
|
||||||
Stick/
|
*.md
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*.yml
|
||||||
|
.dockerignore
|
||||||
|
|||||||
42
.gitignore
vendored
42
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
|
# Database files
|
||||||
/database/tg-bot-database.db
|
/database/tg-bot-database.db
|
||||||
/database/tg-bot-database.db-shm
|
/database/tg-bot-database.db-shm
|
||||||
/database/tg-bot-database.db-wal
|
/database/tg-bot-database.db-wm
|
||||||
/database/test.db
|
/database/test.db
|
||||||
/database/test.db-shm
|
/database/test.db-shm
|
||||||
/database/test.db-wal
|
/database/test.db-wal
|
||||||
@@ -10,7 +11,9 @@
|
|||||||
/settings.ini
|
/settings.ini
|
||||||
/myenv/
|
/myenv/
|
||||||
/venv/
|
/venv/
|
||||||
/.idea/
|
/.venv/
|
||||||
|
|
||||||
|
# Logs
|
||||||
/logs/*.log
|
/logs/*.log
|
||||||
|
|
||||||
# Testing and coverage files
|
# Testing and coverage files
|
||||||
@@ -32,6 +35,7 @@ test.db
|
|||||||
|
|
||||||
# IDE and editor files
|
# IDE and editor files
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
@@ -44,9 +48,43 @@ test.db
|
|||||||
.Trashes
|
.Trashes
|
||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Documentation files
|
||||||
PERFORMANCE_IMPROVEMENTS.md
|
PERFORMANCE_IMPROVEMENTS.md
|
||||||
|
|
||||||
# PID files
|
# PID files
|
||||||
*.pid
|
*.pid
|
||||||
helper_bot.pid
|
helper_bot.pid
|
||||||
voice_bot.pid
|
voice_bot.pid
|
||||||
|
|
||||||
|
# Docker and build artifacts
|
||||||
|
*.tar.gz
|
||||||
|
prometheus-*/
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.log
|
||||||
|
*.pid
|
||||||
|
|
||||||
|
# Python cache
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.cache/
|
||||||
|
.mypy_cache/
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.9.6
|
||||||
1
CHANGES_SUMMARY.md
Normal file
1
CHANGES_SUMMARY.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
37
Dockerfile
37
Dockerfile
@@ -1,37 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1
|
|
||||||
|
|
||||||
# Use a lightweight Python image
|
|
||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
# Prevent Python from writing .pyc files and enable unbuffered logs
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
||||||
PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
# Install system dependencies (if required by Python packages)
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends build-essential \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Create non-root user
|
|
||||||
RUN useradd -m appuser \
|
|
||||||
&& chown -R appuser:appuser /app
|
|
||||||
|
|
||||||
# Install Python dependencies first for better layer caching
|
|
||||||
COPY requirements.txt ./
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# Copy project files
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Ensure runtime directories exist and are writable
|
|
||||||
RUN mkdir -p logs database \
|
|
||||||
&& chown -R appuser:appuser /app
|
|
||||||
|
|
||||||
# Switch to non-root user
|
|
||||||
USER appuser
|
|
||||||
|
|
||||||
# Run the bot
|
|
||||||
CMD ["python", "run_helper.py"]
|
|
||||||
34
Dockerfile.bot
Normal file
34
Dockerfile.bot
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
# Установка системных зависимостей
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Создание рабочей директории
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копирование requirements.txt
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Создание виртуального окружения
|
||||||
|
RUN python -m venv .venv
|
||||||
|
|
||||||
|
# Обновление pip в виртуальном окружении
|
||||||
|
RUN . .venv/bin/activate && pip install --upgrade pip
|
||||||
|
|
||||||
|
# Установка зависимостей в виртуальное окружение
|
||||||
|
RUN . .venv/bin/activate && pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Копирование исходного кода
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Активация виртуального окружения
|
||||||
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
ENV VIRTUAL_ENV="/app/.venv"
|
||||||
|
|
||||||
|
# Открытие порта для метрик
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Команда запуска через виртуальное окружение
|
||||||
|
CMD [".venv/bin/python", "run_helper.py"]
|
||||||
124
Makefile
124
Makefile
@@ -1,88 +1,68 @@
|
|||||||
.PHONY: help test test-db test-coverage test-html clean install test-monitor
|
.PHONY: help build up down logs clean restart status
|
||||||
|
|
||||||
# Default target
|
help: ## Показать справку
|
||||||
help:
|
@echo "🐍 Telegram Bot - Доступные команды (Python 3.9):"
|
||||||
@echo "Available commands:"
|
@echo ""
|
||||||
@echo " install - Install dependencies"
|
@echo "🔧 Основные команды:"
|
||||||
@echo " test - Run all tests"
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||||
@echo " test-db - Run database tests only"
|
@echo ""
|
||||||
@echo " test-bot - Run bot startup and handler tests only"
|
@echo "📊 Мониторинг:"
|
||||||
@echo " test-media - Run media handler tests only"
|
@echo " Prometheus: http://localhost:9090"
|
||||||
@echo " test-errors - Run error handling tests only"
|
@echo " Grafana: http://localhost:3000 (admin/admin)"
|
||||||
@echo " test-utils - Run utility functions tests only"
|
|
||||||
@echo " test-keyboards - Run keyboard and filter tests only"
|
|
||||||
@echo " test-monitor - Test server monitoring module"
|
|
||||||
@echo " test-coverage - Run tests with coverage report (helper_bot + database)"
|
|
||||||
@echo " test-html - Run tests and generate HTML coverage report"
|
|
||||||
@echo " clean - Clean up generated files"
|
|
||||||
@echo " coverage - Show coverage report only"
|
|
||||||
|
|
||||||
# Install dependencies
|
build: ## Собрать все контейнеры с Python 3.9
|
||||||
install:
|
docker-compose build
|
||||||
python3 -m pip install -r requirements.txt
|
|
||||||
python3 -m pip install pytest-cov
|
|
||||||
|
|
||||||
# Run all tests
|
up: ## Запустить все сервисы с Python 3.9
|
||||||
test:
|
docker-compose up -d
|
||||||
python3 -m pytest tests/ -v
|
|
||||||
|
|
||||||
# Run database tests only
|
down: ## Остановить все сервисы
|
||||||
test-db:
|
docker-compose down
|
||||||
python3 -m pytest tests/test_db.py -v
|
|
||||||
|
|
||||||
# Run bot tests only
|
logs: ## Показать логи всех сервисов
|
||||||
test-bot:
|
docker-compose logs -f
|
||||||
python3 -m pytest tests/test_bot.py -v
|
|
||||||
|
|
||||||
# Run media handler tests only
|
logs-bot: ## Показать логи бота
|
||||||
test-media:
|
docker-compose logs -f telegram-bot
|
||||||
python3 -m pytest tests/test_media_handlers.py -v
|
|
||||||
|
|
||||||
# Run error handling tests only
|
logs-prometheus: ## Показать логи Prometheus
|
||||||
test-errors:
|
docker-compose logs -f prometheus
|
||||||
python3 -m pytest tests/test_error_handling.py -v
|
|
||||||
|
|
||||||
# Run utils tests only
|
logs-grafana: ## Показать логи Grafana
|
||||||
test-utils:
|
docker-compose logs -f grafana
|
||||||
python3 -m pytest tests/test_utils.py -v
|
|
||||||
|
|
||||||
# Run keyboard and filter tests only
|
restart: ## Перезапустить все сервисы (с пересборкой Python 3.9)
|
||||||
test-keyboards:
|
docker-compose down
|
||||||
python3 -m pytest tests/test_keyboards_and_filters.py -v
|
docker-compose build
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
# Test server monitoring module
|
status: ## Показать статус контейнеров
|
||||||
test-monitor:
|
docker-compose ps
|
||||||
python3 tests/test_monitor.py
|
|
||||||
|
|
||||||
# Test auto unban scheduler
|
check-python: ## Проверить версию Python в контейнере
|
||||||
test-auto-unban:
|
@echo "🐍 Проверяю версию Python в контейнере..."
|
||||||
python3 -m pytest tests/test_auto_unban_scheduler.py -v
|
@docker exec telegram-bot .venv/bin/python --version || echo "Контейнер не запущен"
|
||||||
|
|
||||||
# Test auto unban integration
|
test-compatibility: ## Тест совместимости с Python 3.8+
|
||||||
test-auto-unban-integration:
|
@echo "🐍 Тестирую совместимость с Python 3.8+..."
|
||||||
python3 -m pytest tests/test_auto_unban_integration.py -v
|
@python3 test_python38_compatibility.py
|
||||||
|
|
||||||
# Run tests with coverage
|
clean: ## Очистить все контейнеры и образы Python 3.9
|
||||||
test-coverage:
|
docker-compose down -v --rmi all
|
||||||
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=term
|
docker system prune -f
|
||||||
|
|
||||||
# Run tests and generate HTML coverage report
|
|
||||||
test-html:
|
|
||||||
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=html:htmlcov --cov-report=term
|
|
||||||
@echo "HTML coverage report generated in htmlcov/index.html"
|
|
||||||
|
|
||||||
# Show coverage report only
|
|
||||||
coverage:
|
|
||||||
python3 -m coverage report --include="helper_bot/*,database/*"
|
|
||||||
|
|
||||||
# Clean up generated files
|
start: build up ## Собрать и запустить все сервисы с Python 3.9
|
||||||
clean:
|
@echo "🐍 Python 3.9 контейнер собран и запущен!"
|
||||||
rm -rf htmlcov/
|
@echo "📊 Prometheus: http://localhost:9090"
|
||||||
rm -f coverage.xml
|
@echo "📈 Grafana: http://localhost:3000 (admin/admin)"
|
||||||
rm -f .coverage
|
@echo "🤖 Бот запущен в контейнере с Python 3.9"
|
||||||
rm -f database/test.db
|
@echo "📝 Логи: make logs"
|
||||||
rm -f test.db
|
|
||||||
rm -f helper_bot.pid
|
start-script: ## Запустить через скрипт start_docker.sh
|
||||||
rm -f voice_bot.pid
|
@echo "🐍 Запуск через скрипт start_docker.sh..."
|
||||||
find . -type d -name "__pycache__" -exec rm -rf {} +
|
@./start_docker.sh
|
||||||
find . -type f -name "*.pyc" -delete
|
|
||||||
|
stop: down ## Остановить все сервисы
|
||||||
|
@echo "🛑 Все сервисы остановлены"
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# This file makes the root directory a Python package
|
|
||||||
|
|
||||||
|
|
||||||
@@ -446,7 +446,7 @@ class AsyncBotDB:
|
|||||||
if conn:
|
if conn:
|
||||||
await conn.close()
|
await conn.close()
|
||||||
|
|
||||||
async def get_post_content(self, last_post_id: int) -> List[Tuple[str, str]]:
|
async def get_post_content(self, last_post_id: int) -> List:
|
||||||
"""Получение контента поста."""
|
"""Получение контента поста."""
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
@@ -484,7 +484,7 @@ class AsyncBotDB:
|
|||||||
if conn:
|
if conn:
|
||||||
await conn.close()
|
await conn.close()
|
||||||
|
|
||||||
async def get_post_ids(self, last_post_id: int) -> List[int]:
|
async def get_post_ids(self, last_post_id: int) -> List:
|
||||||
"""Получение ID постов."""
|
"""Получение ID постов."""
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
@@ -540,7 +540,7 @@ class AsyncBotDB:
|
|||||||
if conn:
|
if conn:
|
||||||
await conn.close()
|
await conn.close()
|
||||||
|
|
||||||
async def get_last_users(self, limit: int = 30) -> List[Tuple[str, int]]:
|
async def get_last_users(self, limit: int = 30) -> List:
|
||||||
"""Получение последних пользователей."""
|
"""Получение последних пользователей."""
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
@@ -626,7 +626,7 @@ class AsyncBotDB:
|
|||||||
if conn:
|
if conn:
|
||||||
await conn.close()
|
await conn.close()
|
||||||
|
|
||||||
async def get_blacklist_users(self, offset: int = 0, limit: int = 10) -> List[Tuple[str, int, str, str]]:
|
async def get_blacklist_users(self, offset: int = 0, limit: int = 10) -> List:
|
||||||
"""Получение пользователей из черного списка."""
|
"""Получение пользователей из черного списка."""
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
@@ -658,7 +658,7 @@ class AsyncBotDB:
|
|||||||
if conn:
|
if conn:
|
||||||
await conn.close()
|
await conn.close()
|
||||||
|
|
||||||
async def get_users_for_unban_today(self, date_to_unban: str) -> List[Tuple[int, str]]:
|
async def get_users_for_unban_today(self, date_to_unban: str) -> List:
|
||||||
"""Получение пользователей для разблокировки сегодня."""
|
"""Получение пользователей для разблокировки сегодня."""
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
|
|
||||||
from logs.custom_logger import logger
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
# Local imports - metrics
|
||||||
|
from helper_bot.utils.metrics import (
|
||||||
|
metrics,
|
||||||
|
track_time,
|
||||||
|
track_errors,
|
||||||
|
db_query_time
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BotDB:
|
class BotDB:
|
||||||
def __init__(self, current_dir, name):
|
def __init__(self, current_dir, name):
|
||||||
@@ -138,6 +146,9 @@ class BotDB:
|
|||||||
finally:
|
finally:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@track_time("add_new_user_in_db", "database")
|
||||||
|
@track_errors("database", "add_new_user_in_db")
|
||||||
|
@db_query_time("add_new_user_in_db", "our_users", "insert")
|
||||||
def add_new_user_in_db(self, user_id: int, first_name: str, full_name: str, username: str, is_bot: bool,
|
def add_new_user_in_db(self, user_id: int, first_name: str, full_name: str, username: str, is_bot: bool,
|
||||||
language_code: str, emoji: str, date_added: str, date_changed: str):
|
language_code: str, emoji: str, date_added: str, date_changed: str):
|
||||||
"""
|
"""
|
||||||
@@ -189,6 +200,9 @@ class BotDB:
|
|||||||
finally:
|
finally:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@track_time("user_exists", "database")
|
||||||
|
@track_errors("database", "user_exists")
|
||||||
|
@db_query_time("user_exists", "our_users", "select")
|
||||||
def user_exists(self, user_id: int):
|
def user_exists(self, user_id: int):
|
||||||
"""
|
"""
|
||||||
Проверяет, существует ли пользователь в базе данных.
|
Проверяет, существует ли пользователь в базе данных.
|
||||||
@@ -426,6 +440,9 @@ class BotDB:
|
|||||||
finally:
|
finally:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@track_time("get_info_about_stickers", "database")
|
||||||
|
@track_errors("database", "get_info_about_stickers")
|
||||||
|
@db_query_time("get_info_about_stickers", "our_users", "select")
|
||||||
def get_info_about_stickers(self, user_id: int):
|
def get_info_about_stickers(self, user_id: int):
|
||||||
"""
|
"""
|
||||||
Проверяет, получил ли пользователь стикеры.
|
Проверяет, получил ли пользователь стикеры.
|
||||||
@@ -459,6 +476,9 @@ class BotDB:
|
|||||||
finally:
|
finally:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@track_time("update_info_about_stickers", "database")
|
||||||
|
@track_errors("database", "update_info_about_stickers")
|
||||||
|
@db_query_time("update_info_about_stickers", "our_users", "update")
|
||||||
def update_info_about_stickers(self, user_id):
|
def update_info_about_stickers(self, user_id):
|
||||||
"""
|
"""
|
||||||
Обновляет информацию о получении стикеров пользователем.
|
Обновляет информацию о получении стикеров пользователем.
|
||||||
@@ -623,6 +643,9 @@ class BotDB:
|
|||||||
finally:
|
finally:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@track_time("add_new_message_in_db", "database")
|
||||||
|
@track_errors("database", "add_new_message_in_db")
|
||||||
|
@db_query_time("add_new_message_in_db", "user_messages", "insert")
|
||||||
def add_new_message_in_db(self, message_text: str, user_id: int, message_id: int, date: str):
|
def add_new_message_in_db(self, message_text: str, user_id: int, message_id: int, date: str):
|
||||||
"""
|
"""
|
||||||
Добавляет новое сообщение пользователя в базу данных.
|
Добавляет новое сообщение пользователя в базу данных.
|
||||||
@@ -657,6 +680,9 @@ class BotDB:
|
|||||||
finally:
|
finally:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@track_time("get_username_and_full_name", "database")
|
||||||
|
@track_errors("database", "get_username_and_full_name")
|
||||||
|
@db_query_time("get_username_and_full_name", "our_users", "select")
|
||||||
def get_username_and_full_name(self, user_id: int):
|
def get_username_and_full_name(self, user_id: int):
|
||||||
"""
|
"""
|
||||||
Получает full_name и username пользователя по ID из базы
|
Получает full_name и username пользователя по ID из базы
|
||||||
@@ -686,6 +712,9 @@ class BotDB:
|
|||||||
finally:
|
finally:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@track_time("update_username_and_full_name", "database")
|
||||||
|
@track_errors("database", "update_username_and_full_name")
|
||||||
|
@db_query_time("update_username_and_full_name", "our_users", "update")
|
||||||
def update_username_and_full_name(self, user_id: int, username: str, full_name: str):
|
def update_username_and_full_name(self, user_id: int, username: str, full_name: str):
|
||||||
"""
|
"""
|
||||||
Обновляет full_name и username пользователя
|
Обновляет full_name и username пользователя
|
||||||
@@ -715,6 +744,9 @@ class BotDB:
|
|||||||
finally:
|
finally:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@track_time("update_date_for_user", "database")
|
||||||
|
@track_errors("database", "update_date_for_user")
|
||||||
|
@db_query_time("update_date_for_user", "our_users", "update")
|
||||||
def update_date_for_user(self, date: str, user_id: int):
|
def update_date_for_user(self, date: str, user_id: int):
|
||||||
"""
|
"""
|
||||||
#TODO: Не возвращается ошибка sqlite3. Error. Тест не перехватывает. Возвращается no such table: our_users
|
#TODO: Не возвращается ошибка sqlite3. Error. Тест не перехватывает. Возвращается no such table: our_users
|
||||||
@@ -742,6 +774,9 @@ class BotDB:
|
|||||||
finally:
|
finally:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@track_time("check_emoji", "database")
|
||||||
|
@track_errors("database", "check_emoji")
|
||||||
|
@db_query_time("check_emoji", "our_users", "select")
|
||||||
def check_emoji(self, emoji: str):
|
def check_emoji(self, emoji: str):
|
||||||
"""
|
"""
|
||||||
Проверяет, есть ли уже такой emoji в таблице.
|
Проверяет, есть ли уже такой emoji в таблице.
|
||||||
@@ -767,6 +802,9 @@ class BotDB:
|
|||||||
finally:
|
finally:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@track_time("update_emoji_for_user", "database")
|
||||||
|
@track_errors("database", "update_emoji_for_user")
|
||||||
|
@db_query_time("update_emoji_for_user", "our_users", "update")
|
||||||
def update_emoji_for_user(self, user_id: int, emoji: str):
|
def update_emoji_for_user(self, user_id: int, emoji: str):
|
||||||
"""
|
"""
|
||||||
Обновляет эмодзи для пользователя в базе если его ранее не было установлено
|
Обновляет эмодзи для пользователя в базе если его ранее не было установлено
|
||||||
@@ -792,6 +830,9 @@ class BotDB:
|
|||||||
finally:
|
finally:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@track_time("check_emoji_for_user", "database")
|
||||||
|
@track_errors("database", "check_emoji_for_user")
|
||||||
|
@db_query_time("check_emoji_for_user", "our_users", "select")
|
||||||
def check_emoji_for_user(self, user_id: int):
|
def check_emoji_for_user(self, user_id: int):
|
||||||
"""
|
"""
|
||||||
Проверяет, есть ли уже у пользователя назначенный emoji.
|
Проверяет, есть ли уже у пользователя назначенный emoji.
|
||||||
|
|||||||
66
docker-compose.yml
Normal file
66
docker-compose.yml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
services:
|
||||||
|
telegram-bot:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.bot
|
||||||
|
container_name: telegram-bot
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8000:8000" # Экспозиция порта для метрик
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=/app
|
||||||
|
volumes:
|
||||||
|
- ./database:/app/database
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./settings.ini:/app/settings.ini
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
depends_on:
|
||||||
|
- prometheus
|
||||||
|
- grafana
|
||||||
|
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:latest
|
||||||
|
container_name: prometheus
|
||||||
|
ports:
|
||||||
|
- "9090:9090"
|
||||||
|
volumes:
|
||||||
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
|
- prometheus_data:/prometheus
|
||||||
|
command:
|
||||||
|
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||||
|
- '--storage.tsdb.path=/prometheus'
|
||||||
|
- '--web.console.libraries=/etc/prometheus/console_libraries'
|
||||||
|
- '--web.console.templates=/etc/prometheus/consoles'
|
||||||
|
- '--storage.tsdb.retention.time=200h'
|
||||||
|
- '--web.enable-lifecycle'
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
container_name: grafana
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- GF_SECURITY_ADMIN_USER=admin
|
||||||
|
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||||
|
- GF_USERS_ALLOW_SIGN_UP=false
|
||||||
|
volumes:
|
||||||
|
- grafana_data:/var/lib/grafana
|
||||||
|
- ./grafana/dashboards:/etc/grafana/provisioning/dashboards
|
||||||
|
- ./grafana/datasources:/etc/grafana/provisioning/datasources
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
depends_on:
|
||||||
|
- prometheus
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
prometheus_data:
|
||||||
|
grafana_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
monitoring:
|
||||||
|
driver: bridge
|
||||||
12
grafana/dashboards/dashboards.yml
Normal file
12
grafana/dashboards/dashboards.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
providers:
|
||||||
|
- name: 'Telegram Bot Dashboards'
|
||||||
|
orgId: 1
|
||||||
|
folder: ''
|
||||||
|
type: file
|
||||||
|
disableDeletion: false
|
||||||
|
updateIntervalSeconds: 10
|
||||||
|
allowUiUpdates: true
|
||||||
|
options:
|
||||||
|
path: /etc/grafana/provisioning/dashboards
|
||||||
577
grafana/dashboards/telegram-bot-dashboard.json
Normal file
577
grafana/dashboards/telegram-bot-dashboard.json
Normal file
@@ -0,0 +1,577 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"liveNow": false,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "PBFA97CFB590B2093"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"vis": false
|
||||||
|
},
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 1,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "PBFA97CFB590B2093"
|
||||||
|
},
|
||||||
|
"expr": "rate(bot_commands_total[5m])",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Commands per Second",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "PBFA97CFB590B2093"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"vis": false
|
||||||
|
},
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "s"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "PBFA97CFB590B2093"
|
||||||
|
},
|
||||||
|
"expr": "histogram_quantile(0.95, rate(method_duration_seconds_bucket[5m]))",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "PBFA97CFB590B2093"
|
||||||
|
},
|
||||||
|
"expr": "histogram_quantile(0.99, rate(method_duration_seconds_bucket[5m]))",
|
||||||
|
"refId": "B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Method Response Time (P95, P99)",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "PBFA97CFB590B2093"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"vis": false
|
||||||
|
},
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 8
|
||||||
|
},
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "PBFA97CFB590B2093"
|
||||||
|
},
|
||||||
|
"expr": "rate(errors_total[5m])",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Errors per Second",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "PBFA97CFB590B2093"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"vis": false
|
||||||
|
},
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 8
|
||||||
|
},
|
||||||
|
"id": 4,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "PBFA97CFB590B2093"
|
||||||
|
},
|
||||||
|
"expr": "active_users",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Active Users",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "PBFA97CFB590B2093"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"vis": false
|
||||||
|
},
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "s"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 16
|
||||||
|
},
|
||||||
|
"id": 5,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "PBFA97CFB590B2093"
|
||||||
|
},
|
||||||
|
"expr": "histogram_quantile(0.95, rate(db_query_duration_seconds_bucket[5m]))",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Database Query Time (P95)",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "PBFA97CFB590B2093"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"vis": false
|
||||||
|
},
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 16
|
||||||
|
},
|
||||||
|
"id": 6,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "PBFA97CFB590B2093"
|
||||||
|
},
|
||||||
|
"expr": "rate(messages_processed_total[5m])",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Messages Processed per Second",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refresh": "5s",
|
||||||
|
"schemaVersion": 38,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": [
|
||||||
|
"telegram",
|
||||||
|
"bot",
|
||||||
|
"monitoring"
|
||||||
|
],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-1h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "Telegram Bot Dashboard",
|
||||||
|
"uid": "telegram-bot",
|
||||||
|
"version": 1,
|
||||||
|
"weekStart": ""
|
||||||
|
}
|
||||||
8
grafana/datasources/prometheus.yml
Normal file
8
grafana/datasources/prometheus.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Prometheus
|
||||||
|
type: prometheus
|
||||||
|
access: proxy
|
||||||
|
url: http://prometheus:9090
|
||||||
|
isDefault: true
|
||||||
189
helper_bot/examples/metrics_usage_examples.py
Normal file
189
helper_bot/examples/metrics_usage_examples.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
"""
|
||||||
|
Examples of how to use metrics decorators in your bot handlers.
|
||||||
|
These examples show how to integrate metrics without modifying existing logic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.types import Message, CallbackQuery
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
||||||
|
# Import metrics decorators
|
||||||
|
from ..utils.metrics import track_time, track_errors, db_query_time, metrics
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
# Example 1: Basic command handler with timing and error tracking
|
||||||
|
@router.message(Command("start"))
|
||||||
|
@track_time("start_command", "private_handler")
|
||||||
|
@track_errors("private_handler", "start_command")
|
||||||
|
async def start_command(message: Message, state: FSMContext):
|
||||||
|
"""Start command handler with metrics."""
|
||||||
|
# Your existing logic here
|
||||||
|
await message.answer("Welcome! Bot started.")
|
||||||
|
|
||||||
|
# Optionally record custom metrics
|
||||||
|
metrics.record_command("start", "private_handler", "user")
|
||||||
|
|
||||||
|
|
||||||
|
# Example 2: Group command handler with custom labels
|
||||||
|
@router.message(Command("help"), F.chat.type.in_({"group", "supergroup"}))
|
||||||
|
@track_time("help_command", "group_handler")
|
||||||
|
@track_errors("group_handler", "help_command")
|
||||||
|
async def help_command(message: Message):
|
||||||
|
"""Help command handler for groups."""
|
||||||
|
await message.answer("Group help information.")
|
||||||
|
|
||||||
|
# Record command with group context
|
||||||
|
metrics.record_command("help", "group_handler", "group_user")
|
||||||
|
|
||||||
|
|
||||||
|
# Example 3: Callback handler with timing
|
||||||
|
@router.callback_query(F.data.startswith("menu:"))
|
||||||
|
@track_time("menu_callback", "callback_handler")
|
||||||
|
@track_errors("callback_handler", "menu_callback")
|
||||||
|
async def menu_callback(callback: CallbackQuery):
|
||||||
|
"""Menu callback handler."""
|
||||||
|
data = callback.data
|
||||||
|
await callback.answer(f"Menu: {data}")
|
||||||
|
|
||||||
|
# Record callback processing
|
||||||
|
metrics.record_message("callback_query", "callback", "callback_handler")
|
||||||
|
|
||||||
|
|
||||||
|
# Example 4: Database operation with query timing
|
||||||
|
@db_query_time("user_lookup", "users", "select")
|
||||||
|
async def get_user_info(user_id: int):
|
||||||
|
"""Example database function with timing."""
|
||||||
|
# Your database query here
|
||||||
|
# result = await db.fetch_one("SELECT * FROM users WHERE id = ?", user_id)
|
||||||
|
return {"user_id": user_id, "status": "active"}
|
||||||
|
|
||||||
|
|
||||||
|
# Example 5: Complex handler with multiple metrics
|
||||||
|
@router.message(Command("stats"))
|
||||||
|
@track_time("stats_command", "admin_handler")
|
||||||
|
@track_errors("admin_handler", "stats_command")
|
||||||
|
async def stats_command(message: Message):
|
||||||
|
"""Stats command with detailed metrics."""
|
||||||
|
try:
|
||||||
|
# Record command execution
|
||||||
|
metrics.record_command("stats", "admin_handler", "admin_user")
|
||||||
|
|
||||||
|
# Your stats logic here
|
||||||
|
stats = await get_bot_stats()
|
||||||
|
|
||||||
|
# Record successful execution
|
||||||
|
await message.answer(f"Bot stats: {stats}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Error is automatically tracked by decorator
|
||||||
|
await message.answer("Error getting stats")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# Example 6: Message handler with message type tracking
|
||||||
|
@router.message()
|
||||||
|
@track_time("message_processing", "general_handler")
|
||||||
|
async def handle_message(message: Message):
|
||||||
|
"""General message handler."""
|
||||||
|
# Message type is automatically detected by middleware
|
||||||
|
# But you can add custom tracking
|
||||||
|
|
||||||
|
if message.photo:
|
||||||
|
# Custom metric for photo processing
|
||||||
|
metrics.record_message("photo", "general", "photo_handler")
|
||||||
|
|
||||||
|
# Your message handling logic
|
||||||
|
await message.answer("Message received")
|
||||||
|
|
||||||
|
|
||||||
|
# Example 7: Error-prone operation with custom error tracking
|
||||||
|
@track_errors("file_handler", "file_upload")
|
||||||
|
async def upload_file(file_data: bytes, filename: str):
|
||||||
|
"""File upload with error tracking."""
|
||||||
|
try:
|
||||||
|
# Your file upload logic
|
||||||
|
# result = await upload_service.upload(file_data, filename)
|
||||||
|
return {"status": "success", "filename": filename}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Custom error metric
|
||||||
|
metrics.record_error(
|
||||||
|
type(e).__name__,
|
||||||
|
"file_handler",
|
||||||
|
"file_upload"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# Example 8: Background task with metrics
|
||||||
|
async def background_metrics_collection():
|
||||||
|
"""Background task for collecting periodic metrics."""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Collect custom metrics
|
||||||
|
active_users = await count_active_users()
|
||||||
|
metrics.set_active_users(active_users, "current")
|
||||||
|
|
||||||
|
# Wait before next collection
|
||||||
|
await asyncio.sleep(300) # 5 minutes
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
metrics.record_error(
|
||||||
|
type(e).__name__,
|
||||||
|
"background_task",
|
||||||
|
"metrics_collection"
|
||||||
|
)
|
||||||
|
await asyncio.sleep(60) # Wait 1 minute on error
|
||||||
|
|
||||||
|
|
||||||
|
# Example 9: Custom metric collection in service
|
||||||
|
class UserService:
|
||||||
|
"""Example service with integrated metrics."""
|
||||||
|
|
||||||
|
@db_query_time("user_creation", "users", "insert")
|
||||||
|
async def create_user(self, user_data: dict):
|
||||||
|
"""Create user with database timing."""
|
||||||
|
# Your user creation logic
|
||||||
|
# user_id = await self.db.execute("INSERT INTO users ...")
|
||||||
|
return {"user_id": 123, "status": "created"}
|
||||||
|
|
||||||
|
@track_time("user_update", "user_service")
|
||||||
|
async def update_user(self, user_id: int, updates: dict):
|
||||||
|
"""Update user with timing."""
|
||||||
|
# Your update logic
|
||||||
|
# await self.db.execute("UPDATE users SET ...")
|
||||||
|
return {"user_id": user_id, "status": "updated"}
|
||||||
|
|
||||||
|
|
||||||
|
# Example 10: Middleware integration example
|
||||||
|
async def custom_middleware(handler, event, data):
|
||||||
|
"""Custom middleware that works with metrics system."""
|
||||||
|
from ..utils.metrics import track_middleware
|
||||||
|
|
||||||
|
async with track_middleware("custom_middleware"):
|
||||||
|
# Your middleware logic
|
||||||
|
result = await handler(event, data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Helper function for stats (placeholder)
|
||||||
|
async def get_bot_stats():
|
||||||
|
"""Get bot statistics."""
|
||||||
|
return {
|
||||||
|
"total_users": 1000,
|
||||||
|
"active_today": 150,
|
||||||
|
"commands_processed": 5000
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Helper function for user counting (placeholder)
|
||||||
|
async def count_active_users():
|
||||||
|
"""Count active users."""
|
||||||
|
return 150
|
||||||
|
|
||||||
|
|
||||||
|
# Import asyncio for background task
|
||||||
|
import asyncio
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
from typing import Annotated, Dict, Any
|
from typing import Dict, Any
|
||||||
|
try:
|
||||||
|
from typing import Annotated
|
||||||
|
except ImportError:
|
||||||
|
from typing_extensions import Annotated
|
||||||
from aiogram import BaseMiddleware
|
from aiogram import BaseMiddleware
|
||||||
from aiogram.types import TelegramObject
|
from aiogram.types import TelegramObject
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import html
|
import html
|
||||||
from tkinter import S
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from aiogram import Router
|
from aiogram import Router
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
"""Constants for group handlers"""
|
"""Constants for group handlers"""
|
||||||
|
|
||||||
from typing import Final
|
from typing import Final, Dict
|
||||||
|
|
||||||
# FSM States
|
# FSM States
|
||||||
FSM_STATES: Final[dict[str, str]] = {
|
FSM_STATES: Final[Dict[str, str]] = {
|
||||||
"CHAT": "CHAT"
|
"CHAT": "CHAT"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Error messages
|
# Error messages
|
||||||
ERROR_MESSAGES: Final[dict[str, str]] = {
|
ERROR_MESSAGES: Final[Dict[str, str]] = {
|
||||||
"NO_REPLY_TO_MESSAGE": "Блять, выдели сообщение!",
|
"NO_REPLY_TO_MESSAGE": "Блять, выдели сообщение!",
|
||||||
"USER_NOT_FOUND": "Не могу найти кому ответить в базе, проебали сообщение."
|
"USER_NOT_FOUND": "Не могу найти кому ответить в базе, проебали сообщение."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Constants for private handlers"""
|
"""Constants for private handlers"""
|
||||||
|
|
||||||
from typing import Final
|
from typing import Final, Dict
|
||||||
|
|
||||||
# FSM States
|
# FSM States
|
||||||
FSM_STATES: Final[dict[str, str]] = {
|
FSM_STATES: Final[Dict[str, str]] = {
|
||||||
"START": "START",
|
"START": "START",
|
||||||
"SUGGEST": "SUGGEST",
|
"SUGGEST": "SUGGEST",
|
||||||
"PRE_CHAT": "PRE_CHAT",
|
"PRE_CHAT": "PRE_CHAT",
|
||||||
@@ -11,7 +11,7 @@ FSM_STATES: Final[dict[str, str]] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Button texts
|
# Button texts
|
||||||
BUTTON_TEXTS: Final[dict[str, str]] = {
|
BUTTON_TEXTS: Final[Dict[str, str]] = {
|
||||||
"SUGGEST_POST": "📢Предложить свой пост",
|
"SUGGEST_POST": "📢Предложить свой пост",
|
||||||
"SAY_GOODBYE": "👋🏼Сказать пока!",
|
"SAY_GOODBYE": "👋🏼Сказать пока!",
|
||||||
"LEAVE_CHAT": "Выйти из чата",
|
"LEAVE_CHAT": "Выйти из чата",
|
||||||
@@ -21,7 +21,7 @@ BUTTON_TEXTS: Final[dict[str, str]] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Error messages
|
# Error messages
|
||||||
ERROR_MESSAGES: Final[dict[str, str]] = {
|
ERROR_MESSAGES: Final[Dict[str, str]] = {
|
||||||
"UNSUPPORTED_CONTENT": (
|
"UNSUPPORTED_CONTENT": (
|
||||||
'Я пока не умею работать с таким сообщением. '
|
'Я пока не умею работать с таким сообщением. '
|
||||||
'Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n'
|
'Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n'
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ from helper_bot.utils.helper_func import (
|
|||||||
check_user_emoji
|
check_user_emoji
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Local imports - metrics
|
||||||
|
from helper_bot.utils.metrics import (
|
||||||
|
metrics,
|
||||||
|
track_time,
|
||||||
|
track_errors,
|
||||||
|
db_query_time
|
||||||
|
)
|
||||||
|
|
||||||
# Local imports - modular components
|
# Local imports - modular components
|
||||||
from .constants import FSM_STATES, BUTTON_TEXTS, ERROR_MESSAGES
|
from .constants import FSM_STATES, BUTTON_TEXTS, ERROR_MESSAGES
|
||||||
from .services import BotSettings, UserService, PostService, StickerService
|
from .services import BotSettings, UserService, PostService, StickerService
|
||||||
@@ -91,16 +99,23 @@ class PrivateHandlers:
|
|||||||
await message.answer('Я перезапущен!', reply_markup=markup, parse_mode='HTML')
|
await message.answer('Я перезапущен!', reply_markup=markup, parse_mode='HTML')
|
||||||
|
|
||||||
@error_handler
|
@error_handler
|
||||||
|
@track_time("start_message_handler", "private_handler")
|
||||||
|
@track_errors("private_handler", "start_message_handler")
|
||||||
async def handle_start_message(self, message: types.Message, state: FSMContext, **kwargs):
|
async def handle_start_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||||
"""Handle start command and return to bot button"""
|
"""Handle start command and return to bot button with metrics tracking"""
|
||||||
|
# Record start command metrics
|
||||||
|
metrics.record_command("start", "private_handler", "user" if not message.from_user.is_bot else "bot")
|
||||||
|
metrics.record_message("command", "private", "private_handler")
|
||||||
|
|
||||||
|
# User service operations with metrics
|
||||||
await self.user_service.log_user_message(message)
|
await self.user_service.log_user_message(message)
|
||||||
await self.user_service.ensure_user_exists(message)
|
await self.user_service.ensure_user_exists(message)
|
||||||
await state.set_state(FSM_STATES["START"])
|
await state.set_state(FSM_STATES["START"])
|
||||||
|
|
||||||
# Send sticker
|
# Send sticker with metrics
|
||||||
await self.sticker_service.send_random_hello_sticker(message)
|
await self.sticker_service.send_random_hello_sticker(message)
|
||||||
|
|
||||||
# Send welcome message
|
# Send welcome message with metrics
|
||||||
markup = get_reply_keyboard(self.db, message.from_user.id)
|
markup = get_reply_keyboard(self.db, message.from_user.id)
|
||||||
hello_message = messages.get_message(get_first_name(message), 'HELLO_MESSAGE')
|
hello_message = messages.get_message(get_first_name(message), 'HELLO_MESSAGE')
|
||||||
await message.answer(hello_message, reply_markup=markup, parse_mode='HTML')
|
await message.answer(hello_message, reply_markup=markup, parse_mode='HTML')
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ from helper_bot.utils.helper_func import (
|
|||||||
)
|
)
|
||||||
from helper_bot.keyboards import get_reply_keyboard_for_post
|
from helper_bot.keyboards import get_reply_keyboard_for_post
|
||||||
|
|
||||||
|
# Local imports - metrics
|
||||||
|
from helper_bot.utils.metrics import (
|
||||||
|
metrics,
|
||||||
|
track_time,
|
||||||
|
track_errors,
|
||||||
|
db_query_time
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseProtocol(Protocol):
|
class DatabaseProtocol(Protocol):
|
||||||
"""Protocol for database operations"""
|
"""Protocol for database operations"""
|
||||||
@@ -65,13 +73,18 @@ class UserService:
|
|||||||
self.db = db
|
self.db = db
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
|
||||||
|
@track_time("update_user_activity", "user_service")
|
||||||
|
@track_errors("user_service", "update_user_activity")
|
||||||
|
@db_query_time("update_user_activity", "users", "update")
|
||||||
async def update_user_activity(self, user_id: int) -> None:
|
async def update_user_activity(self, user_id: int) -> None:
|
||||||
"""Update user's last activity timestamp"""
|
"""Update user's last activity timestamp with metrics tracking"""
|
||||||
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
self.db.update_date_for_user(current_date, user_id)
|
self.db.update_date_for_user(current_date, user_id)
|
||||||
|
|
||||||
|
@track_time("ensure_user_exists", "user_service")
|
||||||
|
@track_errors("user_service", "ensure_user_exists")
|
||||||
async def ensure_user_exists(self, message: types.Message) -> None:
|
async def ensure_user_exists(self, message: types.Message) -> None:
|
||||||
"""Ensure user exists in database, create if needed"""
|
"""Ensure user exists in database, create if needed with metrics tracking"""
|
||||||
user_id = message.from_user.id
|
user_id = message.from_user.id
|
||||||
full_name = message.from_user.full_name
|
full_name = message.from_user.full_name
|
||||||
username = message.from_user.username or "private_username"
|
username = message.from_user.username or "private_username"
|
||||||
@@ -82,14 +95,17 @@ class UserService:
|
|||||||
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
if not self.db.user_exists(user_id):
|
if not self.db.user_exists(user_id):
|
||||||
|
# Record database operation
|
||||||
self.db.add_new_user_in_db(
|
self.db.add_new_user_in_db(
|
||||||
user_id, first_name, full_name, username, is_bot, language_code,
|
user_id, first_name, full_name, username, is_bot, language_code,
|
||||||
"", current_date, current_date
|
"", current_date, current_date
|
||||||
)
|
)
|
||||||
|
metrics.record_db_query("add_new_user", 0.0, "users", "insert")
|
||||||
else:
|
else:
|
||||||
is_need_update = check_username_and_full_name(user_id, username, full_name, self.db)
|
is_need_update = check_username_and_full_name(user_id, username, full_name, self.db)
|
||||||
if is_need_update:
|
if is_need_update:
|
||||||
self.db.update_username_and_full_name(user_id, username, full_name)
|
self.db.update_username_and_full_name(user_id, username, full_name)
|
||||||
|
metrics.record_db_query("update_username_fullname", 0.0, "users", "update")
|
||||||
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
|
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
|
||||||
safe_username = html.escape(username) if username else "Без никнейма"
|
safe_username = html.escape(username) if username else "Без никнейма"
|
||||||
|
|
||||||
@@ -100,9 +116,12 @@ class UserService:
|
|||||||
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}')
|
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}')
|
||||||
|
|
||||||
self.db.update_date_for_user(current_date, user_id)
|
self.db.update_date_for_user(current_date, user_id)
|
||||||
|
metrics.record_db_query("update_date_for_user", 0.0, "users", "update")
|
||||||
|
|
||||||
|
@track_time("log_user_message", "user_service")
|
||||||
|
@track_errors("user_service", "log_user_message")
|
||||||
async def log_user_message(self, message: types.Message) -> None:
|
async def log_user_message(self, message: types.Message) -> None:
|
||||||
"""Forward user message to logs group"""
|
"""Forward user message to logs group with metrics tracking"""
|
||||||
await message.forward(chat_id=self.settings.group_for_logs)
|
await message.forward(chat_id=self.settings.group_for_logs)
|
||||||
|
|
||||||
def get_safe_user_info(self, message: types.Message) -> tuple[str, str]:
|
def get_safe_user_info(self, message: types.Message) -> tuple[str, str]:
|
||||||
@@ -210,7 +229,7 @@ class PostService:
|
|||||||
message_id=media_group_message_id, helper_message_id=help_message_id
|
message_id=media_group_message_id, helper_message_id=help_message_id
|
||||||
)
|
)
|
||||||
|
|
||||||
async def process_post(self, message: types.Message, album: Union[list[types.Message], None] = None) -> None:
|
async def process_post(self, message: types.Message, album: Union[list, None] = None) -> None:
|
||||||
"""Process post based on content type"""
|
"""Process post based on content type"""
|
||||||
first_name = get_first_name(message)
|
first_name = get_first_name(message)
|
||||||
|
|
||||||
@@ -248,8 +267,10 @@ class StickerService:
|
|||||||
def __init__(self, settings: BotSettings) -> None:
|
def __init__(self, settings: BotSettings) -> None:
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
|
||||||
|
@track_time("send_random_hello_sticker", "sticker_service")
|
||||||
|
@track_errors("sticker_service", "send_random_hello_sticker")
|
||||||
async def send_random_hello_sticker(self, message: types.Message) -> None:
|
async def send_random_hello_sticker(self, message: types.Message) -> None:
|
||||||
"""Send random hello sticker"""
|
"""Send random hello sticker with metrics tracking"""
|
||||||
name_stick_hello = list(Path('Stick').rglob('Hello_*'))
|
name_stick_hello = list(Path('Stick').rglob('Hello_*'))
|
||||||
if not name_stick_hello:
|
if not name_stick_hello:
|
||||||
return
|
return
|
||||||
@@ -258,8 +279,10 @@ class StickerService:
|
|||||||
await message.answer_sticker(random_stick_hello)
|
await message.answer_sticker(random_stick_hello)
|
||||||
await asyncio.sleep(0.3)
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
@track_time("send_random_goodbye_sticker", "sticker_service")
|
||||||
|
@track_errors("sticker_service", "send_random_goodbye_sticker")
|
||||||
async def send_random_goodbye_sticker(self, message: types.Message) -> None:
|
async def send_random_goodbye_sticker(self, message: types.Message) -> None:
|
||||||
"""Send random goodbye sticker"""
|
"""Send random goodbye sticker with metrics tracking"""
|
||||||
name_stick_bye = list(Path('Stick').rglob('Universal_*'))
|
name_stick_bye = list(Path('Stick').rglob('Universal_*'))
|
||||||
if not name_stick_bye:
|
if not name_stick_bye:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
from aiogram import types
|
from aiogram import types
|
||||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
|
from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
|
||||||
|
|
||||||
|
# Local imports - metrics
|
||||||
|
from helper_bot.utils.metrics import (
|
||||||
|
metrics,
|
||||||
|
track_time,
|
||||||
|
track_errors
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_reply_keyboard_for_post():
|
def get_reply_keyboard_for_post():
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
@@ -16,6 +23,8 @@ def get_reply_keyboard_for_post():
|
|||||||
return markup
|
return markup
|
||||||
|
|
||||||
|
|
||||||
|
@track_time("get_reply_keyboard", "keyboard_service")
|
||||||
|
@track_errors("keyboard_service", "get_reply_keyboard")
|
||||||
def get_reply_keyboard(BotDB, user_id):
|
def get_reply_keyboard(BotDB, user_id):
|
||||||
builder = ReplyKeyboardBuilder()
|
builder = ReplyKeyboardBuilder()
|
||||||
builder.row(types.KeyboardButton(text="📢Предложить свой пост"))
|
builder.row(types.KeyboardButton(text="📢Предложить свой пост"))
|
||||||
@@ -49,7 +58,7 @@ def get_reply_keyboard_admin():
|
|||||||
return markup
|
return markup
|
||||||
|
|
||||||
|
|
||||||
def create_keyboard_with_pagination(page: int, total_items: int, array_items: list[tuple[any, any]], callback: str):
|
def create_keyboard_with_pagination(page: int, total_items: int, array_items: list, callback: str):
|
||||||
"""
|
"""
|
||||||
Создает клавиатуру с пагинацией для заданного набора элементов и устанавливает необходимый callback
|
Создает клавиатуру с пагинацией для заданного набора элементов и устанавливает необходимый callback
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from helper_bot.handlers.group import group_router
|
|||||||
from helper_bot.handlers.private import private_router
|
from helper_bot.handlers.private import private_router
|
||||||
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
|
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
|
||||||
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||||
|
from helper_bot.middlewares.metrics_middleware import MetricsMiddleware, ErrorMetricsMiddleware
|
||||||
|
|
||||||
|
|
||||||
async def start_bot(bdf):
|
async def start_bot(bdf):
|
||||||
@@ -19,6 +20,12 @@ async def start_bot(bdf):
|
|||||||
), timeout=30.0) # Добавляем таймаут для предотвращения зависаний
|
), timeout=30.0) # Добавляем таймаут для предотвращения зависаний
|
||||||
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
|
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
|
||||||
|
|
||||||
|
# ✅ Middleware для метрик (добавляем первыми)
|
||||||
|
dp.message.middleware(MetricsMiddleware())
|
||||||
|
dp.callback_query.middleware(MetricsMiddleware())
|
||||||
|
dp.message.middleware(ErrorMetricsMiddleware())
|
||||||
|
dp.callback_query.middleware(ErrorMetricsMiddleware())
|
||||||
|
|
||||||
# ✅ Глобальная middleware для всех роутеров
|
# ✅ Глобальная middleware для всех роутеров
|
||||||
dp.update.outer_middleware(DependenciesMiddleware())
|
dp.update.outer_middleware(DependenciesMiddleware())
|
||||||
|
|
||||||
|
|||||||
117
helper_bot/main_with_metrics.py
Normal file
117
helper_bot/main_with_metrics.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
Example integration of metrics monitoring in the main bot file.
|
||||||
|
This shows how to integrate the metrics system without modifying existing handlers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from aiogram import Bot, Dispatcher
|
||||||
|
from aiogram.enums import ParseMode
|
||||||
|
from aiogram.fsm.storage.memory import MemoryStorage
|
||||||
|
|
||||||
|
# Import metrics components
|
||||||
|
from .utils.metrics import metrics
|
||||||
|
from .utils.metrics_exporter import MetricsManager
|
||||||
|
from .middlewares.metrics_middleware import MetricsMiddleware, ErrorMetricsMiddleware
|
||||||
|
|
||||||
|
# Import your existing bot components
|
||||||
|
# from .handlers import ... # Your existing handlers
|
||||||
|
# from .database.db import BotDB # Your existing database class
|
||||||
|
|
||||||
|
|
||||||
|
class BotWithMetrics:
|
||||||
|
"""Bot class with integrated metrics monitoring."""
|
||||||
|
|
||||||
|
def __init__(self, token: str, metrics_port: int = 8000):
|
||||||
|
self.bot = Bot(token=token, parse_mode=ParseMode.HTML)
|
||||||
|
self.storage = MemoryStorage()
|
||||||
|
self.dp = Dispatcher(storage=self.storage)
|
||||||
|
|
||||||
|
# Initialize metrics manager
|
||||||
|
# You can pass your database instance here if needed
|
||||||
|
# self.metrics_manager = MetricsManager(port=metrics_port, db=your_db_instance)
|
||||||
|
self.metrics_manager = MetricsManager(port=metrics_port)
|
||||||
|
|
||||||
|
# Setup middlewares
|
||||||
|
self._setup_middlewares()
|
||||||
|
|
||||||
|
# Setup handlers (your existing handlers)
|
||||||
|
# self._setup_handlers()
|
||||||
|
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def _setup_middlewares(self):
|
||||||
|
"""Setup metrics middlewares."""
|
||||||
|
# Add metrics middleware first to capture all events
|
||||||
|
self.dp.message.middleware(MetricsMiddleware())
|
||||||
|
self.dp.callback_query.middleware(MetricsMiddleware())
|
||||||
|
|
||||||
|
# Add error tracking middleware
|
||||||
|
self.dp.message.middleware(ErrorMetricsMiddleware())
|
||||||
|
self.dp.callback_query.middleware(ErrorMetricsMiddleware())
|
||||||
|
|
||||||
|
# Your existing middlewares can go here
|
||||||
|
# self.dp.message.middleware(YourExistingMiddleware())
|
||||||
|
|
||||||
|
def _setup_handlers(self):
|
||||||
|
"""Setup bot handlers."""
|
||||||
|
# Import and register your existing handlers here
|
||||||
|
# from .handlers.admin import admin_router
|
||||||
|
# from .handlers.private import private_router
|
||||||
|
# from .handlers.group import group_router
|
||||||
|
# from .handlers.callback import callback_router
|
||||||
|
#
|
||||||
|
# self.dp.include_router(admin_router)
|
||||||
|
# self.dp.include_router(private_router)
|
||||||
|
# self.dp.include_router(group_router)
|
||||||
|
# self.dp.include_router(callback_router)
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""Start the bot with metrics."""
|
||||||
|
try:
|
||||||
|
# Start metrics collection
|
||||||
|
await self.metrics_manager.start()
|
||||||
|
self.logger.info("Metrics system started")
|
||||||
|
|
||||||
|
# Start bot polling
|
||||||
|
await self.dp.start_polling(self.bot)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error starting bot: {e}")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
await self.stop()
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Stop the bot and metrics."""
|
||||||
|
try:
|
||||||
|
# Stop metrics collection
|
||||||
|
await self.metrics_manager.stop()
|
||||||
|
self.logger.info("Metrics system stopped")
|
||||||
|
|
||||||
|
# Stop bot
|
||||||
|
await self.bot.session.close()
|
||||||
|
self.logger.info("Bot stopped")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error stopping bot: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage function
|
||||||
|
async def main():
|
||||||
|
"""Main function to run the bot with metrics."""
|
||||||
|
# Your bot token
|
||||||
|
TOKEN = "YOUR_BOT_TOKEN_HERE"
|
||||||
|
|
||||||
|
# Create and start bot
|
||||||
|
bot = BotWithMetrics(TOKEN)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.start()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
await bot.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
173
helper_bot/middlewares/metrics_middleware.py
Normal file
173
helper_bot/middlewares/metrics_middleware.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
"""
|
||||||
|
Metrics middleware for aiogram 3.x.
|
||||||
|
Automatically collects metrics for message processing, command execution, and errors.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any, Awaitable, Callable, Dict
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.types import TelegramObject, Message, CallbackQuery
|
||||||
|
from aiogram.enums import ChatType
|
||||||
|
import time
|
||||||
|
from ..utils.metrics import metrics, track_middleware
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsMiddleware(BaseMiddleware):
|
||||||
|
"""Middleware for automatic metrics collection in aiogram handlers."""
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: TelegramObject,
|
||||||
|
data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""Process event and collect metrics."""
|
||||||
|
|
||||||
|
async with track_middleware("metrics_middleware"):
|
||||||
|
# Record message processing
|
||||||
|
if isinstance(event, Message):
|
||||||
|
await self._record_message_metrics(event, data)
|
||||||
|
elif isinstance(event, CallbackQuery):
|
||||||
|
await self._record_callback_metrics(event, data)
|
||||||
|
|
||||||
|
# Execute handler and collect timing
|
||||||
|
start_time = time.time()
|
||||||
|
try:
|
||||||
|
result = await handler(event, data)
|
||||||
|
duration = time.time() - start_time
|
||||||
|
|
||||||
|
# Record successful execution
|
||||||
|
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
||||||
|
metrics.record_method_duration(
|
||||||
|
handler_name,
|
||||||
|
duration,
|
||||||
|
"handler",
|
||||||
|
"success"
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
duration = time.time() - start_time
|
||||||
|
|
||||||
|
# Record error and timing
|
||||||
|
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
||||||
|
metrics.record_method_duration(
|
||||||
|
handler_name,
|
||||||
|
duration,
|
||||||
|
"handler",
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|
metrics.record_error(
|
||||||
|
type(e).__name__,
|
||||||
|
"handler",
|
||||||
|
handler_name
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _record_message_metrics(self, message: Message, data: Dict[str, Any]):
|
||||||
|
"""Record metrics for message processing."""
|
||||||
|
# Determine message type
|
||||||
|
message_type = "text"
|
||||||
|
if message.photo:
|
||||||
|
message_type = "photo"
|
||||||
|
elif message.video:
|
||||||
|
message_type = "video"
|
||||||
|
elif message.audio:
|
||||||
|
message_type = "audio"
|
||||||
|
elif message.document:
|
||||||
|
message_type = "document"
|
||||||
|
elif message.voice:
|
||||||
|
message_type = "voice"
|
||||||
|
elif message.sticker:
|
||||||
|
message_type = "sticker"
|
||||||
|
elif message.animation:
|
||||||
|
message_type = "animation"
|
||||||
|
|
||||||
|
# Determine chat type
|
||||||
|
chat_type = "private"
|
||||||
|
if message.chat.type == ChatType.GROUP:
|
||||||
|
chat_type = "group"
|
||||||
|
elif message.chat.type == ChatType.SUPERGROUP:
|
||||||
|
chat_type = "supergroup"
|
||||||
|
elif message.chat.type == ChatType.CHANNEL:
|
||||||
|
chat_type = "channel"
|
||||||
|
|
||||||
|
# Determine handler type
|
||||||
|
handler_type = "unknown"
|
||||||
|
if message.text and message.text.startswith('/'):
|
||||||
|
handler_type = "command"
|
||||||
|
# Record command specifically
|
||||||
|
command = message.text.split()[0][1:] # Remove '/' and get command name
|
||||||
|
metrics.record_command(
|
||||||
|
command,
|
||||||
|
"message_handler",
|
||||||
|
"user" if message.from_user else "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Record message processing
|
||||||
|
metrics.record_message(message_type, chat_type, handler_type)
|
||||||
|
|
||||||
|
async def _record_callback_metrics(self, callback: CallbackQuery, data: Dict[str, Any]):
|
||||||
|
"""Record metrics for callback query processing."""
|
||||||
|
# Record callback processing
|
||||||
|
metrics.record_message(
|
||||||
|
"callback_query",
|
||||||
|
"callback",
|
||||||
|
"callback_handler"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Record callback command if available
|
||||||
|
if callback.data:
|
||||||
|
# Extract command from callback data (assuming format like "command:param")
|
||||||
|
parts = callback.data.split(':', 1)
|
||||||
|
if parts:
|
||||||
|
command = parts[0]
|
||||||
|
metrics.record_command(
|
||||||
|
command,
|
||||||
|
"callback_handler",
|
||||||
|
"user" if callback.from_user else "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseMetricsMiddleware(BaseMiddleware):
|
||||||
|
"""Middleware for database operation metrics."""
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: TelegramObject,
|
||||||
|
data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""Process event and collect database metrics."""
|
||||||
|
|
||||||
|
# Check if this handler involves database operations
|
||||||
|
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
||||||
|
|
||||||
|
# You can add specific database operation detection logic here
|
||||||
|
# For now, we'll just pass through and let individual decorators handle it
|
||||||
|
|
||||||
|
return await handler(event, data)
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorMetricsMiddleware(BaseMiddleware):
|
||||||
|
"""Middleware for error tracking and metrics."""
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: TelegramObject,
|
||||||
|
data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""Process event and collect error metrics."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await handler(event, data)
|
||||||
|
except Exception as e:
|
||||||
|
# Record error metrics
|
||||||
|
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
||||||
|
metrics.record_error(
|
||||||
|
type(e).__name__,
|
||||||
|
"handler",
|
||||||
|
handler_name
|
||||||
|
)
|
||||||
|
raise
|
||||||
@@ -3,6 +3,7 @@ import os
|
|||||||
import random
|
import random
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import emoji as _emoji_lib
|
import emoji as _emoji_lib
|
||||||
@@ -14,6 +15,14 @@ from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMe
|
|||||||
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
|
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
|
||||||
from logs.custom_logger import logger
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
# Local imports - metrics
|
||||||
|
from .metrics import (
|
||||||
|
metrics,
|
||||||
|
track_time,
|
||||||
|
track_errors,
|
||||||
|
db_query_time
|
||||||
|
)
|
||||||
|
|
||||||
bdf = get_global_instance()
|
bdf = get_global_instance()
|
||||||
BotDB = bdf.get_db()
|
BotDB = bdf.get_db()
|
||||||
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
|
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
|
||||||
@@ -43,6 +52,8 @@ def safe_html_escape(text: str) -> str:
|
|||||||
return html.escape(str(text))
|
return html.escape(str(text))
|
||||||
|
|
||||||
|
|
||||||
|
@track_time("get_first_name", "helper_func")
|
||||||
|
@track_errors("helper_func", "get_first_name")
|
||||||
def get_first_name(message: types.Message) -> str:
|
def get_first_name(message: types.Message) -> str:
|
||||||
"""
|
"""
|
||||||
Безопасно получает и экранирует имя пользователя для использования в HTML разметке.
|
Безопасно получает и экранирует имя пользователя для использования в HTML разметке.
|
||||||
@@ -234,7 +245,7 @@ async def add_in_db_media(sent_message, bot_db):
|
|||||||
|
|
||||||
|
|
||||||
async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message,
|
async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message,
|
||||||
media_group: list[InputMediaPhoto], bot_db):
|
media_group: List, bot_db):
|
||||||
sent_message = await message.bot.send_media_group(
|
sent_message = await message.bot.send_media_group(
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
media=media_group,
|
media=media_group,
|
||||||
@@ -245,7 +256,7 @@ async def send_media_group_message_to_private_chat(chat_id: int, message: types.
|
|||||||
return message_id
|
return message_id
|
||||||
|
|
||||||
|
|
||||||
async def send_media_group_to_channel(bot, chat_id: int, post_content: list[tuple[str]], post_text: str):
|
async def send_media_group_to_channel(bot, chat_id: int, post_content: List, post_text: str):
|
||||||
"""
|
"""
|
||||||
Отправляет медиа-группу с подписью к последнему файлу.
|
Отправляет медиа-группу с подписью к последнему файлу.
|
||||||
|
|
||||||
@@ -458,6 +469,9 @@ def delete_user_blacklist(user_id: int, bot_db):
|
|||||||
return bot_db.delete_user_blacklist(user_id=user_id)
|
return bot_db.delete_user_blacklist(user_id=user_id)
|
||||||
|
|
||||||
|
|
||||||
|
@track_time("check_username_and_full_name", "helper_func")
|
||||||
|
@track_errors("helper_func", "check_username_and_full_name")
|
||||||
|
@db_query_time("get_username_and_full_name", "users", "select")
|
||||||
def check_username_and_full_name(user_id: int, username: str, full_name: str, bot_db):
|
def check_username_and_full_name(user_id: int, username: str, full_name: str, bot_db):
|
||||||
username_db, full_name_db = bot_db.get_username_and_full_name(user_id=user_id)
|
username_db, full_name_db = bot_db.get_username_and_full_name(user_id=user_id)
|
||||||
return username != username_db or full_name != full_name_db
|
return username != username_db or full_name != full_name_db
|
||||||
@@ -479,6 +493,8 @@ def unban_notifier(self):
|
|||||||
self.bot.send_message(self.GROUP_FOR_MESSAGE, message)
|
self.bot.send_message(self.GROUP_FOR_MESSAGE, message)
|
||||||
|
|
||||||
|
|
||||||
|
@track_time("update_user_info", "helper_func")
|
||||||
|
@track_errors("helper_func", "update_user_info")
|
||||||
async def update_user_info(source: str, message: types.Message):
|
async def update_user_info(source: str, message: types.Message):
|
||||||
# Собираем данные
|
# Собираем данные
|
||||||
full_name = message.from_user.full_name
|
full_name = message.from_user.full_name
|
||||||
@@ -495,10 +511,12 @@ async def update_user_info(source: str, message: types.Message):
|
|||||||
if not BotDB.user_exists(user_id):
|
if not BotDB.user_exists(user_id):
|
||||||
BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, user_emoji, date,
|
BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, user_emoji, date,
|
||||||
date)
|
date)
|
||||||
|
metrics.record_db_query("add_new_user_in_db", 0.0, "users", "insert")
|
||||||
else:
|
else:
|
||||||
is_need_update = check_username_and_full_name(user_id, username, full_name, BotDB)
|
is_need_update = check_username_and_full_name(user_id, username, full_name, BotDB)
|
||||||
if is_need_update:
|
if is_need_update:
|
||||||
BotDB.update_username_and_full_name(user_id, username, full_name)
|
BotDB.update_username_and_full_name(user_id, username, full_name)
|
||||||
|
metrics.record_db_query("update_username_and_full_name", 0.0, "users", "update")
|
||||||
if source != 'voice':
|
if source != 'voice':
|
||||||
await message.answer(
|
await message.answer(
|
||||||
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name}")
|
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name}")
|
||||||
@@ -506,17 +524,25 @@ async def update_user_info(source: str, message: types.Message):
|
|||||||
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}. Новый эмодзи:{user_emoji}')
|
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}. Новый эмодзи:{user_emoji}')
|
||||||
sleep(1)
|
sleep(1)
|
||||||
BotDB.update_date_for_user(date, user_id)
|
BotDB.update_date_for_user(date, user_id)
|
||||||
|
metrics.record_db_query("update_date_for_user", 0.0, "users", "update")
|
||||||
|
|
||||||
|
|
||||||
|
@track_time("check_user_emoji", "helper_func")
|
||||||
|
@track_errors("helper_func", "check_user_emoji")
|
||||||
|
@db_query_time("check_emoji_for_user", "users", "select")
|
||||||
def check_user_emoji(message: types.Message):
|
def check_user_emoji(message: types.Message):
|
||||||
user_id = message.from_user.id
|
user_id = message.from_user.id
|
||||||
user_emoji = BotDB.check_emoji_for_user(user_id=user_id)
|
user_emoji = BotDB.check_emoji_for_user(user_id=user_id)
|
||||||
if user_emoji is None or user_emoji in ("Смайл еще не определен", "Эмоджи не определен", ""):
|
if user_emoji is None or user_emoji in ("Смайл еще не определен", "Эмоджи не определен", ""):
|
||||||
user_emoji = get_random_emoji()
|
user_emoji = get_random_emoji()
|
||||||
BotDB.update_emoji_for_user(user_id=user_id, emoji=user_emoji)
|
BotDB.update_emoji_for_user(user_id=user_id, emoji=user_emoji)
|
||||||
|
metrics.record_db_query("update_emoji_for_user", 0.0, "users", "update")
|
||||||
return user_emoji
|
return user_emoji
|
||||||
|
|
||||||
|
|
||||||
|
@track_time("get_random_emoji", "helper_func")
|
||||||
|
@track_errors("helper_func", "get_random_emoji")
|
||||||
|
@db_query_time("check_emoji", "users", "select")
|
||||||
def get_random_emoji():
|
def get_random_emoji():
|
||||||
attempts = 0
|
attempts = 0
|
||||||
while attempts < 100:
|
while attempts < 100:
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import html
|
import html
|
||||||
|
|
||||||
|
# Local imports - metrics
|
||||||
|
from .metrics import (
|
||||||
|
metrics,
|
||||||
|
track_time,
|
||||||
|
track_errors
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@track_time("get_message", "message_service")
|
||||||
|
@track_errors("message_service", "get_message")
|
||||||
def get_message(username: str, type_message: str):
|
def get_message(username: str, type_message: str):
|
||||||
constants = {
|
constants = {
|
||||||
'HELLO_MESSAGE': "Привет, username!👋🏼&Меня зовут Виби, я бот канала 'Влюбленный Бийск'❤🤖"
|
'HELLO_MESSAGE': "Привет, username!👋🏼&Меня зовут Виби, я бот канала 'Влюбленный Бийск'❤🤖"
|
||||||
|
|||||||
300
helper_bot/utils/metrics.py
Normal file
300
helper_bot/utils/metrics.py
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
"""
|
||||||
|
Metrics module for Telegram bot monitoring with Prometheus.
|
||||||
|
Provides predefined metrics for bot commands, errors, performance, and user activity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST
|
||||||
|
from prometheus_client.core import CollectorRegistry
|
||||||
|
import time
|
||||||
|
from functools import wraps
|
||||||
|
import asyncio
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
|
||||||
|
class BotMetrics:
|
||||||
|
"""Central class for managing all bot metrics."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.registry = CollectorRegistry()
|
||||||
|
|
||||||
|
# Bot commands counter
|
||||||
|
self.bot_commands_total = Counter(
|
||||||
|
'bot_commands_total',
|
||||||
|
'Total number of bot commands processed',
|
||||||
|
['command_type', 'handler_type', 'user_type'],
|
||||||
|
registry=self.registry
|
||||||
|
)
|
||||||
|
|
||||||
|
# Method execution time histogram
|
||||||
|
self.method_duration_seconds = Histogram(
|
||||||
|
'method_duration_seconds',
|
||||||
|
'Time spent executing methods',
|
||||||
|
['method_name', 'handler_type', 'status'],
|
||||||
|
buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
|
||||||
|
registry=self.registry
|
||||||
|
)
|
||||||
|
|
||||||
|
# Errors counter
|
||||||
|
self.errors_total = Counter(
|
||||||
|
'errors_total',
|
||||||
|
'Total number of errors',
|
||||||
|
['error_type', 'handler_type', 'method_name'],
|
||||||
|
registry=self.registry
|
||||||
|
)
|
||||||
|
|
||||||
|
# Active users gauge
|
||||||
|
self.active_users = Gauge(
|
||||||
|
'active_users',
|
||||||
|
'Number of currently active users',
|
||||||
|
['user_type'],
|
||||||
|
registry=self.registry
|
||||||
|
)
|
||||||
|
|
||||||
|
# Database query metrics
|
||||||
|
self.db_query_duration_seconds = Histogram(
|
||||||
|
'db_query_duration_seconds',
|
||||||
|
'Time spent executing database queries',
|
||||||
|
['query_type', 'table_name', 'operation'],
|
||||||
|
buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
|
||||||
|
registry=self.registry
|
||||||
|
)
|
||||||
|
|
||||||
|
# Message processing metrics
|
||||||
|
self.messages_processed_total = Counter(
|
||||||
|
'messages_processed_total',
|
||||||
|
'Total number of messages processed',
|
||||||
|
['message_type', 'chat_type', 'handler_type'],
|
||||||
|
registry=self.registry
|
||||||
|
)
|
||||||
|
|
||||||
|
# Middleware execution metrics
|
||||||
|
self.middleware_duration_seconds = Histogram(
|
||||||
|
'middleware_duration_seconds',
|
||||||
|
'Time spent in middleware execution',
|
||||||
|
['middleware_name', 'status'],
|
||||||
|
buckets=[0.01, 0.05, 0.1, 0.25, 0.5],
|
||||||
|
registry=self.registry
|
||||||
|
)
|
||||||
|
|
||||||
|
def record_command(self, command_type: str, handler_type: str = "unknown", user_type: str = "unknown"):
|
||||||
|
"""Record a bot command execution."""
|
||||||
|
self.bot_commands_total.labels(
|
||||||
|
command_type=command_type,
|
||||||
|
handler_type=handler_type,
|
||||||
|
user_type=user_type
|
||||||
|
).inc()
|
||||||
|
|
||||||
|
def record_error(self, error_type: str, handler_type: str = "unknown", method_name: str = "unknown"):
|
||||||
|
"""Record an error occurrence."""
|
||||||
|
self.errors_total.labels(
|
||||||
|
error_type=error_type,
|
||||||
|
handler_type=handler_type,
|
||||||
|
method_name=method_name
|
||||||
|
).inc()
|
||||||
|
|
||||||
|
def record_method_duration(self, method_name: str, duration: float, handler_type: str = "unknown", status: str = "success"):
|
||||||
|
"""Record method execution duration."""
|
||||||
|
self.method_duration_seconds.labels(
|
||||||
|
method_name=method_name,
|
||||||
|
handler_type=handler_type,
|
||||||
|
status=status
|
||||||
|
).observe(duration)
|
||||||
|
|
||||||
|
def set_active_users(self, count: int, user_type: str = "total"):
|
||||||
|
"""Set the number of active users."""
|
||||||
|
self.active_users.labels(user_type=user_type).set(count)
|
||||||
|
|
||||||
|
def record_db_query(self, query_type: str, duration: float, table_name: str = "unknown", operation: str = "unknown"):
|
||||||
|
"""Record database query duration."""
|
||||||
|
self.db_query_duration_seconds.labels(
|
||||||
|
query_type=query_type,
|
||||||
|
table_name=table_name,
|
||||||
|
operation=operation
|
||||||
|
).observe(duration)
|
||||||
|
|
||||||
|
def record_message(self, message_type: str, chat_type: str = "unknown", handler_type: str = "unknown"):
|
||||||
|
"""Record a processed message."""
|
||||||
|
self.messages_processed_total.labels(
|
||||||
|
message_type=message_type,
|
||||||
|
chat_type=chat_type,
|
||||||
|
handler_type=handler_type
|
||||||
|
).inc()
|
||||||
|
|
||||||
|
def record_middleware(self, middleware_name: str, duration: float, status: str = "success"):
|
||||||
|
"""Record middleware execution duration."""
|
||||||
|
self.middleware_duration_seconds.labels(
|
||||||
|
middleware_name=middleware_name,
|
||||||
|
status=status
|
||||||
|
).observe(duration)
|
||||||
|
|
||||||
|
def get_metrics(self) -> bytes:
|
||||||
|
"""Generate metrics in Prometheus format."""
|
||||||
|
return generate_latest(self.registry)
|
||||||
|
|
||||||
|
|
||||||
|
# Global metrics instance
|
||||||
|
metrics = BotMetrics()
|
||||||
|
|
||||||
|
|
||||||
|
# Decorators for easy metric collection
|
||||||
|
def track_time(method_name: str = None, handler_type: str = "unknown"):
|
||||||
|
"""Decorator to track execution time of functions."""
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
async def async_wrapper(*args, **kwargs):
|
||||||
|
start_time = time.time()
|
||||||
|
try:
|
||||||
|
result = await func(*args, **kwargs)
|
||||||
|
duration = time.time() - start_time
|
||||||
|
metrics.record_method_duration(
|
||||||
|
method_name or func.__name__,
|
||||||
|
duration,
|
||||||
|
handler_type,
|
||||||
|
"success"
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
duration = time.time() - start_time
|
||||||
|
metrics.record_method_duration(
|
||||||
|
method_name or func.__name__,
|
||||||
|
duration,
|
||||||
|
handler_type,
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|
metrics.record_error(
|
||||||
|
type(e).__name__,
|
||||||
|
handler_type,
|
||||||
|
method_name or func.__name__
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def sync_wrapper(*args, **kwargs):
|
||||||
|
start_time = time.time()
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
duration = time.time() - start_time
|
||||||
|
metrics.record_method_duration(
|
||||||
|
method_name or func.__name__,
|
||||||
|
duration,
|
||||||
|
handler_type,
|
||||||
|
"success"
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
duration = time.time() - start_time
|
||||||
|
metrics.record_method_duration(
|
||||||
|
method_name or func.__name__,
|
||||||
|
duration,
|
||||||
|
handler_type,
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|
metrics.record_error(
|
||||||
|
type(e).__name__,
|
||||||
|
handler_type,
|
||||||
|
method_name or func.__name__
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
if asyncio.iscoroutinefunction(func):
|
||||||
|
return async_wrapper
|
||||||
|
return sync_wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def track_errors(handler_type: str = "unknown", method_name: str = None):
|
||||||
|
"""Decorator to track errors in functions."""
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
async def async_wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
metrics.record_error(
|
||||||
|
type(e).__name__,
|
||||||
|
handler_type,
|
||||||
|
method_name or func.__name__
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def sync_wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
metrics.record_error(
|
||||||
|
type(e).__name__,
|
||||||
|
handler_type,
|
||||||
|
method_name or func.__name__
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
if asyncio.iscoroutinefunction(func):
|
||||||
|
return async_wrapper
|
||||||
|
return sync_wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def db_query_time(query_type: str = "unknown", table_name: str = "unknown", operation: str = "unknown"):
|
||||||
|
"""Decorator to track database query execution time."""
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
async def async_wrapper(*args, **kwargs):
|
||||||
|
start_time = time.time()
|
||||||
|
try:
|
||||||
|
result = await func(*args, **kwargs)
|
||||||
|
duration = time.time() - start_time
|
||||||
|
metrics.record_db_query(query_type, duration, table_name, operation)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
duration = time.time() - start_time
|
||||||
|
metrics.record_db_query(query_type, duration, table_name, operation)
|
||||||
|
metrics.record_error(
|
||||||
|
type(e).__name__,
|
||||||
|
"database",
|
||||||
|
func.__name__
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def sync_wrapper(*args, **kwargs):
|
||||||
|
start_time = time.time()
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
duration = time.time() - start_time
|
||||||
|
metrics.record_db_query(query_type, duration, table_name, operation)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
duration = time.time() - start_time
|
||||||
|
metrics.record_db_query(query_type, duration, table_name, operation)
|
||||||
|
metrics.record_error(
|
||||||
|
type(e).__name__,
|
||||||
|
"database",
|
||||||
|
func.__name__
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
if asyncio.iscoroutinefunction(func):
|
||||||
|
return async_wrapper
|
||||||
|
return sync_wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def track_middleware(middleware_name: str):
|
||||||
|
"""Context manager to track middleware execution time."""
|
||||||
|
start_time = time.time()
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
duration = time.time() - start_time
|
||||||
|
metrics.record_middleware(middleware_name, duration, "success")
|
||||||
|
except Exception as e:
|
||||||
|
duration = time.time() - start_time
|
||||||
|
metrics.record_middleware(middleware_name, duration, "error")
|
||||||
|
metrics.record_error(
|
||||||
|
type(e).__name__,
|
||||||
|
"middleware",
|
||||||
|
middleware_name
|
||||||
|
)
|
||||||
|
raise
|
||||||
201
helper_bot/utils/metrics_exporter.py
Normal file
201
helper_bot/utils/metrics_exporter.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
"""
|
||||||
|
Metrics exporter for Prometheus.
|
||||||
|
Provides HTTP endpoint for metrics collection and background metrics collection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from aiohttp import web
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from .metrics import metrics
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsExporter:
|
||||||
|
"""HTTP server for exposing Prometheus metrics."""
|
||||||
|
|
||||||
|
def __init__(self, host: str = "0.0.0.0", port: int = 8000):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.app = web.Application()
|
||||||
|
self.runner: Optional[web.AppRunner] = None
|
||||||
|
self.site: Optional[web.TCPSite] = None
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Setup routes
|
||||||
|
self.app.router.add_get('/metrics', self.metrics_handler)
|
||||||
|
self.app.router.add_get('/health', self.health_handler)
|
||||||
|
self.app.router.add_get('/', self.root_handler)
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""Start the metrics server."""
|
||||||
|
try:
|
||||||
|
self.runner = web.AppRunner(self.app)
|
||||||
|
await self.runner.setup()
|
||||||
|
|
||||||
|
self.site = web.TCPSite(self.runner, self.host, self.port)
|
||||||
|
await self.site.start()
|
||||||
|
|
||||||
|
self.logger.info(f"Metrics server started on {self.host}:{self.port}")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to start metrics server: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Stop the metrics server."""
|
||||||
|
if self.site:
|
||||||
|
await self.site.stop()
|
||||||
|
if self.runner:
|
||||||
|
await self.runner.cleanup()
|
||||||
|
self.logger.info("Metrics server stopped")
|
||||||
|
|
||||||
|
async def metrics_handler(self, request: web.Request) -> web.Response:
|
||||||
|
"""Handle /metrics endpoint for Prometheus."""
|
||||||
|
try:
|
||||||
|
# Log request for debugging
|
||||||
|
self.logger.info(f"Metrics request from {request.remote}: {request.headers.get('User-Agent', 'Unknown')}")
|
||||||
|
|
||||||
|
metrics_data = metrics.get_metrics()
|
||||||
|
self.logger.debug(f"Generated metrics: {len(metrics_data)} bytes")
|
||||||
|
|
||||||
|
return web.Response(
|
||||||
|
body=metrics_data,
|
||||||
|
content_type='text/plain; version=0.0.4'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error generating metrics: {e}")
|
||||||
|
return web.Response(
|
||||||
|
text=f"Error generating metrics: {e}",
|
||||||
|
status=500
|
||||||
|
)
|
||||||
|
|
||||||
|
async def health_handler(self, request: web.Request) -> web.Response:
|
||||||
|
"""Handle /health endpoint for health checks."""
|
||||||
|
return web.json_response({
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "telegram-bot-metrics"
|
||||||
|
})
|
||||||
|
|
||||||
|
async def root_handler(self, request: web.Request) -> web.Response:
|
||||||
|
"""Handle root endpoint with basic info."""
|
||||||
|
return web.json_response({
|
||||||
|
"service": "Telegram Bot Metrics Exporter",
|
||||||
|
"endpoints": {
|
||||||
|
"/metrics": "Prometheus metrics",
|
||||||
|
"/health": "Health check",
|
||||||
|
"/": "This info"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundMetricsCollector:
|
||||||
|
"""Background service for collecting periodic metrics."""
|
||||||
|
|
||||||
|
def __init__(self, db: Optional[Any] = None, interval: int = 60):
|
||||||
|
self.db = db
|
||||||
|
self.interval = interval
|
||||||
|
self.running = False
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""Start background metrics collection."""
|
||||||
|
self.running = True
|
||||||
|
self.logger.info("Background metrics collector started")
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
await self._collect_metrics()
|
||||||
|
await asyncio.sleep(self.interval)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error in background metrics collection: {e}")
|
||||||
|
await asyncio.sleep(self.interval)
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Stop background metrics collection."""
|
||||||
|
self.running = False
|
||||||
|
self.logger.info("Background metrics collector stopped")
|
||||||
|
|
||||||
|
async def _collect_metrics(self):
|
||||||
|
"""Collect periodic metrics."""
|
||||||
|
try:
|
||||||
|
# Collect active users count if database is available
|
||||||
|
if self.db:
|
||||||
|
await self._collect_user_metrics()
|
||||||
|
|
||||||
|
# Collect system metrics
|
||||||
|
await self._collect_system_metrics()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error collecting metrics: {e}")
|
||||||
|
|
||||||
|
async def _collect_user_metrics(self):
|
||||||
|
"""Collect user-related metrics from database."""
|
||||||
|
try:
|
||||||
|
if hasattr(self.db, 'fetch_one'):
|
||||||
|
# Try to get active users from database if it has async methods
|
||||||
|
try:
|
||||||
|
active_users_query = """
|
||||||
|
SELECT COUNT(DISTINCT user_id) as active_users
|
||||||
|
FROM our_users
|
||||||
|
WHERE date_added > datetime('now', '-1 day')
|
||||||
|
"""
|
||||||
|
result = await self.db.fetch_one(active_users_query)
|
||||||
|
if result:
|
||||||
|
metrics.set_active_users(result['active_users'], 'daily')
|
||||||
|
else:
|
||||||
|
metrics.set_active_users(0, 'daily')
|
||||||
|
except Exception as db_error:
|
||||||
|
self.logger.warning(f"Database query failed, using placeholder: {db_error}")
|
||||||
|
metrics.set_active_users(0, 'daily')
|
||||||
|
else:
|
||||||
|
# For now, set a placeholder value
|
||||||
|
metrics.set_active_users(0, 'daily')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error collecting user metrics: {e}")
|
||||||
|
metrics.set_active_users(0, 'daily')
|
||||||
|
|
||||||
|
async def _collect_system_metrics(self):
|
||||||
|
"""Collect system-level metrics."""
|
||||||
|
try:
|
||||||
|
# Example: collect memory usage, CPU usage, etc.
|
||||||
|
# This can be extended based on your needs
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error collecting system metrics: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsManager:
|
||||||
|
"""Main class for managing metrics collection and export."""
|
||||||
|
|
||||||
|
def __init__(self, host: str = "0.0.0.0", port: int = 8000, db: Optional[Any] = None):
|
||||||
|
self.exporter = MetricsExporter(host, port)
|
||||||
|
self.collector = BackgroundMetricsCollector(db)
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""Start metrics collection and export."""
|
||||||
|
try:
|
||||||
|
# Start metrics exporter
|
||||||
|
await self.exporter.start()
|
||||||
|
|
||||||
|
# Start background collector
|
||||||
|
asyncio.create_task(self.collector.start())
|
||||||
|
|
||||||
|
self.logger.info("Metrics manager started successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to start metrics manager: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Stop metrics collection and export."""
|
||||||
|
try:
|
||||||
|
await self.collector.stop()
|
||||||
|
await self.exporter.stop()
|
||||||
|
self.logger.info("Metrics manager stopped successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error stopping metrics manager: {e}")
|
||||||
|
raise
|
||||||
26
prometheus.yml
Normal file
26
prometheus.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
evaluation_interval: 15s
|
||||||
|
|
||||||
|
rule_files:
|
||||||
|
# - "first_rules.yml"
|
||||||
|
# - "second_rules.yml"
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'telegram-bot'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['telegram-bot:8000']
|
||||||
|
metrics_path: '/metrics'
|
||||||
|
scrape_interval: 10s
|
||||||
|
scrape_timeout: 10s
|
||||||
|
honor_labels: true
|
||||||
|
|
||||||
|
- job_name: 'prometheus'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['localhost:9090']
|
||||||
|
|
||||||
|
alerting:
|
||||||
|
alertmanagers:
|
||||||
|
- static_configs:
|
||||||
|
- targets:
|
||||||
|
# - alertmanager:9093
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
[project]
|
||||||
|
name = "telegram-helper-bot"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Telegram bot with monitoring and metrics"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
python_files = ["test_*.py"]
|
python_files = ["test_*.py"]
|
||||||
|
|||||||
13
requirements-dev.txt
Normal file
13
requirements-dev.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Development and testing dependencies
|
||||||
|
-r requirements.txt
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
pytest>=8.0.0
|
||||||
|
pytest-asyncio>=0.23.0
|
||||||
|
pytest-cov>=4.0.0
|
||||||
|
coverage>=7.0.0
|
||||||
|
|
||||||
|
# Development tools
|
||||||
|
black>=23.0.0
|
||||||
|
flake8>=6.0.0
|
||||||
|
mypy>=1.0.0
|
||||||
@@ -13,10 +13,9 @@ psutil~=6.1.0
|
|||||||
# Scheduling
|
# Scheduling
|
||||||
apscheduler~=3.10.4
|
apscheduler~=3.10.4
|
||||||
|
|
||||||
# Testing
|
# Metrics and monitoring
|
||||||
pytest==8.2.2
|
prometheus-client==0.19.0
|
||||||
pytest-asyncio==1.1.0
|
aiohttp==3.9.1
|
||||||
coverage==7.5.4
|
|
||||||
|
|
||||||
# Development tools
|
# Development tools
|
||||||
pluggy==1.5.0
|
pluggy==1.5.0
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from helper_bot.main import start_bot
|
|||||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||||
from helper_bot.server_monitor import ServerMonitor
|
from helper_bot.server_monitor import ServerMonitor
|
||||||
from helper_bot.utils.auto_unban_scheduler import get_auto_unban_scheduler
|
from helper_bot.utils.auto_unban_scheduler import get_auto_unban_scheduler
|
||||||
|
from helper_bot.utils.metrics_exporter import MetricsManager
|
||||||
|
|
||||||
|
|
||||||
async def start_monitoring(bdf, bot):
|
async def start_monitoring(bdf, bot):
|
||||||
@@ -46,6 +47,9 @@ async def main():
|
|||||||
auto_unban_scheduler.set_bot(monitor_bot)
|
auto_unban_scheduler.set_bot(monitor_bot)
|
||||||
auto_unban_scheduler.start_scheduler()
|
auto_unban_scheduler.start_scheduler()
|
||||||
|
|
||||||
|
# Инициализируем метрики
|
||||||
|
metrics_manager = MetricsManager(host="0.0.0.0", port=8000)
|
||||||
|
|
||||||
# Флаг для корректного завершения
|
# Флаг для корректного завершения
|
||||||
shutdown_event = asyncio.Event()
|
shutdown_event = asyncio.Event()
|
||||||
|
|
||||||
@@ -58,9 +62,10 @@ async def main():
|
|||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
signal.signal(signal.SIGTERM, signal_handler)
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
# Запускаем бота и мониторинг
|
# Запускаем бота, мониторинг и метрики
|
||||||
bot_task = asyncio.create_task(start_bot(bdf))
|
bot_task = asyncio.create_task(start_bot(bdf))
|
||||||
monitor_task = asyncio.create_task(monitor.monitor_loop())
|
monitor_task = asyncio.create_task(monitor.monitor_loop())
|
||||||
|
metrics_task = asyncio.create_task(metrics_manager.start())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Ждем сигнала завершения
|
# Ждем сигнала завершения
|
||||||
@@ -80,14 +85,18 @@ async def main():
|
|||||||
print("Останавливаем планировщик автоматического разбана...")
|
print("Останавливаем планировщик автоматического разбана...")
|
||||||
auto_unban_scheduler.stop_scheduler()
|
auto_unban_scheduler.stop_scheduler()
|
||||||
|
|
||||||
|
print("Останавливаем метрики...")
|
||||||
|
await metrics_manager.stop()
|
||||||
|
|
||||||
print("Останавливаем задачи...")
|
print("Останавливаем задачи...")
|
||||||
# Отменяем задачи
|
# Отменяем задачи
|
||||||
bot_task.cancel()
|
bot_task.cancel()
|
||||||
monitor_task.cancel()
|
monitor_task.cancel()
|
||||||
|
metrics_task.cancel()
|
||||||
|
|
||||||
# Ждем завершения задач
|
# Ждем завершения задач
|
||||||
try:
|
try:
|
||||||
await asyncio.gather(bot_task, monitor_task, return_exceptions=True)
|
await asyncio.gather(bot_task, monitor_task, metrics_task, return_exceptions=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка при остановке задач: {e}")
|
print(f"Ошибка при остановке задач: {e}")
|
||||||
|
|
||||||
@@ -97,4 +106,12 @@ async def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
asyncio.run(main())
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except AttributeError:
|
||||||
|
# Fallback for Python 3.6-3.7
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
try:
|
||||||
|
loop.run_until_complete(main())
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|||||||
92
run_metrics_only.py
Normal file
92
run_metrics_only.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Standalone metrics server for testing.
|
||||||
|
Run this to start just the metrics system without the bot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
from helper_bot.utils.metrics_exporter import MetricsManager
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsServer:
|
||||||
|
"""Standalone metrics server."""
|
||||||
|
|
||||||
|
def __init__(self, host: str = "0.0.0.0", port: int = 8000):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.metrics_manager = MetricsManager(host, port)
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""Start the metrics server."""
|
||||||
|
try:
|
||||||
|
await self.metrics_manager.start()
|
||||||
|
self.running = True
|
||||||
|
print(f"🚀 Metrics server started on {self.host}:{self.port}")
|
||||||
|
print(f"📊 Metrics endpoint: http://{self.host}:{self.port}/metrics")
|
||||||
|
print(f"🏥 Health check: http://{self.host}:{self.port}/health")
|
||||||
|
print(f"ℹ️ Info: http://{self.host}:{self.port}/")
|
||||||
|
print("\nPress Ctrl+C to stop the server")
|
||||||
|
|
||||||
|
# Keep the server running
|
||||||
|
while self.running:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error starting metrics server: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Stop the metrics server."""
|
||||||
|
if self.running:
|
||||||
|
self.running = False
|
||||||
|
await self.metrics_manager.stop()
|
||||||
|
print("\n🛑 Metrics server stopped")
|
||||||
|
|
||||||
|
def signal_handler(self, signum, frame):
|
||||||
|
"""Handle shutdown signals."""
|
||||||
|
print(f"\n📡 Received signal {signum}, shutting down...")
|
||||||
|
asyncio.create_task(self.stop())
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main function."""
|
||||||
|
# Parse command line arguments
|
||||||
|
host = "0.0.0.0"
|
||||||
|
port = 8000
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
host = sys.argv[1]
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
port = int(sys.argv[2])
|
||||||
|
|
||||||
|
# Create and start server
|
||||||
|
server = MetricsServer(host, port)
|
||||||
|
|
||||||
|
# Setup signal handlers
|
||||||
|
signal.signal(signal.SIGINT, server.signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, server.signal_handler)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await server.start()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n📡 Keyboard interrupt received")
|
||||||
|
finally:
|
||||||
|
await server.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("🔧 Starting standalone metrics server...")
|
||||||
|
print("Usage: python run_metrics_only.py [host] [port]")
|
||||||
|
print("Default: host=0.0.0.0, port=8000")
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n🛑 Server stopped by user")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Server error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
32
start_docker.sh
Executable file
32
start_docker.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🐍 Запуск Telegram Bot с Python 3.9 (стандартная версия)..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "🔧 Сборка Docker образа с Python 3.9..."
|
||||||
|
make build
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🚀 Запуск сервисов..."
|
||||||
|
make up
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🐍 Проверка версии Python в контейнере..."
|
||||||
|
make check-python
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📦 Проверка установленных пакетов..."
|
||||||
|
docker exec telegram-bot .venv/bin/pip list
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Сервисы успешно запущены!"
|
||||||
|
echo ""
|
||||||
|
echo "📝 Полезные команды:"
|
||||||
|
echo " Логи бота: make logs-bot"
|
||||||
|
echo " Статус: make status"
|
||||||
|
echo " Остановка: make stop"
|
||||||
|
echo " Перезапуск: make restart"
|
||||||
|
echo ""
|
||||||
|
echo "📊 Мониторинг:"
|
||||||
|
echo " Prometheus: http://localhost:9090"
|
||||||
|
echo " Grafana: http://localhost:3000 (admin/admin)"
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
|
|
||||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
|
||||||
from voice_bot.main import start_bot
|
|
||||||
|
|
||||||
bdf = get_global_instance()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
asyncio.run(start_bot(get_global_instance()))
|
|
||||||
Reference in New Issue
Block a user