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:
@@ -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',
|
||||
|
||||
@@ -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("📄 Создание таблиц из схемы")
|
||||
|
||||
69
services/infrastructure/db_metrics_decorator.py
Normal file
69
services/infrastructure/db_metrics_decorator.py
Normal 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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
196
services/infrastructure/metrics_updater.py
Normal file
196
services/infrastructure/metrics_updater.py
Normal 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
|
||||
Reference in New Issue
Block a user