Skip to content

Commit

Permalink
Merge pull request #40 from teknologi-umum/feat/alerter
Browse files Browse the repository at this point in the history
feat(backend): alerting
  • Loading branch information
aldy505 authored Sep 9, 2024
2 parents 17bd6a6 + 2ad74bf commit 57c8f08
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 3 deletions.
87 changes: 87 additions & 0 deletions backend/alerter.go
Original file line number Diff line number Diff line change
@@ -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
}
12 changes: 12 additions & 0 deletions backend/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 16 additions & 1 deletion backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
44 changes: 42 additions & 2 deletions backend/monitor_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
type Processor struct {
historicalWriter *MonitorHistoricalWriter
historicalReader *MonitorHistoricalReader

telegramAlertProvider Alerter
discordAlertProvider Alerter
}

func (m *Processor) ProcessResponse(response Response) {
Expand All @@ -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!")
}
}
}()
}

0 comments on commit 57c8f08

Please sign in to comment.