Dev 6 #9

Merged
KerradKerridi merged 13 commits from dev-6 into master 2025-08-30 11:58:45 +00:00
37 changed files with 2177 additions and 175 deletions
Showing only changes of commit c68db87901 - Show all commits

View File

@@ -1,37 +1,73 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
*$py.class
*.so
*.egg-info/
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Git
.git/
.gitignore
# Byte-compiled / optimized / DLL files
**/__pycache__/
**/*.pyc
**/*.pyo
**/*.pyd
# Logs
logs/*.log
# Local settings
settings_example.ini
# Databases and runtime files
# Database
*.db
*.db-shm
*.db-wal
logs/
# Tests and artifacts
.coverage
# Tests
tests/
test_*.py
.pytest_cache/
htmlcov/
**/tests/
# Stickers and large assets (if not needed at runtime)
Stick/
# Documentation
*.md
docs/
# Docker
Dockerfile*
docker-compose*.yml
.dockerignore

42
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# Database files
/database/tg-bot-database.db
/database/tg-bot-database.db-shm
/database/tg-bot-database.db-wal
/database/tg-bot-database.db-wm
/database/test.db
/database/test.db-shm
/database/test.db-wal
@@ -10,7 +11,9 @@
/settings.ini
/myenv/
/venv/
/.idea/
/.venv/
# Logs
/logs/*.log
# Testing and coverage files
@@ -32,6 +35,7 @@ test.db
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
@@ -44,9 +48,43 @@ test.db
.Trashes
ehthumbs.db
Thumbs.db
# Documentation files
PERFORMANCE_IMPROVEMENTS.md
# PID files
*.pid
helper_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
View File

@@ -0,0 +1 @@
3.9.6

1
CHANGES_SUMMARY.md Normal file
View File

@@ -0,0 +1 @@

View File

@@ -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
View 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
View File

@@ -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:
@echo "Available commands:"
@echo " install - Install dependencies"
@echo " test - Run all tests"
@echo " test-db - Run database tests only"
@echo " test-bot - Run bot startup and handler tests only"
@echo " test-media - Run media handler tests only"
@echo " test-errors - Run error handling tests only"
@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"
help: ## Показать справку
@echo "🐍 Telegram Bot - Доступные команды (Python 3.9):"
@echo ""
@echo "🔧 Основные команды:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
@echo ""
@echo "📊 Мониторинг:"
@echo " Prometheus: http://localhost:9090"
@echo " Grafana: http://localhost:3000 (admin/admin)"
# Install dependencies
install:
python3 -m pip install -r requirements.txt
python3 -m pip install pytest-cov
build: ## Собрать все контейнеры с Python 3.9
docker-compose build
# Run all tests
test:
python3 -m pytest tests/ -v
up: ## Запустить все сервисы с Python 3.9
docker-compose up -d
# Run database tests only
test-db:
python3 -m pytest tests/test_db.py -v
down: ## Остановить все сервисы
docker-compose down
# Run bot tests only
test-bot:
python3 -m pytest tests/test_bot.py -v
logs: ## Показать логи всех сервисов
docker-compose logs -f
# Run media handler tests only
test-media:
python3 -m pytest tests/test_media_handlers.py -v
logs-bot: ## Показать логи бота
docker-compose logs -f telegram-bot
# Run error handling tests only
test-errors:
python3 -m pytest tests/test_error_handling.py -v
logs-prometheus: ## Показать логи Prometheus
docker-compose logs -f prometheus
# Run utils tests only
test-utils:
python3 -m pytest tests/test_utils.py -v
logs-grafana: ## Показать логи Grafana
docker-compose logs -f grafana
# Run keyboard and filter tests only
test-keyboards:
python3 -m pytest tests/test_keyboards_and_filters.py -v
restart: ## Перезапустить все сервисы (с пересборкой Python 3.9)
docker-compose down
docker-compose build
docker-compose up -d
# Test server monitoring module
test-monitor:
python3 tests/test_monitor.py
status: ## Показать статус контейнеров
docker-compose ps
# Test auto unban scheduler
test-auto-unban:
python3 -m pytest tests/test_auto_unban_scheduler.py -v
check-python: ## Проверить версию Python в контейнере
@echo "🐍 Проверяю версию Python в контейнере..."
@docker exec telegram-bot .venv/bin/python --version || echo "Контейнер не запущен"
# Test auto unban integration
test-auto-unban-integration:
python3 -m pytest tests/test_auto_unban_integration.py -v
test-compatibility: ## Тест совместимости с Python 3.8+
@echo "🐍 Тестирую совместимость с Python 3.8+..."
@python3 test_python38_compatibility.py
# Run tests with coverage
test-coverage:
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=term
clean: ## Очистить все контейнеры и образы Python 3.9
docker-compose down -v --rmi all
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
clean:
rm -rf htmlcov/
rm -f coverage.xml
rm -f .coverage
rm -f database/test.db
rm -f test.db
rm -f helper_bot.pid
rm -f voice_bot.pid
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
start: build up ## Собрать и запустить все сервисы с Python 3.9
@echo "🐍 Python 3.9 контейнер собран и запущен!"
@echo "📊 Prometheus: http://localhost:9090"
@echo "📈 Grafana: http://localhost:3000 (admin/admin)"
@echo "🤖 Бот запущен в контейнере с Python 3.9"
@echo "📝 Логи: make logs"
start-script: ## Запустить через скрипт start_docker.sh
@echo "🐍 Запуск через скрипт start_docker.sh..."
@./start_docker.sh
stop: down ## Остановить все сервисы
@echo "🛑 Все сервисы остановлены"

View File

@@ -1,3 +0,0 @@
# This file makes the root directory a Python package

View File

@@ -446,7 +446,7 @@ class AsyncBotDB:
if conn:
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
try:
@@ -484,7 +484,7 @@ class AsyncBotDB:
if conn:
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 постов."""
conn = None
try:
@@ -540,7 +540,7 @@ class AsyncBotDB:
if conn:
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
try:
@@ -626,7 +626,7 @@ class AsyncBotDB:
if conn:
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
try:
@@ -658,7 +658,7 @@ class AsyncBotDB:
if conn:
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
try:

View File

@@ -6,6 +6,14 @@ from concurrent.futures import ThreadPoolExecutor
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:
def __init__(self, current_dir, name):
@@ -138,6 +146,9 @@ class BotDB:
finally:
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,
language_code: str, emoji: str, date_added: str, date_changed: str):
"""
@@ -189,6 +200,9 @@ class BotDB:
finally:
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):
"""
Проверяет, существует ли пользователь в базе данных.
@@ -426,6 +440,9 @@ class BotDB:
finally:
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):
"""
Проверяет, получил ли пользователь стикеры.
@@ -459,6 +476,9 @@ class BotDB:
finally:
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):
"""
Обновляет информацию о получении стикеров пользователем.
@@ -623,6 +643,9 @@ class BotDB:
finally:
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):
"""
Добавляет новое сообщение пользователя в базу данных.
@@ -657,6 +680,9 @@ class BotDB:
finally:
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):
"""
Получает full_name и username пользователя по ID из базы
@@ -686,6 +712,9 @@ class BotDB:
finally:
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):
"""
Обновляет full_name и username пользователя
@@ -715,6 +744,9 @@ class BotDB:
finally:
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):
"""
#TODO: Не возвращается ошибка sqlite3. Error. Тест не перехватывает. Возвращается no such table: our_users
@@ -742,6 +774,9 @@ class BotDB:
finally:
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):
"""
Проверяет, есть ли уже такой emoji в таблице.
@@ -767,6 +802,9 @@ class BotDB:
finally:
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):
"""
Обновляет эмодзи для пользователя в базе если его ранее не было установлено
@@ -792,6 +830,9 @@ class BotDB:
finally:
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):
"""
Проверяет, есть ли уже у пользователя назначенный emoji.

66
docker-compose.yml Normal file
View 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

View 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

View 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": ""
}

View File

@@ -0,0 +1,8 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true

View 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

View File

@@ -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.types import TelegramObject

View File

@@ -1,5 +1,4 @@
import html
from tkinter import S
import traceback
from aiogram import Router

View File

@@ -1,14 +1,14 @@
"""Constants for group handlers"""
from typing import Final
from typing import Final, Dict
# FSM States
FSM_STATES: Final[dict[str, str]] = {
FSM_STATES: Final[Dict[str, str]] = {
"CHAT": "CHAT"
}
# Error messages
ERROR_MESSAGES: Final[dict[str, str]] = {
ERROR_MESSAGES: Final[Dict[str, str]] = {
"NO_REPLY_TO_MESSAGE": "Блять, выдели сообщение!",
"USER_NOT_FOUND": "Не могу найти кому ответить в базе, проебали сообщение."
}

View File

@@ -1,9 +1,9 @@
"""Constants for private handlers"""
from typing import Final
from typing import Final, Dict
# FSM States
FSM_STATES: Final[dict[str, str]] = {
FSM_STATES: Final[Dict[str, str]] = {
"START": "START",
"SUGGEST": "SUGGEST",
"PRE_CHAT": "PRE_CHAT",
@@ -11,7 +11,7 @@ FSM_STATES: Final[dict[str, str]] = {
}
# Button texts
BUTTON_TEXTS: Final[dict[str, str]] = {
BUTTON_TEXTS: Final[Dict[str, str]] = {
"SUGGEST_POST": "📢Предложить свой пост",
"SAY_GOODBYE": "👋🏼Сказать пока!",
"LEAVE_CHAT": "Выйти из чата",
@@ -21,7 +21,7 @@ BUTTON_TEXTS: Final[dict[str, str]] = {
}
# Error messages
ERROR_MESSAGES: Final[dict[str, str]] = {
ERROR_MESSAGES: Final[Dict[str, str]] = {
"UNSUPPORTED_CONTENT": (
'Я пока не умею работать с таким сообщением. '
'Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n'

View File

@@ -24,6 +24,14 @@ from helper_bot.utils.helper_func import (
check_user_emoji
)
# Local imports - metrics
from helper_bot.utils.metrics import (
metrics,
track_time,
track_errors,
db_query_time
)
# Local imports - modular components
from .constants import FSM_STATES, BUTTON_TEXTS, ERROR_MESSAGES
from .services import BotSettings, UserService, PostService, StickerService
@@ -91,16 +99,23 @@ class PrivateHandlers:
await message.answer('Я перезапущен!', reply_markup=markup, parse_mode='HTML')
@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):
"""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.ensure_user_exists(message)
await state.set_state(FSM_STATES["START"])
# Send sticker
# Send sticker with metrics
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)
hello_message = messages.get_message(get_first_name(message), 'HELLO_MESSAGE')
await message.answer(hello_message, reply_markup=markup, parse_mode='HTML')

View File

@@ -30,6 +30,14 @@ from helper_bot.utils.helper_func import (
)
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):
"""Protocol for database operations"""
@@ -65,13 +73,18 @@ class UserService:
self.db = db
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:
"""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")
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:
"""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
full_name = message.from_user.full_name
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")
if not self.db.user_exists(user_id):
# Record database operation
self.db.add_new_user_in_db(
user_id, first_name, full_name, username, is_bot, language_code,
"", current_date, current_date
)
metrics.record_db_query("add_new_user", 0.0, "users", "insert")
else:
is_need_update = check_username_and_full_name(user_id, username, full_name, self.db)
if is_need_update:
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_username = html.escape(username) if username else "Без никнейма"
@@ -100,9 +116,12 @@ class UserService:
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}')
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:
"""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)
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
)
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"""
first_name = get_first_name(message)
@@ -248,8 +267,10 @@ class StickerService:
def __init__(self, settings: BotSettings) -> None:
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:
"""Send random hello sticker"""
"""Send random hello sticker with metrics tracking"""
name_stick_hello = list(Path('Stick').rglob('Hello_*'))
if not name_stick_hello:
return
@@ -258,8 +279,10 @@ class StickerService:
await message.answer_sticker(random_stick_hello)
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:
"""Send random goodbye sticker"""
"""Send random goodbye sticker with metrics tracking"""
name_stick_bye = list(Path('Stick').rglob('Universal_*'))
if not name_stick_bye:
return

View File

@@ -1,6 +1,13 @@
from aiogram import types
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():
builder = InlineKeyboardBuilder()
@@ -16,6 +23,8 @@ def get_reply_keyboard_for_post():
return markup
@track_time("get_reply_keyboard", "keyboard_service")
@track_errors("keyboard_service", "get_reply_keyboard")
def get_reply_keyboard(BotDB, user_id):
builder = ReplyKeyboardBuilder()
builder.row(types.KeyboardButton(text="📢Предложить свой пост"))
@@ -49,7 +58,7 @@ def get_reply_keyboard_admin():
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

View File

@@ -9,6 +9,7 @@ from helper_bot.handlers.group import group_router
from helper_bot.handlers.private import private_router
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
from helper_bot.middlewares.metrics_middleware import MetricsMiddleware, ErrorMetricsMiddleware
async def start_bot(bdf):
@@ -19,6 +20,12 @@ async def start_bot(bdf):
), timeout=30.0) # Добавляем таймаут для предотвращения зависаний
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 для всех роутеров
dp.update.outer_middleware(DependenciesMiddleware())

View 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())

View 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

View File

@@ -3,6 +3,7 @@ import os
import random
from datetime import datetime, timedelta
from time import sleep
from typing import List, Dict, Any, Optional
try:
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 logs.custom_logger import logger
# Local imports - metrics
from .metrics import (
metrics,
track_time,
track_errors,
db_query_time
)
bdf = get_global_instance()
BotDB = bdf.get_db()
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))
@track_time("get_first_name", "helper_func")
@track_errors("helper_func", "get_first_name")
def get_first_name(message: types.Message) -> str:
"""
Безопасно получает и экранирует имя пользователя для использования в 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,
media_group: list[InputMediaPhoto], bot_db):
media_group: List, bot_db):
sent_message = await message.bot.send_media_group(
chat_id=chat_id,
media=media_group,
@@ -245,7 +256,7 @@ async def send_media_group_message_to_private_chat(chat_id: int, message: types.
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)
@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):
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
@@ -479,6 +493,8 @@ def unban_notifier(self):
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):
# Собираем данные
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):
BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, user_emoji, date,
date)
metrics.record_db_query("add_new_user_in_db", 0.0, "users", "insert")
else:
is_need_update = check_username_and_full_name(user_id, username, full_name, BotDB)
if is_need_update:
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':
await message.answer(
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}')
sleep(1)
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):
user_id = message.from_user.id
user_emoji = BotDB.check_emoji_for_user(user_id=user_id)
if user_emoji is None or user_emoji in ("Смайл еще не определен", "Эмоджи не определен", ""):
user_emoji = get_random_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
@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():
attempts = 0
while attempts < 100:

View File

@@ -1,6 +1,15 @@
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):
constants = {
'HELLO_MESSAGE': "Привет, username!👋🏼&Меня зовут Виби, я бот канала 'Влюбленный Бийск'❤🤖"

300
helper_bot/utils/metrics.py Normal file
View 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

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

View File

@@ -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]
testpaths = ["tests"]
python_files = ["test_*.py"]

13
requirements-dev.txt Normal file
View 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

View File

@@ -13,10 +13,9 @@ psutil~=6.1.0
# Scheduling
apscheduler~=3.10.4
# Testing
pytest==8.2.2
pytest-asyncio==1.1.0
coverage==7.5.4
# Metrics and monitoring
prometheus-client==0.19.0
aiohttp==3.9.1
# Development tools
pluggy==1.5.0

View File

@@ -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.server_monitor import ServerMonitor
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):
@@ -46,6 +47,9 @@ async def main():
auto_unban_scheduler.set_bot(monitor_bot)
auto_unban_scheduler.start_scheduler()
# Инициализируем метрики
metrics_manager = MetricsManager(host="0.0.0.0", port=8000)
# Флаг для корректного завершения
shutdown_event = asyncio.Event()
@@ -58,9 +62,10 @@ async def main():
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Запускаем бота и мониторинг
# Запускаем бота, мониторинг и метрики
bot_task = asyncio.create_task(start_bot(bdf))
monitor_task = asyncio.create_task(monitor.monitor_loop())
metrics_task = asyncio.create_task(metrics_manager.start())
try:
# Ждем сигнала завершения
@@ -80,14 +85,18 @@ async def main():
print("Останавливаем планировщик автоматического разбана...")
auto_unban_scheduler.stop_scheduler()
print("Останавливаем метрики...")
await metrics_manager.stop()
print("Останавливаем задачи...")
# Отменяем задачи
bot_task.cancel()
monitor_task.cancel()
metrics_task.cancel()
# Ждем завершения задач
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:
print(f"Ошибка при остановке задач: {e}")
@@ -97,4 +106,12 @@ async def main():
if __name__ == '__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
View 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
View 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)"

View File

@@ -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()))