-
-
Notifications
You must be signed in to change notification settings - Fork 520
feat(alerting): Add Incident.io alerting provider #972
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
bb1ffa9
feat(alerting): added incident.io provider alerting.
ImTheCurse 7e814c5
Tests: added incident.io provider unit tests.
ImTheCurse fd1b09b
Documentation: added incidentio documentation.
ImTheCurse caa98f0
Refactor: Changed documentation + types to an alphabetical order.
ImTheCurse 767ad43
Merge branch 'master' into feat-incident-io
ImTheCurse 8ae19b4
Refactor: change wrong comment.
ImTheCurse 60975c5
Update README.md
ImTheCurse 621fae5
Update alerting/provider/incidentio/incident_io.go
ImTheCurse 51693ae
Update alerting/provider/incidentio/incident_io.go
ImTheCurse 41b6723
Update alerting/provider/incidentio/incident_io.go
ImTheCurse d9b5129
Update alerting/provider/incidentio/incident_io.go
ImTheCurse 888e891
Refactor: changed alertSourceID to url.
ImTheCurse 8754bee
Refactor: changed documentation.
ImTheCurse 9634886
Refactor: refactored tests, removed status from config.
ImTheCurse 2c46f0a
Readme: updated docs.
ImTheCurse dabbec5
Refactor: removed duplication key in favor of ResolveKey.
ImTheCurse ba047a0
Refactor: change variable format.
ImTheCurse cf28ca2
Feat + Test: added support for passing metadata and source url, added…
ImTheCurse 3284c2d
Merge branch 'master' into feat-incident-io
TwiN 851ffa3
Refactor: chaned variable naming
ImTheCurse 426183a
Merge branch 'master' into feat-incident-io
ImTheCurse 70e8ad5
Update alerting/config.go
TwiN dc03d61
Update README.md
TwiN 6684218
Update README.md
TwiN 0b3bda0
Update README.md
TwiN a144588
Apply suggestions from code review
TwiN 6c516f9
Refactor: sort var by abc
ImTheCurse File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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,207 @@ | ||
package incidentio | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/TwiN/gatus/v5/alerting/alert" | ||
"github.com/TwiN/gatus/v5/client" | ||
"github.com/TwiN/gatus/v5/config/endpoint" | ||
"github.com/TwiN/logr" | ||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
const ( | ||
restAPIUrl = "https://api.incident.io/v2/alert_events/http/" | ||
) | ||
|
||
var ( | ||
ErrURLNotSet = errors.New("url not set.") | ||
ErrDuplicateGroupOverride = errors.New("duplicate group override") | ||
ErrAuthTokenNotSet = errors.New("auth-token not set.") | ||
TwiN marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
type Config struct { | ||
URL string `yaml:"url,omitempty"` | ||
AuthToken string `yaml:"auth-token,omitempty"` | ||
SourceURL string `yaml:"source-url,omitempty"` | ||
Metadata map[string]interface{} `yaml:"metadata,omitempty"` | ||
} | ||
|
||
func (cfg *Config) Validate() error { | ||
if len(cfg.URL) == 0 { | ||
return ErrURLNotSet | ||
} | ||
if len(cfg.AuthToken) == 0 { | ||
return ErrAuthTokenNotSet | ||
} | ||
return nil | ||
} | ||
|
||
func (cfg *Config) Merge(override *Config) { | ||
if len(override.URL) > 0 { | ||
cfg.URL = override.URL | ||
} | ||
if len(override.AuthToken) > 0 { | ||
cfg.AuthToken = override.AuthToken | ||
} | ||
if len(override.SourceURL) > 0 { | ||
cfg.SourceURL = override.SourceURL | ||
} | ||
if len(override.Metadata) > 0 { | ||
cfg.Metadata = override.Metadata | ||
} | ||
} | ||
|
||
// AlertProvider is the configuration necessary for sending an alert using incident.io | ||
type AlertProvider struct { | ||
DefaultConfig Config `yaml:",inline"` | ||
|
||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type | ||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"` | ||
|
||
// Overrides is a list of Override that may be prioritized over the default configuration | ||
Overrides []Override `yaml:"overrides,omitempty"` | ||
} | ||
|
||
type Override struct { | ||
Group string `yaml:"group"` | ||
Config `yaml:",inline"` | ||
} | ||
|
||
func (provider *AlertProvider) Validate() error { | ||
registeredGroups := make(map[string]bool) | ||
if provider.Overrides != nil { | ||
for _, override := range provider.Overrides { | ||
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" { | ||
return ErrDuplicateGroupOverride | ||
} | ||
registeredGroups[override.Group] = true | ||
} | ||
} | ||
return provider.DefaultConfig.Validate() | ||
} | ||
|
||
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error { | ||
cfg, err := provider.GetConfig(ep.Group, alert) | ||
if err != nil { | ||
return err | ||
} | ||
buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved)) | ||
req, err := http.NewRequest(http.MethodPost, cfg.URL, buffer) | ||
if err != nil { | ||
return err | ||
} | ||
req.Header.Set("Content-Type", "application/json") | ||
req.Header.Set("Authorization", "Bearer "+cfg.AuthToken) | ||
response, err := client.GetHTTPClient(nil).Do(req) | ||
if err != nil { | ||
return err | ||
} | ||
defer response.Body.Close() | ||
if response.StatusCode > 399 { | ||
body, _ := io.ReadAll(response.Body) | ||
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body)) | ||
} | ||
incidentioResponse := Response{} | ||
err = json.NewDecoder(response.Body).Decode(&incidentioResponse) | ||
if err != nil { | ||
// Silently fail. We don't want to create tons of alerts just because we failed to parse the body. | ||
logr.Errorf("[incident-io.Send] Ran into error decoding pagerduty response: %s", err.Error()) | ||
} | ||
alert.ResolveKey = incidentioResponse.DeduplicationKey | ||
return err | ||
} | ||
|
||
type Body struct { | ||
AlertSourceConfigID string `json:"alert_source_config_id"` | ||
Status string `json:"status"` | ||
Title string `json:"title"` | ||
DeduplicationKey string `json:"deduplication_key,omitempty"` | ||
Description string `json:"description,omitempty"` | ||
SourceURL string `json:"source_url,omitempty"` | ||
Metadata map[string]interface{} `json:"metadata,omitempty"` | ||
} | ||
|
||
type Response struct { | ||
DeduplicationKey string `json:"deduplication_key"` | ||
} | ||
|
||
func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte { | ||
var message, formattedConditionResults, status string | ||
if resolved { | ||
message = "An alert has been resolved after passing successfully " + strconv.Itoa(alert.SuccessThreshold) + " time(s) in a row" | ||
ImTheCurse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
status = "resolved" | ||
} else { | ||
message = "An alert has been triggered due to having failed " + strconv.Itoa(alert.FailureThreshold) + " time(s) in a row" | ||
status = "firing" | ||
} | ||
for _, conditionResult := range result.ConditionResults { | ||
var prefix string | ||
if conditionResult.Success { | ||
prefix = "🟢" | ||
ImTheCurse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else { | ||
prefix = "🔴" | ||
} | ||
// No need for \n since incident.io trims it anyways. | ||
formattedConditionResults += fmt.Sprintf(" %s %s ", prefix, conditionResult.Condition) | ||
} | ||
if len(alert.GetDescription()) > 0 { | ||
message += " with the following description: " + alert.GetDescription() | ||
} | ||
|
||
message += fmt.Sprintf(" and the following conditions: %s ", formattedConditionResults) | ||
var body []byte | ||
alertSourceID := strings.Split(cfg.URL, restAPIUrl)[1] | ||
body, _ = json.Marshal(Body{ | ||
AlertSourceConfigID: alertSourceID, | ||
Title: "Gatus: " + ep.DisplayName(), | ||
Status: status, | ||
DeduplicationKey: alert.ResolveKey, | ||
Description: message, | ||
SourceURL: cfg.SourceURL, | ||
Metadata: cfg.Metadata, | ||
}) | ||
fmt.Printf("%v", string(body)) | ||
return body | ||
|
||
} | ||
func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) { | ||
cfg := provider.DefaultConfig | ||
// Handle group overrides | ||
if provider.Overrides != nil { | ||
for _, override := range provider.Overrides { | ||
if group == override.Group { | ||
cfg.Merge(&override.Config) | ||
break | ||
} | ||
} | ||
} | ||
// Handle alert overrides | ||
if len(alert.ProviderOverride) != 0 { | ||
overrideConfig := Config{} | ||
if err := yaml.Unmarshal(alert.ProviderOverrideAsBytes(), &overrideConfig); err != nil { | ||
return nil, err | ||
} | ||
cfg.Merge(&overrideConfig) | ||
} | ||
// Validate the configuration | ||
err := cfg.Validate() | ||
return &cfg, err | ||
} | ||
|
||
// GetDefaultAlert returns the provider's default alert configuration | ||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert { | ||
return provider.DefaultAlert | ||
} | ||
|
||
func (provider *AlertProvider) ValidateOverrides(group string, alert *alert.Alert) error { | ||
_, err := provider.GetConfig(group, alert) | ||
return err | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.