This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// Package notify formats and delivers monitor state-change alerts to
|
||||
// notification channels. It depends only on models so services can call it
|
||||
// without an import cycle.
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mrhid6/vantage/server/internal/models"
|
||||
)
|
||||
|
||||
// Event describes a monitor state transition worth alerting on.
|
||||
type Event struct {
|
||||
MonitorName string
|
||||
Type string
|
||||
OldStatus string
|
||||
NewStatus string
|
||||
Message string
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// title is a short one-line summary used by the text-based channels.
|
||||
func (e Event) title() string {
|
||||
verb := "recovered"
|
||||
if e.NewStatus == models.StatusDown {
|
||||
verb = "is DOWN"
|
||||
}
|
||||
s := fmt.Sprintf("[Vantage] %s (%s) %s", e.MonitorName, e.Type, verb)
|
||||
if e.Message != "" {
|
||||
s += ": " + e.Message
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Dispatch delivers ev to a single channel, formatting per channel type.
|
||||
func Dispatch(ch models.NotificationChannel, ev Event) error {
|
||||
switch ch.Type {
|
||||
case models.ChannelWebhook:
|
||||
return dispatchWebhook(ch, ev)
|
||||
case models.ChannelDiscord:
|
||||
return dispatchDiscord(ch, ev)
|
||||
case models.ChannelSlack:
|
||||
return dispatchSlack(ch, ev)
|
||||
case models.ChannelTelegram:
|
||||
return dispatchTelegram(ch, ev)
|
||||
case models.ChannelSMTP:
|
||||
return dispatchSMTP(ch, ev)
|
||||
default:
|
||||
return fmt.Errorf("unknown channel type: %s", ch.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Test delivers a synthetic event so users can verify a channel's configuration.
|
||||
func Test(ch models.NotificationChannel) error {
|
||||
return Dispatch(ch, Event{
|
||||
MonitorName: "Test monitor",
|
||||
Type: "http",
|
||||
OldStatus: models.StatusUp,
|
||||
NewStatus: models.StatusDown,
|
||||
Message: "this is a test alert from Vantage",
|
||||
Time: time.Now(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mrhid6/vantage/server/internal/models"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
func postJSON(target string, payload any) error {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := httpClient.Post(target, "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("HTTP %d from %s", resp.StatusCode, target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dispatchWebhook posts the full event as JSON to a user-supplied URL.
|
||||
func dispatchWebhook(ch models.NotificationChannel, ev Event) error {
|
||||
target := ch.Config["url"]
|
||||
if target == "" {
|
||||
return fmt.Errorf("webhook: missing url")
|
||||
}
|
||||
return postJSON(target, map[string]any{
|
||||
"monitor": ev.MonitorName,
|
||||
"type": ev.Type,
|
||||
"old_status": ev.OldStatus,
|
||||
"new_status": ev.NewStatus,
|
||||
"message": ev.Message,
|
||||
"time": ev.Time.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
func dispatchDiscord(ch models.NotificationChannel, ev Event) error {
|
||||
target := ch.Config["url"]
|
||||
if target == "" {
|
||||
return fmt.Errorf("discord: missing url")
|
||||
}
|
||||
return postJSON(target, map[string]string{"content": ev.title()})
|
||||
}
|
||||
|
||||
func dispatchSlack(ch models.NotificationChannel, ev Event) error {
|
||||
target := ch.Config["url"]
|
||||
if target == "" {
|
||||
return fmt.Errorf("slack: missing url")
|
||||
}
|
||||
return postJSON(target, map[string]string{"text": ev.title()})
|
||||
}
|
||||
|
||||
func dispatchTelegram(ch models.NotificationChannel, ev Event) error {
|
||||
token := ch.Config["token"]
|
||||
chatID := ch.Config["chat_id"]
|
||||
if token == "" || chatID == "" {
|
||||
return fmt.Errorf("telegram: missing token or chat_id")
|
||||
}
|
||||
api := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", token)
|
||||
return postJSON(api, map[string]string{"chat_id": chatID, "text": ev.title()})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
|
||||
"github.com/mrhid6/vantage/server/internal/models"
|
||||
)
|
||||
|
||||
// dispatchSMTP sends the alert as a plain-text email. Config keys: host, port,
|
||||
// username, password, from, to. Auth is skipped when username is empty.
|
||||
func dispatchSMTP(ch models.NotificationChannel, ev Event) error {
|
||||
host := ch.Config["host"]
|
||||
port := ch.Config["port"]
|
||||
from := ch.Config["from"]
|
||||
to := ch.Config["to"]
|
||||
if host == "" || port == "" || from == "" || to == "" {
|
||||
return fmt.Errorf("smtp: missing host/port/from/to")
|
||||
}
|
||||
|
||||
title := ev.title()
|
||||
msg := strings.Join([]string{
|
||||
"From: " + from,
|
||||
"To: " + to,
|
||||
"Subject: " + title,
|
||||
"",
|
||||
title,
|
||||
"",
|
||||
"Monitor: " + ev.MonitorName,
|
||||
"Status: " + ev.OldStatus + " -> " + ev.NewStatus,
|
||||
"Time: " + ev.Time.String(),
|
||||
}, "\r\n")
|
||||
|
||||
var auth smtp.Auth
|
||||
if user := ch.Config["username"]; user != "" {
|
||||
auth = smtp.PlainAuth("", user, ch.Config["password"], host)
|
||||
}
|
||||
addr := host + ":" + port
|
||||
recipients := strings.Split(to, ",")
|
||||
for i := range recipients {
|
||||
recipients[i] = strings.TrimSpace(recipients[i])
|
||||
}
|
||||
return smtp.SendMail(addr, auth, from, recipients, []byte(msg))
|
||||
}
|
||||
Reference in New Issue
Block a user