Skip to content

Add abstractions for alerting + Gmail and Grafana implementations #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions alerts/alerts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package alerts

import (
"fmt"
"log"
)

type Alerter interface {
SendAlert(title, content string) error
}

type AlertsConfig struct {
FailOnError bool
Gmail MailerConfig
Grafana GrafanaConfig
}

type AlertsManager struct {
failOnError bool
alerters []Alerter
}

func NewAlertsManager(config AlertsConfig) (*AlertsManager, error) {
alerters := []Alerter{}

if config.Gmail.Enabled {
log.Println("Initializig Gmail alerter ...")
gmailAlerter, err := NewGmailAlerter(config.Gmail)
if err != nil {
return nil, err
}

alerters = append(alerters, gmailAlerter)
log.Println("Gmail alerter ready")
}

if config.Grafana.Enabled {
log.Println("Initializig Grafana alerter ...")
grafanaAlerter, err := NewGrafanaAlerter(config.Grafana)
if err != nil {
return nil, err
}

alerters = append(alerters, grafanaAlerter)
log.Println("Grafana alerter ready")
}

return &AlertsManager{
failOnError: config.FailOnError,
alerters: alerters,
}, nil
}

func (am *AlertsManager) DispatchAlert(repo string, cause error) error {
title := fmt.Sprintf("Minima - failure for %s", repo)
content := fmt.Sprintf("Cause: %v", cause)

for _, a := range am.alerters {
if err := a.SendAlert(title, content); err != nil {
log.Printf("Alerter error: %v\n", err)
if am.failOnError {
return err
}
}
}
return nil
}
59 changes: 59 additions & 0 deletions alerts/gmail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package alerts

import (
"fmt"
"log"

"gopkg.in/gomail.v2"
)

const gmailSMTP = "smtp.gmail.com"
const gmailSMTPPort = 587

type MailerConfig struct {
Enabled bool
Account string
Password string
From string
Recipients []string
}

type GmailAlerter struct {
mailClient gomail.SendCloser
from string
recipients []string
}

func NewGmailAlerter(config MailerConfig) (*GmailAlerter, error) {
d := gomail.NewDialer(gmailSMTP, gmailSMTPPort, config.Account, config.Password)
sender, err := d.Dial()
if err != nil {
return nil, err
}

return &GmailAlerter{
mailClient: sender,
from: config.From,
recipients: config.Recipients,
}, nil
}

// Alerter interface implementation
func (config *GmailAlerter) SendAlert(title, content string) error {
log.Printf("Sending alert via gmail to: %v\n", config.recipients)

if err := config.sendEmail(title, content); err != nil {
return fmt.Errorf("failed to send alert via email: %v", err)
}
return nil
}

func (config *GmailAlerter) sendEmail(subject, body string) error {
m := gomail.NewMessage()
m.SetHeader("From", config.from)
m.SetHeader("To", config.recipients...)
m.SetHeader("Subject", subject)
m.SetBody("text/plain", body)

return gomail.Send(config.mailClient, m)
}
109 changes: 109 additions & 0 deletions alerts/grafana.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package alerts

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)

const (
alertsEndpoint = "/alerts"
healthCheckEndpoint = "/health"
)

type GrafanaConfig struct {
Enabled bool
AlertTitle string
APIUrl string
APIKey string
}

type GrafanaAlerter struct {
httpClient *http.Client
key string
apiURL string
}

type AlertPayload struct {
Title string `json:"title"`
Message string `json:"message"`
AlertStatus string `json:"status"`
}

func NewGrafanaAlerter(config GrafanaConfig) (*GrafanaAlerter, error) {
ga := &GrafanaAlerter{
httpClient: &http.Client{Timeout: 30 * time.Second},
apiURL: config.APIUrl,
key: config.APIKey,
}

if err := ga.checkHealth(); err != nil {
return nil, err
}
return ga, nil
}

// Alerter interface implementation
func (g *GrafanaAlerter) SendAlert(title, content string) error {
fmt.Println("Sending Grafana alert")

if err := g.postAlert(title, content); err != nil {
return fmt.Errorf("failed to send Grafana alert: %v", err)
}
return nil
}

// CheckHealth verifies if the Grafana API is reachable
func (g *GrafanaAlerter) checkHealth() error {
req, err := http.NewRequest("GET", g.apiURL+healthCheckEndpoint, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+g.key)

resp, err := g.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("health check failed for Grafana API, status code: %d", resp.StatusCode)
}

fmt.Println("Grafana API is healthy!")
return nil
}

func (g *GrafanaAlerter) postAlert(title, msg string) error {
alert := AlertPayload{
Title: title,
Message: msg,
AlertStatus: "alerting",
}

jsonData, err := json.Marshal(alert)
if err != nil {
return err
}

req, err := http.NewRequest("POST", g.apiURL+alertsEndpoint, bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+g.key)

resp, err := g.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to send Grafana alert, status code: %d", resp.StatusCode)
}
return nil
}
48 changes: 31 additions & 17 deletions cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,23 @@ import (
"strings"

"github.com/spf13/cobra"
"github.com/uyuni-project/minima/alerts"
"github.com/uyuni-project/minima/get"
"github.com/uyuni-project/minima/updates"
yaml "gopkg.in/yaml.v2"
)

const sccUrl = "https://scc.suse.com"

// Config maps the configuration in minima.yaml
type Config struct {
Alerts alerts.AlertsConfig
Storage get.StorageConfig
SCC get.SCC
OBS updates.OBS
HTTP []get.HTTPRepoConfig
}

// syncCmd represents the sync command
var (
syncCmd = &cobra.Command{
Expand Down Expand Up @@ -54,18 +64,34 @@ var (
Run: func(cmd *cobra.Command, args []string) {
initConfig()

var errorflag bool = false
syncers, err := syncersFromConfig(cfgString)
config, err := parseConfig(cfgString)
if err != nil {
log.Fatal(err)
}

syncers, err := syncersFromConfig(config)
if err != nil {
log.Fatal(err)
}

alertsManager, err := alerts.NewAlertsManager(config.Alerts)
if err != nil {
log.Fatal(err)
errorflag = true
}

var errorflag bool = false
for _, syncer := range syncers {
log.Printf("Processing repo: %s", syncer.URL.String())
repo := syncer.URL.String()
log.Printf("Processing repo: %s", repo)

err := syncer.StoreRepo()
if err != nil {
log.Println(err)
errorflag = true

if err := alertsManager.DispatchAlert(repo, err); err != nil {
log.Fatal(err)
}
} else {
log.Println("...done.")
}
Expand All @@ -80,19 +106,7 @@ var (
skipLegacyPackages bool
)

// Config maps the configuration in minima.yaml
type Config struct {
Storage get.StorageConfig
SCC get.SCC
OBS updates.OBS
HTTP []get.HTTPRepoConfig
}

func syncersFromConfig(configString string) ([]*get.Syncer, error) {
config, err := parseConfig(configString)
if err != nil {
return nil, err
}
func syncersFromConfig(config Config) ([]*get.Syncer, error) {
//---passing the flag value to a global variable in get package, to disables syncing of i586 and i686 rpms (usually inside x86_64)
get.SkipLegacy = skipLegacyPackages

Expand Down
32 changes: 28 additions & 4 deletions cmd/updates.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"time"

"github.com/spf13/cobra"
"github.com/uyuni-project/minima/alerts"
"github.com/uyuni-project/minima/get"
"github.com/uyuni-project/minima/updates"
yaml "gopkg.in/yaml.v2"
Expand Down Expand Up @@ -147,18 +148,41 @@ func muFindAndSync() {
os.Exit(3)
}

syncers, err := syncersFromConfig(string(byteChunk))
config, err := parseConfig(string(byteChunk))
if err != nil {
log.Fatalf("Error parsing configuration: %v", err)
}

syncers, err := syncersFromConfig(config)
if err != nil {
log.Fatal(err)
}

alertsManager, err := alerts.NewAlertsManager(config.Alerts)
if err != nil {
log.Fatal(err)
}

var errorflag bool = false
for _, syncer := range syncers {
log.Printf("Processing repo: %s", syncer.URL.String())
repo := syncer.URL.String()
log.Printf("Processing repo: %s", repo)

err := syncer.StoreRepo()
if err != nil {
log.Fatal(err)
log.Println(err)
errorflag = true

if err := alertsManager.DispatchAlert(repo, err); err != nil {
log.Fatal(err)
}
} else {
log.Println("...done.")
}
log.Println("...done.")
}

if errorflag {
os.Exit(1)
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.36.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v2 v2.4.0
)



require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading