Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docs/services/pagerduty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# OpsGenie

## URL Format

--8<-- "docs/services/pagerduty/config.md"

## Create a Service Integration in PagerDuty

Follow to official [PagerDuty documentation](https://support.pagerduty.com/main/docs/services-and-integrations) to

1. create a service, and then
2. add an 'Events API V2' integration to the service. Note teh value of the `Integration Key`

The host is always `events.pagerduty.com`, so you do not need to explicitly specify it, however you can provide one if
you prefer, of if there is a need to override the default.

```
pagerduty:///eb243592-faa2-4ba2-a551q-1afdf565c889
└───────────────────────────────────┘
integration key


pagerduty://events.pagerduty.com/eb243592-faa2-4ba2-a551q-1afdf565c889
└───────────────────────────────────┘
integration key

```

## 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.

```go
service.Send("An example alert message", &types.Params{
"severity": "critical",
"source": "The source of the alert",
"action": "trigger",

})
```

See the [PagerDuty documentation](https://developer.pagerduty.com/docs/send-alert-event) for details on which fields
are required and what values are permitted for each field

## Passing parameters via URL

You can optionally specify the parameters in the URL:

!!! info ""
pagerduty://events.pagerduty.com/145d44a18bb44a0bc06161d5f541a90a?severity=critical&source=beszel&action=trigger
!!!

Example using the command line:

```shell
shoutrrr send -u 'pagerduty://events.pagerduty.com/145d44a18bb44a0bc06161d5f541a90a?severity=critical&source=beszel&action=trigger' -m 'This is a test'
```


1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ nav:
- Matrix: 'services/matrix.md'
- Ntfy: 'services/ntfy.md'
- OpsGenie: 'services/opsgenie.md'
- PagerDuty: 'services/pagerduty.md'
- Pushbullet: 'services/pushbullet.md'
- Pushover: 'services/pushover.md'
- Rocketchat: 'services/rocketchat.md'
Expand Down
2 changes: 2 additions & 0 deletions pkg/router/servicemap.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/containrrr/shoutrrr/pkg/services/mattermost"
"github.com/containrrr/shoutrrr/pkg/services/ntfy"
"github.com/containrrr/shoutrrr/pkg/services/opsgenie"
"github.com/containrrr/shoutrrr/pkg/services/pagerduty"
"github.com/containrrr/shoutrrr/pkg/services/pushbullet"
"github.com/containrrr/shoutrrr/pkg/services/pushover"
"github.com/containrrr/shoutrrr/pkg/services/rocketchat"
Expand All @@ -38,6 +39,7 @@ var serviceMap = map[string]func() t.Service{
"mattermost": func() t.Service { return &mattermost.Service{} },
"ntfy": func() t.Service { return &ntfy.Service{} },
"opsgenie": func() t.Service { return &opsgenie.Service{} },
"pagerduty": func() t.Service { return &pagerduty.Service{} },
"pushbullet": func() t.Service { return &pushbullet.Service{} },
"pushover": func() t.Service { return &pushover.Service{} },
"rocketchat": func() t.Service { return &rocketchat.Service{} },
Expand Down
56 changes: 56 additions & 0 deletions pkg/services/pagerduty/defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package pagerduty

import (
"fmt"
"reflect"
"strconv"
)

const (
tagUrl = "url"
tagDefault = "default"
)

func setUrlDefaults(config *Config) {
cfg := reflect.TypeOf(*config)
values := getDefaultUrlValues(cfg)

for fieldName, defVal := range values {
field := reflect.ValueOf(config).Elem().FieldByName(fieldName)

switch field.Kind() {
case reflect.String:
field.SetString(defVal)
case reflect.Int:
intVal, err := strconv.Atoi(defVal)
if err != nil {
fmt.Errorf("enable to convert %q to an int: %w", defVal, err)
}
field.SetInt(int64(intVal))
case reflect.Uint16:
intVal, err := strconv.Atoi(defVal)
if err != nil {
fmt.Errorf("enable to convert %q to an int: %w", defVal, err)
}
field.SetUint(uint64(intVal))
}
}
}

// getDefaultUrlValues finds field names tagged with `url` and returns a map of the field name to their default value.
func getDefaultUrlValues(cfg reflect.Type) map[string]string {
fieldToValue := make(map[string]string)

for i := 0; i < cfg.NumField(); i++ {
field := cfg.Field(i)

_, ok := field.Tag.Lookup(tagUrl)
if ok {
defaultVal := field.Tag.Get(tagDefault)
if defaultVal != "" {
fieldToValue[field.Name] = defaultVal
}
}
}
return fieldToValue
}
119 changes: 119 additions & 0 deletions pkg/services/pagerduty/pagerduty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package pagerduty

import (
"bytes"
"encoding/json"
"fmt"
"github.com/containrrr/shoutrrr/pkg/format"
"github.com/containrrr/shoutrrr/pkg/services/standard"
"github.com/containrrr/shoutrrr/pkg/types"
"io"
"net/http"
"net/url"
)

const (
eventEndpointTemplate = "https://%s:%d/v2/enqueue"
)

// Service providing PagerDuty as a notification service
type Service struct {
standard.Standard
config *Config
pkr format.PropKeyResolver
}

func (service *Service) sendAlert(url string, payload EventPayload) 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("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send notification to PagerDuty: %s", err)
}
defer resp.Body.Close()

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("PagerDuty notification returned %d HTTP status code. Cannot read body: %s", resp.StatusCode, err)
}
return fmt.Errorf("PagerDuty 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 types.StdLogger) error {
service.Logger.SetLogger(logger)
service.config = &Config{}
service.pkr = format.NewPropKeyResolver(service.config)

if err := service.setDefaults(); err != nil {
return err
}

return service.config.setURL(&service.pkr, configURL)
}

// Send a notification message to PagerDuty
// See: https://developer.pagerduty.com/docs/events-api-v2-overview
func (service *Service) Send(message string, params *types.Params) error {
config := service.config
endpointURL := fmt.Sprintf(eventEndpointTemplate, config.Host, config.Port)

payload, err := service.newEventPayload(message, params)
if err != nil {
return err
}

return service.sendAlert(endpointURL, payload)
}

func (service *Service) newEventPayload(message string, params *types.Params) (EventPayload, error) {
if params == nil {
params = &types.Params{}
}

// Defensive copy
payloadFields := *service.config

if err := service.pkr.UpdateConfigFromParams(&payloadFields, params); err != nil {
return EventPayload{}, err
}

// The maximum permitted length of this property is 1024 characters.
if len(message) > 1024 {
message = message[:1024]
}

result := EventPayload{
Payload: Payload{
Summary: message,
Severity: payloadFields.Severity,
Source: payloadFields.Source,
},
RoutingKey: payloadFields.IntegrationKey,
EventAction: payloadFields.Action,
}
return result, nil
}

func (service *Service) setDefaults() error {
if err := service.pkr.SetDefaultProps(service.config); err != nil {
return err
}

setUrlDefaults(service.config)
return nil
}
86 changes: 86 additions & 0 deletions pkg/services/pagerduty/pagerduty_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package pagerduty

import (
"fmt"
"net/url"
"strconv"

"github.com/containrrr/shoutrrr/pkg/format"
"github.com/containrrr/shoutrrr/pkg/types"
)

// Config for use within the pagerduty service
type Config struct {
IntegrationKey string `url:"path" desc:"The PagerDuty API integration key"`
Host string `url:"host" desc:"The PagerDuty API host." default:"events.pagerduty.com"`
Port uint16 `url:"port" desc:"The PagerDuty API port." default:"443"`
Severity string `key:"severity" desc:"The perceived severity of the status the event (critical, error, warning, or info); required" default:"error"`
Source string `key:"source" desc:"The unique location of the affected system, preferably a hostname or FQDN; required" default:"default"`
Action string `key:"action" desc:"The type of event (trigger, acknowledge, or resolve)" default:"trigger"`
}

// Enums returns an empty map because the PagerDuty 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 its 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.IntegrationKey),
Scheme: Scheme,
RawQuery: format.BuildQuery(resolver),
}

return result
}

// SetURL updates a ServiceConfig from a URL representation of its 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 its model from reflection
func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error {
config.IntegrationKey = url.Path[1:]

if url.Hostname() != "" {
config.Host = url.Hostname()
}

if url.Port() != "" {
port, err := strconv.ParseUint(url.Port(), 10, 16)
if err != nil {
return err
}
config.Port = uint16(port)
}

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 = "pagerduty"
)
16 changes: 16 additions & 0 deletions pkg/services/pagerduty/pagerduty_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package pagerduty

// EventPayload represents the payload being sent to the PagerDuty Events API v2
//
// See: https://developer.pagerduty.com/docs/events-api-v2-overview
type EventPayload struct {
Payload Payload `json:"payload"`
RoutingKey string `json:"routing_key"`
EventAction string `json:"event_action"`
}

type Payload struct {
Summary string `json:"summary"`
Severity string `json:"severity"`
Source string `json:"source"`
}
Loading