Added alert email

This commit is contained in:
Magnus Root 2026-03-24 12:51:59 +03:00
parent f28ccce6a0
commit 94d45fdde6
8 changed files with 165 additions and 1 deletions

View file

@ -96,6 +96,12 @@ go build -o auspex
| `cfgPrimaryDBHost` | Основная БД | `10.100.10.8` |
| `cfgSecondaryDBHost` | Резервная БД | `10.100.13.5` |
| `SERVICES_TO_CHECK ` | Службы для проверки | `ansible,docker` |
| `EMAIL_SMTP_HOST` | Адрес почтового сервера | `smtp.example.com` |
| `EMAIL_SMTP_PORT` | Порт почтового сервера | `587` |
| `EMAIL_SMTP_USER` | Юзер для отправки | `auspex@example.com` |
| `EMAIL_SMTP_PASSWORD` | Пароль | `secret` |
| `EMAIL_FROM` | Юзер для отправки | `auspex@example.com` |
| `EMAIL_TO` | Получатели | `admin1@example.com, admin2@example.com` |
## Зависимости

View file

@ -193,4 +193,9 @@ func ccCheckAndAlert(cert ccCertInfo, hostname string) {
} else {
log.Println("✅ Алерт успешно отправлен в Telegram")
}
if err := emailSendHTML("Auspex: Срок действия сертификата", message); err != nil {
log.Printf("Ошибка отправки Email: %v", err)
} else {
log.Println("✅ Алерт успешно отправлен на Email")
}
}

View file

@ -27,6 +27,16 @@ var (
cfgTelegramChatID = ""
)
// --- Email / SMTP ---
var (
cfgEmailSMTPHost = ""
cfgEmailSMTPPort = 587
cfgEmailSMTPUser = ""
cfgEmailSMTPPassword = ""
cfgEmailFrom = ""
cfgEmailTo = []string{}
)
// --- PostgreSQL Database ---
var (
cfgDBUser = ""
@ -247,6 +257,36 @@ func applyConfig(env map[string]string) {
}
cfgServicesToCheck = services
}
// Email / SMTP
if v, ok := env["EMAIL_SMTP_HOST"]; ok && v != "" {
cfgEmailSMTPHost = v
}
if v, ok := env["EMAIL_SMTP_PORT"]; ok && v != "" {
if port, err := strconv.Atoi(v); err == nil {
cfgEmailSMTPPort = port
}
}
if v, ok := env["EMAIL_SMTP_USER"]; ok && v != "" {
cfgEmailSMTPUser = v
}
if v, ok := env["EMAIL_SMTP_PASSWORD"]; ok && v != "" {
cfgEmailSMTPPassword = v
}
if v, ok := env["EMAIL_FROM"]; ok && v != "" {
cfgEmailFrom = v
}
if v, ok := env["EMAIL_TO"]; ok && v != "" {
parts := strings.Split(v, ",")
recipients := make([]string, 0, len(parts))
for _, s := range parts {
s = strings.TrimSpace(s)
if s != "" {
recipients = append(recipients, s)
}
}
cfgEmailTo = recipients
}
}
// =============================================================================
@ -260,6 +300,12 @@ func isTelegramConfigured() bool {
cfgTelegramChatID != "YOUR_CHAT_ID"
}
func isEmailConfigured() bool {
return cfgEmailSMTPHost != "" &&
cfgEmailFrom != "" &&
len(cfgEmailTo) > 0
}
// printConfigStatus выводит статус загруженной конфигурации
func printConfigStatus() {
fmt.Println("📋 Статус конфигурации:")
@ -278,6 +324,13 @@ func printConfigStatus() {
fmt.Println(" ⚠️ База данных: не настроена")
}
// Email
if isEmailConfigured() {
fmt.Printf(" ✅ Email: %s → %s (smtp: %s:%d)\n", cfgEmailFrom, strings.Join(cfgEmailTo, ", "), cfgEmailSMTPHost, cfgEmailSMTPPort)
} else {
fmt.Println(" ⚠️ Email: не настроен (EMAIL_SMTP_HOST, EMAIL_FROM, EMAIL_TO)")
}
// Services
if len(cfgServicesToCheck) > 0 {
fmt.Printf(" ✅ Службы для проверки: %s\n", strings.Join(cfgServicesToCheck, ", "))

View file

@ -502,6 +502,11 @@ func dcSendAlert(hostname string, changes []dcStateChange) {
} else {
log.Println("✅ Алерт успешно отправлен в Telegram")
}
if err := emailSendHTML("Auspex: Проблемы с контейнерами", message); err != nil {
log.Printf("❌ Ошибка отправки Email: %v", err)
} else {
log.Println("✅ Алерт успешно отправлен на Email")
}
}
// ── Вспомогательные функции ──────────────────────────────────────────────────

85
email.go Normal file
View file

@ -0,0 +1,85 @@
package main
import (
"fmt"
"log"
"net/smtp"
"strings"
)
// emailSendHTML отправляет HTML-письмо через SMTP.
// Telegram-совместимые теги (<b>, <code>, <i>) корректно отображаются в HTML-письме.
// Если email не настроен или произошла ошибка — пишет в лог и не паникует.
func emailSendHTML(subject, message string) error {
// Проверяем минимально необходимые параметры
if !isEmailConfigured() {
log.Printf("[Email] Не настроен (нужны EMAIL_SMTP_HOST, EMAIL_FROM, EMAIL_TO) — пропускаем")
return fmt.Errorf("email не настроен")
}
// Предупреждаем если авторизация неполная — попытку всё равно делаем,
// некоторые SMTP-серверы работают без auth (например внутренние relay)
if cfgEmailSMTPUser == "" && cfgEmailSMTPPassword == "" {
log.Printf("[Email] ⚠️ EMAIL_SMTP_USER и EMAIL_SMTP_PASSWORD не заданы — попытка без авторизации")
} else if cfgEmailSMTPUser == "" {
log.Printf("[Email] ⚠️ EMAIL_SMTP_USER не задан — авторизация невозможна, попытка без неё")
} else if cfgEmailSMTPPassword == "" {
log.Printf("[Email] ⚠️ EMAIL_SMTP_PASSWORD не задан — авторизация невозможна, попытка без неё")
}
body := emailBuildHTML(subject, message)
addr := fmt.Sprintf("%s:%d", cfgEmailSMTPHost, cfgEmailSMTPPort)
var auth smtp.Auth
if cfgEmailSMTPUser != "" && cfgEmailSMTPPassword != "" {
auth = smtp.PlainAuth("", cfgEmailSMTPUser, cfgEmailSMTPPassword, cfgEmailSMTPHost)
}
header := strings.Join([]string{
"From: " + cfgEmailFrom,
"To: " + strings.Join(cfgEmailTo, ", "),
"Subject: " + subject,
"MIME-Version: 1.0",
"Content-Type: text/html; charset=UTF-8",
"",
}, "\r\n")
msg := []byte(header + "\r\n" + body)
log.Printf("[Email] Отправка: %s → %s (smtp: %s)", cfgEmailFrom, strings.Join(cfgEmailTo, ", "), addr)
if err := smtp.SendMail(addr, auth, cfgEmailFrom, cfgEmailTo, msg); err != nil {
return fmt.Errorf("ошибка отправки письма (smtp: %s): %w", addr, err)
}
return nil
}
// emailBuildHTML оборачивает тело сообщения в HTML-шаблон письма.
// Telegram-теги уже являются валидным HTML, поэтому конвертация не нужна.
func emailBuildHTML(subject, body string) string {
// Заменяем переносы строк на <br> для корректного отображения в письме
htmlBody := strings.ReplaceAll(body, "\n", "<br>\n")
return fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>%s</title>
<style>
body { font-family: Arial, sans-serif; font-size: 14px; color: #222; background: #f9f9f9; padding: 20px; }
.card { background: #fff; border: 1px solid #ddd; border-radius: 6px; padding: 20px; max-width: 640px; }
b { color: #c0392b; }
code { background: #f0f0f0; padding: 2px 5px; border-radius: 3px; font-family: monospace; font-size: 13px; }
i { color: #555; }
.footer { margin-top: 16px; font-size: 12px; color: #999; }
</style>
</head>
<body>
<div class="card">
%s
<div class="footer">Auspex v%s мониторинг инфраструктуры</div>
</div>
</body>
</html>`, subject, htmlBody, appVersion)
}

View file

@ -477,4 +477,9 @@ func lcSendAlerts(hostname string, staleFiles []lcLogFileStatus) {
} else {
log.Println("✅ Алерт успешно отправлен в Telegram")
}
if err := emailSendHTML("Auspex: Устаревшие логи", message); err != nil {
log.Printf("❌ Ошибка отправки Email: %v", err)
} else {
log.Println("✅ Алерт успешно отправлен на Email")
}
}

View file

@ -7,7 +7,7 @@ import (
"strings"
)
const appVersion = "1.2.0"
const appVersion = "1.3.0"
// Цвета терминала (общие для всех модулей)
const (

View file

@ -313,6 +313,11 @@ func scSendAlert(hostname string, changes []scStateChange) {
} else {
log.Println("✅ Алерт успешно отправлен в Telegram")
}
if err := emailSendHTML("Auspex: Проблемы со службами", message); err != nil {
log.Printf("❌ Ошибка отправки Email: %v", err)
} else {
log.Println("✅ Алерт успешно отправлен на Email")
}
}
// ── Вспомогательные функции ──────────────────────────────────────────────────