Initial commit: Message Gateway project
- FastAPI приложение для отправки мониторинговых алертов в мессенджеры - Поддержка Telegram и MAX/VK - Интеграция с Grafana, Zabbix, AlertManager - Автоматическое создание тикетов в Jira - Управление группами мессенджеров через API - Декораторы для авторизации и скрытия эндпоинтов - Подробная документация в папке docs/ Автор: Сергей Антропов Сайт: https://devops.org.ru
This commit is contained in:
6
app/api/__init__.py
Normal file
6
app/api/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
API модули приложения.
|
||||
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
6
app/api/v1/__init__.py
Normal file
6
app/api/v1/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
API версии 1.
|
||||
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
23
app/api/v1/endpoints/__init__.py
Normal file
23
app/api/v1/endpoints/__init__.py
Normal 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",
|
||||
]
|
||||
114
app/api/v1/endpoints/debug.py
Normal file
114
app/api/v1/endpoints/debug.py
Normal 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)}")
|
||||
511
app/api/v1/endpoints/groups.py
Normal file
511
app/api/v1/endpoints/groups.py
Normal 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)}")
|
||||
111
app/api/v1/endpoints/health.py
Normal file
111
app/api/v1/endpoints/health.py
Normal 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
|
||||
}
|
||||
19
app/api/v1/endpoints/jira.py
Normal file
19
app/api/v1/endpoints/jira.py
Normal 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)
|
||||
468
app/api/v1/endpoints/message.py
Normal file
468
app/api/v1/endpoints/message.py
Normal 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
|
||||
)
|
||||
403
app/api/v1/endpoints/monitoring.py
Normal file
403
app/api/v1/endpoints/monitoring.py
Normal 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
27
app/api/v1/router.py
Normal 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)
|
||||
Reference in New Issue
Block a user