From e85286ba39f31e8e50f6cc6e52de2c6ae88b554b Mon Sep 17 00:00:00 2001 From: yuu Date: Mon, 27 May 2024 19:01:26 +0700 Subject: [PATCH 1/2] chore: alert lib and config --- backend/alerter.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++ backend/config.go | 12 +++++++ 2 files changed, 99 insertions(+) create mode 100644 backend/alerter.go diff --git a/backend/alerter.go b/backend/alerter.go new file mode 100644 index 0000000..25b59e2 --- /dev/null +++ b/backend/alerter.go @@ -0,0 +1,87 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +type Alerter interface { + Send(ctx context.Context, msg AlertMessage) error +} + +type AlertMessage struct { + Success bool + StatusCode int + Timestamp time.Time + MonitorID string + MonitorName string + Latency int64 +} + +type TelegramProvider struct { + url string + chatID string +} + +type TelegramProviderConfig struct { + Url string + ChatID string +} + +func NewTelegramAlertProvider(config TelegramProviderConfig) *TelegramProvider { + return &TelegramProvider{ + url: config.Url, + } +} + +func (t TelegramProvider) Send(ctx context.Context, msg AlertMessage) error { + if t.url == "" || t.chatID == "" { + return fmt.Errorf("can't make a telegram alert request: some config is not set") + } + + // Perhaps we can use a template file instead. + title := "🔴 Down" + if msg.Success { + title = "✅ Up" + } + text := fmt.Sprintf(title+` + + **MonitorID:** %s + **MonitorName:** %s + **StatusCode:** %d + **Latency:** %d + **Timestamp:** %s`, + msg.MonitorID, + msg.MonitorName, + msg.StatusCode, + msg.Latency, + msg.Timestamp.Format(time.RFC3339), + ) + payload := map[string]any{ + "chat_id": t.chatID, + "text": text, + "parse_mode": "Markdown", + } + payloadByte, _ := json.Marshal(payload) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, t.url, bytes.NewReader(payloadByte)) + if err != nil { + return fmt.Errorf("failed to send telegram alert: %w", err) + } + defer req.Body.Close() + + req.Header.Set("Content-Type", "application/json") + + client := http.Client{Timeout: time.Second * 3} + + _, err = client.Do(req) + if err != nil { + return fmt.Errorf("failed to make request: %w", err) + } + + return nil +} diff --git a/backend/config.go b/backend/config.go index fad63cb..8338b54 100644 --- a/backend/config.go +++ b/backend/config.go @@ -25,6 +25,14 @@ const ( MonitorTypePing MonitorType = "ping" ) +type AlertProviderType string + +const ( + AlertProviderTypeUnspecified AlertProviderType = "" + AlertProviderTypeTelegram AlertProviderType = "telegram" + AlertProviderTypeDiscord AlertProviderType = "discord" +) + type Monitor struct { // UniqueID specifies unique identifier for each monitor. In any case of the monitor configuration value get // changed (name, description, public monitorIds, etc), if users want to keep the data intact, they should keep the @@ -61,6 +69,10 @@ type Monitor struct { // IcmpPacketSize specifies the packet size that will be used for the ICMP request. It must be greater than zero. // The default packet size is 56 bytes. IcmpPacketSize int `json:"packet_size" yaml:"packet_size" toml:"packet_size"` + // AlertProvider specifies the type of alert provider that will be used to send alerts. It can be a string value such as + // "telegram" or "discord". + // THe default alert provider is "telegram" + AlertProvider AlertProviderType `json:"alert_provider" yam:"alert_provider" toml:"alert_provider"` } func (m Monitor) MarshalJSON() ([]byte, error) { From 2ad74bfb1d8fa5fc9a7d88d0c8b26384080bf0f9 Mon Sep 17 00:00:00 2001 From: yuu Date: Mon, 27 May 2024 19:02:04 +0700 Subject: [PATCH 2/2] chore: implement alerting --- backend/main.go | 17 +++++++++++++- backend/monitor_processor.go | 44 ++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/backend/main.go b/backend/main.go index da158d7..abc0c21 100644 --- a/backend/main.go +++ b/backend/main.go @@ -58,6 +58,16 @@ func main() { log.Warn().Msg("API_KEY is not set") } + telegramChatID, ok := os.LookupEnv("TELEGRAM_CHAT_ID") + if !ok { + log.Warn().Msg("TELEGRAM_CHAT_ID is not set") + } + + telegramUrl, ok := os.LookupEnv("TELEGRAM_URL is not set") + if !ok { + log.Warn().Msg("TELEGRAM_URL is not set") + } + if os.Getenv("ENV") == "" { err := os.Setenv("ENV", "development") if err != nil { @@ -100,7 +110,12 @@ func main() { log.Fatal().Err(err).Msg("failed to migrate database") } - processor := &Processor{} + processor := &Processor{ + telegramAlertProvider: NewTelegramAlertProvider(TelegramProviderConfig{ + Url: telegramUrl, + ChatID: telegramChatID, + }), + } // Create a new worker for _, monitor := range config.Monitors { diff --git a/backend/monitor_processor.go b/backend/monitor_processor.go index d27e48c..07a2cef 100644 --- a/backend/monitor_processor.go +++ b/backend/monitor_processor.go @@ -9,6 +9,9 @@ import ( type Processor struct { historicalWriter *MonitorHistoricalWriter historicalReader *MonitorHistoricalReader + + telegramAlertProvider Alerter + discordAlertProvider Alerter } func (m *Processor) ProcessResponse(response Response) { @@ -35,5 +38,42 @@ func (m *Processor) ProcessResponse(response Response) { log.Error().Err(err).Msg("failed to write historical data") } - // TODO: If the current status is different from the last status, send an alert notification -} + go func() { + if m.telegramAlertProvider == nil && m.discordAlertProvider == nil { + log.Warn().Msg("no alert providers are set") + return + } + + alertMessage := AlertMessage{ + Success: response.Success, + MonitorID: uniqueId, + MonitorName: response.Monitor.Name, + StatusCode: response.StatusCode, + Timestamp: response.Timestamp, + Latency: response.RequestDuration, + } + + lastRawHistorical, err := m.historicalReader.ReadRawLatest(context.Background(), uniqueId) + if err != nil { + log.Error().Err(err).Msg("failed to get raw latest historical data") + return + } + + if lastRawHistorical.Status != status { + switch response.Monitor.AlertProvider { + case AlertProviderTypeTelegram, AlertProviderTypeUnspecified: + if m.telegramAlertProvider == nil { + log.Warn().Msg("telegram alert provider is not set") + return + } + + err := m.telegramAlertProvider.Send(context.Background(), alertMessage) + if err != nil { + log.Error().Err(err).Msg("failed to send alert") + } + case AlertProviderTypeDiscord: + panic("TODO: Implement me!") + } + } + }() +} \ No newline at end of file