logger refactor

This commit is contained in:
2024-12-18 13:40:50 +09:00
parent b8d3b953d2
commit 1e64a432ab
8 changed files with 168 additions and 297 deletions

View File

@@ -1,235 +1,174 @@
import logging import logging
import pymysql import pymysql
from datetime import datetime from datetime import datetime
from urllib.parse import unquote, parse_qs from urllib.parse import unquote, parse_qs
import pytz
from django.utils import timezone from django.utils import timezone
from django.conf import settings
import chardet
import html import html
from hotels.models import Room, Hotel from hotels.models import Room, Hotel
from .models import UserActivityLog, ExternalDBSettings from .models import UserActivityLog, ExternalDBSettings
from touchh.utils.log import CustomLogger
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from decouple import config
class DatabaseConnector: class DatabaseConnector:
"""
Класс для подключения к внешней базе данных.
"""
def __init__(self, db_settings_id): def __init__(self, db_settings_id):
self.db_settings_id = db_settings_id self.db_settings_id = db_settings_id
self.logger = CustomLogger(name="DatabaseConnector", log_level="DEBUG").get_logger()
self.connection = None self.connection = None
self.logger = self.setup_logger() self.db_settings = self.get_db_settings()
self.db_settings = None
def setup_logger(self): def get_db_settings(self):
default_level = logging.INFO # Уровень по умолчанию try:
level_name = getattr(settings, "DATA_SYNC_LOG_LEVEL", "INFO").upper() settings = ExternalDBSettings.objects.get(id=self.db_settings_id)
log_level = getattr(logging, level_name, default_level) self.logger.info(f"Retrieved DB settings: {settings}")
logger.setLevel(log_level) return {
"host": settings.host,
"port": settings.port,
"user": settings.user,
"password": settings.password,
"database": settings.database,
"table_name": settings.table_name
# Настройка обработчика для файла }
handler = logging.FileHandler("data_sync.log") except ExternalDBSettings.DoesNotExist:
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) self.logger.error(f"Settings with ID {self.db_settings_id} not found.")
handler.setLevel(log_level) raise ValueError("Invalid db_settings_id")
except Exception as e:
# Удаляем старые обработчики, чтобы избежать дублирования self.logger.error(f"Error retrieving settings: {e}")
if logger.hasHandlers(): raise
logger.handlers.clear()
logger.addHandler(handler)
# Сообщение о текущем уровне логирования
logger.info(f"Уровень логирования установлен: {logging.getLevelName(log_level)}")
logger.addHandler(handler)
return logger
def connect(self): def connect(self):
"""Подключение к базе данных."""
try: try:
self.db_settings = ExternalDBSettings.objects.get(id=self.db_settings_id) self.logger.info(f"Connecting to DB with settings: {self.db_settings}")
self.connection = pymysql.connect( self.connection = pymysql.connect(
host=self.db_settings.host, host=self.db_settings["host"],
port=self.db_settings.port, port=self.db_settings["port"],
user=self.db_settings.user, user=self.db_settings["user"],
password=self.db_settings.password, password=self.db_settings["password"],
database=self.db_settings.database, database=self.db_settings["database"],
charset="utf8mb4", charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor, cursorclass=pymysql.cursors.DictCursor,
) )
self.logger.info("Подключение к базе данных успешно установлено.") self.logger.info("Database connection established successfully.")
except pymysql.err.OperationalError as e:
self.logger.error(f"Operational error during DB connection: {e}")
raise ConnectionError(f"Operational error: {e}")
except Exception as e: except Exception as e:
self.logger.error(f"Ошибка подключения к БД: {e}") self.logger.error(f"Unexpected database connection error: {e}")
raise ConnectionError(e) raise ConnectionError(e)
def close(self):
"""Закрывает соединение с базой данных."""
if self.connection:
self.connection.close()
self.logger.info("Соединение с базой данных закрыто.")
def execute_query(self, query): def execute_query(self, query):
"""Выполнение запроса и возврат результатов.""" try:
with self.connection.cursor() as cursor: with self.connection.cursor() as cursor:
cursor.execute(query) cursor.execute(query)
return cursor.fetchall() return cursor.fetchall()
except Exception as e:
self.logger.error(f"Query execution error: {e}")
return []
def close(self):
if self.connection:
self.connection.close()
self.logger.info("Database connection closed.")
class DataProcessor: class DataProcessor:
"""
Обрабатывает и сохраняет данные.
"""
def __init__(self, logger): def __init__(self, logger):
self.logger = logger self.logger = logger
def decode_html_entities(self, text): def decode_html_entities(self, text):
"""Декодирует URL и HTML-сущности."""
if text and isinstance(text, str): if text and isinstance(text, str):
text = unquote(text) return html.unescape(unquote(text))
text = html.unescape(text)
try:
detected = chardet.detect(text.encode())
encoding = detected['encoding']
if encoding and encoding != 'utf-8':
text = text.encode(encoding).decode('utf-8', errors='ignore')
except Exception as e:
self.logger.error(f"Ошибка кодировки: {e}")
return text return text
def parse_datetime(self, dt_str): def parse_datetime(self, dt_str):
"""Преобразует строку даты в aware datetime."""
try: try:
if isinstance(dt_str, datetime): if isinstance(dt_str, datetime):
return timezone.make_aware(dt_str) if timezone.is_naive(dt_str) else dt_str return timezone.make_aware(dt_str) if timezone.is_naive(dt_str) else dt_str
if dt_str: if dt_str:
return timezone.make_aware(datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S")) return timezone.make_aware(datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S"))
except Exception as e: except Exception as e:
self.logger.error(f"Ошибка парсинга даты: {e}") self.logger.error(f"Datetime parsing error: {e}")
return None return None
def url_parameters_parser(self, url_parameters): def url_parameters_parser(self, url_parameters):
"""
Парсит строку URL-параметров и возвращает словарь с ключами и значениями.
Пример входа:
url_parameters = "utm_medium=qr-3&utm_content=chisto-pozhit&utm_term=101"
Возвращает:
{
"utm_medium": "qr-3",
"utm_content": "chisto-pozhit",
"utm_term": "101"
}
"""
try: try:
if not url_parameters: if not url_parameters:
return {} return {}
# Декодируем параметры URL
decoded_params = unquote(url_parameters) decoded_params = unquote(url_parameters)
parsed_params = parse_qs(decoded_params) parsed_params = parse_qs(decoded_params)
return {key: value[0] for key, value in parsed_params.items()}
# Преобразуем список значений в строку
result = {key: value[0] for key, value in parsed_params.items()}
return result
except Exception as e: except Exception as e:
self.logger.error(f"Ошибка парсинга URL-параметров: {e}") self.logger.error(f"URL parameters parsing error: {e}")
return {} return {}
class HotelRoomManager: class HotelRoomManager:
"""
Управляет созданием отелей и номеров.
"""
def __init__(self, logger): def __init__(self, logger):
self.logger = logger self.logger = logger
def get_or_create_hotel(self, hotel_id, page_title): def get_or_create_hotel(self, hotel_id, page_title):
"""
Создает или получает отель.
:param hotel_id: Значение из utm_content (индекс отеля)
:param page_title: Название отеля из поля page_title
"""
if not hotel_id: if not hotel_id:
self.logger.warning("Пропущено создание отеля: отсутствует hotel_id.") self.logger.warning("Hotel creation skipped: missing hotel_id.")
return None return None
# Создаем или получаем отель с hotel_id и устанавливаем name по page_title
hotel, created = Hotel.objects.get_or_create( hotel, created = Hotel.objects.get_or_create(
hotel_id=hotel_id, hotel_id=hotel_id,
defaults={ defaults={
"name": html.unescape(page_title) or f"Отель {hotel_id}", "name": html.unescape(page_title) or f"Отель {hotel_id}",
"description": "Автоматически добавленный отель" "description": "Автоматически созданный отель",
} }
) )
if created: if created:
self.logger.info(f"Создан отель '{hotel.name}' с hotel_id: {hotel_id}") self.logger.info(f"Hotel '{hotel.name}' created with hotel_id: {hotel_id}")
else: else:
self.logger.info(f"Отель '{hotel.name}' уже существует с hotel_id: {hotel_id}") self.logger.info(f"Hotel '{hotel.name}' already exists with hotel_id: {hotel_id}")
return hotel return hotel
def get_or_create_room(self, hotel, room_number): def get_or_create_room(self, hotel, room_number):
"""
Создает или получает номер отеля.
:param hotel: Экземпляр модели Hotel
:param room_number: Номер комнаты из utm_term
"""
if not hotel: if not hotel:
self.logger.warning("Пропущено создание номера: отсутствует отель.") self.logger.warning("Room creation skipped: missing hotel.")
return None return None
if not room_number: if not room_number:
self.logger.warning(f"Пропущено создание номера: отсутствует room_number для отеля {hotel.name}.") self.logger.warning(f"Room creation skipped: missing room_number for hotel {hotel.name}.")
return None return None
# Генерация уникального external_id на основе hotel_id и room_number
external_id = f"{hotel.hotel_id}_{room_number}".lower() external_id = f"{hotel.hotel_id}_{room_number}".lower()
# Создаем или получаем номер
room, created = Room.objects.get_or_create( room, created = Room.objects.get_or_create(
hotel=hotel, hotel=hotel,
number=room_number, number=room_number,
defaults={ defaults={
"number": room_number, # Используем room_number как название номера
"external_id": external_id, "external_id": external_id,
"description": "Автоматически добавленный номер" "description": "Автоматически созданная комната",
} }
) )
if created: if created:
self.logger.info(f"Создан номер '{room.number}' (external_id: {external_id}) в отеле '{hotel.name}'") self.logger.info(f"Room '{room.number}' (external_id: {external_id}) created in hotel '{hotel.name}'")
else: else:
self.logger.info(f"Номер '{room.number}' уже существует в отеле '{hotel.name}'") self.logger.info(f"Room '{room.number}' already exists in hotel '{hotel.name}'")
return room return room
class DataSyncManager: class DataSyncManager:
"""
Главный класс для синхронизации данных.
"""
def __init__(self, db_settings_id): def __init__(self, db_settings_id):
self.logger = self.setup_logger() self.logger = CustomLogger(name="DataSyncManager", log_level="DEBUG").get_logger()
self.db_connector = DatabaseConnector(db_settings_id) self.db_connector = DatabaseConnector(db_settings_id)
self.db_settings = self.db_connector.db_settings # Сохраняем настройки базы данных
self.data_processor = DataProcessor(self.logger) self.data_processor = DataProcessor(self.logger)
self.hotel_manager = HotelRoomManager(self.logger) self.hotel_manager = HotelRoomManager(self.logger)
def setup_logger(self):
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler("data_sync.log")
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
logger.addHandler(handler)
return logger
def get_last_saved_record(self): def get_last_saved_record(self):
"""Получает ID последней записи."""
record = UserActivityLog.objects.order_by("-id").first() record = UserActivityLog.objects.order_by("-id").first()
return record.id if record else 0 return record.id if record else 0
def fetch_new_data(self, last_id): def fetch_new_data(self, last_id):
"""Получает новые данные из БД."""
query = f""" query = f"""
SELECT * FROM `{self.db_connector.db_settings.table_name}` SELECT * FROM `{self.db_settings.get('table_name')}`
WHERE id > {last_id} WHERE id > {last_id}
AND url_parameters IS NOT NULL AND url_parameters IS NOT NULL
AND url_parameters LIKE '%utm_medium%' AND url_parameters LIKE '%utm_medium%'
@@ -237,31 +176,27 @@ class DataSyncManager:
ORDER BY id ASC ORDER BY id ASC
LIMIT 1000; LIMIT 1000;
""" """
self.logger.info(f"Запрос на получение новых данных отправлен. \n Содержание запроса: {query}")
self.logger.info(f"Fetching new data with query: {query}")
return self.db_connector.execute_query(query) return self.db_connector.execute_query(query)
def process_and_save_data(self, rows): def process_and_save_data(self, rows):
"""
Обрабатывает и сохраняет данные из внешней базы данных.
"""
for row in rows: for row in rows:
try: try:
# Декодирование URL-параметров
url_params = self.data_processor.decode_html_entities(row.get("url_parameters", "")) url_params = self.data_processor.decode_html_entities(row.get("url_parameters", ""))
params = self.data_processor.url_parameters_parser(url_params) params = self.data_processor.url_parameters_parser(url_params)
timestamp = params.get("timestamp")
date_time = params.get("date_time")
# Извлечение данных
hotel_id = params.get("utm_content") hotel_id = params.get("utm_content")
room_number = params.get("utm_term") room_number = params.get("utm_term")
page_title = row.get("page_title") # Название отеля из page_title page_title = row.get("page_title")
external_id = row.get("id") external_id = row.get("id")
hits = row.get("hits") or 0
# Создание отеля и комнаты
hotel = self.hotel_manager.get_or_create_hotel(hotel_id, page_title) hotel = self.hotel_manager.get_or_create_hotel(hotel_id, page_title)
room = self.hotel_manager.get_or_create_room(hotel, room_number) room = self.hotel_manager.get_or_create_room(hotel, room_number)
page_url = row.get("page_url") page_url = row.get("page_url")
# Заполнение записи
if hits != 0 and page_title is not None:
UserActivityLog.objects.update_or_create( UserActivityLog.objects.update_or_create(
external_id=external_id, external_id=external_id,
defaults={ defaults={
@@ -273,64 +208,53 @@ class DataSyncManager:
"url_parameters": url_params, "url_parameters": url_params,
"page_id": room.id if room else None, "page_id": room.id if room else None,
"page_title": html.unescape(page_title), "page_title": html.unescape(page_title),
"hits": row.get("hits") or 0, "hits": hits,
"page_url": html.unescape(page_url), "page_url": html.unescape(page_url),
} }
) )
self.logger.info(f"Запись ID {external_id} успешно обработана.") else:
self.logger.warning("Invalid data for UserActivityLog.")
self.logger.info(f"Record ID {external_id} processed successfully.")
except Exception as e: except Exception as e:
self.logger.error(f"Ошибка при обработке записи ID {row.get('id')}: {e}") self.logger.error(f"Error processing record ID {row.get('id')}: {e}")
def sync(self): def sync(self):
"""Запускает процесс синхронизации."""
self.db_connector.connect() self.db_connector.connect()
try: try:
last_id = self.get_last_saved_record() last_id = self.get_last_saved_record()
rows = self.fetch_new_data(last_id) rows = self.fetch_new_data(last_id)
self.process_and_save_data(rows) self.process_and_save_data(rows)
self.logger.info("Синхронизация завершена.") self.logger.info("Sync completed.")
finally: finally:
self.db_connector.close() self.db_connector.close()
import logging
from concurrent.futures import ThreadPoolExecutor
from .models import ExternalDBSettings
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
def scheduled_sync(): def scheduled_sync():
""" logger = CustomLogger(name="DatabaseSyncScheduler", log_level="DEBUG").get_logger()
Планировщик синхронизации для всех активных подключений. logger.info("Starting scheduled sync.")
Каждое подключение обрабатывается отдельно.
"""
logger.info("Запуск планировщика синхронизации.")
# Получаем все активные настройки подключения
active_db_settings = ExternalDBSettings.objects.filter(is_active=True) active_db_settings = ExternalDBSettings.objects.filter(is_active=True)
if not active_db_settings.exists(): if not active_db_settings.exists():
logger.warning("Не найдено активных подключений для синхронизации.") logger.warning("No active database connections found.")
return return
logger.info(f"Найдено активных подключений: {len(active_db_settings)}") logger.info(f"Found {len(active_db_settings)} active database connections.")
def sync_task(db_settings): def sync_task(db_settings):
"""
Выполняет синхронизацию для одного подключения.
"""
try: try:
logger.info(f"Начало синхронизации для подключения: {db_settings.name} (ID={db_settings.id})") logger.info(f"Syncing connection: {db_settings.name} (ID={db_settings.id})")
sync_manager = DataSyncManager(db_settings.id) sync_manager = DataSyncManager(db_settings.id)
sync_manager.sync() sync_manager.sync()
logger.info(f"Синхронизация успешно завершена для подключения: {db_settings.name}") logger.info(f"Sync completed for connection: {db_settings}")
except Exception as e: except Exception as e:
logger.error(f"Ошибка синхронизации для подключения {db_settings.name}: {e}") logger.error(f"Error syncing connection {db_settings}: {e}")
# Параллельное выполнение задач синхронизации with ThreadPoolExecutor(max_workers=5) as executor:
with ThreadPoolExecutor(max_workers=5) as executor: # Максимальное количество потоков = 5 futures = [executor.submit(sync_task, db_settings) for db_settings in active_db_settings]
for db_settings in active_db_settings: for future in futures:
executor.submit(sync_task, db_settings) try:
future.result(timeout=300)
except TimeoutError:
logger.error("Sync task timed out.")
logger.info("Планировщик синхронизации завершил работу.") logger.info("Scheduled sync completed.")

View File

@@ -7,6 +7,7 @@ from telegram.ext import Application
from bot.utils.bot_setup import setup_bot from bot.utils.bot_setup import setup_bot
from scheduler.tasks import load_tasks_to_scheduler from scheduler.tasks import load_tasks_to_scheduler
from settings.models import TelegramSettings from settings.models import TelegramSettings
from touchh.utils.log import CustomLogger
class Command(BaseCommand): class Command(BaseCommand):
help = "Запуск Telegram бота и планировщика" help = "Запуск Telegram бота и планировщика"

View File

@@ -6,7 +6,7 @@ import logging
# Настройка логирования # Настройка логирования
logging.basicConfig( logging.basicConfig(
level=logging.INFO, # Уровень логирования (можно DEBUG для полной информации) level=logging.ERROR, # Уровень логирования (можно DEBUG для полной информации)
format="%(asctime)s - %(levelname)s - %(message)s", format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[ handlers=[
logging.FileHandler("bot.log"), # Логи будут записываться в файл bot.log logging.FileHandler("bot.log"), # Логи будут записываться в файл bot.log

View File

@@ -66,4 +66,4 @@ user-agents==2.2.0
yarl==1.18.3 yarl==1.18.3
mysqlclient mysqlclient
chardet chardet
decouple python-decouple

View File

@@ -1,109 +0,0 @@
ace_tools==0.0
aiohappyeyeballs==2.4.4
aiohttp==3.11.10
aiosignal==1.3.1
anyio==4.6.2.post1
APScheduler==3.11.0
asgiref==3.8.1
async-timeout==5.0.1
attrs==24.2.0
certifi==2024.8.30
charset-normalizer==3.4.0
Django==5.1.4
django-filter==24.3
django-jazzmin==3.0.1
django-jet==1.0.8
et_xmlfile==2.0.0
exceptiongroup==1.2.2
fpdf==1.7.2
frozenlist==1.5.0
geoip2==4.8.1
h11==0.14.0
httpcore==1.0.7
httpx==0.28.0
idna==3.10
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
maxminddb==2.6.2
multidict==6.1.0
numpy==2.1.3
openpyxl==3.1.5
pandas==2.2.3
pathspec==0.12.1
pillow==11.0.0
propcache==0.2.1
PyMySQL==1.1.1
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-telegram-bot==21.8
pytz==2024.2
PyYAML==6.0.2
referencing==0.35.1
requests==2.32.3
rpds-py==0.22.3
six==1.17.0
sniffio==1.3.1
sqlparse==0.5.2
typing_extensions==4.12.2
tzdata==2024.2
tzlocal==5.2
ua-parser==1.0.0
ua-parser-builtins==0.18.0.post1
urllib3==2.2.3
user-agents==2.2.0
yarl==1.18.3
ace_tools==0.0
aiohappyeyeballs==2.4.4
aiohttp==3.11.10
aiosignal==1.3.1
anyio==4.6.2.post1
APScheduler==3.11.0
asgiref==3.8.1
async-timeout==5.0.1
attrs==24.2.0
certifi==2024.8.30
charset-normalizer==3.4.0
Django==5.1.4
django-filter==24.3
django-health-check==3.18.3
django-jazzmin==3.0.1
django-jet==1.0.8
et_xmlfile==2.0.0
exceptiongroup==1.2.2
fpdf==1.7.2
frozenlist==1.5.0
geoip2==4.8.1
h11==0.14.0
httpcore==1.0.7
httpx==0.28.0
idna==3.10
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
maxminddb==2.6.2
multidict==6.1.0
numpy==2.1.3
openpyxl==3.1.5
pandas==2.2.3
pathspec==0.12.1
pillow==11.0.0
propcache==0.2.1
PyMySQL==1.1.1
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-telegram-bot==21.8
pytz==2024.2
PyYAML==6.0.2
referencing==0.35.1
requests==2.32.3
rpds-py==0.22.3
six==1.17.0
sniffio==1.3.1
sqlparse==0.5.2
typing_extensions==4.12.2
tzdata==2024.2
tzlocal==5.2
ua-parser==1.0.0
ua-parser-builtins==0.18.0.post1
urllib3==2.2.3
user-agents==2.2.0
yarl==1.18.3

View File

@@ -131,7 +131,7 @@ LOGGING = {
'disable_existing_loggers': False, 'disable_existing_loggers': False,
'handlers': { 'handlers': {
'file': { 'file': {
'level': 'WARNING', 'level': os.getenv("LOG_LEVEL"),
'class': 'logging.FileHandler', 'class': 'logging.FileHandler',
'filename': 'import_hotels.log', # Лог будет записываться в этот файл 'filename': 'import_hotels.log', # Лог будет записываться в этот файл
}, },
@@ -139,12 +139,12 @@ LOGGING = {
'loggers': { 'loggers': {
'django': { 'django': {
'handlers': ['file'], 'handlers': ['file'],
'level': 'WARNING', 'level': os.getenv("LOG_LEVEL"),
'propagate': True, 'propagate': True,
}, },
'antifroud': { 'antifroud': {
'handlers': ['file'], 'handlers': ['file'],
'level': 'WARNING', 'level': os.getenv("LOG_LEVEL"),
'propagate': True, 'propagate': True,
}, },
}, },

0
touchh/utils/__init__.py Normal file
View File

55
touchh/utils/log.py Normal file
View File

@@ -0,0 +1,55 @@
import logging
import os
from dotenv import load_dotenv
# Загрузка переменных из .env
load_dotenv()
class CustomLogger:
"""
Универсальный логгер для использования в проекте.
"""
def __init__(self, name, log_level=None):
"""
Инициализирует логгер.
:param name: Имя логгера (обычно имя функции или класса)
:param log_level: Уровень логирования (по умолчанию из .env)
"""
self.logger = logging.getLogger(name)
# Уровень логирования по умолчанию из .env
default_level = os.getenv("LOG_LEVEL", "INFO").upper()
self.log_level = getattr(logging, log_level.upper(), getattr(logging, default_level, logging.INFO))
self.setup_logger()
def setup_logger(self):
"""
Настраивает логгер с обработчиком и форматом.
"""
self.logger.setLevel(self.log_level)
# Удаляем старые обработчики, чтобы избежать дублирования
if self.logger.hasHandlers():
self.logger.handlers.clear()
# Добавляем обработчик для файла
file_handler = logging.FileHandler("project.log")
file_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
file_handler.setLevel(self.log_level)
# Добавляем обработчик для консоли
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter("%(name)s - %(levelname)s - %(message)s"))
console_handler.setLevel(self.log_level)
self.logger.addHandler(file_handler)
self.logger.addHandler(console_handler)
def get_logger(self):
"""
Возвращает настроенный логгер.
"""
return self.logger