2021-12-10 02:32:38 +01:00
package opsgenie
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
2021-12-10 03:18:25 +01:00
2022-06-21 03:25:14 +02:00
"github.com/TwiN/gatus/v4/alerting/alert"
"github.com/TwiN/gatus/v4/client"
"github.com/TwiN/gatus/v4/core"
2021-12-10 02:32:38 +01:00
)
const (
restAPI = "https://api.opsgenie.com/v2/alerts"
)
type AlertProvider struct {
2021-12-10 03:18:25 +01:00
// APIKey to use for
2021-12-10 02:32:38 +01:00
APIKey string ` yaml:"api-key" `
2021-12-10 03:18:25 +01:00
// Priority to be used in Opsgenie alert payload
//
// default: P1
2021-12-10 02:32:38 +01:00
Priority string ` yaml:"priority" `
2021-12-10 03:18:25 +01:00
// Source define source to be used in Opsgenie alert payload
//
// default: gatus
2021-12-10 02:32:38 +01:00
Source string ` yaml:"source" `
2021-12-10 03:18:25 +01:00
// EntityPrefix is a prefix to be used in entity argument in Opsgenie alert payload
//
// default: gatus-
2021-12-10 02:32:38 +01:00
EntityPrefix string ` yaml:"entity-prefix" `
2021-12-10 03:18:25 +01:00
//AliasPrefix is a prefix to be used in alias argument in Opsgenie alert payload
//
// default: gatus-healthcheck-
2021-12-10 02:32:38 +01:00
AliasPrefix string ` yaml:"alias-prefix" `
2021-12-10 03:18:25 +01:00
// Tags to be used in Opsgenie alert payload
//
// default: []
2021-12-10 02:32:38 +01:00
Tags [ ] string ` yaml:"tags" `
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert * alert . Alert ` yaml:"default-alert,omitempty" `
}
2021-12-10 03:18:25 +01:00
// IsValid returns whether the provider's configuration is valid
2021-12-10 02:32:38 +01:00
func ( provider * AlertProvider ) IsValid ( ) bool {
return len ( provider . APIKey ) > 0
}
// Send an alert using the provider
//
// Relevant: https://docs.opsgenie.com/docs/alert-api
func ( provider * AlertProvider ) Send ( endpoint * core . Endpoint , alert * alert . Alert , result * core . Result , resolved bool ) error {
err := provider . createAlert ( endpoint , alert , result , resolved )
if err != nil {
return err
}
if resolved {
err = provider . closeAlert ( endpoint , alert )
if err != nil {
return err
}
}
if alert . IsSendingOnResolved ( ) {
if resolved {
// The alert has been resolved and there's no error, so we can clear the alert's ResolveKey
alert . ResolveKey = ""
} else {
alert . ResolveKey = provider . alias ( buildKey ( endpoint ) )
}
}
return nil
}
func ( provider * AlertProvider ) createAlert ( endpoint * core . Endpoint , alert * alert . Alert , result * core . Result , resolved bool ) error {
payload := provider . buildCreateRequestBody ( endpoint , alert , result , resolved )
_ , err := provider . sendRequest ( restAPI , http . MethodPost , payload )
return err
}
func ( provider * AlertProvider ) closeAlert ( endpoint * core . Endpoint , alert * alert . Alert ) error {
payload := provider . buildCloseRequestBody ( endpoint , alert )
url := restAPI + "/" + provider . alias ( buildKey ( endpoint ) ) + "/close?identifierType=alias"
_ , err := provider . sendRequest ( url , http . MethodPost , payload )
return err
}
func ( provider * AlertProvider ) sendRequest ( url , method string , payload interface { } ) ( * http . Response , error ) {
body , err := json . Marshal ( payload )
if err != nil {
return nil , fmt . Errorf ( "fail to build alert payload: %v" , payload )
}
request , err := http . NewRequest ( method , url , bytes . NewBuffer ( body ) )
if err != nil {
return nil , err
}
request . Header . Set ( "Content-Type" , "application/json" )
request . Header . Set ( "Authorization" , "GenieKey " + provider . APIKey )
res , err := client . GetHTTPClient ( nil ) . Do ( request )
if err != nil {
return nil , err
}
if res . StatusCode > 399 {
rBody , _ := io . ReadAll ( res . Body )
return nil , fmt . Errorf ( "call to provider alert returned status code %d: %s" , res . StatusCode , string ( rBody ) )
}
return res , nil
}
2021-12-10 03:18:25 +01:00
func ( provider * AlertProvider ) buildCreateRequestBody ( endpoint * core . Endpoint , alert * alert . Alert , result * core . Result , resolved bool ) alertCreateRequest {
2021-12-10 02:32:38 +01:00
var message , description , results string
if resolved {
message = fmt . Sprintf ( "RESOLVED: %s - %s" , endpoint . Name , alert . GetDescription ( ) )
2021-12-12 22:33:16 +01:00
description = fmt . Sprintf ( "An alert for *%s* has been resolved after passing successfully %d time(s) in a row" , endpoint . DisplayName ( ) , alert . SuccessThreshold )
2021-12-10 02:32:38 +01:00
} else {
message = fmt . Sprintf ( "%s - %s" , endpoint . Name , alert . GetDescription ( ) )
2021-12-12 22:33:16 +01:00
description = fmt . Sprintf ( "An alert for *%s* has been triggered due to having failed %d time(s) in a row" , endpoint . DisplayName ( ) , alert . FailureThreshold )
2021-12-10 02:32:38 +01:00
}
if endpoint . Group != "" {
message = fmt . Sprintf ( "[%s] %s" , endpoint . Group , message )
}
for _ , conditionResult := range result . ConditionResults {
var prefix string
if conditionResult . Success {
prefix = "▣"
} else {
prefix = "▢"
}
results += fmt . Sprintf ( "%s - `%s`\n" , prefix , conditionResult . Condition )
}
description = description + "\n" + results
key := buildKey ( endpoint )
details := map [ string ] string {
"endpoint:url" : endpoint . URL ,
"endpoint:group" : endpoint . Group ,
"result:hostname" : result . Hostname ,
"result:ip" : result . IP ,
"result:dns_code" : result . DNSRCode ,
"result:errors" : strings . Join ( result . Errors , "," ) ,
}
for k , v := range details {
if v == "" {
delete ( details , k )
}
}
if result . HTTPStatus > 0 {
details [ "result:http_status" ] = strconv . Itoa ( result . HTTPStatus )
}
2021-12-10 03:18:25 +01:00
return alertCreateRequest {
2021-12-10 02:32:38 +01:00
Message : message ,
Description : description ,
Source : provider . source ( ) ,
Priority : provider . priority ( ) ,
Alias : provider . alias ( key ) ,
Entity : provider . entity ( key ) ,
Tags : provider . Tags ,
Details : details ,
}
}
2021-12-10 03:18:25 +01:00
func ( provider * AlertProvider ) buildCloseRequestBody ( endpoint * core . Endpoint , alert * alert . Alert ) alertCloseRequest {
return alertCloseRequest {
2021-12-10 02:32:38 +01:00
Source : buildKey ( endpoint ) ,
Note : fmt . Sprintf ( "RESOLVED: %s - %s" , endpoint . Name , alert . GetDescription ( ) ) ,
}
}
func ( provider * AlertProvider ) source ( ) string {
source := provider . Source
if source == "" {
return "gatus"
}
return source
}
func ( provider * AlertProvider ) alias ( key string ) string {
alias := provider . AliasPrefix
if alias == "" {
alias = "gatus-healthcheck-"
}
return alias + key
}
func ( provider * AlertProvider ) entity ( key string ) string {
alias := provider . EntityPrefix
if alias == "" {
alias = "gatus-"
}
return alias + key
}
func ( provider * AlertProvider ) priority ( ) string {
priority := provider . Priority
if priority == "" {
return "P1"
}
return priority
}
2021-12-10 03:18:25 +01:00
// GetDefaultAlert returns the provider's default alert configuration
2021-12-10 02:32:38 +01:00
func ( provider AlertProvider ) GetDefaultAlert ( ) * alert . Alert {
return provider . DefaultAlert
}
func buildKey ( endpoint * core . Endpoint ) string {
name := toKebabCase ( endpoint . Name )
if endpoint . Group == "" {
return name
}
return toKebabCase ( endpoint . Group ) + "-" + name
}
func toKebabCase ( val string ) string {
return strings . ToLower ( strings . ReplaceAll ( val , " " , "-" ) )
}
2021-12-10 03:18:25 +01:00
type alertCreateRequest struct {
Message string ` json:"message" `
Priority string ` json:"priority" `
Source string ` json:"source" `
Entity string ` json:"entity" `
Alias string ` json:"alias" `
Description string ` json:"description" `
Tags [ ] string ` json:"tags,omitempty" `
Details map [ string ] string ` json:"details" `
}
type alertCloseRequest struct {
Source string ` json:"source" `
Note string ` json:"note" `
}