Repository: abahmed/kwatch (go)

Issue:
Slack Message tittle and text not working after v0.7.0 upgrade
**Describe the bug**
I have got a problem since the last release (v0.7) in that the slack messages now don't display the title or text I've configured in the kwatch config, they all just show the default text (kwatch detected a crash in the pod. && There is an issue with the container in a pod!). 
I even deleted kwatch entirely and did a fresh installation but the same thing happened. 

**To Reproduce**
Upgrade from v0.6 to v0.7 using:

`kubectl apply -f https://raw.githubusercontent.com/abahmed/kwatch/v0.7.0/deploy/deploy.yaml`

This is a config file I'm using:

```
apiVersion: v1
kind: ConfigMap
metadata:
  name: kwatch
  namespace: kwatch
data:
  config.yaml: |
    maxRecentLogLines: 8  
    ignoreFailedGracefulShutdown: true
    alert:
      slack:
        webhook: https://hooks.slack.com/services/xxxxxxxxxxxxx
        title: "There is a problem in Cluster XXXX" 
```



**Expected behavior**
I should be getting the correct title in the slack channel configured.

**Actual behavior**
I'm getting the default title in slack:

<img width="565" alt="image" src="https://user-images.githubusercontent.com/109222409/217581658-60074f83-e7f2-4f9c-9efa-61adbeb27d00.png">

I don't even have the red dot in the text, but I'm getting it anyways. 

**Version/Commit**
version 0.7.0

Pull request description:
Support event html and markdown formatting
Fixes #186

Changes proposed in this pull request:
- Support event html and markdown formatting
- Fix reading slack title and text from config


Patch:
diff --git a/alertmanager/dingtalk/dingtalk.go b/alertmanager/dingtalk/dingtalk.go
index d76ffa5..411a345 100644
--- a/alertmanager/dingtalk/dingtalk.go
+++ b/alertmanager/dingtalk/dingtalk.go
@@ -10,7 +10,6 @@ import (
 	"io"
 	"net/http"
 	"net/url"
-	"strings"
 	"time"
 
 	"github.com/abahmed/kwatch/config"
@@ -64,19 +63,6 @@ func (d *DingTalk) Name() string {
 
 // SendEvent sends event to the provider
 func (d *DingTalk) SendEvent(e *event.Event) error {
-	// add events part if it exists
-	eventsText := "No events captured"
-	events := strings.TrimSpace(e.Events)
-	if len(events) > 0 {
-		eventsText = e.Events
-	}
-
-	// add logs part if it exists
-	logsText := "No logs captured"
-	logs := strings.TrimSpace(e.Logs)
-	if len(logs) > 0 {
-		logsText = e.Logs
-	}
 
 	// use custom title if it's provided, otherwise use default
 	title := d.title
@@ -84,27 +70,12 @@ func (d *DingTalk) SendEvent(e *event.Event) error {
 		title = constant.DefaultTitle
 	}
 
-	txt := fmt.Sprintf(
-		"Cluster: *%s* \n"+
-			"Name: *%s* \n"+
-			"Container: *%s* \n"+
-			"Namespace: *%s* \n"+
-			"Reason: *%s* \n"+
-			"Logs: *%s* \n "+
-			"Events: *%s* ",
-		d.appCfg.ClusterName,
-		e.Name,
-		e.Container,
-		e.Namespace,
-		e.Reason,
-		logsText,
-		eventsText,
-	)
+	msg := e.FormatMarkdown(d.appCfg.ClusterName, "")
 
 	body := fmt.Sprintf(`{
 		"msgtype": "markdown",
 		"markdown": { "title": "%s", "text: "%s" }
-	}`, title, txt)
+	}`, title, msg)
 
 	return d.sendAPI(body)
 }
diff --git a/alertmanager/feishu/feishu.go b/alertmanager/feishu/feishu.go
index f7e400e..73748f4 100644
--- a/alertmanager/feishu/feishu.go
+++ b/alertmanager/feishu/feishu.go
@@ -6,10 +6,8 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"strings"
 
 	"github.com/abahmed/kwatch/config"
-	"github.com/abahmed/kwatch/constant"
 	"github.com/abahmed/kwatch/event"
 	"github.com/sirupsen/logrus"
 )
@@ -51,7 +49,8 @@ func (r *FeiShu) Name() string {
 
 // SendEvent sends event to the provider
 func (r *FeiShu) SendEvent(e *event.Event) error {
-	return r.sendByFeiShuApi(r.buildRequestBodyFeiShu(e, ""))
+	formattedMsg := e.FormatMarkdown(r.appCfg.ClusterName, "")
+	return r.sendByFeiShuApi(r.buildRequestBodyFeiShu(formattedMsg))
 }
 
 func (r *FeiShu) sendByFeiShuApi(reqBody string) error {
@@ -83,58 +82,17 @@ func (r *FeiShu) sendByFeiShuApi(reqBody string) error {
 
 // SendMessage sends text message to the provider
 func (r *FeiShu) SendMessage(msg string) error {
-	return r.sendByFeiShuApi(
-		r.buildRequestBodyFeiShu(new(event.Event), msg),
-	)
+	return r.sendByFeiShuApi(r.buildRequestBodyFeiShu(msg))
 }
 
 func (r *FeiShu) buildRequestBodyFeiShu(
-	e *event.Event,
-	customMsg string) string {
-	// add events part if it exists
-	eventsText := constant.DefaultEvents
-	events := strings.TrimSpace(e.Events)
-	if len(events) > 0 {
-		eventsText = e.Events
-	}
-
-	// add logs part if it exist
-	logsText := constant.DefaultLogs
-	logs := strings.TrimSpace(e.Logs)
-	if len(logs) > 0 {
-		logsText = e.Logs
-	}
-
-	// build text will be sent in the message use custom text if it's provided,
-	// otherwise use default
-	text := ""
-	if len(customMsg) <= 0 {
-		text = fmt.Sprintf(
-			"**Cluster:** %s\n"+
-				"**Pod:** %s\n"+
-				"**Container:** %s\n"+
-				"**Namespace:** %s\n"+
-				"**Reason:** %s\n"+
-				"**Events:**\n```\n%s\n```\n"+
-				"**Logs:**\n```\n%s\n```",
-			r.appCfg.ClusterName,
-			e.Name,
-			e.Container,
-			e.Namespace,
-			e.Reason,
-			eventsText,
-			logsText,
-		)
-	} else {
-		text = customMsg
-	}
+	text string) string {
 	var content = []feiShuWebhookContent{
 		{
 			Tag:     "markdown",
 			Content: text,
 		},
 	}
-
 	jsonBytes, _ := json.Marshal(content)
 
 	body := "{\"msg_type\": \"interactive\",\"card\": {\"config\": {\"wide_screen_mode\": true},\"header\": {\"title\": {\"tag\": \"plain_text\",\"content\": \"" +
diff --git a/alertmanager/matrix/matrix.go b/alertmanager/matrix/matrix.go
index 4e54921..806a8b5 100644
--- a/alertmanager/matrix/matrix.go
+++ b/alertmanager/matrix/matrix.go
@@ -7,10 +7,8 @@ import (
 	"net/http"
 	"net/url"
 	"regexp"
-	"strings"
 
 	"github.com/abahmed/kwatch/config"
-	"github.com/abahmed/kwatch/constant"
 	"github.com/abahmed/kwatch/event"
 	"github.com/abahmed/kwatch/util"
 	"github.com/sirupsen/logrus"
@@ -66,52 +64,7 @@ func (m *Matrix) SendMessage(msg string) error {
 }
 
 func (m *Matrix) SendEvent(e *event.Event) error {
-	eventsText := constant.DefaultEvents
-	logsText := constant.DefaultLogs
-
-	// add events part if it exists
-	events := strings.TrimSpace(e.Events)
-	if len(events) > 0 {
-		eventsText = e.Events
-	}
-
-	// add logs part if it exists
-	logs := strings.TrimSpace(e.Logs)
-	if len(logs) > 0 {
-		logsText = e.Logs
-	}
-	// use custom title if it's provided, otherwise use default
-	title := m.title
-	if len(title) == 0 {
-		title = constant.DefaultTitle
-	}
-
-	// use custom text if it's provided, otherwise use default
-	text := m.text
-	if len(text) == 0 {
-		text = constant.DefaultText
-	}
-
-	msg := fmt.Sprintf(
-		"%s<br/>"+
-			"<b>Cluster:</b> %s <br/>"+
-			"<b>Pod:</b> %s <br/>"+
-			"<b>Container:</b> %s<br/>"+
-			"<b>Namespace:</b> %s<br/>"+
-			"<b>Reason:</b> %s<br/>"+
-			"<b>Events:</b><br/><blockquote>%s</blockquote>"+
-			"<b>Logs:</b> <br/><blockquote>%s</blockquote>",
-		text,
-		m.appCfg.ClusterName,
-		e.Name,
-		e.Container,
-		e.Namespace,
-		e.Reason,
-		strings.ReplaceAll(eventsText, "\n", "<br/>"),
-		strings.ReplaceAll(logsText, "\n", "<br/>"),
-	)
-
-	return m.sendAPI(msg)
+	return m.sendAPI(e.FormatHtml(m.appCfg.ClusterName, m.text))
 }
 
 func (m *Matrix) sendAPI(formattedMsg string) error {
diff --git a/alertmanager/rocketchat/rocketchat.go b/alertmanager/rocketchat/rocketchat.go
index 2056de5..0686a64 100644
--- a/alertmanager/rocketchat/rocketchat.go
+++ b/alertmanager/rocketchat/rocketchat.go
@@ -6,10 +6,8 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"strings"
 
 	"github.com/abahmed/kwatch/config"
-	"github.com/abahmed/kwatch/constant"
 	"github.com/abahmed/kwatch/event"
 	"github.com/sirupsen/logrus"
 )
@@ -50,7 +48,8 @@ func (r *RocketChat) Name() string {
 
 // SendEvent sends event to the provider
 func (r *RocketChat) SendEvent(e *event.Event) error {
-	return r.sendByRocketChatApi(r.buildRequestBodyRocketChat(e, ""))
+	formattedMsg := e.FormatMarkdown(r.appCfg.ClusterName, r.text)
+	return r.sendByRocketChatApi(r.buildRequestBodyRocketChat(formattedMsg))
 }
 
 func (r *RocketChat) sendByRocketChatApi(reqBody string) error {
@@ -82,54 +81,10 @@ func (r *RocketChat) sendByRocketChatApi(reqBody string) error {
 
 // SendMessage sends text message to the provider
 func (r *RocketChat) SendMessage(msg string) error {
-	return r.sendByRocketChatApi(
-		r.buildRequestBodyRocketChat(new(event.Event), msg),
-	)
+	return r.sendByRocketChatApi(r.buildRequestBodyRocketChat(msg))
 }
 
-func (r *RocketChat) buildRequestBodyRocketChat(
-	e *event.Event,
-	customMsg string) string {
-	// add events part if it exists
-	eventsText := constant.DefaultEvents
-	events := strings.TrimSpace(e.Events)
-	if len(events) > 0 {
-		eventsText = e.Events
-	}
-
-	// add logs part if it exist
-	logsText := constant.DefaultLogs
-	logs := strings.TrimSpace(e.Logs)
-	if len(logs) > 0 {
-		logsText = e.Logs
-	}
-
-	// build text will be sent in the message use custom text if it's provided,
-	// otherwise use default
-	text := r.text
-	if len(customMsg) <= 0 {
-		text = fmt.Sprintf(
-			"%s\n"+
-				"**Cluster:** %s\n"+
-				"**Pod:** %s\n"+
-				"**Container:** %s\n"+
-				"**Namespace:** %s\n"+
-				"**Reason:** %s\n"+
-				"**Events:**\n```\n%s\n```\n"+
-				"**Logs:**\n```\n%s\n```",
-			text,
-			r.appCfg.ClusterName,
-			e.Name,
-			e.Container,
-			e.Namespace,
-			e.Reason,
-			eventsText,
-			logsText,
-		)
-	} else {
-		text = customMsg
-	}
-
+func (r *RocketChat) buildRequestBodyRocketChat(text string) string {
 	msgPayload := &rocketChatWebhookPayload{
 		Text: text,
 	}
diff --git a/alertmanager/slack/slack.go b/alertmanager/slack/slack.go
index 1b2a244..10a06a2 100644
--- a/alertmanager/slack/slack.go
+++ b/alertmanager/slack/slack.go
@@ -44,6 +44,8 @@ func NewSlack(config map[string]string, appCfg *config.App) *Slack {
 	return &Slack{
 		webhook: webhook,
 		channel: config["channel"],
+		title:   config["title"],
+		text:    config["text"],
 		send:    slackClient.PostWebhook,
 		appCfg:  appCfg,
 	}
diff --git a/alertmanager/teams/teams.go b/alertmanager/teams/teams.go
index dce3a19..ee715a1 100644
--- a/alertmanager/teams/teams.go
+++ b/alertmanager/teams/teams.go
@@ -7,10 +7,8 @@ import (
 	"io"
 
 	"net/http"
-	"strings"
 
 	"github.com/abahmed/kwatch/config"
-	"github.com/abahmed/kwatch/constant"
 	"github.com/abahmed/kwatch/event"
 	"github.com/sirupsen/logrus"
 )
@@ -92,49 +90,13 @@ func (t *Teams) SendMessage(msg string) error {
 
 // buildRequestBodyTeams builds formatted string from event
 func (t *Teams) buildRequestBodyTeams(e *event.Event) string {
-	// add events part if it exists
-	eventsText := constant.DefaultEvents
-	events := strings.TrimSpace(e.Events)
-	if len(events) > 0 {
-		eventsText = e.Events
-	}
-
-	// add logs part if it exists
-	logsText := constant.DefaultLogs
-	logs := strings.TrimSpace(e.Logs)
-	if len(logs) > 0 {
-		logsText = e.Logs
-	}
 	// use custom title if it's provided, otherwise use default
 	title := t.title
 	if len(title) == 0 {
 		title = defaultTeamsTitle
 	}
 
-	// use custom text if it's provided, otherwise use default
-	text := t.text
-	if len(text) == 0 {
-		text = constant.DefaultText
-	}
-
-	msg := fmt.Sprintf(
-		"%s\n"+
-			"**Cluster:** %s\n"+
-			"**Pod:** %s\n"+
-			"**Container:** %s\n"+
-			"**Namespace:** %s\n"+
-			"**Reason:** %s\n"+
-			"**Events:**\n```\n%s\n```\n"+
-			"**Logs:**\n```\n%s\n```",
-		text,
-		t.appCfg.ClusterName,
-		e.Name,
-		e.Container,
-		e.Namespace,
-		e.Reason,
-		eventsText,
-		logsText,
-	)
+	msg := e.FormatMarkdown(t.appCfg.ClusterName, t.text)
 	msgPayload := &teamsWebhookPayload{
 		Title: title,
 		Text:  msg,
diff --git a/event/format.go b/event/format.go
new file mode 100644
index 0000000..f35cdfd
--- /dev/null
+++ b/event/format.go
@@ -0,0 +1,92 @@
+package event
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/abahmed/kwatch/constant"
+)
+
+func (e *Event) FormatMarkdown(clusterName, text string) string {
+	// add events part if it exists
+	eventsText := constant.DefaultEvents
+	events := strings.TrimSpace(e.Events)
+	if len(events) > 0 {
+		eventsText = e.Events
+	}
+
+	// add logs part if it exist
+	logsText := constant.DefaultLogs
+	logs := strings.TrimSpace(e.Logs)
+	if len(logs) > 0 {
+		logsText = e.Logs
+	}
+
+	// use custom text if it's provided, otherwise use default
+	if len(text) == 0 {
+		text = constant.DefaultText
+	}
+
+	msg := fmt.Sprintf(
+		"%s\n"+
+			"**Cluster:** %s\n"+
+			"**Pod:** %s\n"+
+			"**Container:** %s\n"+
+			"**Namespace:** %s\n"+
+			"**Reason:** %s\n"+
+			"**Events:**\n```\n%s\n```\n"+
+			"**Logs:**\n```\n%s\n```",
+		text,
+		clusterName, e.Name,
+		e.Container,
+		e.Namespace,
+		e.Reason,
+		eventsText,
+		logsText,
+	)
+
+	return msg
+}
+
+func (e *Event) FormatHtml(clusterName, text string) string {
+	eventsText := constant.DefaultEvents
+	logsText := constant.DefaultLogs
+
+	// add events part if it exists
+	events := strings.TrimSpace(e.Events)
+	if len(events) > 0 {
+		eventsText = e.Events
+	}
+
+	// add logs part if it exists
+	logs := strings.TrimSpace(e.Logs)
+	if len(logs) > 0 {
+		logsText = e.Logs
+	}
+
+	// use custom text if it's provided, otherwise use default
+	if len(text) == 0 {
+		text = constant.DefaultText
+	}
+
+	msg := fmt.Sprintf(
+		"%s<br/>"+
+			"<b>Cluster:</b> %s <br/>"+
+			"<b>Pod:</b> %s <br/>"+
+			"<b>Container:</b> %s<br/>"+
+			"<b>Namespace:</b> %s<br/>"+
+			"<b>Reason:</b> %s<br/>"+
+			"<b>Events:</b><br/><blockquote>%s</blockquote>"+
+			"<b>Logs:</b> <br/><blockquote>%s</blockquote>",
+		text,
+		clusterName,
+		e.Name,
+		e.Container,
+		e.Namespace,
+		e.Reason,
+		strings.ReplaceAll(eventsText, "\n", "<br/>"),
+		strings.ReplaceAll(logsText, "\n", "<br/>"),
+	)
+
+	return msg
+}


Test patch:
diff --git a/alertmanager/feishu/feishu_test.go b/alertmanager/feishu/feishu_test.go
index 61423a2..6280357 100644
--- a/alertmanager/feishu/feishu_test.go
+++ b/alertmanager/feishu/feishu_test.go
@@ -51,8 +51,11 @@ func TestBuildRequestBodyFeiShu(t *testing.T) {
 		Logs:      "test\ntestlogs",
 		Events:    "test",
 	}
-	expectMessage := "{\"msg_type\": \"interactive\",\"card\": {\"config\": {\"wide_screen_mode\": true},\"header\": {\"title\": {\"tag\": \"plain_text\",\"content\": \"\"},\"template\": \"blue\"},\"elements\": [{\"tag\":\"markdown\",\"content\":\"**Cluster:** dev\\n**Pod:** test-pod\\n**Container:** test-container\\n**Namespace:** default\\n**Reason:** OOMKILLED\\n**Events:**\\n```\\ntest\\n```\\n**Logs:**\\n```\\ntest\\ntestlogs\\n```\"}]}}"
-	assertions.Equal(expectMessage, c.buildRequestBodyFeiShu(&ev, ""))
+	formattedMsg := ev.FormatMarkdown(c.appCfg.ClusterName, "")
+
+	expectMessage := "{\"msg_type\": \"interactive\",\"card\": {\"config\": {\"wide_screen_mode\": true},\"header\": {\"title\": {\"tag\": \"plain_text\",\"content\": \"\"},\"template\": \"blue\"},\"elements\": [{\"tag\":\"markdown\",\"content\":\"There is an issue with container in a pod!\\n**Cluster:** dev\\n**Pod:** test-pod\\n**Container:** test-container\\n**Namespace:** default\\n**Reason:** OOMKILLED\\n**Events:**\\n```\\ntest\\n```\\n**Logs:**\\n```\\ntest\\ntestlogs\\n```\"}]}}"
+
+	assertions.Equal(expectMessage, c.buildRequestBodyFeiShu(formattedMsg))
 }
 
 func TestSendMessage(t *testing.T) {