Files
http_logger/docs/superpowers/specs/2026-06-14-notification-channels-design.md
2026-06-14 10:53:43 +03:00

8.6 KiB
Raw Blame History

Настраиваемые каналы уведомлений

Дата: 2026-06-14

Цель

Сейчас http_logger умеет отправлять входящие HTTP-запросы только в Telegram — канал жёстко зашит в main.go. Нужно дать возможность настраивать каналы получения уведомлений через конфиг и добавить два новых: Pachca (Пачка) и Email, используя библиотеку gitea.mediatoday.ru/mt/notify.

Решения (согласованы с пользователем)

  1. Путь = канал. Первый сегмент пути URL выбирает канал: /telegram, /pachca, /email. Каждый путь шлёт ровно в свой канал. Сохраняет текущую логику маршрутизации.
  2. Один экземпляр на тип. В конфиге по одной секции на каждый тип канала.
  3. Форматирование под каждый канал. Извлечение данных запроса — общее, форматирование — на стороне канала (Telegram — markdown-блок, Email — HTML).
  4. Канал не сконфигурирован → 404.
  5. Поднять модуль notify на актуальную версию по новому пути gitea.mediatoday.ru/mt/notify (в старой версии нет Pachca/Email).

Архитектура

Структура данных запроса

BuildMessage разделяется на две части: извлечение данных и форматирование.

// RequestData — извлечённые данные входящего запроса
type RequestData struct {
    Time       time.Time
    Method     string
    Host       string
    URL        string
    RemoteAddr string
    Header     http.Header
    Body       string
}

// ExtractRequest читает запрос один раз (тело с лимитом 1024 байт, как сейчас)
func ExtractRequest(r *http.Request) RequestData

Логика GetRemoteAddr и лимит тела (> 1024"too long") сохраняются.

Интерфейс канала

// Notifier — канал отправки уведомлений
type Notifier interface {
    Send(data RequestData) error
}

Реализации

Каждая реализация сама форматирует RequestData в свой формат.

  • telegramNotifier{bot *notify.Bot, chatID int64} Форматирует тело в ```-блок (как сейчас), отправляет bot.SendTextMessage (ParseMode Markdown).
  • pachcaNotifier{client *notify.Pachca, chatID int64} Форматирует в markdown ```-блок, отправляет client.SendMessage(chatID, text).
  • emailNotifier{auth notify.SmtpAuth, from mail.Address, to []mail.Address, subject string} Форматирует тело в <pre>…</pre> с HTML-экранированием содержимого, отправляет notify.SendEmailHTML(auth, body, subject, from, to...).

Каждая реализация выносится в отдельный файл (telegram.go, pachca.go, email.go) либо группируется в notifiers.go — детали на этапе плана. Цель — не раздувать main.go.

Конфиг

Любую секцию можно опустить — тогда соответствующий путь даёт 404.

listen_addresses:
  - ":8080"
  - "127.0.0.1:9090"

log_dir: "logs"

telegram:
  token: "*****"
  group_id: -10012345
  disable_ipv6: true          # опционально, по умолчанию true

pachca:
  token: "*****"
  chat_id: 12345

email:
  smtp_addr: "smtp.example.com:587"
  username: "user@example.com"
  password: "*****"
  from: "logger@example.com"
  to:
    - "alert@example.com"
  subject: "HTTP Logger"      # опционально, по умолчанию "HTTP Logger"

Структуры Go:

type Config struct {
    ListenAddresses []string         `yaml:"listen_addresses"`
    LogDir          string           `yaml:"log_dir"`
    Telegram        *ConfigTelegram  `yaml:"telegram"`
    Pachca          *ConfigPachca    `yaml:"pachca"`
    Email           *ConfigEmail     `yaml:"email"`
}

type ConfigTelegram struct {
    Token       string `yaml:"token"`
    GroupID     int64  `yaml:"group_id"`
    DisableIPV6 *bool  `yaml:"disable_ipv6"` // nil → true
}

type ConfigPachca struct {
    Token  string `yaml:"token"`
    ChatID int64  `yaml:"chat_id"`
}

type ConfigEmail struct {
    SMTPAddr string   `yaml:"smtp_addr"`
    Username string   `yaml:"username"`
    Password string   `yaml:"password"`
    From     string   `yaml:"from"`
    To       []string `yaml:"to"`
    Subject  string   `yaml:"subject"` // "" → "HTTP Logger"
}

Замечание: исходный конфиг содержал опечатку token:"*****" (без пробела) и имя типа ConfigTelegraam — исправляются в рамках работы.

Сборка каналов на старте

notifiers := map[string]Notifier{}
  • Для каждой заданной секции конфига строим соответствующий Notifier и кладём в map под ключом-именем пути ("telegram", "pachca", "email").
  • Для email: хост для smtp.PlainAuth извлекается из smtp_addr через net.SplitHostPort; auth = smtp.PlainAuth("", username, password, host).
  • Если ни одна секция не задана → log.Fatal("не сконфигурирован ни один канал").
  • Ошибка инициализации конкретного канала (например, NewTelegram) → log.Fatal.

Маршрутизация в хендлере

paths := strings.Split(r.URL.Path, "/")        // "/telegram" → ["", "telegram"]
if len(paths) < 2 { 404 }
n, ok := notifiers[paths[1]]
if !ok { 404 }                                  // неизвестный или несконфигурированный канал
data := ExtractRequest(r)
if err := n.Send(data); err != nil {
    log.Println(err); 502 + err.Error()
} else {
    200 + "OK"
}

Поведение кодов ответа (404 / 502 / 200) сохраняется как в текущей версии.

Обработка ошибок

  • Несконфигурированный/неизвестный путь → 404.
  • Ошибка отправки в канал → 502 + текст ошибки в теле (как сейчас).
  • Ошибки конфигурации/инициализации каналов → log.Fatal на старте.

Зависимости

  • go.mod: заменить git.gm6.ru/icewind/notify на gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34 (доступна в module cache). Обновить go.sum через go mod tidy.
  • Новые импорты: net/mail, net/smtp, html (для экранирования HTML email).

Тестирование

  • ExtractRequest — модульный тест: корректный разбор метода/хоста/URL/заголовков, GetRemoteAddr (X-Forwarded-For → X-Real-IP → RemoteAddr), лимит тела > 1024.
  • Форматирование каждого канала — тест, проверяющий обёртку (```-блок для Telegram/Pachca, <pre> + экранирование для Email).
  • Маршрутизация хендлера — тест через httptest: неизвестный путь → 404, известный канал с моком Notifier200, ошибка Send502. Интерфейс Notifier позволяет подменить реальные каналы моком.
  • Реальная отправка в Telegram/Pachca/Email не тестируется (внешние сервисы).

Вне рамок (YAGNI)

  • Broadcast «один запрос → все каналы».
  • Несколько экземпляров одного типа канала.
  • Именованные маршруты с произвольным набором каналов.
  • Вложения/фото (библиотека умеет, но текущая задача — текст запроса).