- FastAPI приложение для отправки мониторинговых алертов в мессенджеры - Поддержка Telegram и MAX/VK - Интеграция с Grafana, Zabbix, AlertManager - Автоматическое создание тикетов в Jira - Управление группами мессенджеров через API - Декораторы для авторизации и скрытия эндпоинтов - Подробная документация в папке docs/ Автор: Сергей Антропов Сайт: https://devops.org.ru
371 lines
16 KiB
Python
371 lines
16 KiB
Python
"""
|
||
Управление конфигурацией групп для различных мессенджеров.
|
||
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
import json
|
||
import logging
|
||
from typing import Dict, Optional, Any, Union
|
||
import aiofiles
|
||
from datetime import datetime, timedelta
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class GroupsConfig:
|
||
"""Менеджер конфигурации групп для различных мессенджеров с кэшированием."""
|
||
|
||
def __init__(self, config_path: Optional[str] = None):
|
||
"""
|
||
Инициализация менеджера конфигурации.
|
||
|
||
Args:
|
||
config_path: Путь к файлу конфигурации групп.
|
||
"""
|
||
from app.core.config import get_settings
|
||
settings = get_settings()
|
||
self.config_path = config_path or settings.groups_config_path
|
||
self._cache: Optional[Dict[str, Any]] = None
|
||
self._cache_time: Optional[datetime] = None
|
||
self._cache_ttl = timedelta(minutes=5) # Кэш на 5 минут
|
||
self.default_messenger = settings.default_messenger
|
||
|
||
|
||
async def _load_config(self) -> Dict[str, Any]:
|
||
"""
|
||
Загрузить конфигурацию групп из файла.
|
||
|
||
Returns:
|
||
Конфигурация групп в формате:
|
||
{
|
||
"group_name": {
|
||
"messenger": "telegram",
|
||
"chat_id": -1001997464975,
|
||
"thread_id": 0,
|
||
"config": {}
|
||
}
|
||
}
|
||
|
||
Raises:
|
||
FileNotFoundError: Если файл конфигурации не найден.
|
||
json.JSONDecodeError: Если файл содержит некорректный JSON.
|
||
ValueError: Если конфигурация имеет неверный формат.
|
||
"""
|
||
try:
|
||
async with aiofiles.open(self.config_path, 'r', encoding='utf-8') as f:
|
||
content = await f.read()
|
||
config = json.loads(content)
|
||
logger.info(f"Конфигурация групп загружена из {self.config_path}")
|
||
|
||
# Валидация формата конфигурации
|
||
for group_name, group_value in config.items():
|
||
if not isinstance(group_value, dict):
|
||
raise ValueError(
|
||
f"Неверный формат конфигурации для группы '{group_name}': "
|
||
f"ожидается словарь, получен {type(group_value)}. "
|
||
f"Используйте формат: {{'messenger': 'telegram', 'chat_id': ..., 'thread_id': 0, 'config': {{}}}}"
|
||
)
|
||
if "chat_id" not in group_value:
|
||
raise ValueError(
|
||
f"Отсутствует обязательное поле 'chat_id' для группы '{group_name}'"
|
||
)
|
||
if "messenger" not in group_value:
|
||
raise ValueError(
|
||
f"Отсутствует обязательное поле 'messenger' для группы '{group_name}'"
|
||
)
|
||
|
||
return config
|
||
except FileNotFoundError:
|
||
logger.error(f"Файл конфигурации групп не найден: {self.config_path}")
|
||
raise
|
||
except json.JSONDecodeError as e:
|
||
logger.error(f"Ошибка парсинга JSON в файле конфигурации: {e}")
|
||
raise
|
||
|
||
def _is_cache_valid(self) -> bool:
|
||
"""Проверить, валиден ли кэш."""
|
||
if self._cache is None or self._cache_time is None:
|
||
return False
|
||
return datetime.now() - self._cache_time < self._cache_ttl
|
||
|
||
async def get_group_config(self, group_name: str, messenger: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
Получить конфигурацию группы.
|
||
|
||
Args:
|
||
group_name: Имя группы из конфигурации.
|
||
messenger: Тип мессенджера (опционально, для фильтрации).
|
||
|
||
Returns:
|
||
Конфигурация группы или None, если группа не найдена.
|
||
Формат:
|
||
{
|
||
"messenger": "telegram",
|
||
"chat_id": -1001997464975,
|
||
"thread_id": 0,
|
||
"config": {}
|
||
}
|
||
"""
|
||
# Проверяем кэш
|
||
if not self._is_cache_valid():
|
||
try:
|
||
self._cache = await self._load_config()
|
||
self._cache_time = datetime.now()
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
logger.error("Не удалось загрузить конфигурацию групп")
|
||
return None
|
||
|
||
group_config = self._cache.get(group_name)
|
||
if group_config is None:
|
||
logger.warning(f"Группа '{group_name}' не найдена в конфигурации")
|
||
return None
|
||
|
||
# Если указан messenger, проверяем соответствие
|
||
if messenger and group_config.get("messenger") != messenger:
|
||
logger.warning(
|
||
f"Группа '{group_name}' имеет мессенджер '{group_config.get('messenger')}', "
|
||
f"но запрошен '{messenger}'"
|
||
)
|
||
return None
|
||
|
||
logger.info(f"Найдена конфигурация для группы '{group_name}': {group_config}")
|
||
return group_config
|
||
|
||
async def get_chat_id(self, group_name: str, messenger: Optional[str] = None) -> Optional[Union[int, str]]:
|
||
"""
|
||
Получить ID чата по имени группы.
|
||
|
||
Args:
|
||
group_name: Имя группы из конфигурации.
|
||
messenger: Тип мессенджера (опционально).
|
||
|
||
Returns:
|
||
ID чата или None, если группа не найдена.
|
||
"""
|
||
group_config = await self.get_group_config(group_name, messenger)
|
||
if group_config is None:
|
||
return None
|
||
return group_config.get("chat_id")
|
||
|
||
async def refresh_cache(self) -> None:
|
||
"""Принудительно обновить кэш конфигурации."""
|
||
try:
|
||
self._cache = await self._load_config()
|
||
self._cache_time = datetime.now()
|
||
logger.info("Кэш конфигурации групп обновлен")
|
||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||
logger.error(f"Ошибка обновления кэша: {e}")
|
||
|
||
async def _save_config(self, config: Dict[str, Any]) -> None:
|
||
"""
|
||
Сохранить конфигурацию групп в файл.
|
||
|
||
Args:
|
||
config: Нормализованная конфигурация групп в новом формате.
|
||
|
||
Raises:
|
||
IOError: Если не удалось записать файл.
|
||
"""
|
||
try:
|
||
async with aiofiles.open(self.config_path, 'w', encoding='utf-8') as f:
|
||
await f.write(json.dumps(config, indent=2, ensure_ascii=False))
|
||
logger.info(f"Конфигурация групп сохранена в {self.config_path}")
|
||
# Обновляем кэш
|
||
self._cache = config
|
||
self._cache_time = datetime.now()
|
||
except IOError as e:
|
||
logger.error(f"Ошибка записи конфигурации групп: {e}")
|
||
raise
|
||
|
||
async def get_all_groups(self, include_ids: bool = False, messenger: Optional[str] = None) -> Dict[str, Any]:
|
||
"""
|
||
Получить все группы из конфигурации.
|
||
|
||
Args:
|
||
include_ids: Включать ли полную конфигурацию групп (включая ID, мессенджер и т.д.).
|
||
messenger: Фильтр по типу мессенджера (опционально).
|
||
|
||
Returns:
|
||
Словарь с группами.
|
||
Если include_ids=False, возвращает только названия групп.
|
||
Если include_ids=True, возвращает полную конфигурацию групп.
|
||
"""
|
||
if not self._is_cache_valid():
|
||
try:
|
||
self._cache = await self._load_config()
|
||
self._cache_time = datetime.now()
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
logger.error("Не удалось загрузить конфигурацию групп")
|
||
return {}
|
||
|
||
# Фильтруем по мессенджеру, если указан
|
||
filtered_config = self._cache.copy()
|
||
if messenger:
|
||
filtered_config = {
|
||
name: config
|
||
for name, config in filtered_config.items()
|
||
if config.get("messenger") == messenger
|
||
}
|
||
|
||
if include_ids:
|
||
return filtered_config.copy()
|
||
else:
|
||
# Возвращаем только названия групп без конфигурации
|
||
return {name: None for name in filtered_config.keys()}
|
||
|
||
async def create_group(
|
||
self,
|
||
group_name: str,
|
||
chat_id: Union[int, str],
|
||
messenger: str = "telegram",
|
||
thread_id: int = 0,
|
||
config: Optional[Dict[str, Any]] = None
|
||
) -> bool:
|
||
"""
|
||
Создать новую группу в конфигурации.
|
||
|
||
Args:
|
||
group_name: Имя группы.
|
||
chat_id: ID чата (может быть int или str).
|
||
messenger: Тип мессенджера (telegram, max).
|
||
thread_id: ID треда в группе (по умолчанию 0).
|
||
config: Дополнительная конфигурация для мессенджера (опционально).
|
||
|
||
Returns:
|
||
True если группа создана успешно, False если группа уже существует.
|
||
"""
|
||
# Загружаем текущую конфигурацию
|
||
if not self._is_cache_valid():
|
||
try:
|
||
self._cache = await self._load_config()
|
||
self._cache_time = datetime.now()
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
# Если файл не существует, создаем новый
|
||
self._cache = {}
|
||
|
||
# Проверяем, существует ли группа
|
||
if group_name in self._cache:
|
||
logger.warning(f"Группа '{group_name}' уже существует")
|
||
return False
|
||
|
||
# Добавляем группу
|
||
self._cache[group_name] = {
|
||
"messenger": messenger,
|
||
"chat_id": chat_id,
|
||
"thread_id": thread_id,
|
||
"config": config or {}
|
||
}
|
||
await self._save_config(self._cache)
|
||
logger.info(f"Группа '{group_name}' создана с мессенджером '{messenger}' и ID {chat_id}")
|
||
return True
|
||
|
||
async def update_group(
|
||
self,
|
||
group_name: str,
|
||
chat_id: Optional[Union[int, str]] = None,
|
||
messenger: Optional[str] = None,
|
||
thread_id: Optional[int] = None,
|
||
config: Optional[Dict[str, Any]] = None
|
||
) -> bool:
|
||
"""
|
||
Обновить существующую группу в конфигурации.
|
||
|
||
Args:
|
||
group_name: Имя группы.
|
||
chat_id: Новый ID чата (опционально).
|
||
messenger: Новый тип мессенджера (опционально).
|
||
thread_id: Новый ID треда (опционально).
|
||
config: Новая дополнительная конфигурация (опционально).
|
||
|
||
Returns:
|
||
True если группа обновлена успешно, False если группа не найдена.
|
||
"""
|
||
# Загружаем текущую конфигурацию
|
||
if not self._is_cache_valid():
|
||
try:
|
||
self._cache = await self._load_config()
|
||
self._cache_time = datetime.now()
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
logger.error("Не удалось загрузить конфигурацию групп")
|
||
return False
|
||
|
||
# Проверяем, существует ли группа
|
||
if group_name not in self._cache:
|
||
logger.warning(f"Группа '{group_name}' не найдена")
|
||
return False
|
||
|
||
# Обновляем группу (обновляем только указанные поля)
|
||
old_config = self._cache[group_name].copy()
|
||
if chat_id is not None:
|
||
self._cache[group_name]["chat_id"] = chat_id
|
||
if messenger is not None:
|
||
self._cache[group_name]["messenger"] = messenger
|
||
if thread_id is not None:
|
||
self._cache[group_name]["thread_id"] = thread_id
|
||
if config is not None:
|
||
self._cache[group_name]["config"] = config
|
||
|
||
await self._save_config(self._cache)
|
||
logger.info(f"Группа '{group_name}' обновлена: {old_config} -> {self._cache[group_name]}")
|
||
return True
|
||
|
||
async def delete_group(self, group_name: str) -> bool:
|
||
"""
|
||
Удалить группу из конфигурации.
|
||
|
||
Args:
|
||
group_name: Имя группы.
|
||
|
||
Returns:
|
||
True если группа удалена успешно, False если группа не найдена.
|
||
"""
|
||
# Загружаем текущую конфигурацию
|
||
if not self._is_cache_valid():
|
||
try:
|
||
self._cache = await self._load_config()
|
||
self._cache_time = datetime.now()
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
logger.error("Не удалось загрузить конфигурацию групп")
|
||
return False
|
||
|
||
# Проверяем, существует ли группа
|
||
if group_name not in self._cache:
|
||
logger.warning(f"Группа '{group_name}' не найдена")
|
||
return False
|
||
|
||
# Удаляем группу
|
||
del self._cache[group_name]
|
||
await self._save_config(self._cache)
|
||
logger.info(f"Группа '{group_name}' удалена")
|
||
return True
|
||
|
||
|
||
# Глобальный экземпляр менеджера конфигурации (lazy initialization)
|
||
_groups_config_instance = None
|
||
|
||
|
||
def get_groups_config() -> GroupsConfig:
|
||
"""
|
||
Получить экземпляр менеджера конфигурации групп (lazy initialization).
|
||
|
||
Returns:
|
||
Экземпляр GroupsConfig.
|
||
"""
|
||
global _groups_config_instance
|
||
if _groups_config_instance is None:
|
||
_groups_config_instance = GroupsConfig()
|
||
return _groups_config_instance
|
||
|
||
|
||
# Глобальный экземпляр менеджера конфигурации (lazy initialization)
|
||
class _GroupsConfigProxy:
|
||
"""Прокси для ленивой инициализации groups_config."""
|
||
|
||
def __getattr__(self, name):
|
||
"""Получить атрибут из groups_config."""
|
||
return getattr(get_groups_config(), name)
|
||
|
||
|
||
groups_config = _GroupsConfigProxy()
|