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

187
app/main.py Normal file
View File

@@ -0,0 +1,187 @@
"""
Главный модуль приложения Telegram Gateway.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import logging
from fastapi import FastAPI
from app.common.cors import add as add_cors
from app.common.telemetry import add as add_telemetry
from app.api.v1.router import api_router
# Настройка логирования (базовая настройка, детальная настройка в app.common.logger)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Создаем приложение FastAPI
app = FastAPI(
title="Message Gateway",
summary="Отправляем мониторинговые алерты в телеграм/MAX и создаем тикеты в Jira",
description=(
"Приложение для оповещений, приходящих из Grafana/Zabbix/AlertManager "
"посредством вебхука, в телеграм/MAX. С возможностью отправок в треды и создания тикетов в Jira. "
"<br><br><b>Что бы начать отправлять сообщения</b>, добавьте бота "
"<b>@CismGlobalMonitoring_bot</b> в чат и <b>внесите изменения в группы</b>"
),
version="0.2.0",
contact={
"name": "Сергей Антропов",
"url": "https://devops.org.ru/contact/",
"email": "sergey@antropoff.ru",
},
license_info={
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
},
debug=False,
swagger_ui_init_oauth={
"clientId": "api-key",
"appName": "Message Gateway API",
"usePkceWithAuthorizationCodeGrant": False,
}
)
# Добавляем схему безопасности в Swagger
from fastapi.openapi.utils import get_openapi
from fastapi.routing import APIRoute
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
# Собираем пути, которые должны быть скрыты от API
hidden_paths = set()
# Перебираем все routes и находим те, которые помечены как скрытые
for route in app.routes:
# Проверяем только APIRoute
if not isinstance(route, APIRoute):
continue
# Получаем endpoint функцию
endpoint = route.endpoint
# Проверяем, помечен ли endpoint как скрытый от API
if hasattr(endpoint, "__hide_from_api__") and endpoint.__hide_from_api__:
# Получаем полный путь (с учетом префиксов)
path = route.path
# Нормализуем путь (убираем параметры типа {param} для сопоставления)
# Но нам нужно точное сопоставление, поэтому используем полный путь
hidden_paths.add(path)
logger.debug(f"Эндпоинт {path} помечен как скрытый от API")
# Генерируем схему как обычно
openapi_schema = get_openapi(
title=app.title,
version=app.version,
description=app.description,
routes=app.routes,
)
# Удаляем скрытые пути из схемы
if hidden_paths:
paths = openapi_schema.get("paths", {})
# Создаем новый словарь paths без скрытых путей
filtered_paths = {}
for path, path_item in paths.items():
# Проверяем, должен ли путь быть скрыт
# Путь в схеме должен точно совпадать с путем в route.path
# route.path уже содержит все префиксы (router prefix + route path)
should_hide = path in hidden_paths
if not should_hide:
filtered_paths[path] = path_item
else:
logger.debug(f"Удаляем путь {path} из OpenAPI схемы")
openapi_schema["paths"] = filtered_paths
# Добавляем схему безопасности API Key
if "components" not in openapi_schema:
openapi_schema["components"] = {}
if "securitySchemes" not in openapi_schema["components"]:
openapi_schema["components"]["securitySchemes"] = {}
# Определяем имя схемы безопасности, которое использует FastAPI
# FastAPI автоматически генерирует имя на основе класса Security
# Обычно это имя класса в camelCase (например, "APIKeyHeader")
api_key_scheme_name = None
# Перебираем существующие схемы безопасности и находим API Key схему
for scheme_name, scheme_def in openapi_schema["components"].get("securitySchemes", {}).items():
if scheme_def.get("type") == "apiKey" and scheme_def.get("name") == "X-API-Key":
api_key_scheme_name = scheme_name
break
# Если схема не найдена, создаем новую с именем "ApiKeyAuth"
if not api_key_scheme_name:
api_key_scheme_name = "ApiKeyAuth"
openapi_schema["components"]["securitySchemes"][api_key_scheme_name] = {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "API ключ для авторизации. Получите его из переменной окружения API_KEY."
}
else:
# Обновляем существующую схему, если она уже есть
openapi_schema["components"]["securitySchemes"][api_key_scheme_name] = {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "API ключ для авторизации. Получите его из переменной окружения API_KEY."
}
# Если имя схемы не "ApiKeyAuth", переименовываем ее
if api_key_scheme_name != "ApiKeyAuth":
# Сохраняем старую схему
old_scheme = openapi_schema["components"]["securitySchemes"].pop(api_key_scheme_name)
# Создаем новую схему с именем "ApiKeyAuth"
openapi_schema["components"]["securitySchemes"]["ApiKeyAuth"] = old_scheme
# Заменяем все использования старого имени на новое в security эндпоинтов
for path, path_item in openapi_schema.get("paths", {}).items():
for method, operation in path_item.items():
if isinstance(operation, dict) and "security" in operation:
security_list = operation["security"]
for security_item in security_list:
if api_key_scheme_name in security_item:
# Заменяем старое имя на новое
security_item["ApiKeyAuth"] = security_item.pop(api_key_scheme_name)
# Убеждаемся, что схема "ApiKeyAuth" существует
if "ApiKeyAuth" not in openapi_schema["components"]["securitySchemes"]:
openapi_schema["components"]["securitySchemes"]["ApiKeyAuth"] = {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "API ключ для авторизации. Получите его из переменной окружения API_KEY."
}
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
# Добавляем CORS
add_cors(app)
# Добавляем телеметрию
add_telemetry(app)
# Подключаем роутер API v1
app.include_router(api_router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8000,
reload=False
)