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

6
app/api/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""
API модули приложения.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""

6
app/api/v1/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""
API версии 1.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""

View File

@@ -0,0 +1,23 @@
"""
Эндпоинты API версии 1.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from app.api.v1.endpoints import (
health,
monitoring,
debug,
jira,
message,
groups,
)
__all__ = [
"health",
"monitoring",
"debug",
"jira",
"message",
"groups",
]

View File

@@ -0,0 +1,114 @@
"""
Эндпоинты для отладки.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import json
import logging
from typing import Dict, Any
from fastapi import APIRouter, HTTPException, Body
import aiofiles
from app.core.metrics import metrics
from app.core.auth import hide_from_api
# Импортируем settings в функции, чтобы избежать циклических зависимостей
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/debug", tags=["debug"])
@hide_from_api
@router.post(
"/dump",
name="JSON Debug dump",
response_model=Dict[str, Any],
include_in_schema=False, # Скрываем эндпоинт из Swagger UI
responses={
200: {
"description": "Данные успешно сохранены",
"content": {
"application/json": {
"example": {
"status": "ok",
"message": "Данные сохранены в dump.json",
"data": {
"test": "data",
"timestamp": "2024-01-01T00:00:00Z",
"source": "grafana",
"alert": {
"title": "Test alert",
"state": "alerting"
}
}
}
}
}
},
500: {
"description": "Ошибка сохранения данных",
"content": {
"application/json": {
"example": {"detail": "Ошибка записи в файл"}
}
}
}
}
)
async def dump_request(
dump: Dict[str, Any] = Body(
...,
description="JSON данные для сохранения в файл dump.json",
examples=[
{
"test": "data",
"timestamp": "2024-01-01T00:00:00Z",
"source": "grafana",
"alert": {
"title": "Test alert",
"state": "alerting"
}
},
{
"source": "zabbix",
"event": {
"event-id": "8819711",
"event-name": "High CPU utilization",
"status": "PROBLEM"
}
},
{
"source": "alertmanager",
"status": "firing",
"commonLabels": {
"alertname": "HighCPUUsage",
"severity": "critical"
}
}
]
)
) -> Dict[str, Any]:
"""
Сохранить JSON данные в файл для отладки.
Используется для сохранения входящих webhook запросов для анализа.
Подробная документация: см. docs/api/debug.md
"""
metrics.increment_api_endpoint("debug_dump")
logger.info("Получен запрос на сохранение данных для отладки")
try:
dump_path = "/app/app/dump.json"
async with aiofiles.open(dump_path, 'w', encoding='utf-8') as f:
await f.write(json.dumps(dump, indent=4, ensure_ascii=False))
logger.info(f"Данные сохранены в {dump_path}")
return {
"status": "ok",
"message": "Данные сохранены в dump.json",
"data": dump
}
except Exception as e:
logger.error(f"Ошибка сохранения данных: {e}")
raise HTTPException(status_code=500, detail=f"Ошибка записи в файл: {str(e)}")

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)}")

View File

@@ -0,0 +1,111 @@
"""
Эндпоинты для проверки здоровья приложения.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import logging
from fastapi import APIRouter, HTTPException
from typing import Dict, Any
from app.core.metrics import metrics
from app.core.groups import groups_config
from app.core.config import get_settings
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/health", tags=["health"])
@router.get(
"",
name="Проверка здоровья приложения",
response_model=Dict[str, Any],
responses={
200: {
"description": "Приложение работает и готово",
"content": {
"application/json": {
"examples": {
"healthy": {
"summary": "Приложение здорово",
"value": {
"status": "healthy",
"state": "online",
"telegram_bot_configured": True,
"groups_config_available": True
}
},
"not_ready": {
"summary": "Приложение не готово",
"value": {
"status": "not_ready",
"state": "online",
"checks": {
"telegram_bot_configured": False,
"groups_config_available": True
}
}
}
}
}
}
},
503: {
"description": "Приложение не готово к работе",
"content": {
"application/json": {
"example": {
"status": "not_ready",
"checks": {
"telegram_bot_configured": False,
"groups_config_available": True
}
}
}
}
}
}
)
async def health_check() -> Dict[str, Any]:
"""
Проверка здоровья и готовности приложения для Kubernetes probes.
Объединенный endpoint для liveness и readiness probes.
Не требует аутентификации.
Подробная документация: см. docs/api/health.md
"""
metrics.increment_api_endpoint("health")
settings = get_settings()
checks = {
"telegram_bot_configured": bool(settings.telegram_bot_token),
"groups_config_available": False,
}
# Проверяем доступность конфигурации групп
try:
await groups_config.refresh_cache()
checks["groups_config_available"] = True
except Exception as e:
logger.error(f"Ошибка при проверке конфигурации групп: {e}")
checks["groups_config_available"] = False
# Если не все проверки пройдены, возвращаем 503
if not all(checks.values()):
raise HTTPException(
status_code=503,
detail={
"status": "not_ready",
"state": "online",
"checks": checks
}
)
return {
"status": "healthy",
"state": "online",
**checks
}

View File

@@ -0,0 +1,19 @@
"""
Эндпоинты для работы с Jira (только для внутреннего использования).
Создание тикетов по кнопке и работа с маппингом выполняются внутренними процессами.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import logging
from typing import Dict, Any
from fastapi import APIRouter
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/jira", tags=["jira"])
# Этот модуль оставлен пустым, так как все операции с Jira выполняются внутренними процессами
# Создание тикетов происходит автоматически при обработке алертов (см. app/modules/*.py)
# Маппинг загружается автоматически из config/jira_mapping.json (см. app/core/jira_mapping.py)

View File

@@ -0,0 +1,468 @@
"""
Эндпоинты для отправки простых сообщений в мессенджеры.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import logging
from typing import Dict, Any, Optional
from fastapi import APIRouter, HTTPException, Query, Request, Body
from app.core.metrics import metrics
from app.core.groups import groups_config
from app.core.messenger_factory import MessengerFactory
from app.core.auth import require_api_key, require_api_key_dependency
from app.models.message import (
SendMessageRequest,
SendPhotoRequest,
SendVideoRequest,
SendAudioRequest,
SendDocumentRequest
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/message", tags=["message"])
async def _send_message_to_group(
group_name: str,
thread_id: int,
messenger: Optional[str],
send_func,
*args,
**kwargs
) -> Dict[str, Any]:
"""
Вспомогательная функция для отправки сообщений в группу.
Args:
group_name: Имя группы из конфигурации.
thread_id: ID треда в группе (0 для основной группы).
messenger: Тип мессенджера (опционально).
send_func: Функция отправки сообщения.
*args: Дополнительные аргументы для функции отправки.
**kwargs: Дополнительные параметры для функции отправки.
Returns:
Результат отправки сообщения.
Raises:
HTTPException: Если группа не найдена или произошла ошибка отправки.
"""
# Получаем конфигурацию группы
group_config = await groups_config.get_group_config(group_name, messenger)
if group_config is None:
raise HTTPException(
status_code=400,
detail=f"Группа '{group_name}' не найдена в конфигурации"
)
messenger_type = group_config.get("messenger", "telegram")
chat_id = group_config.get("chat_id")
group_thread_id = group_config.get("thread_id", 0)
# Используем thread_id из параметра, если указан, иначе из конфигурации группы
final_thread_id = thread_id if thread_id > 0 else group_thread_id
# Создаем клиент мессенджера
messenger_client = MessengerFactory.create_from_config(group_config)
# Проверяем поддержку тредов
if not messenger_client.supports_threads and final_thread_id > 0:
logger.warning(f"Мессенджер '{messenger_type}' не поддерживает треды, thread_id будет проигнорирован")
final_thread_id = None
elif final_thread_id == 0:
final_thread_id = None
# Вызываем функцию отправки
success = await send_func(
messenger_client,
chat_id=chat_id,
thread_id=final_thread_id,
*args,
**kwargs
)
if not success:
raise HTTPException(
status_code=500,
detail=f"Ошибка отправки сообщения в {messenger_type}"
)
return {
"status": "ok",
"message": f"Сообщение отправлено в чат {group_name}, тред {thread_id if thread_id > 0 else 0}"
}
@require_api_key
@router.post(
"/text",
name="Отправить текстовое сообщение",
response_model=Dict[str, Any],
dependencies=[require_api_key_dependency],
responses={
200: {
"description": "Сообщение отправлено успешно",
"content": {
"application/json": {
"example": {
"status": "ok",
"message": "Сообщение отправлено в чат monitoring, тред 0"
}
}
}
},
400: {
"description": "Ошибка запроса",
"content": {
"application/json": {
"example": {"detail": "Группа 'monitoring' не найдена в конфигурации"}
}
}
},
401: {
"description": "Требуется API ключ",
"content": {
"application/json": {
"example": {"detail": "Неверный или отсутствующий API ключ"}
}
}
},
500: {
"description": "Ошибка отправки сообщения",
"content": {
"application/json": {
"example": {"detail": "Ошибка отправки сообщения в telegram"}
}
}
}
}
)
async def send_text_message(
request: Request,
body: SendMessageRequest = Body(...),
messenger: Optional[str] = Query(None, description="Тип мессенджера (telegram, max). Если не указан, используется из конфигурации группы", examples=["telegram", "max"])
) -> Dict[str, Any]:
"""
Отправить текстовое сообщение в группу мессенджера.
Подробная документация: см. docs/api/message.md
"""
metrics.increment_api_endpoint("message_text")
async def send_func(client, chat_id, thread_id, **kwargs):
return await client.send_message(
chat_id=chat_id,
text=body.text,
thread_id=thread_id,
disable_web_page_preview=body.disable_web_page_preview,
parse_mode=body.parse_mode
)
return await _send_message_to_group(
group_name=body.tg_group,
thread_id=body.tg_thread_id,
messenger=messenger,
send_func=send_func
)
@require_api_key
@router.post(
"/photo",
name="Отправить фото",
response_model=Dict[str, Any],
dependencies=[require_api_key_dependency],
responses={
200: {
"description": "Фото отправлено успешно",
"content": {
"application/json": {
"example": {
"status": "ok",
"message": "Фото отправлено в чат monitoring, тред 0"
}
}
}
},
400: {
"description": "Ошибка запроса",
"content": {
"application/json": {
"example": {"detail": "Группа 'monitoring' не найдена в конфигурации"}
}
}
},
401: {
"description": "Требуется API ключ",
"content": {
"application/json": {
"example": {"detail": "Неверный или отсутствующий API ключ"}
}
}
},
500: {
"description": "Ошибка отправки фото",
"content": {
"application/json": {
"example": {"detail": "Ошибка отправки фото в telegram"}
}
}
}
}
)
async def send_photo(
request: Request,
body: SendPhotoRequest = Body(...),
messenger: Optional[str] = Query(None, description="Тип мессенджера (telegram, max). Если не указан, используется из конфигурации группы", examples=["telegram", "max"])
) -> Dict[str, Any]:
"""
Отправить фото в группу мессенджера.
Подробная документация: см. docs/api/message.md
"""
metrics.increment_api_endpoint("message_photo")
async def send_func(client, chat_id, thread_id, **kwargs):
return await client.send_photo(
chat_id=chat_id,
photo=body.photo,
caption=body.caption,
thread_id=thread_id,
parse_mode=body.parse_mode
)
return await _send_message_to_group(
group_name=body.tg_group,
thread_id=body.tg_thread_id,
messenger=messenger,
send_func=send_func
)
@require_api_key
@router.post(
"/video",
name="Отправить видео",
response_model=Dict[str, Any],
dependencies=[require_api_key_dependency],
responses={
200: {
"description": "Видео отправлено успешно",
"content": {
"application/json": {
"example": {
"status": "ok",
"message": "Видео отправлено в чат monitoring, тред 0"
}
}
}
},
400: {
"description": "Ошибка запроса",
"content": {
"application/json": {
"example": {"detail": "Группа 'monitoring' не найдена в конфигурации"}
}
}
},
401: {
"description": "Требуется API ключ",
"content": {
"application/json": {
"example": {"detail": "Неверный или отсутствующий API ключ"}
}
}
},
500: {
"description": "Ошибка отправки видео",
"content": {
"application/json": {
"example": {"detail": "Ошибка отправки видео в telegram"}
}
}
}
}
)
async def send_video(
request: Request,
body: SendVideoRequest = Body(...),
messenger: Optional[str] = Query(None, description="Тип мессенджера (telegram, max). Если не указан, используется из конфигурации группы", examples=["telegram", "max"])
) -> Dict[str, Any]:
"""
Отправить видео в группу мессенджера.
Подробная документация: см. docs/api/message.md
"""
metrics.increment_api_endpoint("message_video")
async def send_func(client, chat_id, thread_id, **kwargs):
return await client.send_video(
chat_id=chat_id,
video=body.video,
caption=body.caption,
thread_id=thread_id,
parse_mode=body.parse_mode,
duration=body.duration,
width=body.width,
height=body.height
)
return await _send_message_to_group(
group_name=body.tg_group,
thread_id=body.tg_thread_id,
messenger=messenger,
send_func=send_func
)
@require_api_key
@router.post(
"/audio",
name="Отправить аудио",
response_model=Dict[str, Any],
dependencies=[require_api_key_dependency],
responses={
200: {
"description": "Аудио отправлено успешно",
"content": {
"application/json": {
"example": {
"status": "ok",
"message": "Аудио отправлено в чат monitoring, тред 0"
}
}
}
},
400: {
"description": "Ошибка запроса",
"content": {
"application/json": {
"example": {"detail": "Группа 'monitoring' не найдена в конфигурации"}
}
}
},
401: {
"description": "Требуется API ключ",
"content": {
"application/json": {
"example": {"detail": "Неверный или отсутствующий API ключ"}
}
}
},
500: {
"description": "Ошибка отправки аудио",
"content": {
"application/json": {
"example": {"detail": "Ошибка отправки аудио в telegram"}
}
}
}
}
)
async def send_audio(
request: Request,
body: SendAudioRequest = Body(...),
messenger: Optional[str] = Query(None, description="Тип мессенджера (telegram, max). Если не указан, используется из конфигурации группы", examples=["telegram", "max"])
) -> Dict[str, Any]:
"""
Отправить аудио в группу мессенджера.
Подробная документация: см. docs/api/message.md
"""
metrics.increment_api_endpoint("message_audio")
async def send_func(client, chat_id, thread_id, **kwargs):
return await client.send_audio(
chat_id=chat_id,
audio=body.audio,
caption=body.caption,
thread_id=thread_id,
parse_mode=body.parse_mode,
duration=body.duration,
performer=body.performer,
title=body.title
)
return await _send_message_to_group(
group_name=body.tg_group,
thread_id=body.tg_thread_id,
messenger=messenger,
send_func=send_func
)
@require_api_key
@router.post(
"/document",
name="Отправить документ",
response_model=Dict[str, Any],
dependencies=[require_api_key_dependency],
responses={
200: {
"description": "Документ отправлен успешно",
"content": {
"application/json": {
"example": {
"status": "ok",
"message": "Документ отправлен в чат monitoring, тред 0"
}
}
}
},
400: {
"description": "Ошибка запроса",
"content": {
"application/json": {
"example": {"detail": "Группа 'monitoring' не найдена в конфигурации"}
}
}
},
401: {
"description": "Требуется API ключ",
"content": {
"application/json": {
"example": {"detail": "Неверный или отсутствующий API ключ"}
}
}
},
500: {
"description": "Ошибка отправки документа",
"content": {
"application/json": {
"example": {"detail": "Ошибка отправки документа в telegram"}
}
}
}
}
)
async def send_document(
request: Request,
body: SendDocumentRequest = Body(...),
messenger: Optional[str] = Query(None, description="Тип мессенджера (telegram, max). Если не указан, используется из конфигурации группы", examples=["telegram", "max"])
) -> Dict[str, Any]:
"""
Отправить документ в группу мессенджера.
Подробная документация: см. docs/api/message.md
"""
metrics.increment_api_endpoint("message_document")
async def send_func(client, chat_id, thread_id, **kwargs):
return await client.send_document(
chat_id=chat_id,
document=body.document,
caption=body.caption,
thread_id=thread_id,
parse_mode=body.parse_mode,
filename=body.filename
)
return await _send_message_to_group(
group_name=body.tg_group,
thread_id=body.tg_thread_id,
messenger=messenger,
send_func=send_func
)

View File

@@ -0,0 +1,403 @@
"""
Эндпоинты для обработки webhooks из систем мониторинга (Grafana, Zabbix, AlertManager).
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import logging
from fastapi import APIRouter, HTTPException, Path, Body, Query
from typing import Dict, Optional
from app.models.grafana import GrafanaAlert
from app.models.zabbix import ZabbixAlert
from app.models.alertmanager import PrometheusAlert
from app.modules.grafana import send as grafana_send
from app.modules.zabbix import send as zabbix_send
from app.modules.alertmanager import send as alertmanager_send
from app.core.metrics import metrics
logger = logging.getLogger(__name__)
router = APIRouter(tags=["monitoring"])
@router.post(
"/grafana/{group_name}/{thread_id}",
name="Отправка вебхуков из Grafana",
response_model=Dict[str, str],
summary="Отправить алерт из Grafana",
description="Эндпоинт для обработки webhooks из Grafana. **Не требует авторизации.**",
responses={
200: {
"description": "Сообщение успешно отправлено",
"content": {
"application/json": {
"example": {"status": "ok", "message": "Сообщение отправлено"}
}
}
},
400: {
"description": "Некорректные данные запроса",
"content": {
"application/json": {
"example": {"detail": "Неверный формат данных для Grafana алерта"}
}
}
},
404: {
"description": "Группа не найдена",
"content": {
"application/json": {
"example": {"detail": "Группа 'monitoring' не найдена в конфигурации"}
}
}
},
500: {
"description": "Ошибка сервера",
"content": {
"application/json": {
"example": {"detail": "Ошибка отправки сообщения"}
}
}
}
}
)
async def send_grafana_alert(
group_name: str = Path(
...,
description="Имя группы из конфигурации (config/groups.json)",
examples=["monitoring", "alerts", "devops"]
),
thread_id: int = Path(
...,
description="ID треда в группе (0 для отправки в основную группу без треда, поддерживается только для Telegram)",
examples=[0, 123, 456]
),
messenger: Optional[str] = Query(
None,
description="Тип мессенджера (telegram, max). Если не указан, используется из конфигурации группы",
examples=["telegram", "max"]
),
alert: GrafanaAlert = Body(
...,
description="Данные алерта из Grafana",
examples=[
{
"title": "[Alerting] High CPU Usage",
"ruleId": 674180201771804383,
"ruleName": "High CPU Usage Alert",
"state": "alerting",
"evalMatches": [
{
"value": 95.5,
"metric": "cpu_usage_percent",
"tags": {"host": "server01", "instance": "production"}
}
],
"orgId": 1,
"dashboardId": 123,
"panelId": 456,
"tags": {"severity": "critical", "environment": "production"},
"ruleUrl": "http://grafana.cism-ms.ru/alerting/list",
"message": "CPU usage is above 90% threshold for more than 5 minutes"
},
{
"title": "[OK] High CPU Usage",
"ruleId": 674180201771804383,
"ruleName": "High CPU Usage Alert",
"state": "ok",
"evalMatches": [
{
"value": 45.2,
"metric": "cpu_usage_percent",
"tags": {"host": "server01", "instance": "production"}
}
],
"orgId": 1,
"dashboardId": 123,
"panelId": 456,
"tags": {"severity": "critical", "environment": "production"},
"ruleUrl": "http://grafana.cism-ms.ru/alerting/list",
"message": "CPU usage has returned to normal levels"
}
]
)
) -> Dict[str, str]:
"""
Отправить алерт из Grafana в мессенджер.
Принимает webhook от Grafana и отправляет сообщение в указанную группу мессенджера.
Не требует авторизации (API ключ не нужен).
Подробная документация: см. docs/monitoring/grafana.md
"""
metrics.increment_api_endpoint("grafana")
logger.info(f"Получен алерт Grafana для группы {group_name}, тред {thread_id}, мессенджер {messenger}")
try:
await grafana_send(group_name, thread_id, alert, messenger)
return {
"status": "ok",
"message": "Сообщение отправлено"
}
except ValueError as e:
logger.error(f"Ошибка валидации: {e}")
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Ошибка отправки алерта Grafana: {e}")
raise HTTPException(status_code=500, detail=f"Ошибка отправки сообщения: {str(e)}")
@router.post(
"/zabbix/{group_name}/{thread_id}",
name="Отправка вебхуков из Zabbix",
response_model=Dict[str, str],
summary="Отправить алерт из Zabbix",
description="Эндпоинт для обработки webhooks из Zabbix. **Не требует авторизации.**",
responses={
200: {
"description": "Сообщение успешно отправлено",
"content": {
"application/json": {
"example": {"status": "ok", "message": "Сообщение отправлено"}
}
}
},
400: {
"description": "Некорректные данные запроса",
"content": {
"application/json": {
"example": {"detail": "Неверный формат данных для Zabbix алерта"}
}
}
},
404: {
"description": "Группа не найдена",
"content": {
"application/json": {
"example": {"detail": "Группа 'monitoring' не найдена в конфигурации"}
}
}
},
500: {
"description": "Ошибка сервера",
"content": {
"application/json": {
"example": {"detail": "Ошибка отправки сообщения"}
}
}
}
}
)
async def send_zabbix_alert(
group_name: str = Path(
...,
description="Имя группы из конфигурации (config/groups.json)",
examples=["monitoring", "alerts", "devops"]
),
thread_id: int = Path(
...,
description="ID треда в группе (0 для отправки в основную группу без треда, поддерживается только для Telegram)",
examples=[0, 123, 456]
),
messenger: Optional[str] = Query(
None,
description="Тип мессенджера (telegram, max). Если не указан, используется из конфигурации группы",
examples=["telegram", "max"]
),
alert: ZabbixAlert = Body(
...,
description="Данные алерта из Zabbix",
examples=[
{
"link": "https://zabbix.example.com/tr_events.php?triggerid=42667&eventid=8819711",
"status": "PROBLEM",
"action-id": "7",
"alert-subject": "Problem: High CPU utilization (over 90% for 5m)",
"alert-message": "Problem started at 16:48:44 on 2024.02.08\r\nProblem name: High CPU utilization (over 90% for 5m)\r\nHost: pnode28\r\nSeverity: Warning\r\nCurrent utilization: 95.2 %\r\n",
"event-id": "8819711",
"event-name": "High CPU utilization (over 90% for 5m)",
"event-nseverity": "2",
"event-opdata": "Current utilization: 95.2 %",
"event-severity": "Warning",
"host-name": "pnode28",
"host-ip": "10.14.253.38",
"host-port": "10050"
},
{
"link": "https://zabbix.example.com/tr_events.php?triggerid=42667&eventid=8819711",
"status": "OK",
"action-id": "7",
"alert-subject": "Resolved in 1m 0s: High CPU utilization (over 90% for 5m)",
"alert-message": "Problem has been resolved at 16:49:44 on 2024.02.08\r\nProblem name: High CPU utilization (over 90% for 5m)\r\nProblem duration: 1m 0s\r\nHost: pnode28\r\nSeverity: Warning\r\nOriginal problem ID: 8819711\r\n",
"event-id": "8819711",
"event-name": "High CPU utilization (over 90% for 5m)",
"event-nseverity": "2",
"event-opdata": "Current utilization: 70.9 %",
"event-recovery-date": "2024.02.08",
"event-recovery-time": "16:49:44",
"event-duration": "1m 0s",
"event-recovery-name": "High CPU utilization (over 90% for 5m)",
"event-recovery-status": "RESOLVED",
"event-recovery-tags": "Application:CPU",
"event-severity": "Warning",
"host-name": "pnode28",
"host-ip": "10.14.253.38",
"host-port": "10050"
}
]
)
) -> Dict[str, str]:
"""
Отправить алерт из Zabbix в мессенджер.
Принимает webhook от Zabbix и отправляет сообщение в указанную группу мессенджера.
Не требует авторизации (API ключ не нужен).
Подробная документация: см. docs/monitoring/zabbix.md
"""
metrics.increment_api_endpoint("zabbix")
logger.info(f"Получен алерт Zabbix для группы {group_name}, тред {thread_id}, мессенджер {messenger}")
try:
await zabbix_send(group_name, thread_id, alert, messenger)
return {
"status": "ok",
"message": "Сообщение отправлено"
}
except ValueError as e:
logger.error(f"Ошибка валидации: {e}")
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Ошибка отправки алерта Zabbix: {e}")
raise HTTPException(status_code=500, detail=f"Ошибка отправки сообщения: {str(e)}")
@router.post(
"/alertmanager/{k8s_cluster}/{group_name}/{thread_id}",
name="Отправка вебхуков из AlertManager",
response_model=Dict[str, str],
summary="Отправить алерт из AlertManager",
description="Эндпоинт для обработки webhooks из AlertManager. **Не требует авторизации.**",
responses={
200: {
"description": "Сообщение успешно отправлено",
"content": {
"application/json": {
"example": {"status": "ok", "message": "Сообщение отправлено"}
}
}
},
400: {
"description": "Некорректные данные запроса",
"content": {
"application/json": {
"example": {"detail": "Неверный формат данных для AlertManager алерта"}
}
}
},
404: {
"description": "Группа не найдена",
"content": {
"application/json": {
"example": {"detail": "Группа 'monitoring' не найдена в конфигурации"}
}
}
},
500: {
"description": "Ошибка сервера",
"content": {
"application/json": {
"example": {"detail": "Ошибка отправки сообщения"}
}
}
}
}
)
async def send_alertmanager_alert(
k8s_cluster: str = Path(
...,
description="Имя Kubernetes кластера (используется для формирования URL к Grafana/Prometheus)",
examples=["production", "staging", "development"]
),
group_name: str = Path(
...,
description="Имя группы из конфигурации (config/groups.json)",
examples=["monitoring", "alerts", "devops"]
),
thread_id: int = Path(
...,
description="ID треда в группе (0 для отправки в основную группу без треда, поддерживается только для Telegram)",
examples=[0, 123, 456]
),
messenger: Optional[str] = Query(
None,
description="Тип мессенджера (telegram, max). Если не указан, используется из конфигурации группы",
examples=["telegram", "max"]
),
alert: PrometheusAlert = Body(
...,
description="Данные алерта из AlertManager",
examples=[
{
"status": "firing",
"externalURL": "http://alertmanager.example.com",
"commonLabels": {
"alertname": "HighCPUUsage",
"severity": "critical",
"namespace": "production",
"pod": "app-deployment-7d8f9b4c5-abc123",
"container": "app-container"
},
"commonAnnotations": {
"summary": "High CPU usage detected in production namespace",
"description": "CPU usage is above 90% for 5 minutes on pod app-deployment-7d8f9b4c5-abc123",
"runbook_url": "https://wiki.example.com/runbooks/high-cpu-usage"
}
},
{
"status": "resolved",
"externalURL": "http://alertmanager.example.com",
"commonLabels": {
"alertname": "HighCPUUsage",
"severity": "critical",
"namespace": "production",
"pod": "app-deployment-7d8f9b4c5-abc123",
"container": "app-container"
},
"commonAnnotations": {
"summary": "High CPU usage resolved in production namespace",
"description": "CPU usage has returned to normal levels on pod app-deployment-7d8f9b4c5-abc123"
}
}
]
)
) -> Dict[str, str]:
"""
Отправить алерт из AlertManager в мессенджер.
Эндпоинт для обработки webhooks из AlertManager.
Не требует авторизации (API ключ не нужен).
Подробная документация: см. docs/monitoring/alertmanager.md
"""
metrics.increment_api_endpoint("alertmanager")
logger.info(f"Получен алерт AlertManager для кластера {k8s_cluster}, группы {group_name}, тред {thread_id}, мессенджер {messenger}")
try:
if not isinstance(alert, PrometheusAlert):
raise HTTPException(status_code=400, detail="Неверный формат данных для AlertManager алерта")
await alertmanager_send(k8s_cluster, group_name, thread_id, alert, messenger)
return {
"status": "ok",
"message": "Сообщение отправлено"
}
except HTTPException:
raise
except ValueError as e:
logger.error(f"Ошибка валидации: {e}")
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Ошибка отправки алерта AlertManager: {e}")
raise HTTPException(status_code=500, detail=f"Ошибка отправки сообщения: {str(e)}")

27
app/api/v1/router.py Normal file
View File

@@ -0,0 +1,27 @@
"""
Роутер API версии 1.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter
from app.api.v1.endpoints import (
health,
monitoring,
debug,
jira,
message,
groups,
)
# Создаем роутер для API v1
api_router = APIRouter(prefix="/api/v1")
# Подключаем эндпоинты
api_router.include_router(health.router)
api_router.include_router(monitoring.router)
api_router.include_router(debug.router)
api_router.include_router(jira.router)
api_router.include_router(message.router)
api_router.include_router(groups.router)