-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Co-authored-by: Jonas Pfannschmidt <[email protected]>
- Loading branch information
Showing
12 changed files
with
802 additions
and
3 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# OpsGenie | ||
|
||
## URL Format | ||
|
||
## Creating a REST API endpoint in OpsGenie | ||
|
||
1. Open up the Integration List page by clicking on *Settings => Integration List* within the menu | ||
![Screenshot 1](opsgenie/1.png) | ||
|
||
2. Click *API => Add* | ||
|
||
3. Make sure *Create and Update Access* and *Enabled* are checked and click *Save Integration* | ||
![Screenshot 2](opsgenie/2.png) | ||
|
||
4. Copy the *API Key* | ||
|
||
5. Format the service URL | ||
|
||
The host can be either api.opsgenie.com or api.eu.opsgenie.com depending on the location of your instance. See | ||
the [OpsGenie documentation](https://docs.opsgenie.com/docs/alert-api) for details. | ||
|
||
``` | ||
opsgenie://api.opsgenie.com/eb243592-faa2-4ba2-a551q-1afdf565c889 | ||
└───────────────────────────────────┘ | ||
token | ||
``` | ||
|
||
## Passing parameters via code | ||
|
||
If you want to, you can pass additional parameters to the `send` function. | ||
<br/> | ||
The following example contains all parameters that are currently supported. | ||
|
||
```gotemplate | ||
service.Send("An example alert message", &types.Params{ | ||
"alias": "Life is too short for no alias", | ||
"description": "Every alert needs a description", | ||
"responders": `[{"id":"4513b7ea-3b91-438f-b7e4-e3e54af9147c","type":"team"},{"name":"NOC","type":"team"}]`, | ||
"visibleTo": `[{"id":"4513b7ea-3b91-438f-b7e4-e3e54af9147c","type":"team"},{"name":"rocket_team","type":"team"}]`, | ||
"actions": "An action", | ||
"tags": "tag1 tag2", | ||
"details": `{"key1": "value1", "key2": "value2"}`, | ||
"entity": "An example entity", | ||
"source": "The source", | ||
"priority": "P1", | ||
"user": "Dracula", | ||
"note": "Here is a note", | ||
}) | ||
``` | ||
|
||
# Optional parameters | ||
|
||
You can optionally specify the parameters in the URL: | ||
opsgenie://api.opsgenie.com/eb243592-faa2-4ba2-a551q-1afdf565c889?alias=Life+is+too+short+for+no+alias&description=Every+alert+needs+a+description&actions=An+action&tags=["tag1","tag2"]&entity=An+example+entity&source=The+source&priority=P1&user=Dracula¬e=Here+is+a+note | ||
|
||
Example using the command line: | ||
|
||
shoutrrr send -u 'opsgenie://api.eu.opsgenie.com/token?tags=["tag1","tag2"]&description=testing&responders=[{"username":"superuser", "type": "user"}]&entity=Example Entity&source=Example Source&actions=["asdf", "bcde"]' -m "Hello World6" | ||
|
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
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,107 @@ | ||
package opsgenie | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"log" | ||
"net/http" | ||
"net/url" | ||
|
||
"github.com/containrrr/shoutrrr/pkg/format" | ||
"github.com/containrrr/shoutrrr/pkg/services/standard" | ||
"github.com/containrrr/shoutrrr/pkg/types" | ||
) | ||
|
||
const ( | ||
alertEndpointTemplate = "https://%s:%d/v2/alerts" | ||
) | ||
|
||
// Service providing OpsGenie as a notification service | ||
type Service struct { | ||
standard.Standard | ||
config *Config | ||
pkr format.PropKeyResolver | ||
} | ||
|
||
func (service *Service) sendAlert(url string, apiKey string, payload AlertPayload) error { | ||
jsonBody, err := json.Marshal(payload) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
jsonBuffer := bytes.NewBuffer(jsonBody) | ||
|
||
req, err := http.NewRequest("POST", url, jsonBuffer) | ||
if err != nil { | ||
return err | ||
} | ||
req.Header.Add("Authorization", "GenieKey "+apiKey) | ||
req.Header.Add("Content-Type", "application/json") | ||
resp, err := http.DefaultClient.Do(req) | ||
if err != nil { | ||
return fmt.Errorf("failed to send notification to OpsGenie: %s", err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode < 200 || resp.StatusCode >= 300 { | ||
body, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
return fmt.Errorf("OpsGenie notification returned %d HTTP status code. Cannot read body: %s", resp.StatusCode, err) | ||
} | ||
return fmt.Errorf("OpsGenie notification returned %d HTTP status code: %s", resp.StatusCode, body) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Initialize loads ServiceConfig from configURL and sets logger for this Service | ||
func (service *Service) Initialize(configURL *url.URL, logger *log.Logger) error { | ||
service.Logger.SetLogger(logger) | ||
service.config = &Config{} | ||
service.pkr = format.NewPropKeyResolver(service.config) | ||
return service.config.setURL(&service.pkr, configURL) | ||
} | ||
|
||
// Send a notification message to OpsGenie | ||
// See: https://docs.opsgenie.com/docs/alert-api#create-alert | ||
func (service *Service) Send(message string, params *types.Params) error { | ||
config := service.config | ||
url := fmt.Sprintf(alertEndpointTemplate, config.Host, config.Port) | ||
payload, err := service.newAlertPayload(message, params) | ||
if err != nil { | ||
return err | ||
} | ||
return service.sendAlert(url, config.APIKey, payload) | ||
} | ||
|
||
func (service *Service) newAlertPayload(message string, params *types.Params) (AlertPayload, error) { | ||
if params == nil { | ||
params = &types.Params{} | ||
} | ||
|
||
// Defensive copy | ||
payloadFields := *service.config | ||
|
||
if err := service.pkr.UpdateConfigFromParams(&payloadFields, params); err != nil { | ||
return AlertPayload{}, err | ||
} | ||
|
||
result := AlertPayload{ | ||
Message: message, | ||
Alias: payloadFields.Alias, | ||
Description: payloadFields.Description, | ||
Responders: payloadFields.Responders, | ||
VisibleTo: payloadFields.VisibleTo, | ||
Actions: payloadFields.Actions, | ||
Tags: payloadFields.Tags, | ||
Details: payloadFields.Details, | ||
Entity: payloadFields.Entity, | ||
Source: payloadFields.Source, | ||
Priority: payloadFields.Priority, | ||
User: payloadFields.User, | ||
Note: payloadFields.Note, | ||
} | ||
return result, nil | ||
} |
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,96 @@ | ||
package opsgenie | ||
|
||
import ( | ||
"fmt" | ||
"net/url" | ||
"strconv" | ||
|
||
"github.com/containrrr/shoutrrr/pkg/format" | ||
"github.com/containrrr/shoutrrr/pkg/types" | ||
) | ||
|
||
const defaultPort = 443 | ||
|
||
// Config for use within the opsgenie service | ||
type Config struct { | ||
APIKey string `desc:"The OpsGenie API key"` | ||
Host string `desc:"The OpsGenie API host. Use 'api.eu.opsgenie.com' for EU instances" default:"api.opsgenie.com"` | ||
Port uint16 `desc:"The OpsGenie API port." default:"443"` | ||
Alias string `key:"alias" desc:"Client-defined identifier of the alert" optional:"true"` | ||
Description string `key:"description" desc:"Description field of the alert" optional:"true"` | ||
Responders []Entity `key:"responders" desc:"Teams, users, escalations and schedules that the alert will be routed to send notifications" optional:"true"` | ||
VisibleTo []Entity `key:"visibleTo" desc:"Teams and users that the alert will become visible to without sending any notification" optional:"true"` | ||
Actions []string `key:"actions" desc:"Custom actions that will be available for the alert" optional:"true"` | ||
Tags []string `key:"tags" desc:"Tags of the alert" optional:"true"` | ||
Details map[string]string `key:"details" desc:"Map of key-value pairs to use as custom properties of the alert" optional:"true"` | ||
Entity string `key:"entity" desc:"Entity field of the alert that is generally used to specify which domain the Source field of the alert" optional:"true"` | ||
Source string `key:"source" desc:"Source field of the alert" optional:"true"` | ||
Priority string `key:"priority" desc:"Priority level of the alert. Possible values are P1, P2, P3, P4 and P5" optional:"true"` | ||
Note string `key:"note" desc:"Additional note that will be added while creating the alert" optional:"true"` | ||
User string `key:"user" desc:"Display name of the request owner" optional:"true"` | ||
} | ||
|
||
// Enums returns an empty map because the OpsGenie service doesn't use Enums | ||
func (config Config) Enums() map[string]types.EnumFormatter { | ||
return map[string]types.EnumFormatter{} | ||
} | ||
|
||
// GetURL is the public version of getURL that creates a new PropKeyResolver when accessed from outside the package | ||
func (config *Config) GetURL() *url.URL { | ||
resolver := format.NewPropKeyResolver(config) | ||
return config.getURL(&resolver) | ||
} | ||
|
||
// Private version of GetURL that can use an instance of PropKeyResolver instead of rebuilding it's model from reflection | ||
func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL { | ||
host := "" | ||
if config.Port > 0 { | ||
host = fmt.Sprintf("%s:%d", config.Host, config.Port) | ||
} else { | ||
host = config.Host | ||
} | ||
|
||
result := &url.URL{ | ||
Host: host, | ||
Path: fmt.Sprintf("/%s", config.APIKey), | ||
Scheme: Scheme, | ||
RawQuery: format.BuildQuery(resolver), | ||
} | ||
|
||
return result | ||
} | ||
|
||
// SetURL updates a ServiceConfig from a URL representation of it's field values | ||
func (config *Config) SetURL(url *url.URL) error { | ||
resolver := format.NewPropKeyResolver(config) | ||
return config.setURL(&resolver, url) | ||
} | ||
|
||
// Private version of SetURL that can use an instance of PropKeyResolver instead of rebuilding it's model from reflection | ||
func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error { | ||
config.Host = url.Hostname() | ||
config.APIKey = url.Path[1:] | ||
|
||
if url.Port() != "" { | ||
port, err := strconv.ParseUint(url.Port(), 10, 16) | ||
if err != nil { | ||
return err | ||
} | ||
config.Port = uint16(port) | ||
} else { | ||
config.Port = 443 | ||
} | ||
|
||
for key, vals := range url.Query() { | ||
if err := resolver.Set(key, vals[0]); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
const ( | ||
// Scheme is the identifying part of this service's configuration URL | ||
Scheme = "opsgenie" | ||
) |
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,71 @@ | ||
package opsgenie | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
"strings" | ||
) | ||
|
||
// Entity represents either a user or a team | ||
// | ||
// The different variations are: | ||
// | ||
// { "id":"4513b7ea-3b91-438f-b7e4-e3e54af9147c", "type":"team" } | ||
// { "name":"rocket_team", "type":"team" } | ||
// { "id":"bb4d9938-c3c2-455d-aaab-727aa701c0d8", "type":"user" } | ||
// { "username":"[email protected]", "type":"user" } | ||
type Entity struct { | ||
Type string `json:"type"` | ||
ID string `json:"id,omitempty"` | ||
Name string `json:"name,omitempty"` | ||
Username string `json:"username,omitempty"` | ||
} | ||
|
||
// SetFromProp deserializes an entity | ||
func (e *Entity) SetFromProp(propValue string) error { | ||
elements := strings.Split(propValue, ":") | ||
|
||
if len(elements) != 2 { | ||
return fmt.Errorf("invalid entity, should have two elments separated by colon: %q", propValue) | ||
} | ||
e.Type = elements[0] | ||
identifier := elements[1] | ||
isID, err := isOpsGenieID(identifier) | ||
if err != nil { | ||
return fmt.Errorf("invalid entity, cannot parse id/name: %q", identifier) | ||
} | ||
|
||
if isID { | ||
e.ID = identifier | ||
} else if e.Type == "team" { | ||
e.Name = identifier | ||
} else if e.Type == "user" { | ||
e.Username = identifier | ||
} else { | ||
return fmt.Errorf("invalid entity, unexpected entity type: %q", e.Type) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// GetPropValue serializes an entity | ||
func (e *Entity) GetPropValue() (string, error) { | ||
identifier := "" | ||
|
||
if e.ID != "" { | ||
identifier = e.ID | ||
} else if e.Name != "" { | ||
identifier = e.Name | ||
} else if e.Username != "" { | ||
identifier = e.Username | ||
} else { | ||
return "", fmt.Errorf("invalid entity, should have either ID, name or username") | ||
} | ||
|
||
return fmt.Sprintf("%s:%s", e.Type, identifier), nil | ||
} | ||
|
||
// Detects OpsGenie IDs in the form 4513b7ea-3b91-438f-b7e4-e3e54af9147c | ||
func isOpsGenieID(str string) (bool, error) { | ||
return regexp.MatchString(`^[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}$`, str) | ||
} |
Oops, something went wrong.