From b79273e7777e91474ab1b0541a8fec3b81975da7 Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Wed, 14 Feb 2024 18:44:06 +0100 Subject: [PATCH 01/14] Add initial telegram alert documentation --- docs/architecture/alerting.markdown | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/architecture/alerting.markdown b/docs/architecture/alerting.markdown index 6097f6c6..a43a619f 100644 --- a/docs/architecture/alerting.markdown +++ b/docs/architecture/alerting.markdown @@ -29,11 +29,13 @@ subgraph AM["Alerting Manager"] EL --> |Submit alert|SR["SeverityRouter"] SR --> SH["Slack"] SR --> PH["PagerDuty"] + SR --> TH["Telegram"] SR --> CPH["CounterParty Handler"] end CPH --> |"HTTP POST"|TPH["Third Party API"] SH --> |"HTTP POST"|SlackAPI("Slack Webhook API") +TH --> |"HTTP POST"|TelegramBotAPI("Telegram Bot API") PH --> |"HTTP POST"|PagerDutyAPI("PagerDuty API") @@ -79,6 +81,8 @@ Done! You should now see any generated alerts being forwarded to your specified The PagerDuty alert destination is a configurable destination that allows alerts to be sent to a specific PagerDuty services via the use of integration keys. Pessimism also uses the UUID associated with an alert as a deduplication key for PagerDuty. This is done to ensure that PagerDuty will not be spammed with duplicate or incidents. +#### Telegram + ### Alert CoolDowns To ensure that alerts aren't spammed to destinations once invoked, a time based cooldown value (`cooldown_time`) can be defined within the `alert_params` of a heuristic session config. This time value determines how long a heuristic session must wait before being allowed to alert again. From 712fb410bee666646c453ba229d30311d0b2ef2f Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Sun, 18 Feb 2024 18:49:22 +0100 Subject: [PATCH 02/14] Implement Telegram alert client --- internal/client/slack.go | 2 +- internal/client/telegram.go | 105 ++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 internal/client/telegram.go diff --git a/internal/client/slack.go b/internal/client/slack.go index 249ecd32..9dbe2ce2 100644 --- a/internal/client/slack.go +++ b/internal/client/slack.go @@ -43,7 +43,7 @@ type slackClient struct { // NewSlackClient ... Initializer func NewSlackClient(cfg *SlackConfig, name string) SlackClient { if cfg.URL == "" { - logging.NoContext().Warn("No Slack webhook URL not provided") + logging.NoContext().Warn("No Slack webhook URL provided") } return &slackClient{ diff --git a/internal/client/telegram.go b/internal/client/telegram.go new file mode 100644 index 00000000..960ba5bf --- /dev/null +++ b/internal/client/telegram.go @@ -0,0 +1,105 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/base-org/pessimism/internal/core" + "github.com/base-org/pessimism/internal/logging" +) + +type TelegramClient interface { + AlertClient +} + +type TelegramConfig struct { + Token string + ChatID string +} + +type telegramClient struct { + token string + chatID string + client *http.Client +} + +func NewTelegramClient(cfg *TelegramConfig) TelegramClient { + if cfg.Token == "" { + logging.NoContext().Warn("No Telegram token provided") + } + + return &telegramClient{ + token: cfg.Token, + chatID: cfg.ChatID, + client: &http.Client{}, + } +} + +type TelegramPayload struct { + ChatID string `json:"chat_id"` + Text string `json:"text"` +} + +type TelegramAPIResponse struct { + Ok bool `json:"ok"` + Result json.RawMessage `json:"result"` // Might not be needed for basic response handling + Error string `json:"description"` +} + +func (tr *TelegramAPIResponse) ToAlertResponse() *AlertAPIResponse { + if tr.Ok { + return &AlertAPIResponse{ + Status: core.SuccessStatus, + Message: "Message sent successfully", + } + } + return &AlertAPIResponse{ + Status: core.FailureStatus, + Message: tr.Error, + } +} + +func (tc *telegramClient) PostEvent(ctx context.Context, data *AlertEventTrigger) (*AlertAPIResponse, error) { + payload := TelegramPayload{ + ChatID: tc.chatID, + Text: data.Message, + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", tc.token) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payloadBytes)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := tc.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var apiResp TelegramAPIResponse + if err = json.Unmarshal(respBytes, &apiResp); err != nil { + return nil, fmt.Errorf("could not unmarshal telegram response: %w", err) + } + + return apiResp.ToAlertResponse(), nil +} + +func (tc *telegramClient) GetName() string { + return "TelegramClient" +} From cd63dec83ae78b2be23e90dff9b5b050d6d51ae6 Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Tue, 20 Feb 2024 18:15:31 +0100 Subject: [PATCH 03/14] Add Unit test for telegram client --- internal/client/telegram_test.go | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 internal/client/telegram_test.go diff --git a/internal/client/telegram_test.go b/internal/client/telegram_test.go new file mode 100644 index 00000000..4477ad8a --- /dev/null +++ b/internal/client/telegram_test.go @@ -0,0 +1,36 @@ +package client_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/base-org/pessimism/internal/client" + "github.com/base-org/pessimism/internal/core" +) + +func TestTelegramResponseToAlertResponse(t *testing.T) { + // Test case for a successful Telegram response + testTelegramSuccess := &client.TelegramAPIResponse{ + Ok: true, + Result: nil, + Error: "", + } + + // Test case for a failed Telegram response + testTelegramFailure := &client.TelegramAPIResponse{ + Ok: false, + Error: "error message", + } + + resSuc := testTelegramSuccess.ToAlertResponse() + resFail := testTelegramFailure.ToAlertResponse() + + // Assert that the success case is correctly interpreted + assert.Equal(t, core.SuccessStatus, resSuc.Status) + assert.Equal(t, "Message sent successfully", resSuc.Message) + + // Assert that the failure case is correctly interpreted + assert.Equal(t, core.FailureStatus, resFail.Status) + assert.Equal(t, "error message", resFail.Message) +} From 36c3e6e86e56330046b676ca92a2892ea3d246d8 Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Fri, 23 Feb 2024 14:46:04 +0100 Subject: [PATCH 04/14] Implement telegram message format. --- internal/alert/interpolator.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/internal/alert/interpolator.go b/internal/alert/interpolator.go index c5998a6a..af3a5b0c 100644 --- a/internal/alert/interpolator.go +++ b/internal/alert/interpolator.go @@ -39,6 +39,23 @@ const ( ` ) +// Telegram message format +const ( + TelegramMsgFmt = ` + *%s %s* + + Network: *%s* + Severity: *%s* + Session UUID: *%s* + + *Assessment Content:* + %s + + *Message:* + %s + ` +) + type Interpolator struct{} func NewInterpolator() *Interpolator { @@ -62,3 +79,17 @@ func (*Interpolator) PagerDutyMessage(a core.Alert) string { a.Net.String(), a.Content) } + +func (*Interpolator) TelegramMessage(a core.Alert, msg string) string { + sev := cases.Title(language.English).String(a.Sev.String()) + ht := cases.Title(language.English).String(a.HT.String()) + + return fmt.Sprintf(TelegramMsgFmt, + a.Sev.Symbol(), + ht, + a.Net.String(), + sev, + a.HeuristicID.String(), + fmt.Sprintf(CodeBlockFmt, a.Content), // Reusing the Slack code block format + msg) +} From 0f508ac721391800038ad77fe65b11a8ff1a8648 Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Sun, 25 Feb 2024 14:33:55 +0100 Subject: [PATCH 05/14] Add telegram field to alerts-template --- alerts-template.yaml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/alerts-template.yaml b/alerts-template.yaml index cc2e4740..59ff7b1b 100644 --- a/alerts-template.yaml +++ b/alerts-template.yaml @@ -5,15 +5,24 @@ alertRoutes: low_oncall: url: "" channel: "" + telegram: + low_oncall: + bot_token: "" + chat_id: "" medium: slack: medium_oncall: url: "" channel: "" - medium_oncall: + pagerduty: + medium_oncall: config: integration_key: "" + telegram: + medium_oncall: + bot_token: "" + chat_id: "" high: slack: @@ -25,3 +34,7 @@ alertRoutes: integration_key: ${MY_INTEGRATION_KEY} medium_oncall: integration_key: "" + telegram: + high_oncall: + bot_token: "" + chat_id: "" From 6b9c3de55f4c67d984e9a4133beb418c137f0dd4 Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Sun, 25 Feb 2024 15:02:15 +0100 Subject: [PATCH 06/14] Integrate telegram into alert.go --- internal/core/alert.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/core/alert.go b/internal/core/alert.go index 8829a55b..5e15928d 100644 --- a/internal/core/alert.go +++ b/internal/core/alert.go @@ -136,11 +136,14 @@ type SeverityMap struct { type AlertClientCfg struct { Slack map[string]*AlertConfig `yaml:"slack"` PagerDuty map[string]*AlertConfig `yaml:"pagerduty"` + Telegram map[string]*AlertConfig `yaml:"telegram"` } // AlertConfig ... The config for an alert client type AlertConfig struct { - URL StringFromEnv `yaml:"url"` - Channel StringFromEnv `yaml:"channel"` - IntegrationKey StringFromEnv `yaml:"integration_key"` + URL StringFromEnv `yaml:"url"` + Channel StringFromEnv `yaml:"channel"` + IntegrationKey StringFromEnv `yaml:"integration_key"` + TelegramBotToken StringFromEnv `yaml:"telegram_bot_token"` + TelegramChatID StringFromEnv `yaml:"telegram_chat_id"` } From 015eb626f45dd83d64cec061a239ab471e80a6b3 Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Sun, 25 Feb 2024 16:22:55 +0100 Subject: [PATCH 07/14] Configure alert routing for telegram client --- internal/alert/routing.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/internal/alert/routing.go b/internal/alert/routing.go index c1a26b3d..32e3e258 100644 --- a/internal/alert/routing.go +++ b/internal/alert/routing.go @@ -11,11 +11,13 @@ import ( type RoutingDirectory interface { GetPagerDutyClients(sev core.Severity) []client.PagerDutyClient GetSlackClients(sev core.Severity) []client.SlackClient + GetTelegramClients(sev core.Severity) []client.TelegramClient InitializeRouting(params *core.AlertRoutingParams) SetPagerDutyClients([]client.PagerDutyClient, core.Severity) SetSlackClients([]client.SlackClient, core.Severity) GetSNSClient() client.SNSClient SetSNSClient(client.SNSClient) + SetTelegramClients([]client.TelegramClient, core.Severity) } // routingDirectory ... Routing directory implementation @@ -26,6 +28,7 @@ type routingDirectory struct { pagerDutyClients map[core.Severity][]client.PagerDutyClient slackClients map[core.Severity][]client.SlackClient snsClient client.SNSClient + telegramClients map[core.Severity][]client.TelegramClient cfg *Config } @@ -36,6 +39,7 @@ func NewRoutingDirectory(cfg *Config) RoutingDirectory { pagerDutyClients: make(map[core.Severity][]client.PagerDutyClient), slackClients: make(map[core.Severity][]client.SlackClient), snsClient: nil, + telegramClients: make(map[core.Severity][]client.TelegramClient), } } @@ -49,6 +53,11 @@ func (rd *routingDirectory) GetSlackClients(sev core.Severity) []client.SlackCli return rd.slackClients[sev] } +// GetTelegramClients ... Returns the telegram clients for the given severity level +func (rd *routingDirectory) GetTelegramClients(sev core.Severity) []client.TelegramClient { + return rd.telegramClients[sev] +} + // SetSlackClients ... Sets the slack clients for the given severity level func (rd *routingDirectory) SetSlackClients(clients []client.SlackClient, sev core.Severity) { copy(rd.slackClients[sev][0:], clients) @@ -67,6 +76,12 @@ func (rd *routingDirectory) SetPagerDutyClients(clients []client.PagerDutyClient copy(rd.pagerDutyClients[sev][0:], clients) } +// SetTelegramClients ... Sets the telegram clients for the given severity level +func (rd *routingDirectory) SetTelegramClients(clients []client.TelegramClient, sev core.Severity) { + rd.telegramClients[sev] = make([]client.TelegramClient, len(clients)) + copy(rd.telegramClients[sev], clients) +} + // InitializeRouting ... Parses alert routing parameters for each severity level func (rd *routingDirectory) InitializeRouting(params *core.AlertRoutingParams) { rd.snsClient = client.NewSNSClient(rd.cfg.SNSConfig, "sns") @@ -104,4 +119,15 @@ func (rd *routingDirectory) paramsToRouteDirectory(acc *core.AlertClientCfg, sev rd.pagerDutyClients[sev] = append(rd.pagerDutyClients[sev], client) } } + + if acc.Telegram != nil { + for name, cfg := range acc.Telegram { + conf := &client.TelegramConfig{ + Bot: cfg.Bot.String(), + Token: cfg.Token.String(), + } + client := client.NewTelegramClient(conf, name) + rd.telegramClients[sev] = append(rd.telegramClients[sev], client) + } + } } From 933b0181456f91679d4912daad7b5ed2339330af Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Tue, 27 Feb 2024 10:45:03 +0100 Subject: [PATCH 08/14] Implement alert manager for telegram --- internal/alert/manager.go | 33 +++++++++++++++++++++++++++++++++ internal/alert/routing.go | 4 ++-- internal/client/telegram.go | 6 ++++-- internal/core/alert.go | 10 +++++----- internal/core/constants.go | 5 +++++ 5 files changed, 49 insertions(+), 9 deletions(-) diff --git a/internal/alert/manager.go b/internal/alert/manager.go index fe0b4ee2..21366895 100644 --- a/internal/alert/manager.go +++ b/internal/alert/manager.go @@ -165,6 +165,35 @@ func (am *alertManager) handleSNSPublish(alert core.Alert, policy *core.AlertPol return nil } +func (am *alertManager) handleTelegramPost(alert core.Alert, policy *core.AlertPolicy) error { + telegramClients := am.cm.GetTelegramClients(alert.Sev) + if telegramClients == nil { + am.logger.Warn("No telegram clients defined for criticality", zap.Any("alert", alert)) + return nil + } + + // Create Telegram event trigger + event := &client.AlertEventTrigger{ + Message: am.interpolator.TelegramMessage(alert, policy.Msg), + Severity: alert.Sev, + } + + for _, tc := range telegramClients { + resp, err := tc.PostEvent(am.ctx, event) + if err != nil { + return err + } + + if resp.Status != core.SuccessStatus { + return fmt.Errorf("client %s could not post to telegram: %s", tc.GetName(), resp.Message) + } + am.logger.Debug("Successfully posted to Telegram", zap.String("resp", resp.Message)) + am.metrics.RecordAlertGenerated(alert, core.Telegram, tc.GetName()) + } + + return nil +} + // EventLoop ... Event loop for alert manager subsystem func (am *alertManager) EventLoop() error { ticker := time.NewTicker(time.Second * 1) @@ -229,6 +258,10 @@ func (am *alertManager) HandleAlert(alert core.Alert, policy *core.AlertPolicy) if err := am.handleSNSPublish(alert, policy); err != nil { am.logger.Error("could not publish to sns", zap.Error(err)) } + + if err := am.handleTelegramPost(alert, policy); err != nil { + am.logger.Error("could not post to telegram", zap.Error(err)) + } } // Shutdown ... Shuts down the alert manager subsystem diff --git a/internal/alert/routing.go b/internal/alert/routing.go index 32e3e258..4400f596 100644 --- a/internal/alert/routing.go +++ b/internal/alert/routing.go @@ -123,8 +123,8 @@ func (rd *routingDirectory) paramsToRouteDirectory(acc *core.AlertClientCfg, sev if acc.Telegram != nil { for name, cfg := range acc.Telegram { conf := &client.TelegramConfig{ - Bot: cfg.Bot.String(), - Token: cfg.Token.String(), + ChatID: cfg.ChatID.String(), + Token: cfg.Token.String(), } client := client.NewTelegramClient(conf, name) rd.telegramClients[sev] = append(rd.telegramClients[sev], client) diff --git a/internal/client/telegram.go b/internal/client/telegram.go index 960ba5bf..e34a1b21 100644 --- a/internal/client/telegram.go +++ b/internal/client/telegram.go @@ -22,12 +22,13 @@ type TelegramConfig struct { } type telegramClient struct { + name string token string chatID string client *http.Client } -func NewTelegramClient(cfg *TelegramConfig) TelegramClient { +func NewTelegramClient(cfg *TelegramConfig, name string) TelegramClient { if cfg.Token == "" { logging.NoContext().Warn("No Telegram token provided") } @@ -35,6 +36,7 @@ func NewTelegramClient(cfg *TelegramConfig) TelegramClient { return &telegramClient{ token: cfg.Token, chatID: cfg.ChatID, + name: name, client: &http.Client{}, } } @@ -101,5 +103,5 @@ func (tc *telegramClient) PostEvent(ctx context.Context, data *AlertEventTrigger } func (tc *telegramClient) GetName() string { - return "TelegramClient" + return tc.name } diff --git a/internal/core/alert.go b/internal/core/alert.go index 5e15928d..fc913993 100644 --- a/internal/core/alert.go +++ b/internal/core/alert.go @@ -141,9 +141,9 @@ type AlertClientCfg struct { // AlertConfig ... The config for an alert client type AlertConfig struct { - URL StringFromEnv `yaml:"url"` - Channel StringFromEnv `yaml:"channel"` - IntegrationKey StringFromEnv `yaml:"integration_key"` - TelegramBotToken StringFromEnv `yaml:"telegram_bot_token"` - TelegramChatID StringFromEnv `yaml:"telegram_chat_id"` + URL StringFromEnv `yaml:"url"` + Channel StringFromEnv `yaml:"channel"` + IntegrationKey StringFromEnv `yaml:"integration_key"` + Token StringFromEnv `yaml:"telegram_bot_token"` + ChatID StringFromEnv `yaml:"telegram_chat_id"` } diff --git a/internal/core/constants.go b/internal/core/constants.go index 1fe6eb50..867f94ef 100644 --- a/internal/core/constants.go +++ b/internal/core/constants.go @@ -174,6 +174,7 @@ const ( PagerDuty SNS ThirdParty + Telegram ) // String ... Converts an alerting destination type to a string @@ -185,6 +186,8 @@ func (ad AlertDestination) String() string { return "pager_duty" case SNS: return "sns" + case Telegram: + return "telegram" case ThirdParty: return "third_party" default: @@ -199,6 +202,8 @@ func StringToAlertingDestType(stringType string) AlertDestination { return Slack case "pager_duty": return PagerDuty + case "telegram": + return Telegram case "third_party": return ThirdParty } From 6904ea70ba43b1598cdb7f7381327845e4152f6b Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Tue, 27 Feb 2024 16:14:14 +0100 Subject: [PATCH 09/14] Handle error for new telegram client --- internal/alert/routing.go | 9 ++++++++- internal/client/telegram.go | 8 +++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/internal/alert/routing.go b/internal/alert/routing.go index 4400f596..3185c9ef 100644 --- a/internal/alert/routing.go +++ b/internal/alert/routing.go @@ -3,8 +3,11 @@ package alert import ( + "go.uber.org/zap" + "github.com/base-org/pessimism/internal/client" "github.com/base-org/pessimism/internal/core" + "github.com/base-org/pessimism/internal/logging" ) // RoutingDirectory ... Interface for routing directory @@ -126,7 +129,11 @@ func (rd *routingDirectory) paramsToRouteDirectory(acc *core.AlertClientCfg, sev ChatID: cfg.ChatID.String(), Token: cfg.Token.String(), } - client := client.NewTelegramClient(conf, name) + client, err := client.NewTelegramClient(conf, name) + if err != nil { + logging.NoContext().Error("Failed to create Telegram client", zap.String("name", name), zap.Error(err)) + continue + } rd.telegramClients[sev] = append(rd.telegramClients[sev], client) } } diff --git a/internal/client/telegram.go b/internal/client/telegram.go index e34a1b21..8e3b1d6d 100644 --- a/internal/client/telegram.go +++ b/internal/client/telegram.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -28,9 +29,10 @@ type telegramClient struct { client *http.Client } -func NewTelegramClient(cfg *TelegramConfig, name string) TelegramClient { +func NewTelegramClient(cfg *TelegramConfig, name string) (TelegramClient, error) { if cfg.Token == "" { - logging.NoContext().Warn("No Telegram token provided") + logging.NoContext().Warn("No Telegram bot token provided") + return nil, errors.New("No Telegram bot token was provided") } return &telegramClient{ @@ -38,7 +40,7 @@ func NewTelegramClient(cfg *TelegramConfig, name string) TelegramClient { chatID: cfg.ChatID, name: name, client: &http.Client{}, - } + }, nil } type TelegramPayload struct { From 52624e7b0d837fdae038f6b6e868a88817d50d22 Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Thu, 29 Feb 2024 10:15:47 +0100 Subject: [PATCH 10/14] Add some docstring links to telegram documentation --- internal/client/telegram.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/client/telegram.go b/internal/client/telegram.go index 8e3b1d6d..902a60ab 100644 --- a/internal/client/telegram.go +++ b/internal/client/telegram.go @@ -17,6 +17,9 @@ type TelegramClient interface { AlertClient } +// TelegramConfig holds configuration details for creating a new Telegram client. +// Token: The Bot Token provided by BotFather upon creating a new bot (https://core.telegram.org/bots/api). +// ChatID: Unique identifier for the target chat (https://core.telegram.org/constructor/channel). type TelegramConfig struct { Token string ChatID string @@ -78,6 +81,7 @@ func (tc *telegramClient) PostEvent(ctx context.Context, data *AlertEventTrigger return nil, err } + // API endpoint "https://api.telegram.org/bot%s/sendMessage" is used to send messages (https://core.telegram.org/bots/api#sendmessage) url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", tc.token) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payloadBytes)) if err != nil { From 2d737a8b6c14ad20ac04a0312089931b9ece7756 Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Mon, 4 Mar 2024 09:53:42 +0100 Subject: [PATCH 11/14] Generate mock for telegram client --- internal/client/pagerduty.go | 1 + internal/client/slack.go | 1 + internal/mocks/telegram_client.go | 65 +++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 internal/mocks/telegram_client.go diff --git a/internal/client/pagerduty.go b/internal/client/pagerduty.go index 3770bab2..704f0366 100644 --- a/internal/client/pagerduty.go +++ b/internal/client/pagerduty.go @@ -48,6 +48,7 @@ type pagerdutyClient struct { } // NewPagerDutyClient ... Initializer for PagerDuty client +// Todo: implement error handling for client func NewPagerDutyClient(cfg *PagerDutyConfig, name string) PagerDutyClient { if cfg.IntegrationKey == "" { logging.NoContext().Warn("No PagerDuty integration key provided") diff --git a/internal/client/slack.go b/internal/client/slack.go index 9dbe2ce2..3b236be7 100644 --- a/internal/client/slack.go +++ b/internal/client/slack.go @@ -41,6 +41,7 @@ type slackClient struct { } // NewSlackClient ... Initializer +// Todo: implement error handling for client func NewSlackClient(cfg *SlackConfig, name string) SlackClient { if cfg.URL == "" { logging.NoContext().Warn("No Slack webhook URL provided") diff --git a/internal/mocks/telegram_client.go b/internal/mocks/telegram_client.go new file mode 100644 index 00000000..007bdfaa --- /dev/null +++ b/internal/mocks/telegram_client.go @@ -0,0 +1,65 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/base-org/pessimism/internal/client (interfaces: TelegramClient) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + client "github.com/base-org/pessimism/internal/client" + gomock "github.com/golang/mock/gomock" +) + +// MockTelegramClient is a mock of TelegramClient interface. +type MockTelegramClient struct { + ctrl *gomock.Controller + recorder *MockTelegramClientMockRecorder +} + +// MockTelegramClientMockRecorder is the mock recorder for MockTelegramClient. +type MockTelegramClientMockRecorder struct { + mock *MockTelegramClient +} + +// NewMockTelegramClient creates a new mock instance. +func NewMockTelegramClient(ctrl *gomock.Controller) *MockTelegramClient { + mock := &MockTelegramClient{ctrl: ctrl} + mock.recorder = &MockTelegramClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTelegramClient) EXPECT() *MockTelegramClientMockRecorder { + return m.recorder +} + +// GetName mocks base method. +func (m *MockTelegramClient) GetName() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetName") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetName indicates an expected call of GetName. +func (mr *MockTelegramClientMockRecorder) GetName() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockTelegramClient)(nil).GetName)) +} + +// PostEvent mocks base method. +func (m *MockTelegramClient) PostEvent(arg0 context.Context, arg1 *client.AlertEventTrigger) (*client.AlertAPIResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostEvent", arg0, arg1) + ret0, _ := ret[0].(*client.AlertAPIResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PostEvent indicates an expected call of PostEvent. +func (mr *MockTelegramClientMockRecorder) PostEvent(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostEvent", reflect.TypeOf((*MockTelegramClient)(nil).PostEvent), arg0, arg1) +} From 494a0641c8872e071abf45dd205f96f753e3cdc8 Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Tue, 12 Mar 2024 17:26:01 +0100 Subject: [PATCH 12/14] Rebase branch on master and fix conflicts --- internal/alert/manager.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/alert/manager.go b/internal/alert/manager.go index 21366895..64e61177 100644 --- a/internal/alert/manager.go +++ b/internal/alert/manager.go @@ -174,8 +174,8 @@ func (am *alertManager) handleTelegramPost(alert core.Alert, policy *core.AlertP // Create Telegram event trigger event := &client.AlertEventTrigger{ - Message: am.interpolator.TelegramMessage(alert, policy.Msg), - Severity: alert.Sev, + Message: am.interpolator.TelegramMessage(alert, policy.Msg), + Alert: alert, } for _, tc := range telegramClients { From 4c7d36ae5fb2e7eac90c7edbb34ad1ed9b909641 Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Mon, 18 Mar 2024 09:45:34 +0100 Subject: [PATCH 13/14] Update docs on telegram implementation --- README.md | 2 +- alerts-template.yaml | 12 ++++++------ config.env.template | 2 +- docs/alert-routing.md | 10 ++++++++++ 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 75c8f300..b97f7fc3 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ To use the template, run the following command(s): 1. Create local config file (`config.env`) to store all necessary environmental variables. There's already an example `config.env.template` in the repo that stores default env vars. -2. [Download](https://go.dev/doc/install) or upgrade to `golang 1.19`. +2. [Download](https://go.dev/doc/install) or upgrade to `golang 1.21`. 3. Install all project golang dependencies by running `go mod download`. diff --git a/alerts-template.yaml b/alerts-template.yaml index 59ff7b1b..43ebea9d 100644 --- a/alerts-template.yaml +++ b/alerts-template.yaml @@ -7,8 +7,8 @@ alertRoutes: channel: "" telegram: low_oncall: - bot_token: "" - chat_id: "" + telegram_bot_token: "" + telegram_chat_id: "" medium: slack: @@ -21,8 +21,8 @@ alertRoutes: integration_key: "" telegram: medium_oncall: - bot_token: "" - chat_id: "" + telegram_bot_token: "" + telegram_chat_id: "" high: slack: @@ -36,5 +36,5 @@ alertRoutes: integration_key: "" telegram: high_oncall: - bot_token: "" - chat_id: "" + telegrambot_token: "" + telegram_chat_id: "" diff --git a/config.env.template b/config.env.template index 038c55a0..c8329a64 100644 --- a/config.env.template +++ b/config.env.template @@ -18,7 +18,7 @@ BOOTSTRAP_PATH=genesis.json SERVER_HOST=localhost SERVER_PORT=8080 SERVER_KEEP_ALIVE_TIME=10 -SERVER_READ_TIMEOUT=10 +SERVER_READ_TIMEOUT=10 SERVER_WRITE_TIMEOUT=10 SERVER_SHUTDOWN_TIME=10 diff --git a/docs/alert-routing.md b/docs/alert-routing.md index e29a1f2e..287da4f6 100644 --- a/docs/alert-routing.md +++ b/docs/alert-routing.md @@ -37,6 +37,7 @@ Pessimism currently supports the following alert destinations: | slack | Sends alerts to a Slack channel | | pagerduty | Sends alerts to a PagerDuty service | | sns | Sends alerts to an SNS topic defined in .env file | +| telegram | Sends alerts to a Telegram channel | ## Alert Severity @@ -58,6 +59,15 @@ topic. The ARN should be added to the `SNS_TOPIC_ARN` variable found in the `.en The AWS_ENDPOINT is optional and is primarily used for testing with localstack. > Note: Currently, Pessimism only support one SNS topic to publish alerts to. +## Publishing to a Telegram Channel + +It's possible to publish alerts to a Telegram channel by adding the channel's +ID and bot token to the `telegram_bot_token` and `telegram_bot_token` +variables in the `alerts-routing.` configuration file. Generate a bot token by leveraging the +following [guide](https://core.telegram.org/bots#how-do-i-create-a-bot). + +> Note: Currently, Pessimism only support one Telegram channel to publish alerts to. + ## PagerDuty Severity Mapping PagerDuty supports the following severities: `critical`, `error`, `warning`, From 0655209f9aa8f2e47d98ff3c22896a3cb20f9573 Mon Sep 17 00:00:00 2001 From: Richard Gregory Date: Tue, 19 Mar 2024 10:05:21 +0100 Subject: [PATCH 14/14] Update telegram doc and add routing unit test. --- alerts-template.yaml | 12 ++--- docs/alert-routing.md | 4 +- internal/alert/routing.go | 2 +- internal/alert/routing_test.go | 50 +++++++++++++++++++ .../alert/test_data/alert-routing-test.yaml | 16 +++++- internal/core/alert.go | 4 +- 6 files changed, 75 insertions(+), 13 deletions(-) diff --git a/alerts-template.yaml b/alerts-template.yaml index 43ebea9d..59ff7b1b 100644 --- a/alerts-template.yaml +++ b/alerts-template.yaml @@ -7,8 +7,8 @@ alertRoutes: channel: "" telegram: low_oncall: - telegram_bot_token: "" - telegram_chat_id: "" + bot_token: "" + chat_id: "" medium: slack: @@ -21,8 +21,8 @@ alertRoutes: integration_key: "" telegram: medium_oncall: - telegram_bot_token: "" - telegram_chat_id: "" + bot_token: "" + chat_id: "" high: slack: @@ -36,5 +36,5 @@ alertRoutes: integration_key: "" telegram: high_oncall: - telegrambot_token: "" - telegram_chat_id: "" + bot_token: "" + chat_id: "" diff --git a/docs/alert-routing.md b/docs/alert-routing.md index 287da4f6..f12f890d 100644 --- a/docs/alert-routing.md +++ b/docs/alert-routing.md @@ -62,12 +62,10 @@ The AWS_ENDPOINT is optional and is primarily used for testing with localstack. ## Publishing to a Telegram Channel It's possible to publish alerts to a Telegram channel by adding the channel's -ID and bot token to the `telegram_bot_token` and `telegram_bot_token` +ID and bot token to the `chat_id` and `bot_token` variables in the `alerts-routing.` configuration file. Generate a bot token by leveraging the following [guide](https://core.telegram.org/bots#how-do-i-create-a-bot). -> Note: Currently, Pessimism only support one Telegram channel to publish alerts to. - ## PagerDuty Severity Mapping PagerDuty supports the following severities: `critical`, `error`, `warning`, diff --git a/internal/alert/routing.go b/internal/alert/routing.go index 3185c9ef..3fbdb9af 100644 --- a/internal/alert/routing.go +++ b/internal/alert/routing.go @@ -126,8 +126,8 @@ func (rd *routingDirectory) paramsToRouteDirectory(acc *core.AlertClientCfg, sev if acc.Telegram != nil { for name, cfg := range acc.Telegram { conf := &client.TelegramConfig{ - ChatID: cfg.ChatID.String(), Token: cfg.Token.String(), + ChatID: cfg.ChatID.String(), } client, err := client.NewTelegramClient(conf, name) if err != nil { diff --git a/internal/alert/routing_test.go b/internal/alert/routing_test.go index ad1b7164..7f369a40 100644 --- a/internal/alert/routing_test.go +++ b/internal/alert/routing_test.go @@ -26,6 +26,12 @@ func getCfg() *config.Config { URL: "test1", }, }, + Telegram: map[string]*core.AlertConfig{ + "test1": { + Token: "test1", + ChatID: "test1", + }, + }, }, Medium: &core.AlertClientCfg{ PagerDuty: map[string]*core.AlertConfig{ @@ -39,6 +45,12 @@ func getCfg() *config.Config { URL: "test2", }, }, + Telegram: map[string]*core.AlertConfig{ + "test2": { + Token: "test2", + ChatID: "test2", + }, + }, }, High: &core.AlertClientCfg{ PagerDuty: map[string]*core.AlertConfig{ @@ -59,6 +71,16 @@ func getCfg() *config.Config { URL: "test3", }, }, + Telegram: map[string]*core.AlertConfig{ + "test2": { + Token: "test2", + ChatID: "test2", + }, + "test3": { + Token: "test3", + ChatID: "test3", + }, + }, }, }, }, @@ -84,11 +106,14 @@ func Test_AlertClientCfgToClientMap(t *testing.T) { cm.InitializeRouting(cfg.AlertConfig.RoutingParams) assert.Len(t, cm.GetSlackClients(core.LOW), 1) + assert.Len(t, cm.GetTelegramClients(core.LOW), 1) assert.Len(t, cm.GetPagerDutyClients(core.LOW), 0) assert.Len(t, cm.GetSlackClients(core.MEDIUM), 1) + assert.Len(t, cm.GetTelegramClients(core.MEDIUM), 1) assert.Len(t, cm.GetPagerDutyClients(core.MEDIUM), 1) assert.Len(t, cm.GetSlackClients(core.HIGH), 2) assert.Len(t, cm.GetPagerDutyClients(core.HIGH), 2) + assert.Len(t, cm.GetTelegramClients(core.HIGH), 2) }, }, { @@ -107,6 +132,27 @@ func Test_AlertClientCfgToClientMap(t *testing.T) { assert.Len(t, cm.GetPagerDutyClients(core.MEDIUM), 0) assert.Len(t, cm.GetSlackClients(core.HIGH), 2) assert.Len(t, cm.GetPagerDutyClients(core.HIGH), 2) + assert.Len(t, cm.GetTelegramClients(core.HIGH), 2) + }, + }, + { + name: "Test AlertClientCfgToClientMap Telegram Nil", + description: "Test AlertClientCfgToClientMap doesn't fail when telegram is nil", + testLogic: func(t *testing.T) { + cfg := getCfg() + cfg.AlertConfig.RoutingParams.AlertRoutes.Medium.Telegram = nil + cm := alert.NewRoutingDirectory(cfg.AlertConfig) + assert.NotNil(t, cm, "client map is nil") + + cm.InitializeRouting(cfg.AlertConfig.RoutingParams) + assert.Len(t, cm.GetSlackClients(core.LOW), 1) + assert.Len(t, cm.GetPagerDutyClients(core.LOW), 0) + assert.Len(t, cm.GetTelegramClients(core.MEDIUM), 0) + assert.Len(t, cm.GetSlackClients(core.MEDIUM), 1) + assert.Len(t, cm.GetPagerDutyClients(core.MEDIUM), 1) + assert.Len(t, cm.GetSlackClients(core.HIGH), 2) + assert.Len(t, cm.GetPagerDutyClients(core.HIGH), 2) + assert.Len(t, cm.GetTelegramClients(core.HIGH), 2) }, }, { @@ -125,6 +171,7 @@ func Test_AlertClientCfgToClientMap(t *testing.T) { assert.Len(t, cm.GetPagerDutyClients(core.MEDIUM), 1) assert.Len(t, cm.GetSlackClients(core.HIGH), 2) assert.Len(t, cm.GetPagerDutyClients(core.HIGH), 2) + assert.Len(t, cm.GetTelegramClients(core.HIGH), 2) }, }, { @@ -142,10 +189,13 @@ func Test_AlertClientCfgToClientMap(t *testing.T) { assert.Len(t, cm.GetSlackClients(core.LOW), 0) assert.Len(t, cm.GetPagerDutyClients(core.LOW), 0) + assert.Len(t, cm.GetTelegramClients(core.LOW), 0) assert.Len(t, cm.GetSlackClients(core.MEDIUM), 0) assert.Len(t, cm.GetPagerDutyClients(core.MEDIUM), 0) + assert.Len(t, cm.GetTelegramClients(core.MEDIUM), 0) assert.Len(t, cm.GetSlackClients(core.HIGH), 0) assert.Len(t, cm.GetPagerDutyClients(core.HIGH), 0) + assert.Len(t, cm.GetTelegramClients(core.HIGH), 0) }, }, } diff --git a/internal/alert/test_data/alert-routing-test.yaml b/internal/alert/test_data/alert-routing-test.yaml index ce4a046d..283fb61a 100644 --- a/internal/alert/test_data/alert-routing-test.yaml +++ b/internal/alert/test_data/alert-routing-test.yaml @@ -4,6 +4,10 @@ alertRoutes: config: url: "test-low" channel: "#test-low" + telegram: + config: + bot_token: "test-low" + chat_id: "test-low" medium: slack: @@ -13,6 +17,10 @@ alertRoutes: pagerduty: config: integration_key: "test-medium" + telegram: + config: + bot_token: "test-medium" + chat_id: "test-medium" high: slack: @@ -22,9 +30,15 @@ alertRoutes: config_2: url: "test-high-2" channel: "#test-high-2" - pagerduty: config: integration_key: "test-high-1" config_2: integration_key: "test-high-2" + telegram: + config: + bot_token: "test-high" + chat_id: "test-high" + config_2: + bot_token: "test-high-2" + chat_id: "test-high-2" diff --git a/internal/core/alert.go b/internal/core/alert.go index fc913993..a9c74c96 100644 --- a/internal/core/alert.go +++ b/internal/core/alert.go @@ -144,6 +144,6 @@ type AlertConfig struct { URL StringFromEnv `yaml:"url"` Channel StringFromEnv `yaml:"channel"` IntegrationKey StringFromEnv `yaml:"integration_key"` - Token StringFromEnv `yaml:"telegram_bot_token"` - ChatID StringFromEnv `yaml:"telegram_chat_id"` + Token StringFromEnv `yaml:"bot_token"` + ChatID StringFromEnv `yaml:"chat_id"` }