Files
http_logger/docs/superpowers/specs/2026-06-14-notification-channels-design.md
2026-06-14 11:16:42 +03:00

199 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Настраиваемые каналы уведомлений
Дата: 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` разделяется на две части: извлечение данных и форматирование.
```go
// 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"`) сохраняются.
### Интерфейс канала
```go
// 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`.
```yaml
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:
```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` — исправляются в рамках работы.
### Сборка каналов на старте
```go
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`.
### Маршрутизация в хендлере
```go
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`,
известный канал с моком `Notifier``200`, ошибка `Send``502`. Интерфейс
`Notifier` позволяет подменить реальные каналы моком.
- Реальная отправка в Telegram/Pachca/Email не тестируется (внешние сервисы).
## Вне рамок (YAGNI)
- Broadcast «один запрос → все каналы».
- Несколько экземпляров одного типа канала.
- Именованные маршруты с произвольным набором каналов.
- Вложения/фото (библиотека умеет, но текущая задача — текст запроса).