1
0
mirror of https://github.com/nikoksr/notify.git synced 2025-10-08 22:52:08 +02:00

feat(service): PagerDuty service integration (#933)

Co-authored-by: Niko Köser <koeserniko@gmail.com>
This commit is contained in:
Rey David Dominguez Soto
2025-01-15 07:02:51 -06:00
committed by GitHub
parent 1df6960796
commit be564db17f
9 changed files with 652 additions and 0 deletions

View File

@@ -95,6 +95,7 @@ Yes, please! Contributions of all kinds are very welcome! Feel free to check our
| [Mailgun](https://www.mailgun.com) | [service/mailgun](service/mailgun) | [mailgun/mailgun-go](https://github.com/mailgun/mailgun-go) | :heavy_check_mark: |
| [Matrix](https://www.matrix.org) | [service/matrix](service/matrix) | [mautrix/go](https://github.com/mautrix/go) | :heavy_check_mark: |
| [Microsoft Teams](https://www.microsoft.com/microsoft-teams) | [service/msteams](service/msteams) | [atc0005/go-teams-notify](https://github.com/atc0005/go-teams-notify) | :heavy_check_mark: |
| [PagerDuty](https://www.pagerduty.com) | [service/pagerduty](service/pagerduty) | [PagerDuty/go-pagerduty](https://github.com/PagerDuty/go-pagerduty) | :heavy_check_mark: |
| [Plivo](https://www.plivo.com) | [service/plivo](service/plivo) | [plivo/plivo-go](https://github.com/plivo/plivo-go) | :heavy_check_mark: |
| [Pushover](https://pushover.net/) | [service/pushover](service/pushover) | [gregdel/pushover](https://github.com/gregdel/pushover) | :heavy_check_mark: |
| [Pushbullet](https://www.pushbullet.com) | [service/pushbullet](service/pushbullet) | [cschomburg/go-pushbullet](https://github.com/cschomburg/go-pushbullet) | :heavy_check_mark: |

1
go.mod
View File

@@ -63,6 +63,7 @@ require (
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/PagerDuty/go-pagerduty v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect

2
go.sum
View File

@@ -40,6 +40,8 @@ github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo=
github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/PagerDuty/go-pagerduty v1.8.0 h1:MTFqTffIcAervB83U7Bx6HERzLbyaSPL/+oxH3zyluI=
github.com/PagerDuty/go-pagerduty v1.8.0/go.mod h1:nzIeAqyFSJAFkjWKvMzug0JtwDg+V+UoCWjFrfFH5mI=
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20240116134246-a8cbe886bab0 h1:ztLQGVQsey3BjCoh0TvHc/iKTQmkio2OmsIxhuu+EeY=
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20240116134246-a8cbe886bab0/go.mod h1:rjP7sIipbZcagro/6TCk6X0ZeFT2eyudH5+fve/cbBA=
github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s=

View File

@@ -0,0 +1,70 @@
# PagerDuty Notifications
## Prerequisites
Ensure you have a valid PagerDuty API token to authenticate requests.
### Compatibility
This service is compatible with the PagerDuty API for creating incidents.
## Usage
```go
package main
import (
"context"
"log"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/pagerduty"
)
func main() {
// Create a new PagerDuty service. Replace 'your_pagerduty_api_token' with your actual PagerDuty API token.
pagerDutyService, err := pagerduty.New("your_pagerduty_api_token")
if err != nil {
log.Fatalf("failed to create pagerduty service: %s", err)
}
// Set the sender address and add receivers. (required)
pagerDutyService.SetFromAddress("sender@example.com")
pagerDutyService.AddReceivers("ServiceDirectory1", "ServiceDirectory2")
// Set the urgency, priority ID, and notification type. (optional)
pagerDutyService.SetUrgency("high")
pagerDutyService.SetPriorityID("P123456")
pagerDutyService.SetNotificationType("incident")
// Create a notifier instance and register the PagerDuty service to it.
notifier := notify.New()
notifier.UseServices(pagerDutyService)
// Send a notification.
err = notifier.Send(context.Background(), "Test Alert", "This is a test alert from PagerDuty service.")
if err != nil {
log.Fatalf("failed to send notification: %s", err)
}
log.Println("Notification sent successfully")
}
```
## Configuration
### Required Properties
- **API Token**: Your PagerDuty API token.
- **From Address**: The email address of the sender. The author of the incident.
- **Receivers**: List of PagerDuty service directories to send the incident to.
### Optional Properties
- **Urgency**: The urgency of the incident (e.g., "high", "low").
- **PriorityID**: The ID of the priority level assigned to the incident.
- **NotificationType**: Type of notification (default is "incident").
These properties can be set using the respective setter methods provided by the `Config` struct:
- `SetFromAddress(string)`
- `AddReceivers(...string)`
- `SetUrgency(string)`
- `SetPriorityID(string)`
- `SetNotificationType(string)`

View File

@@ -0,0 +1,96 @@
package pagerduty
import (
"errors"
"fmt"
"net/mail"
"github.com/PagerDuty/go-pagerduty"
)
const (
APIReferenceType = "service_reference"
APIPriorityReference = "priority_reference"
DefaultNotificationType = "incident"
)
// Config contains the configuration for the PagerDuty service.
type Config struct {
FromAddress string
Receivers []string
NotificationType string
Urgency string
PriorityID string
}
func NewConfig() *Config {
return &Config{
NotificationType: DefaultNotificationType,
Receivers: make([]string, 0, 1),
}
}
// OK checks if the configuration is valid.
// It returns an error if the configuration is invalid.
func (c *Config) OK() error {
if c.FromAddress == "" {
return errors.New("from address is required")
}
_, err := mail.ParseAddress(c.FromAddress)
if err != nil {
return fmt.Errorf("from address is invalid: %w", err)
}
if len(c.Receivers) == 0 {
return errors.New("at least one receiver is required")
}
if c.NotificationType == "" {
return errors.New("notification type is required")
}
return nil
}
// PriorityReference returns the PriorityID reference if it is set, otherwise it returns nil.
func (c *Config) PriorityReference() *pagerduty.APIReference {
if c.PriorityID == "" {
return nil
}
return &pagerduty.APIReference{
ID: c.PriorityID,
Type: APIPriorityReference,
}
}
// SetFromAddress sets the from address in the configuration.
func (c *Config) SetFromAddress(fromAddress string) {
c.FromAddress = fromAddress
}
// AddReceivers appends the receivers to the configuration.
func (c *Config) AddReceivers(receivers ...string) {
c.Receivers = append(c.Receivers, receivers...)
}
// SetPriorityID sets the PriorityID in the configuration.
func (c *Config) SetPriorityID(priorityID string) {
c.PriorityID = priorityID
}
// SetUrgency sets the urgency in the configuration.
func (c *Config) SetUrgency(urgency string) {
c.Urgency = urgency
}
// SetNotificationType sets the notification type in the configuration.
// If the notification type is empty, it will be set to the default value "incident".
func (c *Config) SetNotificationType(notificationType string) {
if notificationType == "" {
notificationType = DefaultNotificationType
}
c.NotificationType = notificationType
}

View File

@@ -0,0 +1,214 @@
package pagerduty_test
import (
"testing"
gopagerduty "github.com/PagerDuty/go-pagerduty"
"github.com/nikoksr/notify/service/pagerduty"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConfig_NewConfig(t *testing.T) {
t.Parallel()
config := pagerduty.NewConfig()
want := &pagerduty.Config{
Receivers: []string{},
NotificationType: "incident",
}
require.Equal(t, want, config)
}
func TestConfig_OK(t *testing.T) {
t.Parallel()
tests := []struct {
name string
config *pagerduty.Config
wantErr string
}{
{
name: "ok_basic_config",
config: &pagerduty.Config{
FromAddress: "sender@domain.com",
Receivers: []string{"AB1234", "CD5678"},
NotificationType: "incident",
},
wantErr: "",
},
{
name: "ok_complete_config",
config: &pagerduty.Config{
FromAddress: "sender@domain.com",
Receivers: []string{"AB1234", "CD5678"},
Urgency: "high",
PriorityID: "P1234",
NotificationType: "incident",
},
wantErr: "",
},
{
name: "missing_from_address",
config: &pagerduty.Config{
Receivers: []string{"AB1234", "CD5678"},
},
wantErr: "from address is required",
},
{
name: "invalid_from_address",
config: &pagerduty.Config{
FromAddress: "senderdomain.com",
Receivers: []string{"AB1234", "CD5678"},
},
wantErr: "from address is invalid: mail: missing '@' or angle-addr",
},
{
name: "missing_receivers",
config: &pagerduty.Config{
FromAddress: "sender@domain.com",
},
wantErr: "at least one receiver is required",
},
{
name: "missing_notification_type",
config: &pagerduty.Config{
FromAddress: "sender@domain.com",
Receivers: []string{"AB1234", "CD5678"},
},
wantErr: "notification type is required",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
err := test.config.OK()
if test.wantErr == "" {
require.NoError(t, err)
return
}
require.EqualError(t, err, test.wantErr)
})
}
}
func TestConfig_Priority(t *testing.T) {
t.Parallel()
tests := []struct {
name string
config *pagerduty.Config
want *gopagerduty.APIReference
}{
{
name: "default_priority",
config: &pagerduty.Config{},
want: nil,
},
{
name: "priority",
config: &pagerduty.Config{
PriorityID: "P1234",
},
want: &gopagerduty.APIReference{
ID: "P1234",
Type: pagerduty.APIPriorityReference,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
got := test.config.PriorityReference()
require.Equal(t, test.want, got)
})
}
}
func TestConfig_SetFromAddress(t *testing.T) {
t.Parallel()
config := pagerduty.NewConfig()
require.NotNil(t, config)
assert.Empty(t, config.FromAddress)
config.SetFromAddress("sender@domain.com")
assert.Equal(t, "sender@domain.com", config.FromAddress)
}
func TestConfig_AddReceivers(t *testing.T) {
t.Parallel()
config := pagerduty.NewConfig()
require.NotNil(t, config)
assert.Empty(t, config.Receivers)
config.AddReceivers("AB1234", "CD5678")
assert.Equal(t, []string{"AB1234", "CD5678"}, config.Receivers)
}
func TestConfig_SetNotificationType(t *testing.T) {
t.Parallel()
tests := []struct {
name string
config *pagerduty.Config
notificationType string
want string
}{
{
name: "empty_notification_type",
config: &pagerduty.Config{},
notificationType: "",
want: "incident",
},
{
name: "set_notification_type",
config: &pagerduty.Config{},
notificationType: "notification",
want: "notification",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
test.config.SetNotificationType(test.notificationType)
assert.Equal(t, test.want, test.config.NotificationType)
})
}
}
func TestConfig_SetPriorityID(t *testing.T) {
t.Parallel()
config := pagerduty.NewConfig()
require.NotNil(t, config)
assert.Empty(t, config.PriorityID)
config.SetPriorityID("P1234")
assert.Equal(t, "P1234", config.PriorityID)
}
func TestConfig_SetUrgency(t *testing.T) {
t.Parallel()
config := pagerduty.NewConfig()
require.NotNil(t, config)
assert.Empty(t, config.Urgency)
config.SetUrgency("high")
assert.Equal(t, "high", config.Urgency)
}

46
service/pagerduty/doc.go Normal file
View File

@@ -0,0 +1,46 @@
// Package pagerduty provides a notifier implementation that sends notifications via PagerDuty.
// It uses the PagerDuty API to create incidents based on the provided configuration.
//
// Usage:
// To use this package, you need to create a new instance of PagerDuty with your API token.
// You can then configure it with the necessary details like the sender's address and receivers.
// Finally, you can send notifications which will create incidents in PagerDuty.
//
// Example:
//
// package main
//
// import (
// "context"
// "log"
//
// "github.com/nikoksr/notify"
// "github.com/nikoksr/notify/service/pagerduty"
// )
//
// func main() {
// // Create a new PagerDuty service. Replace 'your_pagerduty_api_token' with your actual PagerDuty API token.
// pagerDutyService, err := pagerduty.New("your_pagerduty_api_token")
// if err != nil {
// log.Fatalf("failed to create pagerduty service: %s", err)
// }
//
// // Set the sender address and add receivers.
// pagerDutyService.SetFromAddress("sender@example.com")
// pagerDutyService.AddReceivers("ServiceDirectory1", "ServiceDirectory2")
//
// // Create a notifier instance and register the PagerDuty service to it.
// notifier := notify.New()
// notifier.UseServices(pagerDutyService)
//
// // Send a notification.
// err = notifier.Send(context.Background(), "Test Alert", "This is a test alert from PagerDuty service.")
// if err != nil {
// log.Fatalf("failed to send notification: %s", err)
// }
//
// log.Println("Notification sent successfully")
// }
//
// This package requires a valid PagerDuty API token to authenticate requests.
package pagerduty

View File

@@ -0,0 +1,71 @@
package pagerduty
import (
"context"
"errors"
"fmt"
"github.com/PagerDuty/go-pagerduty"
"github.com/nikoksr/notify"
)
type Client interface {
CreateIncidentWithContext(ctx context.Context, from string, options *pagerduty.CreateIncidentOptions) (*pagerduty.Incident, error) //nolint:lll // acceptable in this case, alternative makes the interface even less readable
}
// Compile-time check to verify that the PagerDuty type implements the notifier.Notifier interface.
var _ notify.Notifier = &PagerDuty{}
type PagerDuty struct {
*Config
Client Client
}
func New(token string, clientOptions ...pagerduty.ClientOptions) (*PagerDuty, error) {
if token == "" {
return nil, errors.New("access token is required")
}
pagerDuty := &PagerDuty{
Config: NewConfig(),
Client: pagerduty.NewClient(token, clientOptions...),
}
return pagerDuty, nil
}
func (s *PagerDuty) Send(ctx context.Context, subject, message string) error {
if err := s.Config.OK(); err != nil {
return fmt.Errorf("invalid configuration: %w", err)
}
incident := s.IncidentOptions(subject, message)
for _, receiver := range s.Config.Receivers {
// set the service ID to the receiver
incident.Service.ID = receiver
_, err := s.Client.CreateIncidentWithContext(ctx, s.Config.FromAddress, incident)
if err != nil {
return fmt.Errorf("create pager duty incident: %w", err)
}
}
return nil
}
func (s *PagerDuty) IncidentOptions(subject, message string) *pagerduty.CreateIncidentOptions {
return &pagerduty.CreateIncidentOptions{
Title: subject,
Service: &pagerduty.APIReference{
ID: "", // service ID will be set per receiver
Type: APIReferenceType,
},
Body: &pagerduty.APIDetails{
Type: s.Config.NotificationType,
Details: message,
},
Priority: s.Config.PriorityReference(),
Urgency: s.Config.Urgency,
}
}

View File

@@ -0,0 +1,151 @@
package pagerduty_test
import (
"context"
"errors"
"testing"
gopagerduty "github.com/PagerDuty/go-pagerduty"
"github.com/nikoksr/notify/service/pagerduty"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type mockClient struct {
mock.Mock
}
func (m *mockClient) CreateIncidentWithContext(
ctx context.Context,
from string,
options *gopagerduty.CreateIncidentOptions,
) (*gopagerduty.Incident, error) {
args := m.Called(ctx, from, options)
if err := args.Error(1); err != nil {
return nil, err
}
incident, isIncident := args.Get(0).(*gopagerduty.Incident)
if !isIncident {
return nil, errors.New("unexpected type for first argument")
}
return incident, nil
}
func TestPagerDuty_New(t *testing.T) {
t.Parallel()
t.Run("successful_new", func(t *testing.T) {
t.Parallel()
service, err := pagerduty.New("fake_token")
require.NoError(t, err)
want := &pagerduty.PagerDuty{
Config: pagerduty.NewConfig(),
Client: gopagerduty.NewClient("fake_token"),
}
assert.Equal(t, want, service)
})
t.Run("fail_new_invalid_token", func(t *testing.T) {
t.Parallel()
_, err := pagerduty.New("")
require.EqualError(t, err, "access token is required")
})
}
func TestPagerDuty_Send(t *testing.T) {
t.Parallel()
tests := []struct {
name string
receivers []string
subject string
message string
mockSetup func(m *mockClient)
expectedError string
expectedCall bool // whether the mock should be called
}{
{
name: "successful_send_to_multiple_receivers",
receivers: []string{"AB1234", "CD5678"},
subject: "Test Subject",
message: "Test Message",
expectedCall: true,
mockSetup: func(m *mockClient) {
m.On("CreateIncidentWithContext", mock.Anything, "sender@domain.com", mock.Anything).
Return(&gopagerduty.Incident{}, nil)
},
},
{
name: "successful_send_to_single_receivers",
receivers: []string{"AB1234"},
subject: "Test Subject",
message: "Test Message",
expectedCall: true,
mockSetup: func(m *mockClient) {
m.On("CreateIncidentWithContext", mock.Anything, "sender@domain.com", mock.Anything).
Return(&gopagerduty.Incident{}, nil)
},
},
{
name: "unsuccessful_send",
receivers: []string{"AB1234"},
subject: "Test Subject",
message: "Test Message",
expectedCall: true,
mockSetup: func(m *mockClient) {
m.On("CreateIncidentWithContext", mock.Anything, "sender@domain.com", mock.Anything).
Return(nil, errors.New("failed to create incident"))
},
expectedError: "create pager duty incident: failed to create incident",
},
{
name: "fail_send_no_receivers",
subject: "Test Subject",
message: "Test Message",
mockSetup: func(m *mockClient) {
m.On("CreateIncidentWithContext", mock.Anything, "sender@domain.com", mock.Anything).
Return(nil, errors.New("invalid configuration: at least one receiver is required"))
},
expectedError: "invalid configuration: at least one receiver is required",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
mockClient := new(mockClient)
service, err := pagerduty.New("fake_token")
require.NoError(t, err)
require.NotNil(t, service)
service.AddReceivers(test.receivers...)
service.SetFromAddress("sender@domain.com")
test.mockSetup(mockClient)
service.Client = mockClient
err = service.Send(context.Background(), test.subject, test.message)
if test.expectedError != "" {
require.EqualError(t, err, test.expectedError)
} else {
require.NoError(t, err)
}
if test.expectedCall {
mockClient.AssertExpectations(t)
}
})
}
}