Skip to content

Commit

Permalink
🚀 Support zenduty (#302)
Browse files Browse the repository at this point in the history
* 🚀 support zenduty

* 🚀 support zenduty
  • Loading branch information
abahmed authored May 29, 2024
1 parent 604677a commit c837b83
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 0 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,18 @@ title
| `alert.feishu.webhook` | FeiShu bot webhook URL |
| `alert.feishu.title` | Customized title in message |

#### Zenduty

<p>
<img src="./assets/zenduty.png" width="45%"/>
</p>
If you want to enable Zenduty, provide IntegrationKey with optional alert type

| Parameter | Description |
|:-------------------------------|:----------------------------|
| `alert.zenduty.integrationKey` | Zenduty integration Key |
| `alert.zenduty.alertType` | Optional alert type of incident: critical, acknowledged, resolved, error, warning, info (default: critical) |

#### Custom webhook

If you want to enable custom webhook, provide url with optional headers and
Expand Down
4 changes: 4 additions & 0 deletions alertmanager/alertmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/abahmed/kwatch/alertmanager/teams"
"github.com/abahmed/kwatch/alertmanager/telegram"
"github.com/abahmed/kwatch/alertmanager/webhook"
"github.com/abahmed/kwatch/alertmanager/zenduty"
"github.com/abahmed/kwatch/config"
"github.com/abahmed/kwatch/event"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -67,7 +68,10 @@ func (a *AlertManager) Init(
pvdr = feishu.NewFeiShu(v, appCfg)
} else if lowerCaseKey == "webhook" {
pvdr = webhook.NewWebhook(v, appCfg)
} else if lowerCaseKey == "zenduty" {
pvdr = zenduty.NewZenduty(v, appCfg)
}

if !reflect.ValueOf(pvdr).IsNil() {
a.providers = append(a.providers, pvdr)
}
Expand Down
3 changes: 3 additions & 0 deletions alertmanager/alertmanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ func TestGetProviders(t *testing.T) {
"webhook": {
"url": "test",
},
"zenduty": {
"integrationKey": "test",
},
}

alertmanager := AlertManager{}
Expand Down
150 changes: 150 additions & 0 deletions alertmanager/zenduty/zenduty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package zenduty

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"slices"

"github.com/abahmed/kwatch/config"
"github.com/abahmed/kwatch/constant"
"github.com/abahmed/kwatch/event"
"github.com/sirupsen/logrus"
)

const (
defaultZendutyTitle = "kwatch detected a crash in pod: %s"
defaultZendutyText = "There is an issue with container (%s) in pod (%s)"
zendutyAPIURL = "https://www.zenduty.com/api/events"
)

var AlertTypes = []string{
"critical",
"acknowledged",
"resolved",
"error",
"warning",
"info",
}

type Zenduty struct {
integrationkey string
url string
alertType string

// reference for general app configuration
appCfg *config.App
}

type zendutyPayload struct {
Message string `json:"message"`
Summary string `json:"summary"`
AlertType string `json:"alert_type"`
}

// NewZenduty returns new zenduty instance
func NewZenduty(config map[string]interface{}, appCfg *config.App) *Zenduty {
integrationKey, ok := config["integrationKey"].(string)
if !ok || len(integrationKey) == 0 {
logrus.Warnf("initializing zenduty with empty webhook url")
return nil
}

logrus.Infof("initializing zenduty with secret apikey")

// If alert type is not provided, or provided with invalid value
// it will fallback to critical type
alertType, ok := config["alertType"].(string)
if !ok || !slices.Contains(AlertTypes, alertType) {
alertType = "critical"
}

return &Zenduty{
integrationkey: integrationKey,
url: zendutyAPIURL,
alertType: alertType,
appCfg: appCfg,
}
}

// Name returns name of the provider
func (m *Zenduty) Name() string {
return "Zenduty"
}

// SendMessage sends text message to the provider
func (m *Zenduty) SendMessage(msg string) error {
return nil
}

// SendEvent sends event to the provider
func (m *Zenduty) SendEvent(e *event.Event) error {
return m.sendAPI(m.buildMessage(e))
}

// sendAPI sends http request to Zenduty API
func (m *Zenduty) sendAPI(content []byte) error {
client := &http.Client{}
buffer := bytes.NewBuffer(content)
url := m.url + "/" + m.integrationkey + "/"
request, err := http.NewRequest(http.MethodPost, url, buffer)
if err != nil {
return err
}

response, err := client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()

if response.StatusCode != 201 {
body, _ := io.ReadAll(response.Body)
return fmt.Errorf(
"call to zenduty alert returned status code %d: %s",
response.StatusCode,
string(body))
}

return nil
}

func (m *Zenduty) buildMessage(e *event.Event) []byte {
payload := zendutyPayload{
AlertType: m.alertType,
}

logs := constant.DefaultLogs
if len(e.Logs) > 0 {
logs = (e.Logs)
}

events := constant.DefaultEvents
if len(e.Events) > 0 {
events = (e.Events)
}

payload.Message = fmt.Sprintf(defaultZendutyTitle, e.Name)
payload.Summary = fmt.Sprintf(
"An alert has been triggered for\n\n"+
"cluster: %s\n"+
"Pod Name: %s\n"+
"Container: %s\n"+
"Namespace: %s\n"+
"Reason: %s\n\n"+
"Events:\n%s\n\n"+
"Logs:\n%s\n\n",
m.appCfg.ClusterName,
e.Name,
e.Container,
e.Namespace,
e.Reason,
events,
logs,
)

str, _ := json.Marshal(payload)
return str
}
131 changes: 131 additions & 0 deletions alertmanager/zenduty/zenduty_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package zenduty

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/abahmed/kwatch/config"
"github.com/abahmed/kwatch/event"
"github.com/stretchr/testify/assert"
)

func TestZendutyEmptyConfig(t *testing.T) {
assert := assert.New(t)

c := NewZenduty(map[string]interface{}{}, &config.App{ClusterName: "dev"})
assert.Nil(c)
}

func TestZenduty(t *testing.T) {
assert := assert.New(t)

configMap := map[string]interface{}{
"integrationKey": "testtest",
}
c := NewZenduty(configMap, &config.App{ClusterName: "dev"})
assert.NotNil(c)

assert.Equal(c.Name(), "Zenduty")
}

func TestSendMessage(t *testing.T) {
assert := assert.New(t)

configMap := map[string]interface{}{
"integrationKey": "test",
}
c := NewZenduty(configMap, &config.App{ClusterName: "dev"})
assert.NotNil(c)

assert.Nil(c.SendMessage("test"))
}

func TestSendEvent(t *testing.T) {
assert := assert.New(t)

s := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"isOk": true}`))
}))

defer s.Close()

configMap := map[string]interface{}{
"integrationKey": "test",
}
c := NewZenduty(configMap, &config.App{ClusterName: "dev"})
assert.NotNil(c)

c.url = s.URL

ev := event.Event{
Name: "test-pod",
Container: "test-container",
Namespace: "default",
Reason: "OOMKILLED",
Logs: "test\ntestlogs",
Events: "event1-event2-event3-event1-event2-event3-event1-event2-" +
"event3\nevent5\nevent6-event8-event11-event12",
}
assert.Nil(c.SendEvent(&ev))
}

func TestSendEventError(t *testing.T) {
assert := assert.New(t)

s := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
}))

defer s.Close()

configMap := map[string]interface{}{
"integrationKey": "test",
}
c := NewZenduty(configMap, &config.App{ClusterName: "dev"})
assert.NotNil(c)

c.url = s.URL

ev := event.Event{
Name: "test-pod",
Container: "test-container",
Namespace: "default",
Reason: "OOMKILLED",
Logs: "test\ntestlogs",
Events: "event1-event2-event3-event1-event2-event3-event1-event2-" +
"event3\nevent5\nevent6-event8-event11-event12",
}
assert.NotNil(c.SendEvent(&ev))
}

func TestInvaildHttpRequest(t *testing.T) {
assert := assert.New(t)

configMap := map[string]interface{}{
"integrationKey": "test",
}
c := NewZenduty(configMap, &config.App{ClusterName: "dev"})
assert.NotNil(c)
c.url = "h ttp://localhost"

ev := event.Event{
Name: "test-pod",
Container: "test-container",
Namespace: "default",
Reason: "OOMKILLED",
Logs: "test\ntestlogs",
Events: "event1-event2-event3-event1-event2-event3-event1-event2-" +
"event3\nevent5\nevent6-event8-event11-event12",
}
assert.NotNil(c.SendEvent(&ev))

c = NewZenduty(configMap, &config.App{ClusterName: "dev"})
assert.NotNil(c)
c.url = "http://localhost:132323"

assert.NotNil(c.SendEvent(&ev))
}
Binary file added assets/zenduty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit c837b83

Please sign in to comment.