Skip to content

Commit ff4b09d

Browse files
JamesHillyardTwiN
andauthored
feat(alerting): Implement new Teams Workflow alert (#847)
* POC Teams Workflow Alerting Signed-off-by: James Hillyard <[email protected]> * Document Teams Workflow Alert Signed-off-by: James Hillyard <[email protected]> * Rename 'teamsworkflow' to 'teams-workflows' Signed-off-by: James Hillyard <[email protected]> * Fix README Table Format Signed-off-by: James Hillyard <[email protected]> * Fix Test to Expect Correct Emoji Signed-off-by: James Hillyard <[email protected]> --------- Signed-off-by: James Hillyard <[email protected]> Co-authored-by: TwiN <[email protected]>
1 parent 29072da commit ff4b09d

File tree

6 files changed

+522
-3
lines changed

6 files changed

+522
-3
lines changed
12.3 KB
Loading

README.md

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
6767
- [Configuring PagerDuty alerts](#configuring-pagerduty-alerts)
6868
- [Configuring Pushover alerts](#configuring-pushover-alerts)
6969
- [Configuring Slack alerts](#configuring-slack-alerts)
70-
- [Configuring Teams alerts](#configuring-teams-alerts)
70+
- [Configuring Teams alerts *(Deprecated)*](#configuring-teams-alerts-deprecated)
71+
- [Configuring Teams Workflow alerts](#configuring-teams-workflow-alerts)
7172
- [Configuring Telegram alerts](#configuring-telegram-alerts)
7273
- [Configuring Twilio alerts](#configuring-twilio-alerts)
7374
- [Configuring AWS SES alerts](#configuring-aws-ses-alerts)
@@ -566,7 +567,8 @@ endpoints:
566567
| `alerting.pagerduty` | Configuration for alerts of type `pagerduty`. <br />See [Configuring PagerDuty alerts](#configuring-pagerduty-alerts). | `{}` |
567568
| `alerting.pushover` | Configuration for alerts of type `pushover`. <br />See [Configuring Pushover alerts](#configuring-pushover-alerts). | `{}` |
568569
| `alerting.slack` | Configuration for alerts of type `slack`. <br />See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` |
569-
| `alerting.teams` | Configuration for alerts of type `teams`. <br />See [Configuring Teams alerts](#configuring-teams-alerts). | `{}` |
570+
| `alerting.teams` | Configuration for alerts of type `teams`. *(Deprecated)* <br />See [Configuring Teams alerts](#configuring-teams-alerts-deprecated). | `{}` |
571+
| `alerting.teams-workflows` | Configuration for alerts of type `teams-workflows`. <br />See [Configuring Teams Workflow alerts](#configuring-teams-workflow-alerts). | `{}` |
570572
| `alerting.telegram` | Configuration for alerts of type `telegram`. <br />See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` |
571573
| `alerting.twilio` | Settings for alerts of type `twilio`. <br />See [Configuring Twilio alerts](#configuring-twilio-alerts). | `{}` |
572574

@@ -1176,7 +1178,12 @@ Here's an example of what the notifications look like:
11761178
![Slack notifications](.github/assets/slack-alerts.png)
11771179

11781180

1179-
#### Configuring Teams alerts
1181+
#### Configuring Teams alerts *(Deprecated)*
1182+
1183+
> [!CAUTION]
1184+
> **Deprecated:** Office 365 Connectors within Microsoft Teams are being retired ([Source: Microsoft DevBlog](https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/)).
1185+
> Existing connectors will continue to work until December 2025. The new [Teams Workflow Alerts](#configuring-teams-workflow-alerts) should be used with Microsoft Workflows instead of this legacy configuration.
1186+
11801187
| Parameter | Description | Default |
11811188
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:--------------------|
11821189
| `alerting.teams` | Configuration for alerts of type `teams` | `{}` |
@@ -1230,6 +1237,61 @@ Here's an example of what the notifications look like:
12301237

12311238
![Teams notifications](.github/assets/teams-alerts.png)
12321239

1240+
#### Configuring Teams Workflow alerts
1241+
1242+
> [!NOTE]
1243+
> This alert is compatible with Workflows for Microsoft Teams. To setup the workflow and get the webhook URL, follow the [Microsoft Documentation](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498).
1244+
1245+
| Parameter | Description | Default |
1246+
|:---------------------------------------------------|:-------------------------------------------------------------------------------------------|:-------------------|
1247+
| `alerting.teams-workflows` | Configuration for alerts of type `teams` | `{}` |
1248+
| `alerting.teams-workflows.webhook-url` | Teams Webhook URL | Required `""` |
1249+
| `alerting.teams-workflows.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
1250+
| `alerting.teams-workflows.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
1251+
| `alerting.teams-workflows.title` | Title of the notification | `"&#x26D1; Gatus"` |
1252+
| `alerting.teams-workflows.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
1253+
| `alerting.teams-workflows.overrides[].webhook-url` | Teams WorkFlow Webhook URL | `""` |
1254+
1255+
```yaml
1256+
alerting:
1257+
teams-workflows:
1258+
webhook-url: "https://********.webhook.office.com/webhookb2/************"
1259+
# You can also add group-specific to keys, which will
1260+
# override the to key above for the specified groups
1261+
overrides:
1262+
- group: "core"
1263+
webhook-url: "https://********.webhook.office.com/webhookb3/************"
1264+
1265+
endpoints:
1266+
- name: website
1267+
url: "https://twin.sh/health"
1268+
interval: 30s
1269+
conditions:
1270+
- "[STATUS] == 200"
1271+
- "[BODY].status == UP"
1272+
- "[RESPONSE_TIME] < 300"
1273+
alerts:
1274+
- type: teams-workflows
1275+
description: "healthcheck failed"
1276+
send-on-resolved: true
1277+
1278+
- name: back-end
1279+
group: core
1280+
url: "https://example.org/"
1281+
interval: 5m
1282+
conditions:
1283+
- "[STATUS] == 200"
1284+
- "[CERTIFICATE_EXPIRATION] > 48h"
1285+
alerts:
1286+
- type: teams-workflows
1287+
description: "healthcheck failed"
1288+
send-on-resolved: true
1289+
```
1290+
1291+
Here's an example of what the notifications look like:
1292+
1293+
![Teams Workflow notifications](.github/assets/teams-workflows-alerts.png)
1294+
12331295

12341296
#### Configuring Telegram alerts
12351297
| Parameter | Description | Default |

alerting/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
2727
"github.com/TwiN/gatus/v5/alerting/provider/slack"
2828
"github.com/TwiN/gatus/v5/alerting/provider/teams"
29+
"github.com/TwiN/gatus/v5/alerting/provider/teamsworkflows"
2930
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
3031
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
3132
"github.com/TwiN/gatus/v5/alerting/provider/zulip"
@@ -90,6 +91,9 @@ type Config struct {
9091
// Teams is the configuration for the teams alerting provider
9192
Teams *teams.AlertProvider `yaml:"teams,omitempty"`
9293

94+
// TeamsWorkflows is the configuration for the teams alerting provider using the new Workflow App Webhook Connector
95+
TeamsWorkflows *teamsworkflows.AlertProvider `yaml:"teams-workflows,omitempty"`
96+
9397
// Telegram is the configuration for the telegram alerting provider
9498
Telegram *telegram.AlertProvider `yaml:"telegram,omitempty"`
9599

alerting/provider/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
2121
"github.com/TwiN/gatus/v5/alerting/provider/slack"
2222
"github.com/TwiN/gatus/v5/alerting/provider/teams"
23+
"github.com/TwiN/gatus/v5/alerting/provider/teamsworkflows"
2324
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
2425
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
2526
"github.com/TwiN/gatus/v5/alerting/provider/zulip"
@@ -80,6 +81,7 @@ var (
8081
_ AlertProvider = (*pushover.AlertProvider)(nil)
8182
_ AlertProvider = (*slack.AlertProvider)(nil)
8283
_ AlertProvider = (*teams.AlertProvider)(nil)
84+
_ AlertProvider = (*teamsworkflows.AlertProvider)(nil)
8385
_ AlertProvider = (*telegram.AlertProvider)(nil)
8486
_ AlertProvider = (*twilio.AlertProvider)(nil)
8587
_ AlertProvider = (*zulip.AlertProvider)(nil)
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package teamsworkflows
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
10+
"github.com/TwiN/gatus/v5/alerting/alert"
11+
"github.com/TwiN/gatus/v5/client"
12+
"github.com/TwiN/gatus/v5/config/endpoint"
13+
)
14+
15+
// AlertProvider is the configuration necessary for sending an alert using Teams
16+
type AlertProvider struct {
17+
WebhookURL string `yaml:"webhook-url"`
18+
19+
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
20+
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
21+
22+
// Overrides is a list of Override that may be prioritized over the default configuration
23+
Overrides []Override `yaml:"overrides,omitempty"`
24+
25+
// Title is the title of the message that will be sent
26+
Title string `yaml:"title,omitempty"`
27+
}
28+
29+
// Override is a case under which the default integration is overridden
30+
type Override struct {
31+
Group string `yaml:"group"`
32+
WebhookURL string `yaml:"webhook-url"`
33+
}
34+
35+
// IsValid returns whether the provider's configuration is valid
36+
func (provider *AlertProvider) IsValid() bool {
37+
registeredGroups := make(map[string]bool)
38+
if provider.Overrides != nil {
39+
for _, override := range provider.Overrides {
40+
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.WebhookURL) == 0 {
41+
return false
42+
}
43+
registeredGroups[override.Group] = true
44+
}
45+
}
46+
return len(provider.WebhookURL) > 0
47+
}
48+
49+
// Send an alert using the provider
50+
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
51+
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
52+
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
53+
if err != nil {
54+
return err
55+
}
56+
request.Header.Set("Content-Type", "application/json")
57+
response, err := client.GetHTTPClient(nil).Do(request)
58+
if err != nil {
59+
return err
60+
}
61+
defer response.Body.Close()
62+
if response.StatusCode > 399 {
63+
body, _ := io.ReadAll(response.Body)
64+
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
65+
}
66+
return err
67+
}
68+
69+
// AdaptiveCardBody represents the structure of an Adaptive Card
70+
type AdaptiveCardBody struct {
71+
Type string `json:"type"`
72+
Version string `json:"version"`
73+
Body []CardBody `json:"body"`
74+
}
75+
76+
// CardBody represents the body of the Adaptive Card
77+
type CardBody struct {
78+
Type string `json:"type"`
79+
Text string `json:"text,omitempty"`
80+
Wrap bool `json:"wrap"`
81+
Separator bool `json:"separator,omitempty"`
82+
Size string `json:"size,omitempty"`
83+
Weight string `json:"weight,omitempty"`
84+
Items []CardBody `json:"items,omitempty"`
85+
Facts []Fact `json:"facts,omitempty"`
86+
FactSet *FactSetBody `json:"factSet,omitempty"`
87+
}
88+
89+
// FactSetBody represents the FactSet in the Adaptive Card
90+
type FactSetBody struct {
91+
Type string `json:"type"`
92+
Facts []Fact `json:"facts"`
93+
}
94+
95+
// Fact represents an individual fact in the FactSet
96+
type Fact struct {
97+
Title string `json:"title"`
98+
Value string `json:"value"`
99+
}
100+
101+
// buildRequestBody builds the request body for the provider
102+
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
103+
var message string
104+
if resolved {
105+
message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row.", ep.DisplayName(), alert.SuccessThreshold)
106+
} else {
107+
message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row.", ep.DisplayName(), alert.FailureThreshold)
108+
}
109+
110+
// Configure default title if it's not provided
111+
title := "&#x26D1; Gatus"
112+
if provider.Title != "" {
113+
title = provider.Title
114+
}
115+
116+
// Build the facts from the condition results
117+
var facts []Fact
118+
for _, conditionResult := range result.ConditionResults {
119+
var key string
120+
if conditionResult.Success {
121+
key = "&#x2705;"
122+
} else {
123+
key = "&#x274C;"
124+
}
125+
facts = append(facts, Fact{
126+
Title: key,
127+
Value: conditionResult.Condition,
128+
})
129+
}
130+
131+
cardContent := AdaptiveCardBody{
132+
Type: "AdaptiveCard",
133+
Version: "1.4", // Version 1.5 and 1.6 doesn't seem to be supported by Teams as of 27/08/2024
134+
Body: []CardBody{
135+
{
136+
Type: "TextBlock",
137+
Text: title,
138+
Size: "Medium",
139+
Weight: "Bolder",
140+
},
141+
{
142+
Type: "TextBlock",
143+
Text: message,
144+
Wrap: true,
145+
},
146+
{
147+
Type: "FactSet",
148+
Facts: facts,
149+
},
150+
},
151+
}
152+
153+
attachment := map[string]interface{}{
154+
"contentType": "application/vnd.microsoft.card.adaptive",
155+
"content": cardContent,
156+
}
157+
158+
payload := map[string]interface{}{
159+
"type": "message",
160+
"attachments": []interface{}{attachment},
161+
}
162+
163+
bodyAsJSON, _ := json.Marshal(payload)
164+
return bodyAsJSON
165+
}
166+
167+
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
168+
func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
169+
if provider.Overrides != nil {
170+
for _, override := range provider.Overrides {
171+
if group == override.Group {
172+
return override.WebhookURL
173+
}
174+
}
175+
}
176+
return provider.WebhookURL
177+
}
178+
179+
// GetDefaultAlert returns the provider's default alert configuration
180+
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
181+
return provider.DefaultAlert
182+
}

0 commit comments

Comments
 (0)