docs: спецификация настраиваемых каналов уведомлений (Telegram/Pachca/Email)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,198 @@
|
|||||||
|
# Настраиваемые каналы уведомлений
|
||||||
|
|
||||||
|
Дата: 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 «один запрос → все каналы».
|
||||||
|
- Несколько экземпляров одного типа канала.
|
||||||
|
- Именованные маршруты с произвольным набором каналов.
|
||||||
|
- Вложения/фото (библиотека умеет, но текущая задача — текст запроса).
|
||||||
Reference in New Issue
Block a user