-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into feature/update-linter
- Loading branch information
Showing
5 changed files
with
805 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package msteams | ||
|
||
const Green = "008000" | ||
const Orange = "ffa500" | ||
const Red = "ff0000" | ||
const Black = "000000" | ||
const White = "ffffff" | ||
|
||
/* | ||
Fact models a fact in a MessageCard, contains a timestamp and trigger data | ||
{ | ||
"name": "10:45", | ||
"value": "someServer = 0.11 (NODATA to WARN)" | ||
} | ||
*/ | ||
type Fact struct { | ||
Name string `json:"name"` | ||
Value string `json:"value"` | ||
} | ||
|
||
/* | ||
Section models a section in a MessageCard, contains Facts and the Trigger description | ||
{ | ||
"activityTitle": "Description", | ||
"activityText": "A trigger description", | ||
"facts": [ | ||
{ | ||
"name": "10:45", | ||
"value": "someServer = 0.11 (NODATA to WARN)" | ||
} | ||
] | ||
} | ||
*/ | ||
type Section struct { | ||
ActivityTitle string `json:"activityTitle"` | ||
ActivityText string `json:"activityText"` | ||
Facts []Fact `json:"facts"` | ||
} | ||
|
||
/* | ||
OpenURITarget creates a clickable target back to the trigger URI in a MessageCard | ||
{ | ||
"os": "default", | ||
"value": "http://moira.tld/trigger/ABCDEF-GH" | ||
} | ||
*/ | ||
type OpenURITarget struct { | ||
Os string `json:"os"` | ||
URI string `json:"uri"` | ||
} | ||
|
||
/* | ||
Action models possible action in a MessageCard, currently limited to OpenURI actions | ||
{ | ||
"@type": "OpenUri", | ||
"name": "Open in Moira" | ||
"targets": [ | ||
{ | ||
"os": "default", | ||
"value": "http://moira.tld/trigger/ABCDEF-GH" | ||
} | ||
] | ||
} | ||
*/ | ||
type Action struct { | ||
Type string `json:"@type"` | ||
Name string `json:"name"` | ||
Targets []OpenURITarget `json:"targets"` | ||
} | ||
|
||
/* | ||
MessageCard models an MSTeams compatible MessageCard | ||
{ | ||
"@context": "https://schema.org/extensions", | ||
"@type": "MessageCard", | ||
"summary": "Moira Alert" | ||
"title" : "WARN Trigger Name [tag1]" | ||
"themeColor": "ffa500" | ||
"sections": [ | ||
{ | ||
"activityTitle": "Description", | ||
"activityText": "A trigger description", | ||
"facts": [ | ||
{ | ||
"name": "10:45", | ||
"value": "someServer = 0.11 (NODATA to WARN)" | ||
} | ||
] | ||
} | ||
] | ||
"potentialAction": [ | ||
{ | ||
"@type": "OpenUri", | ||
"name": "Open in Moira" | ||
"targets": [ | ||
{ | ||
"os": "default", | ||
"value": "http://moira.tld/trigger/ABCDEF-GH" | ||
} | ||
] | ||
} | ||
] | ||
} | ||
*/ | ||
type MessageCard struct { | ||
Context string `json:"@context"` | ||
MessageType string `json:"@type"` | ||
Summary string `json:"summary"` | ||
ThemeColor string `json:"themeColor"` | ||
Title string `json:"title"` | ||
Sections []Section `json:"sections"` | ||
PotentialAction []Action `json:"potentialAction,omitempty"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
package msteams | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"net/url" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/moira-alert/moira" | ||
"github.com/russross/blackfriday/v2" | ||
) | ||
|
||
const context = "http://schema.org/extensions" | ||
const messageType = "MessageCard" | ||
const summary = "Moira Alert" | ||
const teamsBaseURL = "https://outlook.office.com/webhook/" | ||
const teamsOKResponse = "1" | ||
const openUri = "OpenUri" | ||
const openUriMessage = "View in Moira" | ||
const openUriOsDefault = "default" | ||
const activityTitleText = "Description" | ||
|
||
var throttleWarningFact = Fact{ | ||
Name: "Warning", | ||
Value: "Please, *fix your system or tune this trigger* to generate less events.", | ||
} | ||
|
||
var headers = map[string]string{ | ||
"User-Agent": "Moira", | ||
"Content-Type": "application/json", | ||
} | ||
|
||
// Sender implements moira sender interface via MS Teams | ||
type Sender struct { | ||
frontURI string | ||
maxEvents int | ||
logger moira.Logger | ||
location *time.Location | ||
client *http.Client | ||
} | ||
|
||
// Init initialises settings required for full functionality | ||
func (sender *Sender) Init(senderSettings map[string]string, logger moira.Logger, location *time.Location, dateTimeFormat string) error { | ||
sender.logger = logger | ||
sender.location = location | ||
sender.frontURI = senderSettings["front_uri"] | ||
maxEvents, err := strconv.Atoi(senderSettings["max_events"]) | ||
if err != nil { | ||
return fmt.Errorf("max_events should be an integer: %w", err) | ||
} | ||
sender.maxEvents = maxEvents | ||
sender.client = &http.Client{ | ||
Timeout: time.Duration(30) * time.Second, | ||
} | ||
return nil | ||
} | ||
|
||
// SendEvents implements Sender interface Send | ||
func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { | ||
|
||
err := sender.isValidWebhookURL(contact.Value) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
request, err := sender.buildRequest(events, contact, trigger, plot, throttled) | ||
|
||
if err != nil { | ||
return fmt.Errorf("failed to build request: %w", err) | ||
} | ||
|
||
response, err := sender.client.Do(request) | ||
|
||
if err != nil { | ||
return fmt.Errorf("failed to perform request: %w", err) | ||
} | ||
defer response.Body.Close() | ||
|
||
// read the entire response as required by https://golang.org/pkg/net/http/#Client.Do | ||
body, err := ioutil.ReadAll(response.Body) | ||
if err != nil { | ||
return fmt.Errorf("failed to decode response: %w", err) | ||
} | ||
|
||
//handle non 2xx responses | ||
if response.StatusCode >= http.StatusBadRequest && response.StatusCode <= http.StatusNetworkAuthenticationRequired { | ||
return fmt.Errorf("server responded with a non 2xx code: %d", response.StatusCode) | ||
|
||
} | ||
|
||
responseData := string(body) | ||
if responseData != teamsOKResponse { | ||
return fmt.Errorf("teams endpoint responded with an error: %s", responseData) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (sender *Sender) buildMessage(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool) MessageCard { | ||
|
||
title, uri := sender.buildTitleAndURI(events, trigger) | ||
var triggerDescription string | ||
if trigger.Desc != "" { | ||
triggerDescription = string(blackfriday.Run([]byte(trigger.Desc))) | ||
} | ||
facts := sender.buildEventsFacts(events, sender.maxEvents, throttled) | ||
var actions []Action | ||
if uri != "" { | ||
actions = append(actions, Action{ | ||
Type: openUri, | ||
Name: openUriMessage, | ||
Targets: []OpenURITarget{ | ||
{ | ||
Os: openUriOsDefault, | ||
URI: uri, | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
return MessageCard{ | ||
Context: context, | ||
MessageType: messageType, | ||
Summary: summary, | ||
ThemeColor: getColourForState(events.GetSubjectState()), | ||
Title: title, | ||
Sections: []Section{ | ||
{ | ||
ActivityTitle: activityTitleText, | ||
ActivityText: triggerDescription, | ||
Facts: facts, | ||
}, | ||
}, | ||
PotentialAction: actions, | ||
} | ||
} | ||
|
||
func (sender *Sender) buildRequest(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) (*http.Request, error) { | ||
|
||
messageCard := sender.buildMessage(events, trigger, throttled) | ||
requestURL := contact.Value | ||
requestBody, err := json.Marshal(messageCard) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
request, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewBuffer(requestBody)) | ||
if err != nil { | ||
return request, err | ||
} | ||
|
||
for k, v := range headers { | ||
request.Header.Set(k, v) | ||
} | ||
sender.logger.Debugf("created payload '%s' for teams endpoint %s", string(requestBody), request.URL.String()) | ||
return request, nil | ||
} | ||
|
||
func (sender *Sender) buildTitleAndURI(events moira.NotificationEvents, trigger moira.TriggerData) (string, string) { | ||
title := string(events.GetSubjectState()) | ||
|
||
if trigger.Name != "" { | ||
title += " " + trigger.Name | ||
} | ||
|
||
tags := trigger.GetTags() | ||
if tags != "" { | ||
title = fmt.Sprintf("%s %s", title, tags) | ||
} | ||
triggerURI := trigger.GetTriggerURI(sender.frontURI) | ||
|
||
return title, triggerURI | ||
} | ||
|
||
// buildEventsFacts builds Facts from moira events | ||
// if n is negative buildEventsFacts does not limit the Facts array | ||
func (sender *Sender) buildEventsFacts(events moira.NotificationEvents, maxEvents int, throttled bool) []Fact { | ||
var facts []Fact | ||
|
||
eventsPrinted := 0 | ||
for _, event := range events { | ||
line := fmt.Sprintf("%s = %s (%s to %s)", event.Metric, event.GetMetricValue(), event.OldState, event.State) | ||
if len(moira.UseString(event.Message)) > 0 { | ||
line += fmt.Sprintf(". %s", moira.UseString(event.Message)) | ||
} | ||
facts = append(facts, Fact{ | ||
Name: event.FormatTimestamp(sender.location), | ||
Value: "```" + line + "```", | ||
}) | ||
|
||
if maxEvents != -1 && len(facts) > maxEvents { | ||
facts = append(facts, Fact{ | ||
Name: "Info", | ||
Value: "```" + fmt.Sprintf("\n...and %d more events.", len(events)-eventsPrinted) + "```", | ||
}) | ||
break | ||
} | ||
eventsPrinted++ | ||
} | ||
|
||
if throttled { | ||
facts = append(facts, throttleWarningFact) | ||
} | ||
return facts | ||
} | ||
|
||
func (sender *Sender) isValidWebhookURL(webhookURL string) error { | ||
// basic URL check | ||
_, err := url.Parse(webhookURL) | ||
if err != nil { | ||
return err | ||
} | ||
// only pass MS teams webhook URLs | ||
hasPrefix := strings.HasPrefix(webhookURL, teamsBaseURL) | ||
if !hasPrefix { | ||
return fmt.Errorf("%s is an invalid ms teams webhook url", webhookURL) | ||
} | ||
return nil | ||
} | ||
|
||
func getColourForState(state moira.State) string { | ||
switch state := state; state { | ||
case moira.StateOK: | ||
return Green | ||
case moira.StateWARN: | ||
return Orange | ||
case moira.StateERROR: | ||
return Red | ||
case moira.StateNODATA: | ||
return Black | ||
default: | ||
return White //unhandled state | ||
} | ||
} |
Oops, something went wrong.