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

View File

@@ -5,6 +5,8 @@
from .database import DatabaseService
from .logger import get_logger, setup_logging
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 .logging_decorators import (
log_function_call, log_business_event, log_fsm_transition,
@@ -21,6 +23,8 @@ __all__ = [
'DatabaseService',
'get_logger', 'setup_logging',
'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',
'log_function_call', 'log_business_event', 'log_fsm_transition',
'log_handler', 'log_service', 'log_business', 'log_fsm',

View File

@@ -22,11 +22,11 @@ class DatabaseService:
def __init__(self, db_path: str):
self.db_path = db_path
# Инициализируем CRUD операции
self.users = UserCRUD(db_path)
self.questions = QuestionCRUD(db_path)
self.user_blocks = UserBlockCRUD(db_path)
self.user_settings = UserSettingsCRUD(db_path)
# Инициализируем CRUD операции с передачей логгера
self.users = UserCRUD(db_path, logger)
self.questions = QuestionCRUD(db_path, logger)
self.user_blocks = UserBlockCRUD(db_path, logger)
self.user_settings = UserSettingsCRUD(db_path, logger)
async def init(self):
"""Инициализация базы данных и создание таблиц"""
@@ -59,7 +59,7 @@ class DatabaseService:
return
# Читаем схему из файла
schema_path = Path(__file__).parent.parent / "database" / "schema.sql"
schema_path = Path(__file__).parent.parent.parent / "database" / "schema.sql"
if schema_path.exists():
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 aiohttp import ClientSession, web
from aiohttp.web import Request, Response
from aiohttp.web import Request, Response, json_response
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
@@ -41,7 +41,8 @@ class HTTPServer:
try:
# Получаем метрики
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 запроса
duration = time.time() - start_time
@@ -50,7 +51,7 @@ class HTTPServer:
return Response(
text=metrics_data,
content_type=content_type,
content_type='text/plain; version=0.0.4',
status=HTTP_STATUS_OK
)
@@ -104,8 +105,8 @@ class HTTPServer:
self.metrics_service.record_http_request_duration("GET", "/health", duration)
self.metrics_service.increment_http_requests("GET", "/health", http_status)
return Response(
json=health_status,
return json_response(
health_status,
status=http_status
)
@@ -116,8 +117,8 @@ class HTTPServer:
self.metrics_service.increment_http_requests("GET", "/health", 500)
self.metrics_service.increment_errors(type(e).__name__, "health_handler")
return Response(
json={"status": "error", "message": str(e)},
return json_response(
{"status": "error", "message": str(e)},
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.increment_http_requests("GET", "/ready", http_status)
return Response(
json=ready_status,
return json_response(
ready_status,
status=http_status
)
@@ -161,8 +162,8 @@ class HTTPServer:
self.metrics_service.increment_http_requests("GET", "/ready", 500)
self.metrics_service.increment_errors(type(e).__name__, "ready_handler")
return Response(
json={"status": "error", "message": str(e)},
return json_response(
{"status": "error", "message": str(e)},
status=HTTP_STATUS_INTERNAL_SERVER_ERROR
)
@@ -278,8 +279,8 @@ class HTTPServer:
self.metrics_service.record_http_request_duration("GET", "/", duration)
self.metrics_service.increment_http_requests("GET", "/", 200)
return Response(
json=info,
return json_response(
info,
status=HTTP_STATUS_OK
)
@@ -290,8 +291,8 @@ class HTTPServer:
self.metrics_service.increment_http_requests("GET", "/", 500)
self.metrics_service.increment_errors(type(e).__name__, "root_handler")
return Response(
json={"error": str(e)},
return json_response(
{"error": str(e)},
status=HTTP_STATUS_INTERNAL_SERVER_ERROR
)

View File

@@ -127,6 +127,27 @@ class MetricsService:
['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(
'anon_bot_pagination_requests_total',
@@ -237,13 +258,25 @@ class MetricsService:
self.db_query_duration.labels(operation=operation, table=table).observe(duration)
def record_db_connection(self, status: str):
"""Записать метрики подключения к БД"""
"""Записать метрики подключения к БД (только для реальных соединений пула)"""
self.db_connections_total.labels(status=status).inc()
if status == "opened":
self.db_connections_active.inc()
elif status == "closed":
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"):
"""Записать время пагинации"""
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