feat(alertmanager): simplify and test e2e alertmanager (#7217)
* refactor(alertmanager): complete e2e testing and simplify * fix(alertmanager): fix typo * fix(alertmanager): set to true for prometheus
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -76,3 +76,5 @@ dist/
|
|||||||
|
|
||||||
# ignore user_scripts that is fetched by init-clickhouse
|
# ignore user_scripts that is fetched by init-clickhouse
|
||||||
deploy/common/clickhouse/user_scripts/
|
deploy/common/clickhouse/user_scripts/
|
||||||
|
|
||||||
|
queries.active
|
||||||
3
go.mod
3
go.mod
@@ -55,6 +55,7 @@ require (
|
|||||||
github.com/soheilhy/cmux v0.1.5
|
github.com/soheilhy/cmux v0.1.5
|
||||||
github.com/srikanthccv/ClickHouse-go-mock v0.9.0
|
github.com/srikanthccv/ClickHouse-go-mock v0.9.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
|
github.com/tidwall/gjson v1.18.0
|
||||||
github.com/uptrace/bun v1.2.9
|
github.com/uptrace/bun v1.2.9
|
||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.9
|
github.com/uptrace/bun/dialect/pgdialect v1.2.9
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
|
||||||
@@ -204,6 +205,8 @@ require (
|
|||||||
github.com/smarty/assertions v1.15.0 // indirect
|
github.com/smarty/assertions v1.15.0 // indirect
|
||||||
github.com/spf13/cobra v1.8.1 // indirect
|
github.com/spf13/cobra v1.8.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||||
github.com/tklauser/numcpus v0.7.0 // indirect
|
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -891,7 +891,13 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
|
|||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
|
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
|
||||||
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
|
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
|
||||||
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
|
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ var (
|
|||||||
type Alertmanager interface {
|
type Alertmanager interface {
|
||||||
factory.Service
|
factory.Service
|
||||||
// GetAlerts gets the alerts from the alertmanager per organization.
|
// GetAlerts gets the alerts from the alertmanager per organization.
|
||||||
GetAlerts(context.Context, string, alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error)
|
GetAlerts(context.Context, string, alertmanagertypes.GettableAlertsParams) (alertmanagertypes.DeprecatedGettableAlerts, error)
|
||||||
|
|
||||||
// PutAlerts puts the alerts into the alertmanager per organization.
|
// PutAlerts puts the alerts into the alertmanager per organization.
|
||||||
PutAlerts(context.Context, string, alertmanagertypes.PostableAlerts) error
|
PutAlerts(context.Context, string, alertmanagertypes.PostableAlerts) error
|
||||||
@@ -23,18 +23,30 @@ type Alertmanager interface {
|
|||||||
// TestReceiver sends a test alert to a receiver.
|
// TestReceiver sends a test alert to a receiver.
|
||||||
TestReceiver(context.Context, string, alertmanagertypes.Receiver) error
|
TestReceiver(context.Context, string, alertmanagertypes.Receiver) error
|
||||||
|
|
||||||
|
// TestAlert sends an alert to a list of receivers.
|
||||||
|
TestAlert(ctx context.Context, orgID string, alert *alertmanagertypes.PostableAlert, receivers []string) error
|
||||||
|
|
||||||
// ListChannels lists all channels for the organization.
|
// ListChannels lists all channels for the organization.
|
||||||
ListChannels(context.Context, string) ([]*alertmanagertypes.Channel, error)
|
ListChannels(context.Context, string) ([]*alertmanagertypes.Channel, error)
|
||||||
|
|
||||||
|
// ListAllChannels lists all channels for all organizations. It is used by the legacy alertmanager only.
|
||||||
|
ListAllChannels(context.Context) ([]*alertmanagertypes.Channel, error)
|
||||||
|
|
||||||
// GetChannelByID gets a channel for the organization.
|
// GetChannelByID gets a channel for the organization.
|
||||||
GetChannelByID(context.Context, string, int) (*alertmanagertypes.Channel, error)
|
GetChannelByID(context.Context, string, int) (*alertmanagertypes.Channel, error)
|
||||||
|
|
||||||
// UpdateChannel updates a channel for the organization.
|
// UpdateChannel updates a channel for the organization.
|
||||||
UpdateChannelByReceiver(context.Context, string, alertmanagertypes.Receiver) error
|
UpdateChannelByReceiverAndID(context.Context, string, alertmanagertypes.Receiver, int) error
|
||||||
|
|
||||||
// CreateChannel creates a channel for the organization.
|
// CreateChannel creates a channel for the organization.
|
||||||
CreateChannel(context.Context, string, alertmanagertypes.Receiver) error
|
CreateChannel(context.Context, string, alertmanagertypes.Receiver) error
|
||||||
|
|
||||||
// DeleteChannelByID deletes a channel for the organization.
|
// DeleteChannelByID deletes a channel for the organization.
|
||||||
DeleteChannelByID(context.Context, string, int) error
|
DeleteChannelByID(context.Context, string, int) error
|
||||||
|
|
||||||
|
// SetConfig sets the config for the organization.
|
||||||
|
SetConfig(context.Context, *alertmanagertypes.Config) error
|
||||||
|
|
||||||
|
// GetConfig gets the config for the organization.
|
||||||
|
GetConfig(context.Context, string) (*alertmanagertypes.Config, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import (
|
|||||||
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Notifier is responsible for dispatching alert notifications to an alertmanager.
|
// Batcher is responsible for batching alerts and broadcasting them on a channel.
|
||||||
type Batcher struct {
|
type Batcher struct {
|
||||||
|
// C is the channel on which alerts are sent to alertmanager
|
||||||
C chan alertmanagertypes.PostableAlerts
|
C chan alertmanagertypes.PostableAlerts
|
||||||
|
|
||||||
// logger
|
// logger
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
|
||||||
@@ -23,14 +25,21 @@ type Batcher struct {
|
|||||||
|
|
||||||
// more channel to signal the sender goroutine to send alerts
|
// more channel to signal the sender goroutine to send alerts
|
||||||
moreC chan struct{}
|
moreC chan struct{}
|
||||||
|
|
||||||
|
// stop channel to signal the sender goroutine to stop
|
||||||
stopC chan struct{}
|
stopC chan struct{}
|
||||||
mtx sync.RWMutex
|
|
||||||
|
// mutex to synchronize access to the queue
|
||||||
|
queueMtx sync.RWMutex
|
||||||
|
|
||||||
|
// wait group to wait for all goroutines to finish
|
||||||
|
goroutinesWg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(logger *slog.Logger, config Config) *Batcher {
|
func New(logger *slog.Logger, config Config) *Batcher {
|
||||||
batcher := &Batcher{
|
batcher := &Batcher{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
queue: make(alertmanagertypes.PostableAlerts, config.Capacity),
|
queue: make(alertmanagertypes.PostableAlerts, 0, config.Capacity),
|
||||||
config: config,
|
config: config,
|
||||||
moreC: make(chan struct{}, 1),
|
moreC: make(chan struct{}, 1),
|
||||||
stopC: make(chan struct{}),
|
stopC: make(chan struct{}),
|
||||||
@@ -41,22 +50,27 @@ func New(logger *slog.Logger, config Config) *Batcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start dispatches notifications continuously.
|
// Start dispatches notifications continuously.
|
||||||
func (n *Batcher) Start(ctx context.Context) error {
|
func (batcher *Batcher) Start(ctx context.Context) error {
|
||||||
|
batcher.goroutinesWg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
n.logger.InfoContext(ctx, "starting alertmanager batcher")
|
defer batcher.goroutinesWg.Done()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-batcher.stopC:
|
||||||
|
for batcher.queueLen() > 0 {
|
||||||
|
alerts := batcher.next()
|
||||||
|
batcher.C <- alerts
|
||||||
|
}
|
||||||
|
close(batcher.C)
|
||||||
return
|
return
|
||||||
case <-n.stopC:
|
case <-batcher.moreC:
|
||||||
return
|
|
||||||
case <-n.moreC:
|
|
||||||
}
|
}
|
||||||
alerts := n.nextBatch()
|
alerts := batcher.next()
|
||||||
n.C <- alerts
|
batcher.C <- alerts
|
||||||
// If the queue still has items left, kick off the next iteration.
|
// If the queue still has items left, kick off the next iteration.
|
||||||
if n.queueLen() > 0 {
|
if batcher.queueLen() > 0 {
|
||||||
n.setMore()
|
batcher.setMore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -64,69 +78,67 @@ func (n *Batcher) Start(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Batcher) queueLen() int {
|
// Add queues the given alerts for processing.
|
||||||
n.mtx.RLock()
|
func (batcher *Batcher) Add(ctx context.Context, alerts ...*alertmanagertypes.PostableAlert) {
|
||||||
defer n.mtx.RUnlock()
|
batcher.queueMtx.Lock()
|
||||||
|
defer batcher.queueMtx.Unlock()
|
||||||
|
|
||||||
return len(n.queue)
|
// Queue capacity should be significantly larger than a single alert
|
||||||
|
// batch could be.
|
||||||
|
if d := len(alerts) - batcher.config.Capacity; d > 0 {
|
||||||
|
alerts = alerts[d:]
|
||||||
|
batcher.logger.WarnContext(ctx, "alert batch larger than queue capacity, dropping alerts", "num_dropped", d, "capacity", batcher.config.Capacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the queue is full, remove the oldest alerts in favor
|
||||||
|
// of newer ones.
|
||||||
|
if d := (len(batcher.queue) + len(alerts)) - batcher.config.Capacity; d > 0 {
|
||||||
|
batcher.queue = batcher.queue[d:]
|
||||||
|
batcher.logger.WarnContext(ctx, "alert batch queue full, dropping alerts", "num_dropped", d)
|
||||||
|
}
|
||||||
|
|
||||||
|
batcher.queue = append(batcher.queue, alerts...)
|
||||||
|
|
||||||
|
// Notify sending goroutine that there are alerts to be processed.
|
||||||
|
batcher.setMore()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Batcher) nextBatch() alertmanagertypes.PostableAlerts {
|
// Stop shuts down the batcher.
|
||||||
n.mtx.Lock()
|
func (batcher *Batcher) Stop(ctx context.Context) {
|
||||||
defer n.mtx.Unlock()
|
close(batcher.stopC)
|
||||||
|
batcher.goroutinesWg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (batcher *Batcher) queueLen() int {
|
||||||
|
batcher.queueMtx.RLock()
|
||||||
|
defer batcher.queueMtx.RUnlock()
|
||||||
|
|
||||||
|
return len(batcher.queue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (batcher *Batcher) next() alertmanagertypes.PostableAlerts {
|
||||||
|
batcher.queueMtx.Lock()
|
||||||
|
defer batcher.queueMtx.Unlock()
|
||||||
|
|
||||||
var alerts alertmanagertypes.PostableAlerts
|
var alerts alertmanagertypes.PostableAlerts
|
||||||
|
|
||||||
if len(n.queue) > n.config.Size {
|
if len(batcher.queue) > batcher.config.Size {
|
||||||
alerts = append(make(alertmanagertypes.PostableAlerts, 0, n.config.Size), n.queue[:n.config.Size]...)
|
alerts = append(make(alertmanagertypes.PostableAlerts, 0, batcher.config.Size), batcher.queue[:batcher.config.Size]...)
|
||||||
n.queue = n.queue[n.config.Size:]
|
batcher.queue = batcher.queue[batcher.config.Size:]
|
||||||
} else {
|
} else {
|
||||||
alerts = append(make(alertmanagertypes.PostableAlerts, 0, len(n.queue)), n.queue...)
|
alerts = append(make(alertmanagertypes.PostableAlerts, 0, len(batcher.queue)), batcher.queue...)
|
||||||
n.queue = n.queue[:0]
|
batcher.queue = batcher.queue[:0]
|
||||||
}
|
}
|
||||||
|
|
||||||
return alerts
|
return alerts
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send queues the given notification requests for processing.
|
|
||||||
// Panics if called on a handler that is not running.
|
|
||||||
func (n *Batcher) Send(ctx context.Context, alerts ...*alertmanagertypes.PostableAlert) {
|
|
||||||
n.mtx.Lock()
|
|
||||||
defer n.mtx.Unlock()
|
|
||||||
|
|
||||||
// Queue capacity should be significantly larger than a single alert
|
|
||||||
// batch could be.
|
|
||||||
if d := len(alerts) - n.config.Capacity; d > 0 {
|
|
||||||
alerts = alerts[d:]
|
|
||||||
n.logger.WarnContext(ctx, "Alert batch larger than queue capacity, dropping alerts", "num_dropped", d)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the queue is full, remove the oldest alerts in favor
|
|
||||||
// of newer ones.
|
|
||||||
if d := (len(n.queue) + len(alerts)) - n.config.Capacity; d > 0 {
|
|
||||||
n.queue = n.queue[d:]
|
|
||||||
|
|
||||||
n.logger.WarnContext(ctx, "Alert notification queue full, dropping alerts", "num_dropped", d)
|
|
||||||
}
|
|
||||||
n.queue = append(n.queue, alerts...)
|
|
||||||
|
|
||||||
// Notify sending goroutine that there are alerts to be processed.
|
|
||||||
n.setMore()
|
|
||||||
}
|
|
||||||
|
|
||||||
// setMore signals that the alert queue has items.
|
// setMore signals that the alert queue has items.
|
||||||
func (n *Batcher) setMore() {
|
func (batcher *Batcher) setMore() {
|
||||||
// If we cannot send on the channel, it means the signal already exists
|
// If we cannot send on the channel, it means the signal already exists
|
||||||
// and has not been consumed yet.
|
// and has not been consumed yet.
|
||||||
select {
|
select {
|
||||||
case n.moreC <- struct{}{}:
|
case batcher.moreC <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop shuts down the notification handler.
|
|
||||||
func (n *Batcher) Stop(ctx context.Context) {
|
|
||||||
n.logger.InfoContext(ctx, "Stopping alertmanager batcher")
|
|
||||||
close(n.moreC)
|
|
||||||
close(n.stopC)
|
|
||||||
}
|
|
||||||
|
|||||||
64
pkg/alertmanager/alertmanagerbatcher/batcher_test.go
Normal file
64
pkg/alertmanager/alertmanagerbatcher/batcher_test.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package alertmanagerbatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBatcherWithOneAlertAndDefaultConfigs(t *testing.T) {
|
||||||
|
batcher := New(slog.New(slog.NewTextHandler(io.Discard, nil)), NewConfig())
|
||||||
|
batcher.Start(context.Background())
|
||||||
|
|
||||||
|
batcher.Add(context.Background(), &alertmanagertypes.PostableAlert{Alert: alertmanagertypes.AlertModel{
|
||||||
|
Labels: map[string]string{"alertname": "test"},
|
||||||
|
}})
|
||||||
|
|
||||||
|
alerts := <-batcher.C
|
||||||
|
assert.Equal(t, 1, len(alerts))
|
||||||
|
|
||||||
|
batcher.Stop(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBatcherWithBatchSize(t *testing.T) {
|
||||||
|
batcher := New(slog.New(slog.NewTextHandler(io.Discard, nil)), Config{Size: 2, Capacity: 4})
|
||||||
|
batcher.Start(context.Background())
|
||||||
|
|
||||||
|
var alerts alertmanagertypes.PostableAlerts
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
alerts = append(alerts, &alertmanagertypes.PostableAlert{Alert: alertmanagertypes.AlertModel{
|
||||||
|
Labels: map[string]string{"alertname": "test"},
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
batcher.Add(context.Background(), alerts...)
|
||||||
|
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
alerts := <-batcher.C
|
||||||
|
assert.Equal(t, 2, len(alerts))
|
||||||
|
}
|
||||||
|
|
||||||
|
batcher.Stop(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBatcherWithCClosed(t *testing.T) {
|
||||||
|
batcher := New(slog.New(slog.NewTextHandler(io.Discard, nil)), Config{Size: 2, Capacity: 4})
|
||||||
|
batcher.Start(context.Background())
|
||||||
|
|
||||||
|
var alerts alertmanagertypes.PostableAlerts
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
alerts = append(alerts, &alertmanagertypes.PostableAlert{Alert: alertmanagertypes.AlertModel{
|
||||||
|
Labels: map[string]string{"alertname": "test"},
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
batcher.Add(context.Background(), alerts...)
|
||||||
|
|
||||||
|
batcher.Stop(context.Background())
|
||||||
|
|
||||||
|
for alerts := range batcher.C {
|
||||||
|
assert.Equal(t, 2, len(alerts))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
package alertmanagerbatcher
|
package alertmanagerbatcher
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// Capacity is the maximum number of alerts that can be buffered in the batcher.
|
||||||
Capacity int
|
Capacity int
|
||||||
Size int
|
|
||||||
|
// Size is the number of alerts to send in each batch.
|
||||||
|
Size int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() Config {
|
func NewConfig() Config {
|
||||||
return Config{
|
return Config{
|
||||||
Capacity: 1000,
|
Capacity: 10000,
|
||||||
Size: 64,
|
Size: 64,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ type Server struct {
|
|||||||
|
|
||||||
func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registerer, srvConfig Config, orgID string, stateStore alertmanagertypes.StateStore) (*Server, error) {
|
func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registerer, srvConfig Config, orgID string, stateStore alertmanagertypes.StateStore) (*Server, error) {
|
||||||
server := &Server{
|
server := &Server{
|
||||||
logger: logger.With("pkg", "go.signoz.io/pkg/alertmanager/server"),
|
logger: logger.With("pkg", "go.signoz.io/pkg/alertmanager/alertmanagerserver"),
|
||||||
registry: registry,
|
registry: registry,
|
||||||
srvConfig: srvConfig,
|
srvConfig: srvConfig,
|
||||||
orgID: orgID,
|
orgID: orgID,
|
||||||
@@ -84,7 +84,10 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
|
|||||||
|
|
||||||
silencesSnapshot := ""
|
silencesSnapshot := ""
|
||||||
if state != nil {
|
if state != nil {
|
||||||
silencesSnapshot = state.Silences
|
silencesSnapshot, err = state.Get(alertmanagertypes.SilenceStateName)
|
||||||
|
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Initialize silences
|
// Initialize silences
|
||||||
server.silences, err = silence.New(silence.Options{
|
server.silences, err = silence.New(silence.Options{
|
||||||
@@ -103,7 +106,10 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
|
|||||||
|
|
||||||
nflogSnapshot := ""
|
nflogSnapshot := ""
|
||||||
if state != nil {
|
if state != nil {
|
||||||
nflogSnapshot = state.NFLog
|
nflogSnapshot, err = state.Get(alertmanagertypes.NFLogStateName)
|
||||||
|
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize notification log
|
// Initialize notification log
|
||||||
@@ -308,7 +314,51 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) TestReceiver(ctx context.Context, receiver alertmanagertypes.Receiver) error {
|
func (server *Server) TestReceiver(ctx context.Context, receiver alertmanagertypes.Receiver) error {
|
||||||
return alertmanagertypes.TestReceiver(ctx, receiver, server.tmpl, server.logger)
|
return alertmanagertypes.TestReceiver(ctx, receiver, server.tmpl, server.logger, alertmanagertypes.NewTestAlert(receiver, time.Now(), time.Now()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) TestAlert(ctx context.Context, postableAlert *alertmanagertypes.PostableAlert, receivers []string) error {
|
||||||
|
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(alertmanagertypes.PostableAlerts{postableAlert}, time.Duration(server.srvConfig.Global.ResolveTimeout), time.Now())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Join(err...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(alerts) != 1 {
|
||||||
|
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "expected 1 alert, got %d", len(alerts))
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan error, len(receivers))
|
||||||
|
for _, receiverName := range receivers {
|
||||||
|
go func(receiverName string) {
|
||||||
|
receiver, err := server.alertmanagerConfig.GetReceiver(receiverName)
|
||||||
|
if err != nil {
|
||||||
|
ch <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ch <- alertmanagertypes.TestReceiver(ctx, receiver, server.tmpl, server.logger, alerts[0])
|
||||||
|
}(receiverName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
for i := 0; i < len(receivers); i++ {
|
||||||
|
if err := <-ch; err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs != nil {
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) Hash() string {
|
||||||
|
if server.alertmanagerConfig == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return server.alertmanagerConfig.StoreableConfig().Hash
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) Stop(ctx context.Context) error {
|
func (server *Server) Stop(ctx context.Context) error {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func TestServerPutAlerts(t *testing.T) {
|
|||||||
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
|
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.NoError(t, amConfig.CreateReceiver(&config.Route{Receiver: "test-receiver", Continue: true}, alertmanagertypes.Receiver{
|
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
|
||||||
Name: "test-receiver",
|
Name: "test-receiver",
|
||||||
WebhookConfigs: []*config.WebhookConfig{
|
WebhookConfigs: []*config.WebhookConfig{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package sqlalertmanagerstore
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
"go.signoz.io/signoz/pkg/errors"
|
"go.signoz.io/signoz/pkg/errors"
|
||||||
"go.signoz.io/signoz/pkg/sqlstore"
|
"go.signoz.io/signoz/pkg/sqlstore"
|
||||||
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
||||||
@@ -45,46 +48,20 @@ func (store *config) Get(ctx context.Context, orgID string) (*alertmanagertypes.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set implements alertmanagertypes.ConfigStore.
|
// Set implements alertmanagertypes.ConfigStore.
|
||||||
func (store *config) Set(ctx context.Context, config *alertmanagertypes.Config, cb func(context.Context) error) error {
|
func (store *config) Set(ctx context.Context, config *alertmanagertypes.Config) error {
|
||||||
tx, err := store.sqlstore.BunDB().BeginTx(ctx, nil)
|
if _, err := store.
|
||||||
if err != nil {
|
sqlstore.
|
||||||
return err
|
BunDB().
|
||||||
}
|
|
||||||
|
|
||||||
defer tx.Rollback() //nolint:errcheck
|
|
||||||
|
|
||||||
if _, err = tx.
|
|
||||||
NewInsert().
|
NewInsert().
|
||||||
Model(config.StoreableConfig()).
|
Model(config.StoreableConfig()).
|
||||||
On("CONFLICT (org_id) DO UPDATE").
|
On("CONFLICT (org_id) DO UPDATE").
|
||||||
Set("config = ?", string(config.StoreableConfig().Config)).
|
Set("config = ?", config.StoreableConfig().Config).
|
||||||
|
Set("hash = ?", config.StoreableConfig().Hash).
|
||||||
Set("updated_at = ?", config.StoreableConfig().UpdatedAt).
|
Set("updated_at = ?", config.StoreableConfig().UpdatedAt).
|
||||||
Exec(ctx); err != nil {
|
Exec(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
channels := config.Channels()
|
|
||||||
if len(channels) != 0 {
|
|
||||||
if _, err = tx.NewInsert().
|
|
||||||
Model(&channels).
|
|
||||||
On("CONFLICT (name) DO UPDATE").
|
|
||||||
Set("data = EXCLUDED.data").
|
|
||||||
Set("updated_at = EXCLUDED.updated_at").
|
|
||||||
Exec(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cb != nil {
|
|
||||||
if err = cb(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,3 +81,176 @@ func (store *config) ListOrgs(ctx context.Context) ([]string, error) {
|
|||||||
|
|
||||||
return orgIDs, nil
|
return orgIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (store *config) CreateChannel(ctx context.Context, channel *alertmanagertypes.Channel, cb func(context.Context) error) error {
|
||||||
|
tx, err := store.sqlstore.BunDB().BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tx.Rollback() //nolint:errcheck
|
||||||
|
|
||||||
|
if _, err = tx.NewInsert().
|
||||||
|
Model(channel).
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cb != nil {
|
||||||
|
if err = cb(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *config) GetChannelByID(ctx context.Context, orgID string, id int) (*alertmanagertypes.Channel, error) {
|
||||||
|
channel := new(alertmanagertypes.Channel)
|
||||||
|
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(channel).
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, errors.Newf(errors.TypeNotFound, alertmanagertypes.ErrCodeAlertmanagerChannelNotFound, "cannot find channel with id %d", id)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return channel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *config) UpdateChannel(ctx context.Context, orgID string, channel *alertmanagertypes.Channel, cb func(context.Context) error) error {
|
||||||
|
tx, err := store.sqlstore.BunDB().BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tx.Rollback() //nolint:errcheck
|
||||||
|
|
||||||
|
_, err = tx.NewUpdate().
|
||||||
|
Model(channel).
|
||||||
|
WherePK().
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cb != nil {
|
||||||
|
if err = cb(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *config) DeleteChannelByID(ctx context.Context, orgID string, id int, cb func(context.Context) error) error {
|
||||||
|
channel := new(alertmanagertypes.Channel)
|
||||||
|
|
||||||
|
tx, err := store.sqlstore.BunDB().BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tx.Rollback() //nolint:errcheck
|
||||||
|
|
||||||
|
_, err = tx.NewDelete().
|
||||||
|
Model(channel).
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cb != nil {
|
||||||
|
if err = cb(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *config) ListChannels(ctx context.Context, orgID string) ([]*alertmanagertypes.Channel, error) {
|
||||||
|
var channels []*alertmanagertypes.Channel
|
||||||
|
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(&channels).
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *config) ListAllChannels(ctx context.Context) ([]*alertmanagertypes.Channel, error) {
|
||||||
|
var channels []*alertmanagertypes.Channel
|
||||||
|
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(&channels).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *config) GetMatchers(ctx context.Context, orgID string) (map[string][]string, error) {
|
||||||
|
type matcher struct {
|
||||||
|
bun.BaseModel `bun:"table:rules"`
|
||||||
|
ID int `bun:"id,pk"`
|
||||||
|
Data string `bun:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
matchers := []matcher{}
|
||||||
|
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Column("id", "data").
|
||||||
|
Model(&matchers).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
matchersMap := make(map[string][]string)
|
||||||
|
for _, matcher := range matchers {
|
||||||
|
receivers := gjson.Get(matcher.Data, "preferredChannels").Array()
|
||||||
|
for _, receiver := range receivers {
|
||||||
|
matchersMap[strconv.Itoa(matcher.ID)] = append(matchersMap[strconv.Itoa(matcher.ID)], receiver.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchersMap, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func NewAPI(alertmanager Alertmanager) *API {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) GetAlerts(req *http.Request, rw http.ResponseWriter) {
|
func (api *API) GetAlerts(rw http.ResponseWriter, req *http.Request) {
|
||||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ func (api *API) GetAlerts(req *http.Request, rw http.ResponseWriter) {
|
|||||||
render.Success(rw, http.StatusOK, alerts)
|
render.Success(rw, http.StatusOK, alerts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) TestReceiver(req *http.Request, rw http.ResponseWriter) {
|
func (api *API) TestReceiver(rw http.ResponseWriter, req *http.Request) {
|
||||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ func (api *API) TestReceiver(req *http.Request, rw http.ResponseWriter) {
|
|||||||
render.Success(rw, http.StatusNoContent, nil)
|
render.Success(rw, http.StatusNoContent, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) ListChannels(req *http.Request, rw http.ResponseWriter) {
|
func (api *API) ListChannels(rw http.ResponseWriter, req *http.Request) {
|
||||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -100,7 +100,20 @@ func (api *API) ListChannels(req *http.Request, rw http.ResponseWriter) {
|
|||||||
render.Success(rw, http.StatusOK, channels)
|
render.Success(rw, http.StatusOK, channels)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) GetChannelByID(req *http.Request, rw http.ResponseWriter) {
|
func (api *API) ListAllChannels(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
channels, err := api.alertmanager.ListAllChannels(ctx)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Success(rw, http.StatusOK, channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) GetChannelByID(rw http.ResponseWriter, req *http.Request) {
|
||||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -137,7 +150,7 @@ func (api *API) GetChannelByID(req *http.Request, rw http.ResponseWriter) {
|
|||||||
render.Success(rw, http.StatusOK, channel)
|
render.Success(rw, http.StatusOK, channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) UpdateChannelByID(req *http.Request, rw http.ResponseWriter) {
|
func (api *API) UpdateChannelByID(rw http.ResponseWriter, req *http.Request) {
|
||||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -147,6 +160,24 @@ func (api *API) UpdateChannelByID(req *http.Request, rw http.ResponseWriter) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(req)
|
||||||
|
if vars == nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is required in path"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idString, ok := vars["id"]
|
||||||
|
if !ok {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is required in path"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(idString)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid integer"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(req.Body)
|
body, err := io.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, err)
|
render.Error(rw, err)
|
||||||
@@ -160,7 +191,7 @@ func (api *API) UpdateChannelByID(req *http.Request, rw http.ResponseWriter) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = api.alertmanager.UpdateChannelByReceiver(ctx, claims.OrgID, receiver)
|
err = api.alertmanager.UpdateChannelByReceiverAndID(ctx, claims.OrgID, receiver, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, err)
|
render.Error(rw, err)
|
||||||
return
|
return
|
||||||
@@ -169,7 +200,7 @@ func (api *API) UpdateChannelByID(req *http.Request, rw http.ResponseWriter) {
|
|||||||
render.Success(rw, http.StatusNoContent, nil)
|
render.Success(rw, http.StatusNoContent, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) DeleteChannelByID(req *http.Request, rw http.ResponseWriter) {
|
func (api *API) DeleteChannelByID(rw http.ResponseWriter, req *http.Request) {
|
||||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -206,7 +237,7 @@ func (api *API) DeleteChannelByID(req *http.Request, rw http.ResponseWriter) {
|
|||||||
render.Success(rw, http.StatusNoContent, nil)
|
render.Success(rw, http.StatusNoContent, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) CreateChannel(req *http.Request, rw http.ResponseWriter) {
|
func (api *API) CreateChannel(rw http.ResponseWriter, req *http.Request) {
|
||||||
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package alertmanager
|
package alertmanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -9,9 +11,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Config is the config for the alertmanager server.
|
|
||||||
alertmanagerserver.Config `mapstructure:",squash"`
|
|
||||||
|
|
||||||
// Provider is the provider for the alertmanager service.
|
// Provider is the provider for the alertmanager service.
|
||||||
Provider string `mapstructure:"provider"`
|
Provider string `mapstructure:"provider"`
|
||||||
|
|
||||||
@@ -25,11 +24,14 @@ type Config struct {
|
|||||||
type Signoz struct {
|
type Signoz struct {
|
||||||
// PollInterval is the interval at which the alertmanager is synced.
|
// PollInterval is the interval at which the alertmanager is synced.
|
||||||
PollInterval time.Duration `mapstructure:"poll_interval"`
|
PollInterval time.Duration `mapstructure:"poll_interval"`
|
||||||
|
|
||||||
|
// Config is the config for the alertmanager server.
|
||||||
|
alertmanagerserver.Config `mapstructure:",squash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Legacy struct {
|
type Legacy struct {
|
||||||
// URL is the URL of the legacy alertmanager.
|
// ApiURL is the URL of the legacy signoz alertmanager.
|
||||||
URL *url.URL `mapstructure:"url"`
|
ApiURL string `mapstructure:"api_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfigFactory() factory.ConfigFactory {
|
func NewConfigFactory() factory.ConfigFactory {
|
||||||
@@ -38,14 +40,28 @@ func NewConfigFactory() factory.ConfigFactory {
|
|||||||
|
|
||||||
func newConfig() factory.Config {
|
func newConfig() factory.Config {
|
||||||
return Config{
|
return Config{
|
||||||
Config: alertmanagerserver.NewConfig(),
|
Provider: "legacy",
|
||||||
Provider: "signoz",
|
Legacy: Legacy{
|
||||||
|
ApiURL: "http://alertmanager:9093/api",
|
||||||
|
},
|
||||||
Signoz: Signoz{
|
Signoz: Signoz{
|
||||||
PollInterval: 15 * time.Second,
|
PollInterval: 15 * time.Second,
|
||||||
|
Config: alertmanagerserver.NewConfig(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) Validate() error {
|
func (c Config) Validate() error {
|
||||||
|
if c.Provider == "legacy" {
|
||||||
|
if c.Legacy.ApiURL == "" {
|
||||||
|
return errors.New("api_url is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := url.Parse(c.Legacy.ApiURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("api_url %q is invalid: %w", c.Legacy.ApiURL, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
48
pkg/alertmanager/config_test.go
Normal file
48
pkg/alertmanager/config_test.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package alertmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.signoz.io/signoz/pkg/config"
|
||||||
|
"go.signoz.io/signoz/pkg/config/envprovider"
|
||||||
|
"go.signoz.io/signoz/pkg/factory"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewWithEnvProvider(t *testing.T) {
|
||||||
|
t.Setenv("SIGNOZ_ALERTMANAGER_PROVIDER", "legacy")
|
||||||
|
t.Setenv("SIGNOZ_ALERTMANAGER_LEGACY_API__URL", "http://localhost:9093/api")
|
||||||
|
|
||||||
|
conf, err := config.New(
|
||||||
|
context.Background(),
|
||||||
|
config.ResolverConfig{
|
||||||
|
Uris: []string{"env:"},
|
||||||
|
ProviderFactories: []config.ProviderFactory{
|
||||||
|
envprovider.NewFactory(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]factory.ConfigFactory{
|
||||||
|
NewConfigFactory(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
actual := &Config{}
|
||||||
|
err = conf.Unmarshal("alertmanager", actual)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
def := NewConfigFactory().New().(Config)
|
||||||
|
|
||||||
|
expected := &Config{
|
||||||
|
Provider: "legacy",
|
||||||
|
Legacy: Legacy{
|
||||||
|
ApiURL: "http://localhost:9093/api",
|
||||||
|
},
|
||||||
|
Signoz: def.Signoz,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, actual)
|
||||||
|
assert.NoError(t, actual.Validate())
|
||||||
|
}
|
||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
"go.signoz.io/signoz/pkg/alertmanager"
|
"go.signoz.io/signoz/pkg/alertmanager"
|
||||||
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerbatcher"
|
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerbatcher"
|
||||||
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
|
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
|
||||||
@@ -17,6 +19,11 @@ import (
|
|||||||
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type postableAlert struct {
|
||||||
|
*alertmanagertypes.PostableAlert
|
||||||
|
Receivers []string `json:"receivers"`
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
alertsPath string = "/v1/alerts"
|
alertsPath string = "/v1/alerts"
|
||||||
routesPath string = "/v1/routes"
|
routesPath string = "/v1/routes"
|
||||||
@@ -29,6 +36,7 @@ type provider struct {
|
|||||||
client *http.Client
|
client *http.Client
|
||||||
configStore alertmanagertypes.ConfigStore
|
configStore alertmanagertypes.ConfigStore
|
||||||
batcher *alertmanagerbatcher.Batcher
|
batcher *alertmanagerbatcher.Batcher
|
||||||
|
url *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
|
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
|
||||||
@@ -41,6 +49,11 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
|||||||
settings := factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/signoz/pkg/alertmanager/legacyalertmanager")
|
settings := factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/signoz/pkg/alertmanager/legacyalertmanager")
|
||||||
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
|
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
|
||||||
|
|
||||||
|
url, err := url.Parse(config.Legacy.ApiURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &provider{
|
return &provider{
|
||||||
config: config,
|
config: config,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
@@ -49,6 +62,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
|||||||
},
|
},
|
||||||
configStore: configStore,
|
configStore: configStore,
|
||||||
batcher: alertmanagerbatcher.New(settings.Logger(), alertmanagerbatcher.NewConfig()),
|
batcher: alertmanagerbatcher.New(settings.Logger(), alertmanagerbatcher.NewConfig()),
|
||||||
|
url: url,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,22 +71,18 @@ func (provider *provider) Start(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer provider.batcher.Stop(ctx)
|
|
||||||
|
|
||||||
for {
|
for alerts := range provider.batcher.C {
|
||||||
select {
|
if err := provider.putAlerts(ctx, "", alerts); err != nil {
|
||||||
case <-ctx.Done():
|
provider.settings.Logger().Error("failed to send alerts to alertmanager", "error", err)
|
||||||
return nil
|
|
||||||
case alerts := <-provider.batcher.C:
|
|
||||||
if err := provider.putAlerts(ctx, "", alerts); err != nil {
|
|
||||||
provider.settings.Logger().Error("failed to send alerts to alertmanager", "error", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
|
func (provider *provider) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.DeprecatedGettableAlerts, error) {
|
||||||
url := provider.config.Legacy.URL.JoinPath(alertsPath)
|
url := provider.url.JoinPath(alertsPath)
|
||||||
url.RawQuery = params.RawQuery
|
url.RawQuery = params.RawQuery
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
|
||||||
@@ -92,23 +102,45 @@ func (provider *provider) GetAlerts(ctx context.Context, orgID string, params al
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var alerts alertmanagertypes.GettableAlerts
|
if resp.StatusCode/100 != 2 {
|
||||||
if err := json.Unmarshal(body, &alerts); err != nil {
|
return nil, fmt.Errorf("bad response status %v", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var alerts alertmanagertypes.DeprecatedGettableAlerts
|
||||||
|
if err := json.Unmarshal([]byte(gjson.GetBytes(body, "data").Raw), &alerts); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return alerts, nil
|
return alerts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) PutAlerts(ctx context.Context, _ string, alerts alertmanagertypes.PostableAlerts) error {
|
func (provider *provider) PutAlerts(ctx context.Context, orgID string, alerts alertmanagertypes.PostableAlerts) error {
|
||||||
provider.batcher.Send(ctx, alerts...)
|
provider.batcher.Add(ctx, alerts...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) putAlerts(ctx context.Context, _ string, alerts alertmanagertypes.PostableAlerts) error {
|
func (provider *provider) putAlerts(ctx context.Context, orgID string, alerts alertmanagertypes.PostableAlerts) error {
|
||||||
url := provider.config.Legacy.URL.JoinPath(alertsPath)
|
cfg, err := provider.configStore.Get(ctx, orgID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
body, err := json.Marshal(alerts)
|
legacyAlerts := make([]postableAlert, len(alerts))
|
||||||
|
for i, alert := range alerts {
|
||||||
|
receivers, err := cfg.ReceiverNamesFromRuleID(alert.Alert.Labels["ruleID"])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
legacyAlerts[i] = postableAlert{
|
||||||
|
PostableAlert: alert,
|
||||||
|
Receivers: receivers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
url := provider.url.JoinPath(alertsPath)
|
||||||
|
|
||||||
|
body, err := json.Marshal(legacyAlerts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -135,7 +167,7 @@ func (provider *provider) putAlerts(ctx context.Context, _ string, alerts alertm
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) TestReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
func (provider *provider) TestReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
||||||
url := provider.config.Legacy.URL.JoinPath(testReceiverPath)
|
url := provider.url.JoinPath(testReceiverPath)
|
||||||
|
|
||||||
body, err := json.Marshal(receiver)
|
body, err := json.Marshal(receiver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -163,49 +195,65 @@ func (provider *provider) TestReceiver(ctx context.Context, orgID string, receiv
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) ListChannels(ctx context.Context, orgID string) ([]*alertmanagertypes.Channel, error) {
|
func (provider *provider) TestAlert(ctx context.Context, orgID string, alert *alertmanagertypes.PostableAlert, receivers []string) error {
|
||||||
config, err := provider.configStore.Get(ctx, orgID)
|
url := provider.url.JoinPath(alertsPath)
|
||||||
|
|
||||||
|
legacyAlert := postableAlert{
|
||||||
|
PostableAlert: alert,
|
||||||
|
Receivers: receivers,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(legacyAlert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
channels := config.Channels()
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewBuffer(body))
|
||||||
channelList := make([]*alertmanagertypes.Channel, 0, len(channels))
|
if err != nil {
|
||||||
for _, channel := range channels {
|
return err
|
||||||
channelList = append(channelList, channel)
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := provider.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return channelList, nil
|
defer resp.Body.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
// Any HTTP status 2xx is OK.
|
||||||
|
if resp.StatusCode/100 != 2 {
|
||||||
|
return fmt.Errorf("bad response status %v", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) ListChannels(ctx context.Context, orgID string) ([]*alertmanagertypes.Channel, error) {
|
||||||
|
return provider.configStore.ListChannels(ctx, orgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) ListAllChannels(ctx context.Context) ([]*alertmanagertypes.Channel, error) {
|
||||||
|
return provider.configStore.ListAllChannels(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) GetChannelByID(ctx context.Context, orgID string, channelID int) (*alertmanagertypes.Channel, error) {
|
func (provider *provider) GetChannelByID(ctx context.Context, orgID string, channelID int) (*alertmanagertypes.Channel, error) {
|
||||||
config, err := provider.configStore.Get(ctx, orgID)
|
return provider.configStore.GetChannelByID(ctx, orgID, channelID)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
channels := config.Channels()
|
|
||||||
channel, err := alertmanagertypes.GetChannelByID(channels, channelID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return channel, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) UpdateChannelByReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
func (provider *provider) UpdateChannelByReceiverAndID(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver, id int) error {
|
||||||
config, err := provider.configStore.Get(ctx, orgID)
|
channel, err := provider.configStore.GetChannelByID(ctx, orgID, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = config.UpdateReceiver(alertmanagertypes.NewRouteFromReceiver(receiver), receiver)
|
err = channel.Update(receiver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = provider.configStore.Set(ctx, config, func(ctx context.Context) error {
|
err = provider.configStore.UpdateChannel(ctx, orgID, channel, func(ctx context.Context) error {
|
||||||
url := provider.config.Legacy.URL.JoinPath(routesPath)
|
url := provider.url.JoinPath(routesPath)
|
||||||
|
|
||||||
body, err := json.Marshal(receiver)
|
body, err := json.Marshal(receiver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -240,18 +288,10 @@ func (provider *provider) UpdateChannelByReceiver(ctx context.Context, orgID str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) CreateChannel(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
func (provider *provider) CreateChannel(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
||||||
config, err := provider.configStore.Get(ctx, orgID)
|
channel := alertmanagertypes.NewChannelFromReceiver(receiver, orgID)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = config.CreateReceiver(alertmanagertypes.NewRouteFromReceiver(receiver), receiver)
|
err := provider.configStore.CreateChannel(ctx, channel, func(ctx context.Context) error {
|
||||||
if err != nil {
|
url := provider.url.JoinPath(routesPath)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = provider.configStore.Set(ctx, config, func(ctx context.Context) error {
|
|
||||||
url := provider.config.Legacy.URL.JoinPath(routesPath)
|
|
||||||
|
|
||||||
body, err := json.Marshal(receiver)
|
body, err := json.Marshal(receiver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -286,24 +326,13 @@ func (provider *provider) CreateChannel(ctx context.Context, orgID string, recei
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, channelID int) error {
|
func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, channelID int) error {
|
||||||
config, err := provider.configStore.Get(ctx, orgID)
|
err := provider.configStore.DeleteChannelByID(ctx, orgID, channelID, func(ctx context.Context) error {
|
||||||
if err != nil {
|
channel, err := provider.configStore.GetChannelByID(ctx, orgID, channelID)
|
||||||
return err
|
if err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
channels := config.Channels()
|
url := provider.url.JoinPath(routesPath)
|
||||||
channel, err := alertmanagertypes.GetChannelByID(channels, channelID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = config.DeleteReceiver(channel.Name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = provider.configStore.Set(ctx, config, func(ctx context.Context) error {
|
|
||||||
url := provider.config.Legacy.URL.JoinPath(routesPath)
|
|
||||||
|
|
||||||
body, err := json.Marshal(map[string]string{"name": channel.Name})
|
body, err := json.Marshal(map[string]string{"name": channel.Name})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -337,6 +366,15 @@ func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, c
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (provider *provider) SetConfig(ctx context.Context, config *alertmanagertypes.Config) error {
|
||||||
|
return provider.configStore.Set(ctx, config)
|
||||||
|
}
|
||||||
|
|
||||||
func (provider *provider) Stop(ctx context.Context) error {
|
func (provider *provider) Stop(ctx context.Context) error {
|
||||||
|
provider.batcher.Stop(ctx)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (provider *provider) GetConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) {
|
||||||
|
return provider.configStore.Get(ctx, orgID)
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
// config is the config for the alertmanager service
|
// config is the config for the alertmanager service
|
||||||
config Config
|
config alertmanagerserver.Config
|
||||||
|
|
||||||
// stateStore is the state store for the alertmanager service
|
// stateStore is the state store for the alertmanager service
|
||||||
stateStore alertmanagertypes.StateStore
|
stateStore alertmanagertypes.StateStore
|
||||||
@@ -30,7 +30,7 @@ type Service struct {
|
|||||||
serversMtx sync.RWMutex
|
serversMtx sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, settings factory.ScopedProviderSettings, config Config, stateStore alertmanagertypes.StateStore, configStore alertmanagertypes.ConfigStore) *Service {
|
func New(ctx context.Context, settings factory.ScopedProviderSettings, config alertmanagerserver.Config, stateStore alertmanagertypes.StateStore, configStore alertmanagertypes.ConfigStore) *Service {
|
||||||
service := &Service{
|
service := &Service{
|
||||||
config: config,
|
config: config,
|
||||||
stateStore: stateStore,
|
stateStore: stateStore,
|
||||||
@@ -53,13 +53,23 @@ func (service *Service) SyncServers(ctx context.Context) error {
|
|||||||
for _, orgID := range orgIDs {
|
for _, orgID := range orgIDs {
|
||||||
config, err := service.getConfig(ctx, orgID)
|
config, err := service.getConfig(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
service.settings.Logger().Error("failed to get alertmanagerconfig for org", "orgID", orgID, "error", err)
|
service.settings.Logger().Error("failed to get alertmanager config for org", "orgID", orgID, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
service.servers[orgID], err = alertmanagerserver.New(ctx, service.settings.Logger(), service.settings.PrometheusRegisterer(), service.config.Config, orgID, service.stateStore)
|
// If the server is not present, create it and sync the config
|
||||||
if err != nil {
|
if _, ok := service.servers[orgID]; !ok {
|
||||||
service.settings.Logger().Error("failed to create alertmanagerserver", "orgID", orgID, "error", err)
|
server, err := service.newServer(ctx, orgID)
|
||||||
|
if err != nil {
|
||||||
|
service.settings.Logger().Error("failed to create alertmanager server", "orgID", orgID, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
service.servers[orgID] = server
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.servers[orgID].Hash() == config.StoreableConfig().Hash {
|
||||||
|
service.settings.Logger().Debug("skipping alertmanager sync for org", "orgID", orgID, "hash", config.StoreableConfig().Hash)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,13 +84,18 @@ func (service *Service) SyncServers(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *Service) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
|
func (service *Service) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.DeprecatedGettableAlerts, error) {
|
||||||
server, err := service.getServer(orgID)
|
server, err := service.getServer(orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return server.GetAlerts(ctx, params)
|
alerts, err := server.GetAlerts(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return alertmanagertypes.NewDeprecatedGettableAlertsFromGettableAlerts(alerts), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *Service) PutAlerts(ctx context.Context, orgID string, alerts alertmanagertypes.PostableAlerts) error {
|
func (service *Service) PutAlerts(ctx context.Context, orgID string, alerts alertmanagertypes.PostableAlerts) error {
|
||||||
@@ -101,6 +116,15 @@ func (service *Service) TestReceiver(ctx context.Context, orgID string, receiver
|
|||||||
return server.TestReceiver(ctx, receiver)
|
return server.TestReceiver(ctx, receiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *Service) TestAlert(ctx context.Context, orgID string, alert *alertmanagertypes.PostableAlert, receivers []string) error {
|
||||||
|
server, err := service.getServer(orgID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return server.TestAlert(ctx, alert, receivers)
|
||||||
|
}
|
||||||
|
|
||||||
func (service *Service) Stop(ctx context.Context) error {
|
func (service *Service) Stop(ctx context.Context) error {
|
||||||
for _, server := range service.servers {
|
for _, server := range service.servers {
|
||||||
server.Stop(ctx)
|
server.Stop(ctx)
|
||||||
@@ -109,6 +133,36 @@ func (service *Service) Stop(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *Service) newServer(ctx context.Context, orgID string) (*alertmanagerserver.Server, error) {
|
||||||
|
config, err := service.getConfig(ctx, orgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := alertmanagerserver.New(ctx, service.settings.Logger(), service.settings.PrometheusRegisterer(), service.config, orgID, service.stateStore)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeCompareAndSelectHash := config.StoreableConfig().Hash
|
||||||
|
config, err = service.compareAndSelectConfig(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if beforeCompareAndSelectHash == config.StoreableConfig().Hash {
|
||||||
|
service.settings.Logger().Debug("skipping config store update for org", "orgID", orgID, "hash", config.StoreableConfig().Hash)
|
||||||
|
return server, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.configStore.Set(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return server, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (service *Service) getConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) {
|
func (service *Service) getConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) {
|
||||||
config, err := service.configStore.Get(ctx, orgID)
|
config, err := service.configStore.Get(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,13 +175,55 @@ func (service *Service) getConfig(ctx context.Context, orgID string) (*alertmana
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, err
|
config.SetGlobalConfig(service.config.Global)
|
||||||
|
if config.AlertmanagerConfig().Route == nil {
|
||||||
|
config.SetRouteConfig(service.config.Route)
|
||||||
|
} else {
|
||||||
|
config.UpdateRouteConfig(service.config.Route)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compareAndSelectConfig compares the existing config with the config derived from channels.
|
||||||
|
// If the hash of the config and the channels mismatch, the config derived from channels is returned.
|
||||||
|
func (service *Service) compareAndSelectConfig(ctx context.Context, incomingConfig *alertmanagertypes.Config) (*alertmanagertypes.Config, error) {
|
||||||
|
channels, err := service.configStore.ListChannels(ctx, incomingConfig.StoreableConfig().OrgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
matchers, err := service.configStore.GetMatchers(ctx, incomingConfig.StoreableConfig().OrgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := alertmanagertypes.NewConfigFromChannels(service.config.Global, service.config.Route, channels, incomingConfig.StoreableConfig().OrgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for ruleID, receivers := range matchers {
|
||||||
|
err = config.CreateRuleIDMatcher(ruleID, receivers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if incomingConfig.StoreableConfig().Hash != config.StoreableConfig().Hash {
|
||||||
|
service.settings.Logger().InfoContext(ctx, "mismatch found, updating config to match channels and matchers")
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return incomingConfig, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (service *Service) getServer(orgID string) (*alertmanagerserver.Server, error) {
|
func (service *Service) getServer(orgID string) (*alertmanagerserver.Server, error) {
|
||||||
|
service.serversMtx.RLock()
|
||||||
|
defer service.serversMtx.RUnlock()
|
||||||
|
|
||||||
server, ok := service.servers[orgID]
|
server, ok := service.servers[orgID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerNotFound, "alertmanager not found for org %s", orgID)
|
return nil, errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerNotFound, "alertmanager not found for org %s", orgID)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"go.signoz.io/signoz/pkg/alertmanager"
|
"go.signoz.io/signoz/pkg/alertmanager"
|
||||||
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
|
"go.signoz.io/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
|
||||||
|
"go.signoz.io/signoz/pkg/errors"
|
||||||
"go.signoz.io/signoz/pkg/factory"
|
"go.signoz.io/signoz/pkg/factory"
|
||||||
"go.signoz.io/signoz/pkg/sqlstore"
|
"go.signoz.io/signoz/pkg/sqlstore"
|
||||||
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
"go.signoz.io/signoz/pkg/types/alertmanagertypes"
|
||||||
@@ -17,6 +18,7 @@ type provider struct {
|
|||||||
settings factory.ScopedProviderSettings
|
settings factory.ScopedProviderSettings
|
||||||
configStore alertmanagertypes.ConfigStore
|
configStore alertmanagertypes.ConfigStore
|
||||||
stateStore alertmanagertypes.StateStore
|
stateStore alertmanagertypes.StateStore
|
||||||
|
stopC chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
|
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
|
||||||
@@ -26,15 +28,15 @@ func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanager
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore) (*provider, error) {
|
func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore) (*provider, error) {
|
||||||
settings := factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/signoz/pkg/alertmanager/internalalertmanager")
|
settings := factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/signoz/pkg/alertmanager/signozalertmanager")
|
||||||
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
|
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
|
||||||
stateStore := sqlalertmanagerstore.NewStateStore(sqlstore)
|
stateStore := sqlalertmanagerstore.NewStateStore(sqlstore)
|
||||||
|
|
||||||
return &provider{
|
p := &provider{
|
||||||
service: alertmanager.New(
|
service: alertmanager.New(
|
||||||
ctx,
|
ctx,
|
||||||
settings,
|
settings,
|
||||||
config,
|
config.Signoz.Config,
|
||||||
stateStore,
|
stateStore,
|
||||||
configStore,
|
configStore,
|
||||||
),
|
),
|
||||||
@@ -42,15 +44,23 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
|||||||
config: config,
|
config: config,
|
||||||
configStore: configStore,
|
configStore: configStore,
|
||||||
stateStore: stateStore,
|
stateStore: stateStore,
|
||||||
}, nil
|
stopC: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) Start(ctx context.Context) error {
|
func (provider *provider) Start(ctx context.Context) error {
|
||||||
|
if err := provider.service.SyncServers(ctx); err != nil {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "failed to sync alertmanager servers", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(provider.config.Signoz.PollInterval)
|
ticker := time.NewTicker(provider.config.Signoz.PollInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-provider.stopC:
|
||||||
return nil
|
return nil
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
if err := provider.service.SyncServers(ctx); err != nil {
|
if err := provider.service.SyncServers(ctx); err != nil {
|
||||||
@@ -61,10 +71,11 @@ func (provider *provider) Start(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) Stop(ctx context.Context) error {
|
func (provider *provider) Stop(ctx context.Context) error {
|
||||||
|
close(provider.stopC)
|
||||||
return provider.service.Stop(ctx)
|
return provider.service.Stop(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
|
func (provider *provider) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.DeprecatedGettableAlerts, error) {
|
||||||
return provider.service.GetAlerts(ctx, orgID, params)
|
return provider.service.GetAlerts(ctx, orgID, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,78 +87,68 @@ func (provider *provider) TestReceiver(ctx context.Context, orgID string, receiv
|
|||||||
return provider.service.TestReceiver(ctx, orgID, receiver)
|
return provider.service.TestReceiver(ctx, orgID, receiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (provider *provider) TestAlert(ctx context.Context, orgID string, alert *alertmanagertypes.PostableAlert, receivers []string) error {
|
||||||
|
return provider.service.TestAlert(ctx, orgID, alert, receivers)
|
||||||
|
}
|
||||||
|
|
||||||
func (provider *provider) ListChannels(ctx context.Context, orgID string) ([]*alertmanagertypes.Channel, error) {
|
func (provider *provider) ListChannels(ctx context.Context, orgID string) ([]*alertmanagertypes.Channel, error) {
|
||||||
config, err := provider.configStore.Get(ctx, orgID)
|
return provider.configStore.ListChannels(ctx, orgID)
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
channels := config.Channels()
|
func (provider *provider) ListAllChannels(ctx context.Context) ([]*alertmanagertypes.Channel, error) {
|
||||||
channelList := make([]*alertmanagertypes.Channel, 0, len(channels))
|
return nil, errors.Newf(errors.TypeUnsupported, errors.CodeUnsupported, "not supported by provider signoz")
|
||||||
for _, channel := range channels {
|
|
||||||
channelList = append(channelList, channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
return channelList, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) GetChannelByID(ctx context.Context, orgID string, channelID int) (*alertmanagertypes.Channel, error) {
|
func (provider *provider) GetChannelByID(ctx context.Context, orgID string, channelID int) (*alertmanagertypes.Channel, error) {
|
||||||
config, err := provider.configStore.Get(ctx, orgID)
|
return provider.configStore.GetChannelByID(ctx, orgID, channelID)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
channels := config.Channels()
|
|
||||||
channel, err := alertmanagertypes.GetChannelByID(channels, channelID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return channel, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) UpdateChannelByReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
func (provider *provider) UpdateChannelByReceiverAndID(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver, id int) error {
|
||||||
|
channel, err := provider.configStore.GetChannelByID(ctx, orgID, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
config, err := provider.configStore.Get(ctx, orgID)
|
config, err := provider.configStore.Get(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = config.UpdateReceiver(alertmanagertypes.NewRouteFromReceiver(receiver), receiver)
|
if err := config.UpdateReceiver(receiver); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = provider.configStore.Set(ctx, config, nil)
|
if err := provider.configStore.Set(ctx, config); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if err := channel.Update(receiver); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.configStore.UpdateChannel(ctx, orgID, channel, alertmanagertypes.ConfigStoreNoopCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, channelID int) error {
|
func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, channelID int) error {
|
||||||
|
channel, err := provider.configStore.GetChannelByID(ctx, orgID, channelID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
config, err := provider.configStore.Get(ctx, orgID)
|
config, err := provider.configStore.Get(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
channels := config.Channels()
|
if err := config.DeleteReceiver(channel.Name); err != nil {
|
||||||
channel, err := alertmanagertypes.GetChannelByID(channels, channelID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = config.DeleteReceiver(channel.Name)
|
if err := provider.configStore.Set(ctx, config); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = provider.configStore.Set(ctx, config, nil)
|
return provider.configStore.DeleteChannelByID(ctx, orgID, channelID, alertmanagertypes.ConfigStoreNoopCallback)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) CreateChannel(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
func (provider *provider) CreateChannel(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
||||||
@@ -156,15 +157,22 @@ func (provider *provider) CreateChannel(ctx context.Context, orgID string, recei
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = config.CreateReceiver(alertmanagertypes.NewRouteFromReceiver(receiver), receiver)
|
if err := config.CreateReceiver(receiver); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = provider.configStore.Set(ctx, config, nil)
|
if err := provider.configStore.Set(ctx, config); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
channel := alertmanagertypes.NewChannelFromReceiver(receiver, orgID)
|
||||||
|
return provider.configStore.CreateChannel(ctx, channel, alertmanagertypes.ConfigStoreNoopCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) SetConfig(ctx context.Context, config *alertmanagertypes.Config) error {
|
||||||
|
return provider.configStore.Set(ctx, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) GetConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) {
|
||||||
|
return provider.configStore.Get(ctx, orgID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,15 @@ type migrator struct {
|
|||||||
|
|
||||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, sqlstore sqlstore.SQLStore, migrations *migrate.Migrations, config Config) SQLMigrator {
|
func New(ctx context.Context, providerSettings factory.ProviderSettings, sqlstore sqlstore.SQLStore, migrations *migrate.Migrations, config Config) SQLMigrator {
|
||||||
return &migrator{
|
return &migrator{
|
||||||
migrator: migrate.NewMigrator(sqlstore.BunDB(), migrations, migrate.WithTableName(migrationTableName), migrate.WithLocksTableName(migrationLockTableName)),
|
migrator: migrate.NewMigrator(
|
||||||
|
sqlstore.BunDB(),
|
||||||
|
migrations,
|
||||||
|
migrate.WithTableName(migrationTableName),
|
||||||
|
migrate.WithLocksTableName(migrationLockTableName),
|
||||||
|
// This is to ensure that the migration is marked as applied only on success. If the migration fails, no entry is made in the migration table
|
||||||
|
// and the migration will be retried.
|
||||||
|
migrate.WithMarkAppliedOnSuccess(true),
|
||||||
|
),
|
||||||
settings: factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/signoz/pkg/sqlmigrator"),
|
settings: factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/signoz/pkg/sqlmigrator"),
|
||||||
config: config,
|
config: config,
|
||||||
dialect: sqlstore.BunDB().Dialect().Name().String(),
|
dialect: sqlstore.BunDB().Dialect().Name().String(),
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// An alias for the Alert type from the alertmanager package.
|
||||||
|
AlertModel = models.Alert
|
||||||
|
|
||||||
// An alias for the Alert type from the alertmanager package.
|
// An alias for the Alert type from the alertmanager package.
|
||||||
Alert = types.Alert
|
Alert = types.Alert
|
||||||
|
|
||||||
@@ -38,12 +41,51 @@ type (
|
|||||||
GettableAlerts = models.GettableAlerts
|
GettableAlerts = models.GettableAlerts
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type DeprecatedGettableAlert struct {
|
||||||
|
*model.Alert
|
||||||
|
Status types.AlertStatus `json:"status"`
|
||||||
|
Receivers []string `json:"receivers"`
|
||||||
|
Fingerprint string `json:"fingerprint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeprecatedGettableAlerts = []*DeprecatedGettableAlert
|
||||||
|
|
||||||
// An alias for the GettableAlertsParams type from the alertmanager package.
|
// An alias for the GettableAlertsParams type from the alertmanager package.
|
||||||
type GettableAlertsParams struct {
|
type GettableAlertsParams struct {
|
||||||
alert.GetAlertsParams
|
alert.GetAlertsParams
|
||||||
RawQuery string
|
RawQuery string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewDeprecatedGettableAlertsFromGettableAlerts(gettableAlerts GettableAlerts) DeprecatedGettableAlerts {
|
||||||
|
deprecatedGettableAlerts := make(DeprecatedGettableAlerts, 0, len(gettableAlerts))
|
||||||
|
|
||||||
|
for _, gettableAlert := range gettableAlerts {
|
||||||
|
receivers := make([]string, 0, len(gettableAlert.Receivers))
|
||||||
|
for _, receiver := range gettableAlert.Receivers {
|
||||||
|
receivers = append(receivers, *receiver.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
deprecatedGettableAlerts = append(deprecatedGettableAlerts, &DeprecatedGettableAlert{
|
||||||
|
Alert: &model.Alert{
|
||||||
|
Labels: v2.APILabelSetToModelLabelSet(gettableAlert.Labels),
|
||||||
|
Annotations: v2.APILabelSetToModelLabelSet(gettableAlert.Annotations),
|
||||||
|
StartsAt: time.Time(*gettableAlert.StartsAt),
|
||||||
|
EndsAt: time.Time(*gettableAlert.EndsAt),
|
||||||
|
GeneratorURL: string(gettableAlert.GeneratorURL),
|
||||||
|
},
|
||||||
|
Status: types.AlertStatus{
|
||||||
|
State: types.AlertState(*gettableAlert.Status.State),
|
||||||
|
SilencedBy: gettableAlert.Status.SilencedBy,
|
||||||
|
InhibitedBy: gettableAlert.Status.InhibitedBy,
|
||||||
|
},
|
||||||
|
Fingerprint: *gettableAlert.Fingerprint,
|
||||||
|
Receivers: receivers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return deprecatedGettableAlerts
|
||||||
|
}
|
||||||
|
|
||||||
// Converts a slice of Alert to a slice of PostableAlert.
|
// Converts a slice of Alert to a slice of PostableAlert.
|
||||||
func NewPostableAlertsFromAlerts(alerts []*types.Alert) PostableAlerts {
|
func NewPostableAlertsFromAlerts(alerts []*types.Alert) PostableAlerts {
|
||||||
postableAlerts := make(PostableAlerts, 0, len(alerts))
|
postableAlerts := make(PostableAlerts, 0, len(alerts))
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrCodeAlertmanagerChannelNotFound = errors.MustNewCode("alertmanager_channel_not_found")
|
ErrCodeAlertmanagerChannelNotFound = errors.MustNewCode("alertmanager_channel_not_found")
|
||||||
|
ErrCodeAlertmanagerChannelNameMismatch = errors.MustNewCode("alertmanager_channel_name_mismatch")
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -20,7 +21,7 @@ var (
|
|||||||
receiverTypeRegex = regexp.MustCompile(`^(.+)_configs`)
|
receiverTypeRegex = regexp.MustCompile(`^(.+)_configs`)
|
||||||
)
|
)
|
||||||
|
|
||||||
type Channels = map[string]*Channel
|
type Channels = []*Channel
|
||||||
|
|
||||||
type GettableChannels = []*Channel
|
type GettableChannels = []*Channel
|
||||||
|
|
||||||
@@ -104,20 +105,6 @@ func NewReceiverFromChannel(channel *Channel) (Receiver, error) {
|
|||||||
return receiver, nil
|
return receiver, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChannelsFromConfig(c *config.Config, orgID string) Channels {
|
|
||||||
channels := Channels{}
|
|
||||||
for _, receiver := range c.Receivers {
|
|
||||||
channel := NewChannelFromReceiver(receiver, orgID)
|
|
||||||
if channel == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
channels[channel.Name] = channel
|
|
||||||
}
|
|
||||||
|
|
||||||
return channels
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConfigFromChannels(globalConfig GlobalConfig, routeConfig RouteConfig, channels Channels, orgID string) (*Config, error) {
|
func NewConfigFromChannels(globalConfig GlobalConfig, routeConfig RouteConfig, channels Channels, orgID string) (*Config, error) {
|
||||||
cfg, err := NewDefaultConfig(
|
cfg, err := NewDefaultConfig(
|
||||||
globalConfig,
|
globalConfig,
|
||||||
@@ -134,7 +121,7 @@ func NewConfigFromChannels(globalConfig GlobalConfig, routeConfig RouteConfig, c
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cfg.CreateReceiver(&config.Route{Receiver: channel.Name, Continue: true}, receiver)
|
err = cfg.CreateReceiver(receiver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -143,12 +130,38 @@ func NewConfigFromChannels(globalConfig GlobalConfig, routeConfig RouteConfig, c
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetChannelByID(channels Channels, id int) (*Channel, error) {
|
func GetChannelByID(channels Channels, id int) (int, *Channel, error) {
|
||||||
for _, channel := range channels {
|
for i, channel := range channels {
|
||||||
if channel.ID == id {
|
if channel.ID == id {
|
||||||
return channel, nil
|
return i, channel, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerChannelNotFound, "cannot find channel with id %d", id)
|
return 0, nil, errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerChannelNotFound, "cannot find channel with id %d", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetChannelByName(channels Channels, name string) (int, *Channel, error) {
|
||||||
|
for i, channel := range channels {
|
||||||
|
if channel.Name == name {
|
||||||
|
return i, channel, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, nil, errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerChannelNotFound, "cannot find channel with name %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Channel) Update(receiver Receiver) error {
|
||||||
|
channel := NewChannelFromReceiver(receiver, c.OrgID)
|
||||||
|
if channel == nil {
|
||||||
|
return errors.Newf(errors.TypeInvalidInput, ErrCodeAlertmanagerChannelNotFound, "cannot find channel with id %d", c.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Name != channel.Name {
|
||||||
|
return errors.Newf(errors.TypeInvalidInput, ErrCodeAlertmanagerChannelNameMismatch, "cannot update channel name")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data = channel.Data
|
||||||
|
c.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,91 +10,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewChannelsFromAlertmanagerConfig(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
orgID string
|
|
||||||
alertmanagerConfig *config.Config
|
|
||||||
expectedChannels Channels
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "DefaultReceiver",
|
|
||||||
orgID: "1",
|
|
||||||
alertmanagerConfig: &config.Config{
|
|
||||||
Receivers: []config.Receiver{
|
|
||||||
{
|
|
||||||
Name: DefaultReceiverName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedChannels: Channels{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "OneEmailReceiver",
|
|
||||||
orgID: "1",
|
|
||||||
alertmanagerConfig: &config.Config{
|
|
||||||
Receivers: []config.Receiver{
|
|
||||||
{
|
|
||||||
Name: "email-receiver",
|
|
||||||
EmailConfigs: []*config.EmailConfig{
|
|
||||||
{
|
|
||||||
To: "test@example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedChannels: Channels{
|
|
||||||
"email-receiver": {
|
|
||||||
Name: "email-receiver",
|
|
||||||
Type: "email",
|
|
||||||
Data: `{"name":"email-receiver","email_configs":[{"send_resolved":false,"to":"test@example.com","smarthost":""}]}`,
|
|
||||||
OrgID: "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "OneSlackReceiver",
|
|
||||||
orgID: "1",
|
|
||||||
alertmanagerConfig: &config.Config{
|
|
||||||
Receivers: []config.Receiver{
|
|
||||||
{
|
|
||||||
Name: "slack-receiver",
|
|
||||||
SlackConfigs: []*config.SlackConfig{
|
|
||||||
{
|
|
||||||
Channel: "#alerts",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedChannels: Channels{
|
|
||||||
"slack-receiver": {
|
|
||||||
Name: "slack-receiver",
|
|
||||||
Type: "slack",
|
|
||||||
Data: `{"name":"slack-receiver","slack_configs":[{"send_resolved":false,"channel":"#alerts"}]}`,
|
|
||||||
OrgID: "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
actualChannels := NewChannelsFromConfig(tc.alertmanagerConfig, tc.orgID)
|
|
||||||
assert.Equal(t, len(tc.expectedChannels), len(actualChannels))
|
|
||||||
for _, channel := range actualChannels {
|
|
||||||
expectedChannel, ok := tc.expectedChannels[channel.Name]
|
|
||||||
assert.True(t, ok)
|
|
||||||
assert.Equal(t, expectedChannel.Name, channel.Name)
|
|
||||||
assert.Equal(t, expectedChannel.Type, channel.Type)
|
|
||||||
assert.Equal(t, expectedChannel.Data, channel.Data)
|
|
||||||
assert.Equal(t, expectedChannel.OrgID, channel.OrgID)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewConfigFromChannels(t *testing.T) {
|
func TestNewConfigFromChannels(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -105,7 +20,7 @@ func TestNewConfigFromChannels(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "OneEmailChannel",
|
name: "OneEmailChannel",
|
||||||
channels: Channels{
|
channels: Channels{
|
||||||
"email-receiver": {
|
{
|
||||||
Name: "email-receiver",
|
Name: "email-receiver",
|
||||||
Type: "email",
|
Type: "email",
|
||||||
Data: `{"name":"email-receiver","email_configs":[{"to":"test@example.com"}]}`,
|
Data: `{"name":"email-receiver","email_configs":[{"to":"test@example.com"}]}`,
|
||||||
@@ -117,7 +32,7 @@ func TestNewConfigFromChannels(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "OneSlackChannel",
|
name: "OneSlackChannel",
|
||||||
channels: Channels{
|
channels: Channels{
|
||||||
"slack-receiver": {
|
{
|
||||||
Name: "slack-receiver",
|
Name: "slack-receiver",
|
||||||
Type: "slack",
|
Type: "slack",
|
||||||
Data: `{"name":"slack-receiver","slack_configs":[{"channel":"#alerts","api_url":"https://slack.com/api/test","send_resolved":true}]}`,
|
Data: `{"name":"slack-receiver","slack_configs":[{"channel":"#alerts","api_url":"https://slack.com/api/test","send_resolved":true}]}`,
|
||||||
@@ -129,7 +44,7 @@ func TestNewConfigFromChannels(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "OnePagerdutyChannel",
|
name: "OnePagerdutyChannel",
|
||||||
channels: Channels{
|
channels: Channels{
|
||||||
"pagerduty-receiver": {
|
{
|
||||||
Name: "pagerduty-receiver",
|
Name: "pagerduty-receiver",
|
||||||
Type: "pagerduty",
|
Type: "pagerduty",
|
||||||
Data: `{"name":"pagerduty-receiver","pagerduty_configs":[{"service_key":"test"}]}`,
|
Data: `{"name":"pagerduty-receiver","pagerduty_configs":[{"service_key":"test"}]}`,
|
||||||
@@ -141,12 +56,12 @@ func TestNewConfigFromChannels(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "OnePagerdutyAndOneSlackChannel",
|
name: "OnePagerdutyAndOneSlackChannel",
|
||||||
channels: Channels{
|
channels: Channels{
|
||||||
"pagerduty-receiver": {
|
{
|
||||||
Name: "pagerduty-receiver",
|
Name: "pagerduty-receiver",
|
||||||
Type: "pagerduty",
|
Type: "pagerduty",
|
||||||
Data: `{"name":"pagerduty-receiver","pagerduty_configs":[{"service_key":"test"}]}`,
|
Data: `{"name":"pagerduty-receiver","pagerduty_configs":[{"service_key":"test"}]}`,
|
||||||
},
|
},
|
||||||
"slack-receiver": {
|
{
|
||||||
Name: "slack-receiver",
|
Name: "slack-receiver",
|
||||||
Type: "slack",
|
Type: "slack",
|
||||||
Data: `{"name":"slack-receiver","slack_configs":[{"channel":"#alerts","api_url":"https://slack.com/api/test","send_resolved":true}]}`,
|
Data: `{"name":"slack-receiver","slack_configs":[{"channel":"#alerts","api_url":"https://slack.com/api/test","send_resolved":true}]}`,
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import (
|
|||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dario.cat/mergo"
|
"dario.cat/mergo"
|
||||||
"github.com/prometheus/alertmanager/config"
|
"github.com/prometheus/alertmanager/config"
|
||||||
|
"github.com/prometheus/alertmanager/pkg/labels"
|
||||||
|
commoncfg "github.com/prometheus/common/config"
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
"go.signoz.io/signoz/pkg/errors"
|
"go.signoz.io/signoz/pkg/errors"
|
||||||
@@ -41,6 +44,7 @@ type StoreableConfig struct {
|
|||||||
|
|
||||||
ID uint64 `bun:"id,pk,autoincrement"`
|
ID uint64 `bun:"id,pk,autoincrement"`
|
||||||
Config string `bun:"config"`
|
Config string `bun:"config"`
|
||||||
|
Hash string `bun:"hash"`
|
||||||
CreatedAt time.Time `bun:"created_at"`
|
CreatedAt time.Time `bun:"created_at"`
|
||||||
UpdatedAt time.Time `bun:"updated_at"`
|
UpdatedAt time.Time `bun:"updated_at"`
|
||||||
OrgID string `bun:"org_id"`
|
OrgID string `bun:"org_id"`
|
||||||
@@ -53,25 +57,19 @@ type Config struct {
|
|||||||
|
|
||||||
// storeableConfig is the representation of the config in the store
|
// storeableConfig is the representation of the config in the store
|
||||||
storeableConfig *StoreableConfig
|
storeableConfig *StoreableConfig
|
||||||
|
|
||||||
// channels is the list of channels
|
|
||||||
channels Channels
|
|
||||||
|
|
||||||
// orgID is the organization ID
|
|
||||||
orgID string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig(c *config.Config, orgID string) *Config {
|
func NewConfig(c *config.Config, orgID string) *Config {
|
||||||
channels := NewChannelsFromConfig(c, orgID)
|
raw := string(newRawFromConfig(c))
|
||||||
return &Config{
|
return &Config{
|
||||||
alertmanagerConfig: c,
|
alertmanagerConfig: c,
|
||||||
storeableConfig: &StoreableConfig{
|
storeableConfig: &StoreableConfig{
|
||||||
Config: string(newRawFromConfig(c)),
|
Config: raw,
|
||||||
|
Hash: fmt.Sprintf("%x", newConfigHash(raw)),
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
},
|
},
|
||||||
channels: channels,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,20 +79,12 @@ func NewConfigFromStoreableConfig(sc *StoreableConfig) (*Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
channels := NewChannelsFromConfig(alertmanagerConfig, sc.OrgID)
|
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
alertmanagerConfig: alertmanagerConfig,
|
alertmanagerConfig: alertmanagerConfig,
|
||||||
storeableConfig: sc,
|
storeableConfig: sc,
|
||||||
channels: channels,
|
|
||||||
orgID: sc.OrgID,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouteFromReceiver(receiver Receiver) *config.Route {
|
|
||||||
return &config.Route{Receiver: receiver.Name, Continue: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDefaultConfig(globalConfig GlobalConfig, routeConfig RouteConfig, orgID string) (*Config, error) {
|
func NewDefaultConfig(globalConfig GlobalConfig, routeConfig RouteConfig, orgID string) (*Config, error) {
|
||||||
err := mergo.Merge(&globalConfig, config.DefaultGlobalConfig())
|
err := mergo.Merge(&globalConfig, config.DefaultGlobalConfig())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -134,6 +124,43 @@ func newRawFromConfig(c *config.Config) []byte {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newConfigHash(s string) [16]byte {
|
||||||
|
return md5.Sum([]byte(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) SetGlobalConfig(globalConfig GlobalConfig) {
|
||||||
|
c.alertmanagerConfig.Global = &globalConfig
|
||||||
|
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||||
|
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||||
|
c.storeableConfig.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) SetRouteConfig(routeConfig RouteConfig) {
|
||||||
|
c.alertmanagerConfig.Route = &config.Route{
|
||||||
|
Receiver: DefaultReceiverName,
|
||||||
|
GroupByStr: routeConfig.GroupByStr,
|
||||||
|
GroupInterval: (*model.Duration)(&routeConfig.GroupInterval),
|
||||||
|
GroupWait: (*model.Duration)(&routeConfig.GroupWait),
|
||||||
|
RepeatInterval: (*model.Duration)(&routeConfig.RepeatInterval),
|
||||||
|
}
|
||||||
|
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||||
|
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||||
|
c.storeableConfig.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) UpdateRouteConfig(routeConfig RouteConfig) {
|
||||||
|
for _, route := range c.alertmanagerConfig.Route.Routes {
|
||||||
|
route.GroupByStr = routeConfig.GroupByStr
|
||||||
|
route.GroupInterval = (*model.Duration)(&routeConfig.GroupInterval)
|
||||||
|
route.GroupWait = (*model.Duration)(&routeConfig.GroupWait)
|
||||||
|
route.RepeatInterval = (*model.Duration)(&routeConfig.RepeatInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||||
|
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||||
|
c.storeableConfig.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Config) AlertmanagerConfig() *config.Config {
|
func (c *Config) AlertmanagerConfig() *config.Config {
|
||||||
return c.alertmanagerConfig
|
return c.alertmanagerConfig
|
||||||
}
|
}
|
||||||
@@ -142,65 +169,43 @@ func (c *Config) StoreableConfig() *StoreableConfig {
|
|||||||
return c.storeableConfig
|
return c.storeableConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Channels() Channels {
|
func (c *Config) CreateReceiver(receiver config.Receiver) error {
|
||||||
return c.channels
|
if receiver.Name == "" {
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) OrgID() string {
|
|
||||||
return c.orgID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) Raw() []byte {
|
|
||||||
return newRawFromConfig(c.alertmanagerConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) Hash() [16]byte {
|
|
||||||
return md5.Sum(newRawFromConfig(c.alertmanagerConfig))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) CreateReceiver(route *config.Route, receiver config.Receiver) error {
|
|
||||||
if route == nil {
|
|
||||||
return errors.New(errors.TypeInvalidInput, ErrCodeAlertmanagerConfigInvalid, "route is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if route.Receiver == "" || receiver.Name == "" {
|
|
||||||
return errors.New(errors.TypeInvalidInput, ErrCodeAlertmanagerConfigInvalid, "receiver is mandatory in route and receiver")
|
return errors.New(errors.TypeInvalidInput, ErrCodeAlertmanagerConfigInvalid, "receiver is mandatory in route and receiver")
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that receiver name is not already used
|
// check that receiver name is not already used
|
||||||
for _, existingReceiver := range c.alertmanagerConfig.Receivers {
|
for _, existingReceiver := range c.alertmanagerConfig.Receivers {
|
||||||
if existingReceiver.Name == receiver.Name || existingReceiver.Name == route.Receiver {
|
if existingReceiver.Name == receiver.Name {
|
||||||
return errors.New(errors.TypeInvalidInput, ErrCodeAlertmanagerConfigConflict, "the receiver name has to be unique, please choose a different name")
|
return errors.New(errors.TypeInvalidInput, ErrCodeAlertmanagerConfigConflict, "the receiver name has to be unique, please choose a different name")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// must set continue on route to allow subsequent
|
|
||||||
// routes to work. may have to rethink on this after
|
|
||||||
// adding matchers and filters in upstream
|
|
||||||
route.Continue = true
|
|
||||||
|
|
||||||
c.alertmanagerConfig.Route.Routes = append(c.alertmanagerConfig.Route.Routes, route)
|
c.alertmanagerConfig.Route.Routes = append(c.alertmanagerConfig.Route.Routes, newRouteFromReceiver(receiver))
|
||||||
c.alertmanagerConfig.Receivers = append(c.alertmanagerConfig.Receivers, receiver)
|
c.alertmanagerConfig.Receivers = append(c.alertmanagerConfig.Receivers, receiver)
|
||||||
|
|
||||||
if err := c.alertmanagerConfig.UnmarshalYAML(func(i interface{}) error { return nil }); err != nil {
|
if err := c.alertmanagerConfig.UnmarshalYAML(func(i interface{}) error { return nil }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
channel := NewChannelFromReceiver(receiver, c.orgID)
|
|
||||||
if channel != nil {
|
|
||||||
c.channels[channel.Name] = channel
|
|
||||||
}
|
|
||||||
|
|
||||||
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||||
|
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||||
c.storeableConfig.UpdatedAt = time.Now()
|
c.storeableConfig.UpdatedAt = time.Now()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) UpdateReceiver(route *config.Route, receiver config.Receiver) error {
|
func (c *Config) GetReceiver(name string) (Receiver, error) {
|
||||||
if route == nil {
|
for _, receiver := range c.alertmanagerConfig.Receivers {
|
||||||
return errors.New(errors.TypeInvalidInput, ErrCodeAlertmanagerConfigInvalid, "route is nil")
|
if receiver.Name == name {
|
||||||
|
return receiver, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if route.Receiver == "" || receiver.Name == "" {
|
return Receiver{}, errors.Newf(errors.TypeInvalidInput, ErrCodeAlertmanagerChannelNotFound, "channel with name %q not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) UpdateReceiver(receiver config.Receiver) error {
|
||||||
|
if receiver.Name == "" {
|
||||||
return errors.New(errors.TypeInvalidInput, ErrCodeAlertmanagerConfigInvalid, "receiver is mandatory in route and receiver")
|
return errors.New(errors.TypeInvalidInput, ErrCodeAlertmanagerConfigInvalid, "receiver is mandatory in route and receiver")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,24 +213,12 @@ func (c *Config) UpdateReceiver(route *config.Route, receiver config.Receiver) e
|
|||||||
for i, existingReceiver := range c.alertmanagerConfig.Receivers {
|
for i, existingReceiver := range c.alertmanagerConfig.Receivers {
|
||||||
if existingReceiver.Name == receiver.Name {
|
if existingReceiver.Name == receiver.Name {
|
||||||
c.alertmanagerConfig.Receivers[i] = receiver
|
c.alertmanagerConfig.Receivers[i] = receiver
|
||||||
channel := NewChannelFromReceiver(receiver, c.orgID)
|
|
||||||
if channel != nil {
|
|
||||||
c.channels[channel.Name] = channel
|
|
||||||
c.channels[channel.Name].UpdatedAt = time.Now()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
routes := c.alertmanagerConfig.Route.Routes
|
|
||||||
for i, existingRoute := range routes {
|
|
||||||
if existingRoute.Receiver == route.Receiver {
|
|
||||||
c.alertmanagerConfig.Route.Routes[i] = route
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||||
|
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||||
c.storeableConfig.UpdatedAt = time.Now()
|
c.storeableConfig.UpdatedAt = time.Now()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -247,31 +240,121 @@ func (c *Config) DeleteReceiver(name string) error {
|
|||||||
for i, existingReceiver := range c.alertmanagerConfig.Receivers {
|
for i, existingReceiver := range c.alertmanagerConfig.Receivers {
|
||||||
if existingReceiver.Name == name {
|
if existingReceiver.Name == name {
|
||||||
c.alertmanagerConfig.Receivers = append(c.alertmanagerConfig.Receivers[:i], c.alertmanagerConfig.Receivers[i+1:]...)
|
c.alertmanagerConfig.Receivers = append(c.alertmanagerConfig.Receivers[:i], c.alertmanagerConfig.Receivers[i+1:]...)
|
||||||
delete(c.channels, name)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||||
|
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||||
c.storeableConfig.UpdatedAt = time.Now()
|
c.storeableConfig.UpdatedAt = time.Now()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) CreateRuleIDMatcher(ruleID string, receiverNames []string) error {
|
||||||
|
if c.alertmanagerConfig.Route == nil {
|
||||||
|
return errors.New(errors.TypeInvalidInput, ErrCodeAlertmanagerConfigInvalid, "route is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
routes := c.alertmanagerConfig.Route.Routes
|
||||||
|
for i, route := range routes {
|
||||||
|
if slices.Contains(receiverNames, route.Receiver) {
|
||||||
|
matcher, err := labels.NewMatcher(labels.MatchEqual, "ruleId", ruleID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.alertmanagerConfig.Route.Routes[i].Matchers = append(c.alertmanagerConfig.Route.Routes[i].Matchers, matcher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||||
|
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||||
|
c.storeableConfig.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) UpdateRuleIDMatcher(ruleID string, receiverNames []string) error {
|
||||||
|
err := c.DeleteRuleIDMatcher(ruleID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.CreateRuleIDMatcher(ruleID, receiverNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) DeleteRuleIDMatcher(ruleID string) error {
|
||||||
|
routes := c.alertmanagerConfig.Route.Routes
|
||||||
|
for i, r := range routes {
|
||||||
|
j := slices.IndexFunc(r.Matchers, func(m *labels.Matcher) bool {
|
||||||
|
return m.Name == "ruleId" && m.Value == ruleID
|
||||||
|
})
|
||||||
|
if j != -1 {
|
||||||
|
c.alertmanagerConfig.Route.Routes[i].Matchers = slices.Delete(r.Matchers, j, j+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||||
|
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||||
|
c.storeableConfig.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ReceiverNamesFromRuleID(ruleID string) ([]string, error) {
|
||||||
|
receiverNames := make([]string, 0)
|
||||||
|
routes := c.alertmanagerConfig.Route.Routes
|
||||||
|
for _, r := range routes {
|
||||||
|
for _, m := range r.Matchers {
|
||||||
|
if m.Name == "ruleId" && m.Value == ruleID {
|
||||||
|
receiverNames = append(receiverNames, r.Receiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return receiverNames, nil
|
||||||
|
}
|
||||||
|
|
||||||
type ConfigStore interface {
|
type ConfigStore interface {
|
||||||
// Set creates or updates a config.
|
// Set creates or updates a config.
|
||||||
Set(context.Context, *Config, func(context.Context) error) error
|
Set(context.Context, *Config) error
|
||||||
|
|
||||||
// Get returns the config for the given orgID
|
// Get returns the config for the given orgID
|
||||||
Get(context.Context, string) (*Config, error)
|
Get(context.Context, string) (*Config, error)
|
||||||
|
|
||||||
// ListOrgs returns the list of orgs
|
// ListOrgs returns the list of orgs
|
||||||
ListOrgs(context.Context) ([]string, error)
|
ListOrgs(context.Context) ([]string, error)
|
||||||
|
|
||||||
|
// CreateChannel creates a new channel.
|
||||||
|
CreateChannel(context.Context, *Channel, func(context.Context) error) error
|
||||||
|
|
||||||
|
// GetChannelByID returns the channel for the given id.
|
||||||
|
GetChannelByID(context.Context, string, int) (*Channel, error)
|
||||||
|
|
||||||
|
// UpdateChannel updates a channel.
|
||||||
|
UpdateChannel(context.Context, string, *Channel, func(context.Context) error) error
|
||||||
|
|
||||||
|
// DeleteChannelByID deletes a channel.
|
||||||
|
DeleteChannelByID(context.Context, string, int, func(context.Context) error) error
|
||||||
|
|
||||||
|
// ListChannels returns the list of channels.
|
||||||
|
ListChannels(context.Context, string) ([]*Channel, error)
|
||||||
|
|
||||||
|
// ListAllChannels returns the list of channels for all organizations.
|
||||||
|
ListAllChannels(context.Context) ([]*Channel, error)
|
||||||
|
|
||||||
|
// GetMatchers gets a list of matchers per organization.
|
||||||
|
// Matchers is an array of ruleId to receiver names.
|
||||||
|
GetMatchers(context.Context, string) (map[string][]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ConfigStoreNoopCallback = func(ctx context.Context) error { return nil }
|
||||||
|
|
||||||
// MarshalSecretValue if set to true will expose Secret type
|
// MarshalSecretValue if set to true will expose Secret type
|
||||||
// through the marshal interfaces. We need to store the actual value of the secret
|
// through the marshal interfaces. We need to store the actual value of the secret
|
||||||
// in the database, so we need to set this to true.
|
// in the database, so we need to set this to true.
|
||||||
func init() {
|
func init() {
|
||||||
|
commoncfg.MarshalSecretValue = true
|
||||||
config.MarshalSecretValue = true
|
config.MarshalSecretValue = true
|
||||||
}
|
}
|
||||||
|
|||||||
215
pkg/types/alertmanagertypes/config_test.go
Normal file
215
pkg/types/alertmanagertypes/config_test.go
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
package alertmanagertypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/prometheus/alertmanager/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateRuleIDMatcher(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
orgID string
|
||||||
|
receivers []config.Receiver
|
||||||
|
ruleIDToReceivers map[string][]string
|
||||||
|
expectedRoutes []map[string]any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OneSlackReceiver",
|
||||||
|
orgID: "1",
|
||||||
|
receivers: []config.Receiver{
|
||||||
|
{
|
||||||
|
Name: "slack-receiver",
|
||||||
|
SlackConfigs: []*config.SlackConfig{
|
||||||
|
{
|
||||||
|
Channel: "#alerts",
|
||||||
|
APIURL: &config.SecretURL{URL: &url.URL{Scheme: "https", Host: "slack.com", Path: "/api/test"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ruleIDToReceivers: map[string][]string{"test-rule": {"slack-receiver"}},
|
||||||
|
expectedRoutes: []map[string]any{{"receiver": "slack-receiver", "continue": true, "matchers": []any{"ruleId=\"test-rule\""}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SlackAndEmailReceivers",
|
||||||
|
orgID: "1",
|
||||||
|
receivers: []config.Receiver{
|
||||||
|
{
|
||||||
|
Name: "slack-receiver",
|
||||||
|
SlackConfigs: []*config.SlackConfig{
|
||||||
|
{
|
||||||
|
Channel: "#alerts",
|
||||||
|
APIURL: &config.SecretURL{URL: &url.URL{Scheme: "https", Host: "slack.com", Path: "/api/test"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "email-receiver",
|
||||||
|
EmailConfigs: []*config.EmailConfig{
|
||||||
|
{
|
||||||
|
To: "test@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ruleIDToReceivers: map[string][]string{"test-rule": {"slack-receiver", "email-receiver"}},
|
||||||
|
expectedRoutes: []map[string]any{{"receiver": "slack-receiver", "continue": true, "matchers": []any{"ruleId=\"test-rule\""}}, {"receiver": "email-receiver", "continue": true, "matchers": []any{"ruleId=\"test-rule\""}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ReceiverDoesNotExist",
|
||||||
|
orgID: "1",
|
||||||
|
receivers: []config.Receiver{
|
||||||
|
{
|
||||||
|
Name: "slack-receiver",
|
||||||
|
SlackConfigs: []*config.SlackConfig{
|
||||||
|
{
|
||||||
|
Channel: "#alerts",
|
||||||
|
APIURL: &config.SecretURL{URL: &url.URL{Scheme: "https", Host: "slack.com", Path: "/api/test"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ruleIDToReceivers: map[string][]string{"test-rule": {"does-not-exist"}},
|
||||||
|
expectedRoutes: []map[string]any{{"receiver": "slack-receiver", "continue": true}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MultipleAlertsOnOneSlackReceiver",
|
||||||
|
orgID: "1",
|
||||||
|
receivers: []config.Receiver{
|
||||||
|
{
|
||||||
|
Name: "slack-receiver",
|
||||||
|
SlackConfigs: []*config.SlackConfig{
|
||||||
|
{
|
||||||
|
Channel: "#alerts",
|
||||||
|
APIURL: &config.SecretURL{URL: &url.URL{Scheme: "https", Host: "slack.com", Path: "/api/test"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ruleIDToReceivers: map[string][]string{"test-rule-1": {"slack-receiver", "does-not-exist"}, "test-rule-2": {"slack-receiver"}},
|
||||||
|
expectedRoutes: []map[string]any{{"receiver": "slack-receiver", "continue": true, "matchers": []any{"ruleId=\"test-rule-1\"", "ruleId=\"test-rule-2\""}}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cfg, err := NewDefaultConfig(GlobalConfig{SMTPSmarthost: config.HostPort{Host: "localhost", Port: "25"}, SMTPFrom: "test@example.com"}, RouteConfig{}, tc.orgID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, receiver := range tc.receivers {
|
||||||
|
err := cfg.CreateReceiver(receiver)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for ruleID, receiverNames := range tc.ruleIDToReceivers {
|
||||||
|
err = cfg.CreateRuleIDMatcher(ruleID, receiverNames)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
routes, err := json.Marshal(cfg.alertmanagerConfig.Route.Routes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var actualRoutes []map[string]any
|
||||||
|
err = json.Unmarshal(routes, &actualRoutes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.ElementsMatch(t, tc.expectedRoutes, actualRoutes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRuleIDMatcher(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
orgID string
|
||||||
|
receivers []config.Receiver
|
||||||
|
ruleIDToReceivers map[string][]string
|
||||||
|
ruleIDsToDelete []string
|
||||||
|
expectedRoutes []map[string]any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "DeleteEmailAndSlackMatchers",
|
||||||
|
orgID: "1",
|
||||||
|
receivers: []config.Receiver{
|
||||||
|
{
|
||||||
|
Name: "slack-receiver",
|
||||||
|
SlackConfigs: []*config.SlackConfig{
|
||||||
|
{
|
||||||
|
Channel: "#alerts",
|
||||||
|
APIURL: &config.SecretURL{URL: &url.URL{Scheme: "https", Host: "slack.com", Path: "/api/test"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "email-receiver",
|
||||||
|
EmailConfigs: []*config.EmailConfig{
|
||||||
|
{
|
||||||
|
To: "test@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ruleIDToReceivers: map[string][]string{"test-rule": {"email-receiver", "slack-receiver"}},
|
||||||
|
ruleIDsToDelete: []string{"test-rule"},
|
||||||
|
expectedRoutes: []map[string]any{{"receiver": "slack-receiver", "continue": true}, {"receiver": "email-receiver", "continue": true}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AlertNameDoesNotExist",
|
||||||
|
orgID: "1",
|
||||||
|
receivers: []config.Receiver{
|
||||||
|
{
|
||||||
|
Name: "slack-receiver",
|
||||||
|
SlackConfigs: []*config.SlackConfig{
|
||||||
|
{
|
||||||
|
Channel: "#alerts",
|
||||||
|
APIURL: &config.SecretURL{URL: &url.URL{Scheme: "https", Host: "slack.com", Path: "/api/test"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "email-receiver",
|
||||||
|
EmailConfigs: []*config.EmailConfig{
|
||||||
|
{
|
||||||
|
To: "test@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ruleIDToReceivers: map[string][]string{"test-rule": {"email-receiver", "slack-receiver"}},
|
||||||
|
ruleIDsToDelete: []string{"does-not-exist"},
|
||||||
|
expectedRoutes: []map[string]any{{"receiver": "slack-receiver", "continue": true, "matchers": []any{"ruleId=\"test-rule\""}}, {"receiver": "email-receiver", "continue": true, "matchers": []any{"ruleId=\"test-rule\""}}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cfg, err := NewDefaultConfig(GlobalConfig{SMTPSmarthost: config.HostPort{Host: "localhost", Port: "25"}, SMTPFrom: "test@example.com"}, RouteConfig{}, tc.orgID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, receiver := range tc.receivers {
|
||||||
|
err := cfg.CreateReceiver(receiver)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for ruleID, receiverNames := range tc.ruleIDToReceivers {
|
||||||
|
err = cfg.CreateRuleIDMatcher(ruleID, receiverNames)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ruleID := range tc.ruleIDsToDelete {
|
||||||
|
err = cfg.DeleteRuleIDMatcher(ruleID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
routes, err := json.Marshal(cfg.alertmanagerConfig.Route.Routes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var actualRoutes []map[string]any
|
||||||
|
err = json.Unmarshal(routes, &actualRoutes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.ElementsMatch(t, tc.expectedRoutes, actualRoutes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,16 +29,17 @@ func NewReceiver(input string) (Receiver, error) {
|
|||||||
return receiver, nil
|
return receiver, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newRouteFromReceiver(receiver Receiver) *config.Route {
|
||||||
|
return &config.Route{Receiver: receiver.Name, Continue: true}
|
||||||
|
}
|
||||||
|
|
||||||
func NewReceiverIntegrations(nc Receiver, tmpl *template.Template, logger *slog.Logger) ([]notify.Integration, error) {
|
func NewReceiverIntegrations(nc Receiver, tmpl *template.Template, logger *slog.Logger) ([]notify.Integration, error) {
|
||||||
return receiver.BuildReceiverIntegrations(nc, tmpl, logger)
|
return receiver.BuildReceiverIntegrations(nc, tmpl, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReceiver(ctx context.Context, receiver Receiver, tmpl *template.Template, logger *slog.Logger) error {
|
func TestReceiver(ctx context.Context, receiver Receiver, tmpl *template.Template, logger *slog.Logger, alert *Alert) error {
|
||||||
now := time.Now()
|
ctx = notify.WithGroupKey(ctx, fmt.Sprintf("%s-%s-%d", receiver.Name, alert.Labels.Fingerprint(), time.Now().Unix()))
|
||||||
testAlert := NewTestAlert(receiver, now, now)
|
ctx = notify.WithGroupLabels(ctx, alert.Labels)
|
||||||
|
|
||||||
ctx = notify.WithGroupKey(ctx, fmt.Sprintf("%s-%s-%d", receiver.Name, testAlert.Labels.Fingerprint(), now.Unix()))
|
|
||||||
ctx = notify.WithGroupLabels(ctx, testAlert.Labels)
|
|
||||||
ctx = notify.WithReceiverName(ctx, receiver.Name)
|
ctx = notify.WithReceiverName(ctx, receiver.Name)
|
||||||
|
|
||||||
integrations, err := NewReceiverIntegrations(receiver, tmpl, logger)
|
integrations, err := NewReceiverIntegrations(receiver, tmpl, logger)
|
||||||
@@ -46,7 +47,7 @@ func TestReceiver(ctx context.Context, receiver Receiver, tmpl *template.Templat
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = integrations[0].Notify(ctx, testAlert); err != nil {
|
if _, err = integrations[0].Notify(ctx, alert); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user