Initial commit: Message Gateway project

- FastAPI приложение для отправки мониторинговых алертов в мессенджеры
- Поддержка Telegram и MAX/VK
- Интеграция с Grafana, Zabbix, AlertManager
- Автоматическое создание тикетов в Jira
- Управление группами мессенджеров через API
- Декораторы для авторизации и скрытия эндпоинтов
- Подробная документация в папке docs/

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
2025-11-12 20:25:11 +03:00
commit b90def35ed
72 changed files with 10609 additions and 0 deletions

View File

@@ -0,0 +1,511 @@
"""
Эндпоинты для управления группами мессенджеров.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import logging
from typing import Dict, Any, Optional
from fastapi import APIRouter, HTTPException, Query, Path, Body, Request
from app.core.metrics import metrics
from app.core.groups import groups_config
from app.core.auth import require_api_key, require_api_key_dependency, require_api_key_optional
from app.models.group import (
CreateGroupRequest,
UpdateGroupRequest,
DeleteGroupRequest,
GroupInfo
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/groups", tags=["groups"])
@router.get(
"/messengers",
name="Получить список поддерживаемых мессенджеров",
response_model=Dict[str, Any],
responses={
200: {
"description": "Список поддерживаемых мессенджеров",
"content": {
"application/json": {
"example": {
"status": "ok",
"messengers": [
{
"type": "telegram",
"name": "Telegram",
"supports_threads": True,
"enabled": True
},
{
"type": "max",
"name": "MAX/VK",
"supports_threads": False,
"enabled": False
}
]
}
}
}
}
}
)
async def get_messengers() -> Dict[str, Any]:
"""
Получить список поддерживаемых мессенджеров.
Returns:
Список поддерживаемых мессенджеров с их характеристиками.
Подробная документация: см. docs/api/groups.md
"""
from app.core.config import get_settings
settings = get_settings()
messengers = [
{
"type": "telegram",
"name": "Telegram",
"supports_threads": True,
"enabled": settings.telegram_enabled
},
{
"type": "max",
"name": "MAX/VK",
"supports_threads": False,
"enabled": settings.max_enabled
}
]
return {
"status": "ok",
"messengers": messengers
}
@router.get(
"",
name="Получить список групп",
response_model=Dict[str, Any],
responses={
200: {
"description": "Список групп успешно получен",
"content": {
"application/json": {
"examples": {
"without_api_key": {
"summary": "Без API ключа (только названия)",
"value": {
"status": "ok",
"groups": [
{"name": "monitoring", "chat_id": None},
{"name": "alerts", "chat_id": None}
],
"count": 2
}
},
"with_api_key": {
"summary": "С API ключом (полная информация)",
"value": {
"status": "ok",
"groups": [
{
"name": "monitoring",
"messenger": "telegram",
"chat_id": -1001234567890,
"thread_id": 0
},
{
"name": "alerts_max",
"messenger": "max",
"chat_id": "123456789",
"thread_id": None
}
],
"count": 2
}
}
}
}
}
},
401: {
"description": "Неверный API ключ",
"content": {
"application/json": {
"example": {"detail": "Неверный или отсутствующий API ключ"}
}
}
},
500: {
"description": "Ошибка сервера",
"content": {
"application/json": {
"example": {"detail": "Ошибка получения списка групп"}
}
}
}
}
)
async def get_groups(
request: Request,
api_key_header: Optional[bool] = require_api_key_optional
) -> Dict[str, Any]:
"""
Получить список всех групп.
Без API ключа: возвращает только названия групп без ID.
С API ключом (заголовок X-API-Key): возвращает полную информацию о группах включая ID.
Подробная документация: см. docs/api/groups.md
"""
metrics.increment_api_endpoint("groups_list")
# Если API ключ валиден, возвращаем полную информацию
include_ids = api_key_header is True
# Получаем группы
try:
groups_dict = await groups_config.get_all_groups(include_ids=include_ids)
# Формируем список групп
groups = []
for name, group_config in groups_dict.items():
if include_ids:
# Возвращаем полную информацию о группе
if isinstance(group_config, dict) and group_config is not None:
groups.append(GroupInfo(
name=name,
messenger=group_config.get("messenger"),
chat_id=group_config.get("chat_id"),
thread_id=group_config.get("thread_id")
))
else:
# Если group_config is None, значит include_ids=False, но мы здесь не должны быть
groups.append(GroupInfo(
name=name,
messenger=None,
chat_id=None,
thread_id=None
))
else:
# Возвращаем только название группы (group_config будет None)
groups.append(GroupInfo(
name=name,
messenger=None,
chat_id=None,
thread_id=None
))
return {
"status": "ok",
"groups": [group.model_dump() for group in groups],
"count": len(groups)
}
except Exception as e:
logger.error(f"Ошибка получения списка групп: {e}")
raise HTTPException(status_code=500, detail=f"Ошибка получения списка групп: {str(e)}")
@require_api_key
@router.post(
"",
name="Создать группу",
response_model=Dict[str, Any],
dependencies=[require_api_key_dependency],
responses={
200: {
"description": "Группа успешно создана",
"content": {
"application/json": {
"example": {
"status": "ok",
"message": "Группа 'monitoring' создана с ID -1001234567890"
}
}
}
},
400: {
"description": "Ошибка запроса (группа уже существует или неверные данные)",
"content": {
"application/json": {
"examples": {
"group_exists": {
"summary": "Группа уже существует",
"value": {"detail": "Группа 'monitoring' уже существует"}
},
"invalid_data": {
"summary": "Неверные данные",
"value": {"detail": "Неверный формат данных"}
}
}
}
}
},
401: {
"description": "Ошибка авторизации (неверный API ключ)",
"content": {
"application/json": {
"example": {"detail": "Неверный или отсутствующий API ключ"}
}
}
},
500: {
"description": "Ошибка сервера",
"content": {
"application/json": {
"example": {"detail": "Ошибка создания группы"}
}
}
}
}
)
async def create_group(
request: Request,
body: CreateGroupRequest = Body(
...,
examples=[
{
"group_name": "monitoring",
"messenger": "telegram",
"chat_id": -1001234567890,
"thread_id": 0
},
{
"group_name": "max_alerts",
"messenger": "max",
"chat_id": "123456789",
"thread_id": 0,
"config": {
"access_token": "your_max_access_token"
}
}
]
)
) -> Dict[str, Any]:
"""
Создать новую группу в конфигурации.
Требуется API ключ в заголовке X-API-Key.
Подробная документация: см. docs/api/groups.md
"""
metrics.increment_api_endpoint("groups_create")
# Создаем группу
try:
success = await groups_config.create_group(
group_name=body.group_name,
chat_id=body.chat_id,
messenger=body.messenger,
thread_id=body.thread_id,
config=body.config
)
if not success:
raise HTTPException(
status_code=400,
detail=f"Группа '{body.group_name}' уже существует"
)
return {
"status": "ok",
"message": f"Группа '{body.group_name}' создана с мессенджером '{body.messenger}' и ID {body.chat_id}"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Ошибка создания группы: {e}")
raise HTTPException(status_code=500, detail=f"Ошибка создания группы: {str(e)}")
@require_api_key
@router.put(
"/{group_name}",
name="Обновить группу",
response_model=Dict[str, Any],
dependencies=[require_api_key_dependency],
responses={
200: {
"description": "Группа успешно обновлена",
"content": {
"application/json": {
"example": {
"status": "ok",
"message": "Группа 'monitoring' обновлена с ID -1001234567891"
}
}
}
},
400: {
"description": "Ошибка запроса (группа не найдена или неверные данные)",
"content": {
"application/json": {
"example": {"detail": "Группа 'monitoring' не найдена"}
}
}
},
401: {
"description": "Ошибка авторизации (неверный API ключ)",
"content": {
"application/json": {
"example": {"detail": "Неверный или отсутствующий API ключ"}
}
}
},
500: {
"description": "Ошибка сервера",
"content": {
"application/json": {
"example": {"detail": "Ошибка обновления группы"}
}
}
}
}
)
async def update_group(
request: Request,
group_name: str = Path(
...,
description="Имя группы для обновления",
examples=["monitoring", "alerts", "devops"]
),
body: UpdateGroupRequest = Body(
...,
examples=[
{
"chat_id": -1001234567891,
"messenger": "telegram",
"thread_id": 0
},
{
"chat_id": "123456789",
"messenger": "max",
"config": {
"access_token": "your_access_token",
"api_version": "5.131"
}
}
]
)
) -> Dict[str, Any]:
"""
Обновить существующую группу в конфигурации.
Требуется API ключ в заголовке X-API-Key.
Подробная документация: см. docs/api/groups.md
"""
metrics.increment_api_endpoint("groups_update")
# Обновляем группу
try:
success = await groups_config.update_group(
group_name=group_name,
chat_id=body.chat_id,
messenger=body.messenger,
thread_id=body.thread_id,
config=body.config
)
if not success:
raise HTTPException(
status_code=400,
detail=f"Группа '{group_name}' не найдена"
)
return {
"status": "ok",
"message": f"Группа '{group_name}' обновлена"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Ошибка обновления группы: {e}")
raise HTTPException(status_code=500, detail=f"Ошибка обновления группы: {str(e)}")
@require_api_key
@router.delete(
"/{group_name}",
name="Удалить группу",
response_model=Dict[str, Any],
dependencies=[require_api_key_dependency],
responses={
200: {
"description": "Группа успешно удалена",
"content": {
"application/json": {
"example": {
"status": "ok",
"message": "Группа 'monitoring' удалена"
}
}
}
},
400: {
"description": "Ошибка запроса (группа не найдена)",
"content": {
"application/json": {
"example": {"detail": "Группа 'monitoring' не найдена"}
}
}
},
401: {
"description": "Ошибка авторизации (неверный API ключ)",
"content": {
"application/json": {
"example": {"detail": "Неверный или отсутствующий API ключ"}
}
}
},
500: {
"description": "Ошибка сервера",
"content": {
"application/json": {
"example": {"detail": "Ошибка удаления группы"}
}
}
}
}
)
async def delete_group(
request: Request,
group_name: str = Path(
...,
description="Имя группы для удаления",
examples=["monitoring", "alerts", "devops"]
)
) -> Dict[str, Any]:
"""
Удалить группу из конфигурации.
Требуется API ключ в заголовке X-API-Key.
Подробная документация: см. docs/api/groups.md
"""
metrics.increment_api_endpoint("groups_delete")
# Удаляем группу
try:
success = await groups_config.delete_group(group_name)
if not success:
raise HTTPException(
status_code=400,
detail=f"Группа '{group_name}' не найдена"
)
return {
"status": "ok",
"message": f"Группа '{group_name}' удалена"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Ошибка удаления группы: {e}")
raise HTTPException(status_code=500, detail=f"Ошибка удаления группы: {str(e)}")