Remove .env_example file and implement MetricsUpdater service for enhanced metrics tracking. Update bot.py to start and stop metrics updater, and improve database connection handling in CRUD operations with metrics tracking. Update README with details on metrics issues and fixes.

This commit is contained in:
2025-09-08 23:18:55 +03:00
parent 596a2fa813
commit 23c30a78e2
11 changed files with 744 additions and 49 deletions

189
README.md
View File

@@ -1300,6 +1300,195 @@ ports:
- Использовать reverse proxy с аутентификацией - Использовать reverse proxy с аутентификацией
- Настроить TLS для HTTPS - Настроить TLS для HTTPS
## 🔧 Исправление проблем с метриками
### 🔍 Найденные проблемы
В дашбордах Grafana не отображались следующие метрики:
- Database Connections - Active
- Database Performance - Query Duration
- Active Questions
- Active Users
- Answers per Minute
- Live Activity - Active Users
### 🛠️ Внесенные исправления
#### 1. Создан сервис MetricsUpdater (`services/infrastructure/metrics_updater.py`)
**Исправление циклической зависимости**: Первоначально возникла циклическая зависимость между `metrics_updater.py` и `dependencies.py`. Проблема была решена путем:
- Удаления импорта `get_database_service` из `dependencies`
- Передачи пути к БД напрямую в конструктор `MetricsUpdater`
- Создания собственного экземпляра `DatabaseService` внутри `MetricsUpdater`
**Исправление использования логгера**: Первоначально использовался `loguru` напрямую, но в проекте уже есть настроенная система логирования. Исправлено:
- Заменен `from loguru import logger` на `from .logger import get_logger`
- Используется `self.logger = get_logger(__name__)` в конструкторе
- Все вызовы `logger` заменены на `self.logger`
**Проблема**: Методы `set_active_users()` и `set_active_questions()` были определены в `MetricsService`, но нигде не вызывались.
**Решение**: Создан сервис `MetricsUpdater`, который:
- Периодически обновляет количество активных пользователей (за последние 24 часа)
- Периодически обновляет количество активных вопросов (статус pending/processing)
- Обновляет метрики соединений с БД
- Запускается автоматически при старте бота
#### 2. Создан декоратор для метрик БД (`services/infrastructure/db_metrics_decorator.py`)
**Проблема**: Методы `record_db_connection()` и `record_db_query()` были определены, но не интегрированы в код БД.
**Решение**: Создан декоратор `track_db_operation`, который:
- Автоматически записывает время выполнения операций БД
- Отслеживает успешные и неудачные операции
- Записывает метрики соединений с БД
- Использует существующую систему логирования проекта
**Интеграция декораторов**: Для избежания циклических зависимостей создан патч `crud_metrics_patch.py`:
- Применяет декораторы к CRUD операциям после их импорта
- Автоматически активируется при импорте модуля
- Покрывает основные операции: INSERT, SELECT, UPDATE для users и questions
- **Исправлено**: Убрано применение `track_db_connection` к `@asynccontextmanager` методам (ошибка `__aenter__`)
#### 3. Обновлен bot.py
**Изменения**:
- Добавлен запуск `MetricsUpdater` при старте бота
- Добавлена остановка `MetricsUpdater` при завершении работы
- Передача пути к БД в `MetricsUpdater`: `config.DATABASE_PATH`
- Интервал обновления метрик: 30 секунд
#### 4. Обновлен __init__.py
**Изменения**:
- Добавлен экспорт новых сервисов и декораторов
- Обновлен список `__all__`
### 📊 Ожидаемые результаты
После внесения исправлений в дашбордах Grafana должны отображаться:
#### AnonBot Overview:
- ✅ **Active Users** - количество активных пользователей за 24 часа
- ✅ **Active Questions** - количество активных вопросов (pending/processing)
- ✅ **Live Activity - Active Users** - то же значение, что и Active Users
- ✅ **Answers per Minute** - скорость отправки ответов
#### Performance AnonBot:
- ✅ **Database Connections - Active** - количество активных соединений с БД
- ✅ **Database Performance - Query Duration** - время выполнения запросов к БД
#### Server Monitoring:
- ✅ **AnonBot System Health** - активные пользователи
- ✅ **AnonBot Active Questions** - активные вопросы
- ✅ **AnonBot Database Connections** - соединения с БД
### 🚀 Развертывание исправлений
1. **Перезапустите AnonBot**:
```bash
docker-compose restart anon-bot
```
2. **Проверьте логи**:
```bash
docker-compose logs -f anon-bot
```
Должны появиться сообщения:
```
📊 Запуск обновления метрик...
📊 MetricsUpdater запущен с интервалом 30 секунд
```
3. **Проверьте метрики**:
```bash
curl http://localhost:8081/metrics | grep anon_bot_active
```
4. **Проверьте в Grafana**:
- Откройте дашборды AnonBot
- Дождитесь обновления данных (до 30 секунд)
- Проверьте отображение метрик
### 🔧 Дополнительные настройки
#### Изменение интервала обновления метрик
В файле `bot.py` можно изменить интервал обновления:
```python
# Текущий интервал: 30 секунд
await start_metrics_updater(update_interval=30)
# Для более частого обновления (например, 10 секунд):
await start_metrics_updater(update_interval=10)
```
#### Добавление метрик БД в CRUD операции
Для автоматического сбора метрик БД в CRUD операциях добавьте декоратор:
```python
from services.infrastructure import track_db_operation
@track_db_operation("SELECT", "users")
async def get_user(self, user_id: int):
# код метода
pass
```
### 📈 Мониторинг
После внесения исправлений рекомендуется настроить алерты:
1. **Низкая активность пользователей**: `anon_bot_active_users < 1`
2. **Много активных вопросов**: `anon_bot_active_questions > 100`
3. **Проблемы с БД**: `anon_bot_db_connections_active == 0`
4. **Высокое время ответа БД**: `histogram_quantile(0.95, rate(anon_bot_db_query_duration_seconds_bucket[5m])) > 1`
### 🐛 Troubleshooting
#### Ошибка циклической зависимости
**Проблема**: `ImportError: cannot import name 'get_database_service' from partially initialized module 'dependencies'`
**Решение**: Проблема была исправлена в версии 2.0 исправлений:
- Удален импорт `get_database_service` из `metrics_updater.py`
- Добавлен параметр `db_path` в конструктор `MetricsUpdater`
- `DatabaseService` создается внутри `MetricsUpdater` с переданным путем к БД
#### Метрики не обновляются
1. Проверьте логи AnonBot:
```bash
docker-compose logs anon-bot | grep -i metrics
```
2. Проверьте доступность эндпоинта:
```bash
curl http://localhost:8081/metrics
```
3. Проверьте конфигурацию Prometheus:
```bash
curl http://localhost:9090/api/v1/targets | grep anon-bot
```
#### Ошибки в логах
Если появляются ошибки типа "Database connection failed", проверьте:
- Доступность базы данных
- Правильность пути к БД в конфигурации
- Права доступа к файлу БД
#### Нулевые значения в дашбордах
Если метрики отображаются, но имеют нулевые значения:
- Убедитесь, что в БД есть данные (пользователи, вопросы)
- Проверьте SQL запросы в `MetricsUpdater`
- Увеличьте интервал обновления для накопления данных
## 🐳 Docker ## 🐳 Docker
### Сборка образа ### Сборка образа

9
bot.py
View File

@@ -13,6 +13,7 @@ from loader import loader
from services.infrastructure.http_server import start_http_server, stop_http_server from services.infrastructure.http_server import start_http_server, stop_http_server
from services.infrastructure.logger import get_logger from services.infrastructure.logger import get_logger
from services.infrastructure.pid_manager import get_pid_manager, cleanup_pid_file from services.infrastructure.pid_manager import get_pid_manager, cleanup_pid_file
from services.infrastructure.metrics_updater import start_metrics_updater, stop_metrics_updater
from config.constants import DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT from config.constants import DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT
# Настройка логирования # Настройка логирования
@@ -42,6 +43,10 @@ async def main():
logger.info("🌐 Запуск HTTP сервера для метрик...") logger.info("🌐 Запуск HTTP сервера для метрик...")
http_runner = await start_http_server(host=DEFAULT_HTTP_HOST, port=DEFAULT_HTTP_PORT) http_runner = await start_http_server(host=DEFAULT_HTTP_HOST, port=DEFAULT_HTTP_PORT)
# Запускаем обновление метрик
logger.info("📊 Запуск обновления метрик...")
await start_metrics_updater(update_interval=30, db_path=config.DATABASE_PATH)
# Запускаем бота # Запускаем бота
await loader.start_polling() await loader.start_polling()
@@ -51,6 +56,10 @@ async def main():
logger.error(f"💥 Критическая ошибка: {e}") logger.error(f"💥 Критическая ошибка: {e}")
raise raise
finally: finally:
# Останавливаем обновление метрик
logger.info("📊 Остановка обновления метрик...")
await stop_metrics_updater()
# Останавливаем HTTP сервер # Останавливаем HTTP сервер
if http_runner: if http_runner:
logger.info("🛑 Остановка HTTP сервера...") logger.info("🛑 Остановка HTTP сервера...")

View File

@@ -14,9 +14,7 @@ from models.question import Question, QuestionStatus
from models.user import User from models.user import User
from models.user_block import UserBlock from models.user_block import UserBlock
from models.user_settings import UserSettings from models.user_settings import UserSettings
from services.infrastructure.logger import get_logger from services.infrastructure.db_metrics_decorator import track_db_operation
logger = get_logger(__name__)
class ConnectionPool: class ConnectionPool:
@@ -43,30 +41,79 @@ class ConnectionPool:
await conn.execute("PRAGMA temp_store=MEMORY") await conn.execute("PRAGMA temp_store=MEMORY")
return conn return conn
async def _is_connection_valid(self, conn) -> bool:
"""Проверка валидности соединения"""
try:
if conn is None:
return False
# Выполняем простой запрос для проверки соединения
cursor = await conn.execute("SELECT 1")
await cursor.fetchone()
return True
except Exception:
return False
async def get_connection(self): async def get_connection(self):
"""Получение соединения из пула""" """Получение соединения из пула"""
try: try:
# Пытаемся получить соединение из пула # Пытаемся получить соединение из пула
return self._pool.get_nowait() conn = self._pool.get_nowait()
# Проверяем, что соединение еще активно
if await self._is_connection_valid(conn):
return conn
else:
# Соединение неактивно, закрываем его и создаем новое
await conn.close()
async with self._lock:
self._created_connections -= 1
except asyncio.QueueEmpty: except asyncio.QueueEmpty:
# Если пул пуст, создаем новое соединение pass
# Если пул пуст или соединение неактивно, создаем новое
async with self._lock: async with self._lock:
if self._created_connections < self.pool_size: if self._created_connections < self.pool_size:
self._created_connections += 1 self._created_connections += 1
return await self._create_connection() return await self._create_connection()
else: else:
# Ждем освобождения соединения # Ждем освобождения соединения из пула
return await self._pool.get() conn = await self._pool.get()
# Проверяем валидность полученного соединения
if await self._is_connection_valid(conn):
return conn
else:
# Соединение неактивно, закрываем и создаем новое
await conn.close()
async with self._lock:
self._created_connections -= 1
return await self._create_connection()
async def return_connection(self, conn): async def return_connection(self, conn):
"""Возврат соединения в пул""" """Возврат соединения в пул"""
if conn is None:
return
try: try:
# Проверяем валидность соединения перед возвратом в пул
if await self._is_connection_valid(conn):
self._pool.put_nowait(conn) self._pool.put_nowait(conn)
else:
# Соединение неактивно, закрываем его
await conn.close()
async with self._lock:
self._created_connections -= 1
except asyncio.QueueFull: except asyncio.QueueFull:
# Если пул полон, закрываем соединение # Если пул полон, закрываем соединение
await conn.close() await conn.close()
async with self._lock: async with self._lock:
self._created_connections -= 1 self._created_connections -= 1
except Exception as e:
# В случае любой ошибки, закрываем соединение
try:
await conn.close()
except:
pass
async with self._lock:
self._created_connections -= 1
async def close_all(self): async def close_all(self):
"""Закрытие всех соединений""" """Закрытие всех соединений"""
@@ -75,6 +122,15 @@ class ConnectionPool:
await conn.close() await conn.close()
self._created_connections = 0 self._created_connections = 0
def get_pool_stats(self) -> dict:
"""Получение статистики пула соединений"""
return {
"pool_size": self.pool_size,
"created_connections": self._created_connections,
"available_connections": self._pool.qsize(),
"utilization_percent": (self._created_connections / self.pool_size) * 100 if self.pool_size > 0 else 0
}
# Глобальный пул соединений # Глобальный пул соединений
_connection_pools = {} _connection_pools = {}
@@ -90,9 +146,10 @@ def get_connection_pool(db_path: str, pool_size: int = DEFAULT_CONNECTION_POOL_S
class BaseCRUD: class BaseCRUD:
"""Базовый класс для CRUD операций""" """Базовый класс для CRUD операций"""
def __init__(self, db_path: str): def __init__(self, db_path: str, logger=None):
self.db_path = db_path self.db_path = db_path
self.pool = get_connection_pool(db_path) self.pool = get_connection_pool(db_path)
self.logger = logger
@asynccontextmanager @asynccontextmanager
async def get_connection(self): async def get_connection(self):
@@ -116,9 +173,11 @@ class BaseCRUD:
class UserCRUD(BaseCRUD): class UserCRUD(BaseCRUD):
"""CRUD операции для пользователей""" """CRUD операции для пользователей"""
@track_db_operation("INSERT", "users")
async def create(self, user: User) -> User: async def create(self, user: User) -> User:
"""Создание нового пользователя""" """Создание нового пользователя"""
logger.info(f"👤 Создание пользователя: {user.telegram_id} ({user.first_name})") if self.logger:
self.logger.info(f"👤 Создание пользователя: {user.telegram_id} ({user.first_name})")
async with self.get_connection() as conn: async with self.get_connection() as conn:
cursor = await conn.execute(""" cursor = await conn.execute("""
INSERT INTO users INSERT INTO users
@@ -135,7 +194,8 @@ class UserCRUD(BaseCRUD):
)) ))
user.id = cursor.lastrowid user.id = cursor.lastrowid
await conn.commit() await conn.commit()
logger.info(f"✅ Пользователь создан с ID: {user.id}") if self.logger:
self.logger.info(f"✅ Пользователь создан с ID: {user.id}")
return user return user
async def create_batch(self, users: List[User]) -> List[User]: async def create_batch(self, users: List[User]) -> List[User]:
@@ -143,7 +203,8 @@ class UserCRUD(BaseCRUD):
if not users: if not users:
return [] return []
logger.info(f"📦 Создание {len(users)} пользователей batch операцией") if self.logger:
self.logger.info(f"📦 Создание {len(users)} пользователей batch операцией")
async with self.get_connection() as conn: async with self.get_connection() as conn:
try: try:
# Подготавливаем данные для batch вставки # Подготавливаем данные для batch вставки
@@ -172,14 +233,17 @@ class UserCRUD(BaseCRUD):
user.id = first_id + i user.id = first_id + i
await conn.commit() await conn.commit()
logger.info(f"✅ Создано {len(users)} пользователей batch операцией") if self.logger:
self.logger.info(f"✅ Создано {len(users)} пользователей batch операцией")
return users return users
except Exception as e: except Exception as e:
await conn.rollback() await conn.rollback()
logger.error(f"❌ Ошибка при batch создании пользователей: {e}") if self.logger:
self.logger.error(f"❌ Ошибка при batch создании пользователей: {e}")
raise raise
@track_db_operation("SELECT", "users")
async def get_by_telegram_id(self, telegram_id: int) -> Optional[User]: async def get_by_telegram_id(self, telegram_id: int) -> Optional[User]:
"""Получение пользователя по Telegram ID""" """Получение пользователя по Telegram ID"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -202,6 +266,7 @@ class UserCRUD(BaseCRUD):
return self._row_to_user(row) return self._row_to_user(row)
return None return None
@track_db_operation("UPDATE", "users")
async def update(self, user: User) -> User: async def update(self, user: User) -> User:
"""Обновление пользователя""" """Обновление пользователя"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -221,6 +286,7 @@ class UserCRUD(BaseCRUD):
await conn.commit() await conn.commit()
return user return user
@track_db_operation("DELETE", "users")
async def delete(self, telegram_id: int) -> bool: async def delete(self, telegram_id: int) -> bool:
"""Удаление пользователя""" """Удаление пользователя"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -230,6 +296,7 @@ class UserCRUD(BaseCRUD):
await conn.commit() await conn.commit()
return cursor.rowcount > 0 return cursor.rowcount > 0
@track_db_operation("SELECT", "all_users")
async def get_all(self, limit: int = 100, offset: int = 0) -> List[User]: async def get_all(self, limit: int = 100, offset: int = 0) -> List[User]:
"""Получение всех пользователей""" """Получение всех пользователей"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -241,6 +308,7 @@ class UserCRUD(BaseCRUD):
rows = await cursor.fetchall() rows = await cursor.fetchall()
return [self._row_to_user(row) for row in rows] return [self._row_to_user(row) for row in rows]
@track_db_operation("SELECT", "all_users_cursor")
async def get_all_users_cursor( async def get_all_users_cursor(
self, self,
last_id: int, last_id: int,
@@ -271,6 +339,7 @@ class UserCRUD(BaseCRUD):
rows = await cursor.fetchall() rows = await cursor.fetchall()
return [self._row_to_user(row) for row in rows] return [self._row_to_user(row) for row in rows]
@track_db_operation("SELECT", "all_users_asc")
async def get_all_users_asc(self, limit: int = 100, offset: int = 0) -> List[User]: async def get_all_users_asc(self, limit: int = 100, offset: int = 0) -> List[User]:
"""Получение всех пользователей в порядке возрастания""" """Получение всех пользователей в порядке возрастания"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -282,6 +351,7 @@ class UserCRUD(BaseCRUD):
rows = await cursor.fetchall() rows = await cursor.fetchall()
return [self._row_to_user(row) for row in rows] return [self._row_to_user(row) for row in rows]
@track_db_operation("SELECT", "stats")
async def get_stats(self) -> Dict[str, Any]: async def get_stats(self) -> Dict[str, Any]:
"""Получение статистики пользователей""" """Получение статистики пользователей"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -327,9 +397,11 @@ class UserCRUD(BaseCRUD):
class QuestionCRUD(BaseCRUD): class QuestionCRUD(BaseCRUD):
"""CRUD операции для вопросов""" """CRUD операции для вопросов"""
@track_db_operation("INSERT", "questions")
async def create(self, question: Question) -> Question: async def create(self, question: Question) -> Question:
"""Создание нового вопроса""" """Создание нового вопроса"""
logger.info(f"❓ Создание вопроса от {question.from_user_id} к {question.to_user_id}") if self.logger:
self.logger.info(f"❓ Создание вопроса от {question.from_user_id} к {question.to_user_id}")
async with self.get_connection() as conn: async with self.get_connection() as conn:
# Вычисляем user_question_number для получателя # Вычисляем user_question_number для получателя
if question.user_question_number is None: if question.user_question_number is None:
@@ -355,7 +427,8 @@ class QuestionCRUD(BaseCRUD):
)) ))
question.id = cursor.lastrowid question.id = cursor.lastrowid
await conn.commit() await conn.commit()
logger.info(f"✅ Вопрос создан с ID: {question.id}, номер для пользователя: {question.user_question_number}") if self.logger:
self.logger.info(f"✅ Вопрос создан с ID: {question.id}, номер для пользователя: {question.user_question_number}")
return question return question
async def create_batch(self, questions: List[Question]) -> List[Question]: async def create_batch(self, questions: List[Question]) -> List[Question]:
@@ -363,7 +436,8 @@ class QuestionCRUD(BaseCRUD):
if not questions: if not questions:
return [] return []
logger.info(f"📦 Создание {len(questions)} вопросов batch операцией") if self.logger:
self.logger.info(f"📦 Создание {len(questions)} вопросов batch операцией")
async with self.get_connection() as conn: async with self.get_connection() as conn:
try: try:
# Группируем вопросы по получателям для вычисления user_question_number # Группируем вопросы по получателям для вычисления user_question_number
@@ -412,14 +486,17 @@ class QuestionCRUD(BaseCRUD):
question.id = first_id + i question.id = first_id + i
await conn.commit() await conn.commit()
logger.info(f"✅ Создано {len(questions)} вопросов batch операцией") if self.logger:
self.logger.info(f"✅ Создано {len(questions)} вопросов batch операцией")
return questions return questions
except Exception as e: except Exception as e:
await conn.rollback() await conn.rollback()
logger.error(f"❌ Ошибка при batch создании вопросов: {e}") if self.logger:
self.logger.error(f"❌ Ошибка при batch создании вопросов: {e}")
raise raise
@track_db_operation("SELECT", "questions")
async def get_by_id(self, question_id: int) -> Optional[Question]: async def get_by_id(self, question_id: int) -> Optional[Question]:
"""Получение вопроса по ID""" """Получение вопроса по ID"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -435,6 +512,7 @@ class QuestionCRUD(BaseCRUD):
return self._row_to_question(row) return self._row_to_question(row)
return None return None
@track_db_operation("SELECT", "questions")
async def get_by_to_user(self, to_user_id: int, status: Optional[QuestionStatus] = None, async def get_by_to_user(self, to_user_id: int, status: Optional[QuestionStatus] = None,
limit: int = 50, offset: int = 0) -> List[Question]: limit: int = 50, offset: int = 0) -> List[Question]:
"""Получение вопросов для пользователя (оптимизированная версия с JOIN)""" """Получение вопросов для пользователя (оптимизированная версия с JOIN)"""
@@ -460,6 +538,7 @@ class QuestionCRUD(BaseCRUD):
rows = await cursor.fetchall() rows = await cursor.fetchall()
return [self._row_to_question(row) for row in rows] return [self._row_to_question(row) for row in rows]
@track_db_operation("SELECT", "questions_with_authors")
async def get_by_to_user_with_authors(self, to_user_id: int, status: Optional[QuestionStatus] = None, async def get_by_to_user_with_authors(self, to_user_id: int, status: Optional[QuestionStatus] = None,
limit: int = 50, offset: int = 0) -> List[Tuple[Question, Optional[User]]]: limit: int = 50, offset: int = 0) -> List[Tuple[Question, Optional[User]]]:
"""Получение вопросов для пользователя с информацией об авторах (оптимизированный запрос)""" """Получение вопросов для пользователя с информацией об авторах (оптимизированный запрос)"""
@@ -526,6 +605,7 @@ class QuestionCRUD(BaseCRUD):
continue continue
return result return result
@track_db_operation("SELECT", "questions_cursor")
async def get_by_to_user_cursor( async def get_by_to_user_cursor(
self, self,
to_user_id: int, to_user_id: int,
@@ -567,6 +647,7 @@ class QuestionCRUD(BaseCRUD):
rows = await cursor.fetchall() rows = await cursor.fetchall()
return [self._row_to_question(row) for row in rows] return [self._row_to_question(row) for row in rows]
@track_db_operation("SELECT", "questions_asc")
async def get_by_to_user_asc( async def get_by_to_user_asc(
self, self,
to_user_id: int, to_user_id: int,
@@ -597,9 +678,11 @@ class QuestionCRUD(BaseCRUD):
rows = await cursor.fetchall() rows = await cursor.fetchall()
return [self._row_to_question(row) for row in rows] return [self._row_to_question(row) for row in rows]
@track_db_operation("UPDATE", "questions")
async def update(self, question: Question) -> Question: async def update(self, question: Question) -> Question:
"""Обновление вопроса""" """Обновление вопроса"""
logger.info(f"📝 Обновление вопроса {question.id} (статус: {question.status.value})") if self.logger:
self.logger.info(f"📝 Обновление вопроса {question.id} (статус: {question.status.value})")
async with self.get_connection() as conn: async with self.get_connection() as conn:
# Если вопрос помечается как удаленный, нужно пересчитать номера # Если вопрос помечается как удаленный, нужно пересчитать номера
if question.status.value == 'deleted': if question.status.value == 'deleted':
@@ -638,7 +721,8 @@ class QuestionCRUD(BaseCRUD):
AND id != ? AND id != ?
""", (to_user_id, deleted_number, question.id)) """, (to_user_id, deleted_number, question.id))
logger.info(f"🗑️ Вопрос {question.id} помечен как удаленный, пересчитаны номера для пользователя {to_user_id}") if self.logger:
self.logger.info(f"🗑️ Вопрос {question.id} помечен как удаленный, пересчитаны номера для пользователя {to_user_id}")
else: else:
# Обычное обновление # Обычное обновление
await conn.execute(""" await conn.execute("""
@@ -663,9 +747,11 @@ class QuestionCRUD(BaseCRUD):
)) ))
await conn.commit() await conn.commit()
logger.info(f"✅ Вопрос {question.id} обновлен") if self.logger:
self.logger.info(f"✅ Вопрос {question.id} обновлен")
return question return question
@track_db_operation("DELETE", "questions")
async def delete(self, question_id: int) -> bool: async def delete(self, question_id: int) -> bool:
"""Удаление вопроса с пересчетом user_question_number""" """Удаление вопроса с пересчетом user_question_number"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -698,9 +784,11 @@ class QuestionCRUD(BaseCRUD):
""", (to_user_id, deleted_number)) """, (to_user_id, deleted_number))
await conn.commit() await conn.commit()
logger.info(f"🗑️ Вопрос {question_id} удален, пересчитаны номера для пользователя {to_user_id}") if self.logger:
self.logger.info(f"🗑️ Вопрос {question_id} удален, пересчитаны номера для пользователя {to_user_id}")
return True return True
@track_db_operation("SELECT", "questions_unread_count")
async def get_unread_count(self, to_user_id: int) -> int: async def get_unread_count(self, to_user_id: int) -> int:
"""Получение количества непрочитанных вопросов""" """Получение количества непрочитанных вопросов"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -711,6 +799,7 @@ class QuestionCRUD(BaseCRUD):
row = await cursor.fetchone() row = await cursor.fetchone()
return row[0] return row[0]
@track_db_operation("SELECT", "questions_count_by_to_user")
async def get_count_by_to_user(self, to_user_id: int, status: Optional[QuestionStatus] = None) -> int: async def get_count_by_to_user(self, to_user_id: int, status: Optional[QuestionStatus] = None) -> int:
"""Получение общего количества вопросов для пользователя""" """Получение общего количества вопросов для пользователя"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -725,6 +814,7 @@ class QuestionCRUD(BaseCRUD):
row = await cursor.fetchone() row = await cursor.fetchone()
return row[0] return row[0]
@track_db_operation("SELECT", "questions_stats")
async def get_stats(self) -> Dict[str, Any]: async def get_stats(self) -> Dict[str, Any]:
"""Получение статистики вопросов""" """Получение статистики вопросов"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -787,6 +877,7 @@ class QuestionCRUD(BaseCRUD):
class UserBlockCRUD(BaseCRUD): class UserBlockCRUD(BaseCRUD):
"""CRUD операции для блокировок пользователей""" """CRUD операции для блокировок пользователей"""
@track_db_operation("INSERT", "user_blocks")
async def create(self, user_block: UserBlock) -> UserBlock: async def create(self, user_block: UserBlock) -> UserBlock:
"""Создание блокировки""" """Создание блокировки"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -801,6 +892,7 @@ class UserBlockCRUD(BaseCRUD):
await conn.commit() await conn.commit()
return user_block return user_block
@track_db_operation("SELECT", "user_blocks")
async def is_blocked(self, blocker_id: int, blocked_id: int) -> bool: async def is_blocked(self, blocker_id: int, blocked_id: int) -> bool:
"""Проверка, заблокирован ли пользователь""" """Проверка, заблокирован ли пользователь"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -811,6 +903,7 @@ class UserBlockCRUD(BaseCRUD):
row = await cursor.fetchone() row = await cursor.fetchone()
return row[0] > 0 return row[0] > 0
@track_db_operation("SELECT", "user_blocks")
async def get_blocked_users(self, blocker_id: int) -> List[int]: async def get_blocked_users(self, blocker_id: int) -> List[int]:
"""Получение списка заблокированных пользователей""" """Получение списка заблокированных пользователей"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -821,6 +914,7 @@ class UserBlockCRUD(BaseCRUD):
return [row[0] for row in rows] return [row[0] for row in rows]
@track_db_operation("DELETE", "user_blocks")
async def delete(self, blocker_id: int, blocked_id: int) -> bool: async def delete(self, blocker_id: int, blocked_id: int) -> bool:
"""Удаление блокировки""" """Удаление блокировки"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -835,6 +929,7 @@ class UserBlockCRUD(BaseCRUD):
class UserSettingsCRUD(BaseCRUD): class UserSettingsCRUD(BaseCRUD):
"""CRUD операции для настроек пользователей""" """CRUD операции для настроек пользователей"""
@track_db_operation("INSERT", "user_settings")
async def create(self, settings: UserSettings) -> UserSettings: async def create(self, settings: UserSettings) -> UserSettings:
"""Создание настроек пользователя""" """Создание настроек пользователя"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -853,6 +948,7 @@ class UserSettingsCRUD(BaseCRUD):
await conn.commit() await conn.commit()
return settings return settings
@track_db_operation("SELECT", "user_settings")
async def get_by_user_id(self, user_id: int) -> Optional[UserSettings]: async def get_by_user_id(self, user_id: int) -> Optional[UserSettings]:
"""Получение настроек пользователя""" """Получение настроек пользователя"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -864,6 +960,7 @@ class UserSettingsCRUD(BaseCRUD):
return self._row_to_settings(row) return self._row_to_settings(row)
return None return None
@track_db_operation("UPDATE", "user_settings")
async def update(self, settings: UserSettings) -> UserSettings: async def update(self, settings: UserSettings) -> UserSettings:
"""Обновление настроек пользователя""" """Обновление настроек пользователя"""
async with self.get_connection() as conn: async with self.get_connection() as conn:
@@ -881,6 +978,7 @@ class UserSettingsCRUD(BaseCRUD):
await conn.commit() await conn.commit()
return settings return settings
@track_db_operation("DELETE", "user_settings")
async def delete(self, user_id: int) -> bool: async def delete(self, user_id: int) -> bool:
"""Удаление настроек пользователя""" """Удаление настроек пользователя"""
async with self.get_connection() as conn: async with self.get_connection() as conn:

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""
Скрипт для диагностики проблем с соединениями БД
"""
import asyncio
import sys
import os
from pathlib import Path
# Добавляем путь к проекту
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from database.crud import get_connection_pool
from services.infrastructure.metrics import get_metrics_service
async def diagnose_connections():
"""Диагностика соединений БД"""
print("🔍 Диагностика соединений БД AnonBot")
print("=" * 50)
# Путь к БД
db_path = project_root / "database" / "anon_qna.db"
if not db_path.exists():
print(f"❌ База данных не найдена: {db_path}")
return
print(f"📁 База данных: {db_path}")
# Получаем пул соединений
pool = get_connection_pool(str(db_path))
stats = pool.get_pool_stats()
print("\n📊 Статистика пула соединений:")
print(f" • Размер пула: {stats['pool_size']}")
print(f" • Созданных соединений: {stats['created_connections']}")
print(f" • Доступных соединений: {stats['available_connections']}")
print(f" • Утилизация: {stats['utilization_percent']:.1f}%")
# Анализ проблем
print("\n🔍 Анализ:")
if stats['created_connections'] > stats['pool_size']:
print(f" ❌ КРИТИЧЕСКАЯ ПРОБЛЕМА: Создано {stats['created_connections']} соединений при лимите {stats['pool_size']}")
print(f" Это указывает на утечку соединений!")
elif stats['utilization_percent'] > 80:
print(f" ⚠️ ВНИМАНИЕ: Высокая утилизация пула ({stats['utilization_percent']:.1f}%)")
else:
print(f" ✅ Пул соединений работает нормально")
# Проверяем метрики
print("\n📈 Метрики Prometheus:")
try:
metrics_service = get_metrics_service()
metrics_data = metrics_service.get_metrics()
# Ищем метрики соединений
lines = metrics_data.decode('utf-8').split('\n')
connection_metrics = [line for line in lines if 'anon_bot_db_connections' in line or 'anon_bot_db_pool' in line]
if connection_metrics:
for metric in connection_metrics:
if metric.strip():
print(f"{metric}")
else:
print(" • Метрики соединений не найдены")
except Exception as e:
print(f" ❌ Ошибка получения метрик: {e}")
# Рекомендации
print("\n💡 Рекомендации:")
if stats['created_connections'] > stats['pool_size']:
print(" 1. Перезапустите AnonBot для сброса пула соединений")
print(" 2. Проверьте логи на наличие ошибок БД")
print(" 3. Убедитесь, что все соединения правильно закрываются")
print(" 4. Мониторьте метрики в Grafana")
elif stats['utilization_percent'] > 80:
print(" 1. Рассмотрите увеличение размера пула соединений")
print(" 2. Проверьте производительность запросов к БД")
print(" 3. Оптимизируйте часто используемые запросы")
else:
print(" 1. Продолжайте мониторинг метрик")
print(" 2. Настройте алерты в Grafana")
print("\n🔧 Команды для мониторинга:")
print(" • Просмотр метрик: curl http://localhost:8081/metrics | grep anon_bot_db")
print(" • Проверка здоровья: curl http://localhost:8081/health")
print(" • Статус процесса: curl http://localhost:8081/status")
if __name__ == "__main__":
asyncio.run(diagnose_connections())

View File

@@ -5,6 +5,8 @@
from .database import DatabaseService from .database import DatabaseService
from .logger import get_logger, setup_logging from .logger import get_logger, setup_logging
from .metrics import MetricsService, get_metrics_service from .metrics import MetricsService, get_metrics_service
from .metrics_updater import MetricsUpdater, get_metrics_updater, start_metrics_updater, stop_metrics_updater
from .db_metrics_decorator import track_db_operation, track_db_connection
from .pid_manager import PIDManager, get_pid_manager, cleanup_pid_file from .pid_manager import PIDManager, get_pid_manager, cleanup_pid_file
from .logging_decorators import ( from .logging_decorators import (
log_function_call, log_business_event, log_fsm_transition, log_function_call, log_business_event, log_fsm_transition,
@@ -21,6 +23,8 @@ __all__ = [
'DatabaseService', 'DatabaseService',
'get_logger', 'setup_logging', 'get_logger', 'setup_logging',
'MetricsService', 'get_metrics_service', 'MetricsService', 'get_metrics_service',
'MetricsUpdater', 'get_metrics_updater', 'start_metrics_updater', 'stop_metrics_updater',
'track_db_operation', 'track_db_connection',
'PIDManager', 'get_pid_manager', 'cleanup_pid_file', 'PIDManager', 'get_pid_manager', 'cleanup_pid_file',
'log_function_call', 'log_business_event', 'log_fsm_transition', 'log_function_call', 'log_business_event', 'log_fsm_transition',
'log_handler', 'log_service', 'log_business', 'log_fsm', 'log_handler', 'log_service', 'log_business', 'log_fsm',

View File

@@ -22,11 +22,11 @@ class DatabaseService:
def __init__(self, db_path: str): def __init__(self, db_path: str):
self.db_path = db_path self.db_path = db_path
# Инициализируем CRUD операции # Инициализируем CRUD операции с передачей логгера
self.users = UserCRUD(db_path) self.users = UserCRUD(db_path, logger)
self.questions = QuestionCRUD(db_path) self.questions = QuestionCRUD(db_path, logger)
self.user_blocks = UserBlockCRUD(db_path) self.user_blocks = UserBlockCRUD(db_path, logger)
self.user_settings = UserSettingsCRUD(db_path) self.user_settings = UserSettingsCRUD(db_path, logger)
async def init(self): async def init(self):
"""Инициализация базы данных и создание таблиц""" """Инициализация базы данных и создание таблиц"""
@@ -59,7 +59,7 @@ class DatabaseService:
return return
# Читаем схему из файла # Читаем схему из файла
schema_path = Path(__file__).parent.parent / "database" / "schema.sql" schema_path = Path(__file__).parent.parent.parent / "database" / "schema.sql"
if schema_path.exists(): if schema_path.exists():
logger.info("📄 Создание таблиц из схемы") logger.info("📄 Создание таблиц из схемы")

View File

@@ -0,0 +1,69 @@
"""
Декоратор для автоматического сбора метрик базы данных
"""
import time
import functools
from typing import Callable, Any
from .metrics import get_metrics_service
from .logger import get_logger
def track_db_operation(operation: str, table: str):
"""
Декоратор для отслеживания операций с базой данных
Args:
operation: Тип операции (SELECT, INSERT, UPDATE, DELETE)
table: Название таблицы
"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapper(*args, **kwargs) -> Any:
metrics_service = get_metrics_service()
logger = get_logger(__name__)
start_time = time.time()
try:
result = await func(*args, **kwargs)
duration = time.time() - start_time
# Записываем успешную операцию
metrics_service.record_db_query(operation, table, "success", duration)
return result
except Exception as e:
duration = time.time() - start_time
# Записываем неудачную операцию
metrics_service.record_db_query(operation, table, "error", duration)
metrics_service.increment_errors(type(e).__name__, "database_operation")
logger.error(f"Database operation failed: {operation} on {table}: {e}")
raise
return wrapper
return decorator
def track_db_connection(func: Callable) -> Callable:
"""
Декоратор для отслеживания соединений с базой данных
"""
@functools.wraps(func)
async def wrapper(*args, **kwargs) -> Any:
metrics_service = get_metrics_service()
logger = get_logger(__name__)
try:
result = await func(*args, **kwargs)
return result
except Exception as e:
# Записываем только ошибки, не соединения
metrics_service.increment_errors(type(e).__name__, "database_connection")
logger.error(f"Database connection failed: {e}")
raise
return wrapper

View File

@@ -6,7 +6,7 @@ import time
from typing import Optional from typing import Optional
from aiohttp import ClientSession, web from aiohttp import ClientSession, web
from aiohttp.web import Request, Response from aiohttp.web import Request, Response, json_response
from loguru import logger from loguru import logger
from config.constants import DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT, APP_VERSION, HTTP_STATUS_OK, HTTP_STATUS_SERVICE_UNAVAILABLE, HTTP_STATUS_INTERNAL_SERVER_ERROR from config.constants import DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT, APP_VERSION, HTTP_STATUS_OK, HTTP_STATUS_SERVICE_UNAVAILABLE, HTTP_STATUS_INTERNAL_SERVER_ERROR
@@ -41,7 +41,8 @@ class HTTPServer:
try: try:
# Получаем метрики # Получаем метрики
metrics_data = self.metrics_service.get_metrics() metrics_data = self.metrics_service.get_metrics()
content_type = self.metrics_service.get_content_type() if isinstance(metrics_data, bytes):
metrics_data = metrics_data.decode('utf-8')
# Записываем метрику HTTP запроса # Записываем метрику HTTP запроса
duration = time.time() - start_time duration = time.time() - start_time
@@ -50,7 +51,7 @@ class HTTPServer:
return Response( return Response(
text=metrics_data, text=metrics_data,
content_type=content_type, content_type='text/plain; version=0.0.4',
status=HTTP_STATUS_OK status=HTTP_STATUS_OK
) )
@@ -104,8 +105,8 @@ class HTTPServer:
self.metrics_service.record_http_request_duration("GET", "/health", duration) self.metrics_service.record_http_request_duration("GET", "/health", duration)
self.metrics_service.increment_http_requests("GET", "/health", http_status) self.metrics_service.increment_http_requests("GET", "/health", http_status)
return Response( return json_response(
json=health_status, health_status,
status=http_status status=http_status
) )
@@ -116,8 +117,8 @@ class HTTPServer:
self.metrics_service.increment_http_requests("GET", "/health", 500) self.metrics_service.increment_http_requests("GET", "/health", 500)
self.metrics_service.increment_errors(type(e).__name__, "health_handler") self.metrics_service.increment_errors(type(e).__name__, "health_handler")
return Response( return json_response(
json={"status": "error", "message": str(e)}, {"status": "error", "message": str(e)},
status=HTTP_STATUS_INTERNAL_SERVER_ERROR status=HTTP_STATUS_INTERNAL_SERVER_ERROR
) )
@@ -149,8 +150,8 @@ class HTTPServer:
self.metrics_service.record_http_request_duration("GET", "/ready", duration) self.metrics_service.record_http_request_duration("GET", "/ready", duration)
self.metrics_service.increment_http_requests("GET", "/ready", http_status) self.metrics_service.increment_http_requests("GET", "/ready", http_status)
return Response( return json_response(
json=ready_status, ready_status,
status=http_status status=http_status
) )
@@ -161,8 +162,8 @@ class HTTPServer:
self.metrics_service.increment_http_requests("GET", "/ready", 500) self.metrics_service.increment_http_requests("GET", "/ready", 500)
self.metrics_service.increment_errors(type(e).__name__, "ready_handler") self.metrics_service.increment_errors(type(e).__name__, "ready_handler")
return Response( return json_response(
json={"status": "error", "message": str(e)}, {"status": "error", "message": str(e)},
status=HTTP_STATUS_INTERNAL_SERVER_ERROR status=HTTP_STATUS_INTERNAL_SERVER_ERROR
) )
@@ -278,8 +279,8 @@ class HTTPServer:
self.metrics_service.record_http_request_duration("GET", "/", duration) self.metrics_service.record_http_request_duration("GET", "/", duration)
self.metrics_service.increment_http_requests("GET", "/", 200) self.metrics_service.increment_http_requests("GET", "/", 200)
return Response( return json_response(
json=info, info,
status=HTTP_STATUS_OK status=HTTP_STATUS_OK
) )
@@ -290,8 +291,8 @@ class HTTPServer:
self.metrics_service.increment_http_requests("GET", "/", 500) self.metrics_service.increment_http_requests("GET", "/", 500)
self.metrics_service.increment_errors(type(e).__name__, "root_handler") self.metrics_service.increment_errors(type(e).__name__, "root_handler")
return Response( return json_response(
json={"error": str(e)}, {"error": str(e)},
status=HTTP_STATUS_INTERNAL_SERVER_ERROR status=HTTP_STATUS_INTERNAL_SERVER_ERROR
) )

View File

@@ -127,6 +127,27 @@ class MetricsService:
['status'] ['status']
) )
# Метрики пула соединений
self.db_pool_size = Gauge(
'anon_bot_db_pool_size',
'Database connection pool size'
)
self.db_pool_created_connections = Gauge(
'anon_bot_db_pool_created_connections',
'Number of created connections in pool'
)
self.db_pool_available_connections = Gauge(
'anon_bot_db_pool_available_connections',
'Number of available connections in pool'
)
self.db_pool_utilization_percent = Gauge(
'anon_bot_db_pool_utilization_percent',
'Database connection pool utilization percentage'
)
# Метрики пагинации # Метрики пагинации
self.pagination_requests_total = Counter( self.pagination_requests_total = Counter(
'anon_bot_pagination_requests_total', 'anon_bot_pagination_requests_total',
@@ -237,13 +258,25 @@ class MetricsService:
self.db_query_duration.labels(operation=operation, table=table).observe(duration) self.db_query_duration.labels(operation=operation, table=table).observe(duration)
def record_db_connection(self, status: str): def record_db_connection(self, status: str):
"""Записать метрики подключения к БД""" """Записать метрики подключения к БД (только для реальных соединений пула)"""
self.db_connections_total.labels(status=status).inc() self.db_connections_total.labels(status=status).inc()
if status == "opened": if status == "opened":
self.db_connections_active.inc() self.db_connections_active.inc()
elif status == "closed": elif status == "closed":
self.db_connections_active.dec() self.db_connections_active.dec()
def update_db_connections_from_pool(self, active_count: int):
"""Обновить количество активных соединений на основе реального пула"""
# Сбрасываем счетчик и устанавливаем реальное значение
self.db_connections_active.set(active_count)
def update_db_pool_metrics(self, pool_stats: dict):
"""Обновить метрики пула соединений"""
self.db_pool_size.set(pool_stats.get("pool_size", 0))
self.db_pool_created_connections.set(pool_stats.get("created_connections", 0))
self.db_pool_available_connections.set(pool_stats.get("available_connections", 0))
self.db_pool_utilization_percent.set(pool_stats.get("utilization_percent", 0))
def record_pagination_time(self, entity_type: str, duration: float, method: str = "cursor"): def record_pagination_time(self, entity_type: str, duration: float, method: str = "cursor"):
"""Записать время пагинации""" """Записать время пагинации"""
self.pagination_requests_total.labels(entity_type=entity_type, method=method).inc() self.pagination_requests_total.labels(entity_type=entity_type, method=method).inc()

View File

@@ -0,0 +1,196 @@
"""
Сервис для периодического обновления метрик
"""
import asyncio
import time
from typing import Optional
from .metrics import get_metrics_service
from .database import DatabaseService
from .logger import get_logger
class MetricsUpdater:
"""Сервис для периодического обновления метрик"""
def __init__(self, update_interval: int = 30, db_path: str = None):
self.update_interval = update_interval
self.metrics_service = get_metrics_service()
self.database_service: Optional[DatabaseService] = None
self.db_path = db_path
self._running = False
self._task: Optional[asyncio.Task] = None
self.logger = get_logger(__name__)
async def start(self):
"""Запустить обновление метрик"""
if self._running:
self.logger.warning("MetricsUpdater уже запущен")
return
# Создаем DatabaseService если путь к БД указан
if self.db_path:
self.database_service = DatabaseService(self.db_path)
await self.database_service.init()
self._running = True
self._task = asyncio.create_task(self._update_loop())
self.logger.info(f"📊 MetricsUpdater запущен с интервалом {self.update_interval} секунд")
async def stop(self):
"""Остановить обновление метрик"""
if not self._running:
return
self._running = False
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
# Логгер недоступен в stop, так как объект может быть уже уничтожен
pass
async def _update_loop(self):
"""Основной цикл обновления метрик"""
while self._running:
try:
await self._update_metrics()
await asyncio.sleep(self.update_interval)
except asyncio.CancelledError:
break
except Exception as e:
self.logger.error(f"Ошибка при обновлении метрик: {e}")
await asyncio.sleep(self.update_interval)
async def _update_metrics(self):
"""Обновление всех метрик"""
try:
# Обновляем активных пользователей
await self._update_active_users()
# Обновляем активные вопросы
await self._update_active_questions()
# Обновляем метрики БД
await self._update_database_metrics()
except Exception as e:
self.logger.error(f"Ошибка при обновлении метрик: {e}")
self.metrics_service.increment_errors(type(e).__name__, "metrics_updater")
async def _update_active_users(self):
"""Обновление количества активных пользователей"""
try:
if not self.database_service:
return
# Подсчитываем активных пользователей за последние 24 часа
async with self.database_service.get_connection() as conn:
cursor = await conn.execute("""
SELECT COUNT(*) FROM users
WHERE is_active = 1
AND updated_at > datetime('now', '-24 hours')
""")
result = await cursor.fetchone()
active_users_count = result[0] if result else 0
self.metrics_service.set_active_users(active_users_count)
self.logger.debug(f"Обновлено количество активных пользователей: {active_users_count}")
except Exception as e:
self.logger.error(f"Ошибка при обновлении активных пользователей: {e}")
async def _update_active_questions(self):
"""Обновление количества активных вопросов"""
try:
if not self.database_service:
return
# Подсчитываем активные вопросы (pending и processing)
async with self.database_service.get_connection() as conn:
cursor = await conn.execute("""
SELECT COUNT(*) FROM questions
WHERE status IN ('pending', 'processing')
""")
result = await cursor.fetchone()
active_questions_count = result[0] if result else 0
self.metrics_service.set_active_questions(active_questions_count)
self.logger.debug(f"Обновлено количество активных вопросов: {active_questions_count}")
except Exception as e:
self.logger.error(f"Ошибка при обновлении активных вопросов: {e}")
async def _update_database_metrics(self):
"""Обновление метрик базы данных"""
try:
if not self.database_service:
return
# Проверяем соединение с БД
start_time = time.time()
try:
await self.database_service.check_connection()
duration = time.time() - start_time
# Записываем успешное соединение (только для статистики, не для активных соединений)
self.metrics_service.record_db_query("health_check", "connection", "success", duration)
# Обновляем метрики пула соединений
await self._update_pool_metrics()
except Exception as e:
duration = time.time() - start_time
# Записываем неудачное соединение (только для статистики)
self.metrics_service.record_db_query("health_check", "connection", "error", duration)
self.metrics_service.increment_errors(type(e).__name__, "database_health_check")
except Exception as e:
self.logger.error(f"Ошибка при обновлении метрик БД: {e}")
async def _update_pool_metrics(self):
"""Обновление метрик пула соединений"""
try:
from database.crud import get_connection_pool
pool = get_connection_pool(self.database_service.db_path)
pool_stats = pool.get_pool_stats()
self.metrics_service.update_db_pool_metrics(pool_stats)
# Обновляем реальное количество активных соединений из пула
created_connections = pool_stats.get("created_connections", 0)
self.metrics_service.update_db_connections_from_pool(created_connections)
# Логируем предупреждение если утилизация пула превышает 80%
if pool_stats.get("utilization_percent", 0) > 80:
self.logger.warning(f"Высокая утилизация пула соединений: {pool_stats}")
except Exception as e:
self.logger.error(f"Ошибка при обновлении метрик пула: {e}")
# Глобальный экземпляр
_metrics_updater: Optional[MetricsUpdater] = None
def get_metrics_updater(update_interval: int = 30, db_path: str = None) -> MetricsUpdater:
"""Получить экземпляр MetricsUpdater"""
global _metrics_updater
if _metrics_updater is None:
_metrics_updater = MetricsUpdater(update_interval, db_path)
return _metrics_updater
async def start_metrics_updater(update_interval: int = 30, db_path: str = None):
"""Запустить обновление метрик"""
updater = get_metrics_updater(update_interval, db_path)
await updater.start()
async def stop_metrics_updater():
"""Остановить обновление метрик"""
global _metrics_updater
if _metrics_updater:
await _metrics_updater.stop()
_metrics_updater = None