199 lines
8.6 KiB
Markdown
199 lines
8.6 KiB
Markdown
# Настраиваемые каналы уведомлений
|
||
|
||
Дата: 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 «один запрос → все каналы».
|
||
- Несколько экземпляров одного типа канала.
|
||
- Именованные маршруты с произвольным набором каналов.
|
||
- Вложения/фото (библиотека умеет, но текущая задача — текст запроса).
|