diff --git a/README.md b/README.md index 59e4cf7..2cc41f7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,20 @@ # http_logger +HTTP-логгер: принимает входящие HTTP-запросы и пересылает их в настроенный +канал уведомлений. Канал выбирается первым сегментом пути: + +- `POST /telegram` → Telegram +- `POST /pachca` → Pachca (Пачка, входящий вебхук) +- `POST /email` → Email + +Каналы настраиваются в `config.yaml` (пример — `config.yaml.default`). +Несконфигурированный путь возвращает `404`, ошибка отправки — `502`. + +## Сборка + + ./build.sh + +## Запуск + + cp config.yaml.default config.yaml # отредактировать токены/адреса + ./bin/http_logger diff --git a/config.yaml.default b/config.yaml.default index f166b8d..06ef636 100644 --- a/config.yaml.default +++ b/config.yaml.default @@ -4,6 +4,21 @@ listen_addresses: log_dir: "logs" +# Любую секцию можно опустить — тогда соответствующий путь вернёт 404. + telegram: - token:"*****" + token: "*****" group_id: -10012345 + disable_ipv6: true # опционально, по умолчанию true + +pachca: + webhook_url: "https://api.pachca.com/webhooks/XXXX" # входящий вебхук + +email: + smtp_addr: "smtp.example.com:587" + username: "user@example.com" + password: "*****" + from: "logger@example.com" + to: + - "alert@example.com" + subject: "HTTP Logger" # опционально diff --git a/docs/superpowers/plans/2026-06-14-notification-channels.md b/docs/superpowers/plans/2026-06-14-notification-channels.md index ad2f385..ba52f61 100644 --- a/docs/superpowers/plans/2026-06-14-notification-channels.md +++ b/docs/superpowers/plans/2026-06-14-notification-channels.md @@ -99,7 +99,8 @@ type ConfigTelegram struct { } type ConfigPachca struct { - WebhookURL string `yaml:"webhook_url"` // входящий вебхук Pachca + Token string `yaml:"token"` + ChatID int64 `yaml:"chat_id"` } type ConfigEmail struct { @@ -393,17 +394,11 @@ Expected: FAIL / build error — `formatMarkdown` и `formatHTML` не суще package main import ( - "bytes" - "encoding/json" "fmt" "html" - "io" - "net" - "net/http" "net/mail" "net/smtp" "strings" - "time" "gitea.mediatoday.ru/mt/notify" ) @@ -451,28 +446,16 @@ func (t *telegramNotifier) Send(d RequestData) error { return err } -// --- Pachca (входящий вебхук) --- +// --- Pachca --- type pachcaNotifier struct { - webhookURL string - client *http.Client + client *notify.Pachca + chatID int64 } func (p *pachcaNotifier) Send(d RequestData) error { - payload, err := json.Marshal(map[string]string{"message": formatMarkdown(d)}) - if err != nil { - return err - } - resp, err := p.client.Post(p.webhookURL, "application/json", bytes.NewReader(payload)) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("pachca webhook: HTTP %d: %s", resp.StatusCode, string(body)) - } - return nil + _, err := p.client.SendMessage(p.chatID, formatMarkdown(d)) + return err } // --- Email --- @@ -505,12 +488,9 @@ func BuildNotifiers(cfg *Config) (map[string]Notifier, error) { } if cfg.Pachca != nil { - if cfg.Pachca.WebhookURL == "" { - return nil, fmt.Errorf("pachca: не задан webhook_url") - } notifiers["pachca"] = &pachcaNotifier{ - webhookURL: cfg.Pachca.WebhookURL, - client: &http.Client{Timeout: 30 * time.Second}, + client: notify.NewPachca(cfg.Pachca.Token), + chatID: cfg.Pachca.ChatID, } } @@ -556,12 +536,14 @@ func BuildNotifiers(cfg *Config) (map[string]Notifier, error) { Добавить хелпер извлечения хоста (в notifier.go, под `BuildNotifiers`): ```go +import "net" // добавить в блок импортов notifier.go + func splitHostPortLenient(addr string) (host, port string, err error) { return net.SplitHostPort(addr) } ``` -> Примечание: `splitHostPortLenient` — тонкая обёртка над `net.SplitHostPort`; можно вызвать `net.SplitHostPort` напрямую и убрать хелпер. Оставлено для явности. `"net"` уже включён в блок импортов выше. +> Примечание: `splitHostPortLenient` — тонкая обёртка над `net.SplitHostPort`; можно вызвать `net.SplitHostPort` напрямую и убрать хелпер. Оставлено для явности. Не забудь добавить `"net"` в импорты notifier.go. - [ ] **Step 4: Запустить тесты — убедиться, что проходят** @@ -775,7 +757,8 @@ telegram: disable_ipv6: true # опционально, по умолчанию true pachca: - webhook_url: "https://api.pachca.com/webhooks/XXXX" # входящий вебхук + token: "*****" + chat_id: 12345 email: smtp_addr: "smtp.example.com:587" diff --git a/docs/superpowers/specs/2026-06-14-notification-channels-design.md b/docs/superpowers/specs/2026-06-14-notification-channels-design.md index 4296d64..f289a5d 100644 --- a/docs/superpowers/specs/2026-06-14-notification-channels-design.md +++ b/docs/superpowers/specs/2026-06-14-notification-channels-design.md @@ -61,13 +61,8 @@ type Notifier interface { - **`telegramNotifier{bot *notify.Bot, chatID int64}`** Форматирует тело в ` ``` `-блок (как сейчас), отправляет `bot.SendTextMessage` (ParseMode Markdown). -- **`pachcaNotifier{webhookURL string, client *http.Client}`** - Использует **входящий вебхук** Pachca (incoming webhook), а не API-токен. - Форматирует в markdown ` ``` `-блок и POST-ит `{"message": text}` (JSON, - `Content-Type: application/json`) на `webhook_url`. Токен/chat_id не нужны — - идентификатор в URL и есть авторизация; сообщение уходит во все групповые чаты, - где состоит бот. Библиотечный `notify.Pachca` (Bearer API + chat_id) для этого - НЕ используется. Проверено: тестовый POST вернул `200`. +- **`pachcaNotifier{client *notify.Pachca, chatID int64}`** + Форматирует в markdown ` ``` `-блок, отправляет `client.SendMessage(chatID, text)`. - **`emailNotifier{auth notify.SmtpAuth, from mail.Address, to []mail.Address, subject string}`** Форматирует тело в `
…` с HTML-экранированием содержимого, отправляет `notify.SendEmailHTML(auth, body, subject, from, to...)`. @@ -93,7 +88,8 @@ telegram: disable_ipv6: true # опционально, по умолчанию true pachca: - webhook_url: "https://api.pachca.com/webhooks/XXXX" # входящий вебхук + token: "*****" + chat_id: 12345 email: smtp_addr: "smtp.example.com:587" @@ -123,7 +119,8 @@ type ConfigTelegram struct { } type ConfigPachca struct { - WebhookURL string `yaml:"webhook_url"` // входящий вебхук Pachca + Token string `yaml:"token"` + ChatID int64 `yaml:"chat_id"` } type ConfigEmail struct { diff --git a/go.mod b/go.mod index b02f4fd..84a17f2 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,10 @@ module http_logger -go 1.25.4 +go 1.25.1 require ( - gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34 + git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7 gopkg.in/yaml.v3 v3.0.1 ) -require ( - gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9 // indirect - github.com/clipperhouse/uax29/v2 v2.2.0 // indirect - github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect - github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da // indirect - golang.org/x/image v0.34.0 // indirect - golang.org/x/text v0.32.0 // indirect -) +require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect diff --git a/go.sum b/go.sum index 6d56441..73855f7 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,7 @@ -gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9 h1:gtCO1GXbS6E3hWYWTCB3opvpDNV0W14KX86p/x5defQ= -gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9/go.mod h1:3+Vee5jTDPlMJaL/qwJkDFUI0ARxbzSmxRgrag/ZlMk= -gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34 h1:Vi87778hM8bSFogImH2sjKacTJONuCveAzK+2PcpEak= -gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34/go.mod h1:f173p7S2hvqGlc+YUhX7F2h9OJ8CiTNDH3v+gAn4id0= -github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= -github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7 h1:F1SySndj7+K7dm0yOIq/OODRfgw3xpoJ7/mz06rNv0g= +git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7/go.mod h1:ydEvkfAmC16BxZL3+t0sq5+viZ3QNY/KH231RZ0sFwg= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da h1:hhmnjfzz7szp75AyXxn8tDfEA0oU4REQLmpuW6zNAOY= -github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da/go.mod h1:Q5ljvYIBpukMH+wgB8kcPV1i9NX8TqU++8GgBKq3pt0= -golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8= -golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/main.go b/main.go index 1dc7804..b67ae16 100644 --- a/main.go +++ b/main.go @@ -17,9 +17,6 @@ func main() { if err != nil { log.Fatalf("Ошибка инициализации каналов: %v", err) } - if len(notifiers) == 0 { - log.Fatal("не сконфигурирован ни один канал уведомлений") - } http.HandleFunc("/", makeHandler(notifiers)) diff --git a/notifier.go b/notifier.go index 0b29ff8..693cb2e 100644 --- a/notifier.go +++ b/notifier.go @@ -96,10 +96,6 @@ func (e *emailNotifier) Send(d RequestData) error { return notify.SendEmailHTML(e.auth, formatHTML(d), e.subject, e.from, e.to...) } -func splitHostPortLenient(addr string) (host, port string, err error) { - return net.SplitHostPort(addr) -} - // BuildNotifiers собирает map каналов из конфига. Добавляет только заданные секции. func BuildNotifiers(cfg *Config) (map[string]Notifier, error) { notifiers := map[string]Notifier{} @@ -127,7 +123,7 @@ func BuildNotifiers(cfg *Config) (map[string]Notifier, error) { } if cfg.Email != nil { - host, _, err := splitHostPortLenient(cfg.Email.SMTPAddr) + host, _, err := net.SplitHostPort(cfg.Email.SMTPAddr) if err != nil { return nil, fmt.Errorf("email smtp_addr: %w", err) } diff --git a/notifier_test.go b/notifier_test.go index 5396950..f322223 100644 --- a/notifier_test.go +++ b/notifier_test.go @@ -91,3 +91,58 @@ func TestHandlerSendError(t *testing.T) { t.Fatalf("expected 502, got %d", rec.Code) } } + +func TestBuildNotifiersEmpty(t *testing.T) { + n, err := BuildNotifiers(&Config{}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if len(n) != 0 { + t.Fatalf("expected empty map, got %d", len(n)) + } +} + +func TestBuildNotifiersPachcaMissingURL(t *testing.T) { + _, err := BuildNotifiers(&Config{Pachca: &ConfigPachca{}}) + if err == nil { + t.Fatal("expected error for empty webhook_url") + } +} + +func TestBuildNotifiersEmailNoRecipients(t *testing.T) { + _, err := BuildNotifiers(&Config{Email: &ConfigEmail{ + SMTPAddr: "smtp.example.com:587", + From: "logger@example.com", + To: nil, + }}) + if err == nil { + t.Fatal("expected error for zero recipients") + } +} + +func TestBuildNotifiersEmailBadSMTPAddr(t *testing.T) { + _, err := BuildNotifiers(&Config{Email: &ConfigEmail{ + SMTPAddr: "no-port", + From: "logger@example.com", + To: []string{"a@example.com"}, + }}) + if err == nil { + t.Fatal("expected error for smtp_addr without port") + } +} + +func TestBuildNotifiersEmailOK(t *testing.T) { + n, err := BuildNotifiers(&Config{Email: &ConfigEmail{ + SMTPAddr: "smtp.example.com:587", + Username: "u", + Password: "p", + From: "logger@example.com", + To: []string{"a@example.com"}, + }}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if _, ok := n["email"]; !ok { + t.Fatal("email notifier not built") + } +}