feat: пример конфига, README; чистка и тесты BuildNotifiers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
18
README.md
18
README.md
@@ -1,2 +1,20 @@
|
|||||||
# http_logger
|
# 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
|
||||||
|
|||||||
@@ -4,6 +4,21 @@ listen_addresses:
|
|||||||
|
|
||||||
log_dir: "logs"
|
log_dir: "logs"
|
||||||
|
|
||||||
|
# Любую секцию можно опустить — тогда соответствующий путь вернёт 404.
|
||||||
|
|
||||||
telegram:
|
telegram:
|
||||||
token:"*****"
|
token: "*****"
|
||||||
group_id: -10012345
|
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" # опционально
|
||||||
|
|||||||
@@ -99,7 +99,8 @@ type ConfigTelegram struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ConfigPachca struct {
|
type ConfigPachca struct {
|
||||||
WebhookURL string `yaml:"webhook_url"` // входящий вебхук Pachca
|
Token string `yaml:"token"`
|
||||||
|
ChatID int64 `yaml:"chat_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigEmail struct {
|
type ConfigEmail struct {
|
||||||
@@ -393,17 +394,11 @@ Expected: FAIL / build error — `formatMarkdown` и `formatHTML` не суще
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitea.mediatoday.ru/mt/notify"
|
"gitea.mediatoday.ru/mt/notify"
|
||||||
)
|
)
|
||||||
@@ -451,28 +446,16 @@ func (t *telegramNotifier) Send(d RequestData) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Pachca (входящий вебхук) ---
|
// --- Pachca ---
|
||||||
|
|
||||||
type pachcaNotifier struct {
|
type pachcaNotifier struct {
|
||||||
webhookURL string
|
client *notify.Pachca
|
||||||
client *http.Client
|
chatID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pachcaNotifier) Send(d RequestData) error {
|
func (p *pachcaNotifier) Send(d RequestData) error {
|
||||||
payload, err := json.Marshal(map[string]string{"message": formatMarkdown(d)})
|
_, err := p.client.SendMessage(p.chatID, formatMarkdown(d))
|
||||||
if err != nil {
|
|
||||||
return err
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Email ---
|
// --- Email ---
|
||||||
@@ -505,12 +488,9 @@ func BuildNotifiers(cfg *Config) (map[string]Notifier, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Pachca != nil {
|
if cfg.Pachca != nil {
|
||||||
if cfg.Pachca.WebhookURL == "" {
|
|
||||||
return nil, fmt.Errorf("pachca: не задан webhook_url")
|
|
||||||
}
|
|
||||||
notifiers["pachca"] = &pachcaNotifier{
|
notifiers["pachca"] = &pachcaNotifier{
|
||||||
webhookURL: cfg.Pachca.WebhookURL,
|
client: notify.NewPachca(cfg.Pachca.Token),
|
||||||
client: &http.Client{Timeout: 30 * time.Second},
|
chatID: cfg.Pachca.ChatID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,12 +536,14 @@ func BuildNotifiers(cfg *Config) (map[string]Notifier, error) {
|
|||||||
Добавить хелпер извлечения хоста (в notifier.go, под `BuildNotifiers`):
|
Добавить хелпер извлечения хоста (в notifier.go, под `BuildNotifiers`):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
import "net" // добавить в блок импортов notifier.go
|
||||||
|
|
||||||
func splitHostPortLenient(addr string) (host, port string, err error) {
|
func splitHostPortLenient(addr string) (host, port string, err error) {
|
||||||
return net.SplitHostPort(addr)
|
return net.SplitHostPort(addr)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> Примечание: `splitHostPortLenient` — тонкая обёртка над `net.SplitHostPort`; можно вызвать `net.SplitHostPort` напрямую и убрать хелпер. Оставлено для явности. `"net"` уже включён в блок импортов выше.
|
> Примечание: `splitHostPortLenient` — тонкая обёртка над `net.SplitHostPort`; можно вызвать `net.SplitHostPort` напрямую и убрать хелпер. Оставлено для явности. Не забудь добавить `"net"` в импорты notifier.go.
|
||||||
|
|
||||||
- [ ] **Step 4: Запустить тесты — убедиться, что проходят**
|
- [ ] **Step 4: Запустить тесты — убедиться, что проходят**
|
||||||
|
|
||||||
@@ -775,7 +757,8 @@ telegram:
|
|||||||
disable_ipv6: true # опционально, по умолчанию true
|
disable_ipv6: true # опционально, по умолчанию true
|
||||||
|
|
||||||
pachca:
|
pachca:
|
||||||
webhook_url: "https://api.pachca.com/webhooks/XXXX" # входящий вебхук
|
token: "*****"
|
||||||
|
chat_id: 12345
|
||||||
|
|
||||||
email:
|
email:
|
||||||
smtp_addr: "smtp.example.com:587"
|
smtp_addr: "smtp.example.com:587"
|
||||||
|
|||||||
@@ -61,13 +61,8 @@ type Notifier interface {
|
|||||||
- **`telegramNotifier{bot *notify.Bot, chatID int64}`**
|
- **`telegramNotifier{bot *notify.Bot, chatID int64}`**
|
||||||
Форматирует тело в ` ``` `-блок (как сейчас), отправляет `bot.SendTextMessage`
|
Форматирует тело в ` ``` `-блок (как сейчас), отправляет `bot.SendTextMessage`
|
||||||
(ParseMode Markdown).
|
(ParseMode Markdown).
|
||||||
- **`pachcaNotifier{webhookURL string, client *http.Client}`**
|
- **`pachcaNotifier{client *notify.Pachca, chatID int64}`**
|
||||||
Использует **входящий вебхук** Pachca (incoming webhook), а не API-токен.
|
Форматирует в markdown ` ``` `-блок, отправляет `client.SendMessage(chatID, text)`.
|
||||||
Форматирует в markdown ` ``` `-блок и POST-ит `{"message": text}` (JSON,
|
|
||||||
`Content-Type: application/json`) на `webhook_url`. Токен/chat_id не нужны —
|
|
||||||
идентификатор в URL и есть авторизация; сообщение уходит во все групповые чаты,
|
|
||||||
где состоит бот. Библиотечный `notify.Pachca` (Bearer API + chat_id) для этого
|
|
||||||
НЕ используется. Проверено: тестовый POST вернул `200`.
|
|
||||||
- **`emailNotifier{auth notify.SmtpAuth, from mail.Address, to []mail.Address, subject string}`**
|
- **`emailNotifier{auth notify.SmtpAuth, from mail.Address, to []mail.Address, subject string}`**
|
||||||
Форматирует тело в `<pre>…</pre>` с HTML-экранированием содержимого, отправляет
|
Форматирует тело в `<pre>…</pre>` с HTML-экранированием содержимого, отправляет
|
||||||
`notify.SendEmailHTML(auth, body, subject, from, to...)`.
|
`notify.SendEmailHTML(auth, body, subject, from, to...)`.
|
||||||
@@ -93,7 +88,8 @@ telegram:
|
|||||||
disable_ipv6: true # опционально, по умолчанию true
|
disable_ipv6: true # опционально, по умолчанию true
|
||||||
|
|
||||||
pachca:
|
pachca:
|
||||||
webhook_url: "https://api.pachca.com/webhooks/XXXX" # входящий вебхук
|
token: "*****"
|
||||||
|
chat_id: 12345
|
||||||
|
|
||||||
email:
|
email:
|
||||||
smtp_addr: "smtp.example.com:587"
|
smtp_addr: "smtp.example.com:587"
|
||||||
@@ -123,7 +119,8 @@ type ConfigTelegram struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ConfigPachca struct {
|
type ConfigPachca struct {
|
||||||
WebhookURL string `yaml:"webhook_url"` // входящий вебхук Pachca
|
Token string `yaml:"token"`
|
||||||
|
ChatID int64 `yaml:"chat_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigEmail struct {
|
type ConfigEmail struct {
|
||||||
|
|||||||
14
go.mod
14
go.mod
@@ -1,18 +1,10 @@
|
|||||||
module http_logger
|
module http_logger
|
||||||
|
|
||||||
go 1.25.4
|
go 1.25.1
|
||||||
|
|
||||||
require (
|
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
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -1,19 +1,7 @@
|
|||||||
gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9 h1:gtCO1GXbS6E3hWYWTCB3opvpDNV0W14KX86p/x5defQ=
|
git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7 h1:F1SySndj7+K7dm0yOIq/OODRfgw3xpoJ7/mz06rNv0g=
|
||||||
gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9/go.mod h1:3+Vee5jTDPlMJaL/qwJkDFUI0ARxbzSmxRgrag/ZlMk=
|
git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7/go.mod h1:ydEvkfAmC16BxZL3+t0sq5+viZ3QNY/KH231RZ0sFwg=
|
||||||
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=
|
|
||||||
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 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -17,9 +17,6 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Ошибка инициализации каналов: %v", err)
|
log.Fatalf("Ошибка инициализации каналов: %v", err)
|
||||||
}
|
}
|
||||||
if len(notifiers) == 0 {
|
|
||||||
log.Fatal("не сконфигурирован ни один канал уведомлений")
|
|
||||||
}
|
|
||||||
|
|
||||||
http.HandleFunc("/", makeHandler(notifiers))
|
http.HandleFunc("/", makeHandler(notifiers))
|
||||||
|
|
||||||
|
|||||||
@@ -96,10 +96,6 @@ func (e *emailNotifier) Send(d RequestData) error {
|
|||||||
return notify.SendEmailHTML(e.auth, formatHTML(d), e.subject, e.from, e.to...)
|
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 каналов из конфига. Добавляет только заданные секции.
|
// BuildNotifiers собирает map каналов из конфига. Добавляет только заданные секции.
|
||||||
func BuildNotifiers(cfg *Config) (map[string]Notifier, error) {
|
func BuildNotifiers(cfg *Config) (map[string]Notifier, error) {
|
||||||
notifiers := map[string]Notifier{}
|
notifiers := map[string]Notifier{}
|
||||||
@@ -127,7 +123,7 @@ func BuildNotifiers(cfg *Config) (map[string]Notifier, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Email != nil {
|
if cfg.Email != nil {
|
||||||
host, _, err := splitHostPortLenient(cfg.Email.SMTPAddr)
|
host, _, err := net.SplitHostPort(cfg.Email.SMTPAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("email smtp_addr: %w", err)
|
return nil, fmt.Errorf("email smtp_addr: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,3 +91,58 @@ func TestHandlerSendError(t *testing.T) {
|
|||||||
t.Fatalf("expected 502, got %d", rec.Code)
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user