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

236
app/core/jira_client.py Normal file
View File

@@ -0,0 +1,236 @@
"""
Клиент для работы с Jira API.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import logging
from typing import Optional, Dict, Any
from jira import JIRA
from jira.exceptions import JIRAError
logger = logging.getLogger(__name__)
class JiraClient:
"""Клиент для работы с Jira API."""
def __init__(
self,
url: str,
email: str,
api_token: str
):
"""
Инициализация клиента Jira.
Args:
url: URL Jira сервера.
email: Email пользователя Jira.
api_token: API токен Jira.
"""
self.url = url.rstrip('/')
self.email = email
self.api_token = api_token
self._client: Optional[JIRA] = None
def get_client(self) -> JIRA:
"""
Получить экземпляр клиента Jira (создается при первом обращении).
Returns:
Экземпляр JIRA клиента.
"""
if self._client is None:
try:
self._client = JIRA(
server=self.url,
basic_auth=(self.email, self.api_token)
)
logger.info(f"Jira клиент подключен к {self.url}")
except JIRAError as e:
logger.error(f"Ошибка подключения к Jira: {e}")
raise
return self._client
def create_issue(
self,
project: str,
summary: str,
description: str,
issue_type: str = "Bug",
assignee: Optional[str] = None,
priority: Optional[str] = None,
labels: Optional[list] = None,
components: Optional[list] = None
) -> Optional[str]:
"""
Создать тикет в Jira.
Args:
project: Ключ проекта Jira.
summary: Заголовок тикета.
description: Описание тикета.
issue_type: Тип задачи.
assignee: Email исполнителя (опционально).
priority: Приоритет задачи (опционально).
labels: Список меток (опционально).
components: Список компонентов (опционально).
Returns:
Ключ созданного тикета (например, "MON-123") или None в случае ошибки.
"""
try:
client = self.get_client()
# Формируем словарь для создания тикета
issue_dict = {
'project': {'key': project},
'summary': summary,
'description': description,
'issuetype': {'name': issue_type}
}
# Добавляем приоритет, если указан
if priority:
issue_dict['priority'] = {'name': priority}
# Добавляем метки, если указаны
if labels:
issue_dict['labels'] = labels
# Добавляем компоненты, если указаны
if components:
issue_dict['components'] = [{'name': comp} for comp in components]
# Создаем тикет
issue = client.create_issue(fields=issue_dict)
# Назначаем исполнителя, если указан
if assignee:
try:
# Пытаемся найти пользователя по email или username
users = client.search_users(query=assignee)
if users:
# Назначаем первого найденного пользователя
user_account_id = users[0].accountId
client.assign_issue(issue, user_account_id)
logger.info(f"Исполнитель {assignee} (accountId: {user_account_id}) назначен на тикет {issue.key}")
else:
logger.warning(f"Пользователь {assignee} не найден в Jira, тикет создан без исполнителя")
except JIRAError as e:
logger.error(f"Ошибка назначения исполнителя {assignee}: {e}")
except Exception as e:
logger.error(f"Неожиданная ошибка при назначении исполнителя: {e}")
logger.info(f"Тикет {issue.key} создан в Jira")
return issue.key
except JIRAError as e:
logger.error(f"Ошибка создания тикета в Jira: {e}")
return None
except Exception as e:
logger.error(f"Неожиданная ошибка при создании тикета: {e}")
return None
def get_issue_url(self, issue_key: str) -> str:
"""
Получить URL тикета в Jira.
Args:
issue_key: Ключ тикета (например, "MON-123").
Returns:
URL тикета в Jira.
"""
return f"{self.url}/browse/{issue_key}"
def update_issue(
self,
issue_key: str,
summary: Optional[str] = None,
description: Optional[str] = None,
assignee: Optional[str] = None,
priority: Optional[str] = None,
labels: Optional[list] = None
) -> bool:
"""
Обновить тикет в Jira.
Args:
issue_key: Ключ тикета.
summary: Новый заголовок (опционально).
description: Новое описание (опционально).
assignee: Новый исполнитель (опционально).
priority: Новый приоритет (опционально).
labels: Новые метки (опционально).
Returns:
True если тикет обновлен успешно, False в противном случае.
"""
try:
client = self.get_client()
issue = client.issue(issue_key)
update_dict = {}
if summary:
update_dict['summary'] = [{'set': summary}]
if description:
update_dict['description'] = [{'set': description}]
if priority:
update_dict['priority'] = [{'set': {'name': priority}}]
if labels:
update_dict['labels'] = [{'set': labels}]
if update_dict:
issue.update(fields=update_dict)
if assignee:
try:
users = client.search_users(query=assignee)
if users:
user_account_id = users[0].accountId
client.assign_issue(issue, user_account_id)
logger.info(f"Исполнитель {assignee} (accountId: {user_account_id}) назначен на тикет {issue_key}")
else:
logger.warning(f"Пользователь {assignee} не найден в Jira")
except JIRAError as e:
logger.error(f"Ошибка назначения исполнителя {assignee}: {e}")
except Exception as e:
logger.error(f"Неожиданная ошибка при назначении исполнителя: {e}")
logger.info(f"Тикет {issue_key} обновлен в Jira")
return True
except JIRAError as e:
logger.error(f"Ошибка обновления тикета {issue_key}: {e}")
return False
except Exception as e:
logger.error(f"Неожиданная ошибка при обновлении тикета: {e}")
return False
def add_comment(self, issue_key: str, comment: str) -> bool:
"""
Добавить комментарий к тикету.
Args:
issue_key: Ключ тикета.
comment: Текст комментария.
Returns:
True если комментарий добавлен успешно, False в противном случае.
"""
try:
client = self.get_client()
issue = client.issue(issue_key)
issue.add_comment(comment)
logger.info(f"Комментарий добавлен к тикету {issue_key}")
return True
except JIRAError as e:
logger.error(f"Ошибка добавления комментария к тикету {issue_key}: {e}")
return False
except Exception as e:
logger.error(f"Неожиданная ошибка при добавлении комментария: {e}")
return False