Compare commits

..

1 Commits

Author SHA1 Message Date
primus-bot[bot]
aa96ec6fe9 chore(release): bump to v0.126.0 (#11472)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-05-27 07:39:43 +00:00
23 changed files with 178 additions and 550 deletions

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.125.1
image: signoz/signoz:v0.126.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.125.1
image: signoz/signoz:v0.126.0
ports:
- "8080:8080" # signoz port
volumes:

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.125.1}
image: signoz/signoz:${VERSION:-v0.126.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.125.1}
image: signoz/signoz:${VERSION:-v0.126.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -25,7 +25,7 @@ type Alertmanager interface {
PutAlerts(context.Context, string, alertmanagertypes.PostableAlerts) error
// 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, ruleID string, receiversMap map[*alertmanagertypes.PostableAlert][]string) error
@@ -40,10 +40,10 @@ type Alertmanager interface {
GetChannelByID(context.Context, string, valuer.UUID) (*alertmanagertypes.Channel, error)
// UpdateChannel updates a channel for the organization.
UpdateChannelByReceiverAndID(context.Context, string, *alertmanagertypes.Receiver, valuer.UUID) error
UpdateChannelByReceiverAndID(context.Context, string, alertmanagertypes.Receiver, valuer.UUID) error
// CreateChannel creates a channel for the organization.
CreateChannel(context.Context, string, *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)
CreateChannel(context.Context, string, alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)
// DeleteChannelByID deletes a channel for the organization.
DeleteChannelByID(context.Context, string, valuer.UUID) error

View File

@@ -26,8 +26,8 @@ var customNotifierIntegrations = []string{
msteamsv2.Integration,
}
func NewReceiverIntegrations(nc *alertmanagertypes.Receiver, tmpl *template.Template, logger *slog.Logger, templater alertmanagertypes.Templater) ([]notify.Integration, error) {
upstreamIntegrations, err := receiver.BuildReceiverIntegrations(*nc.Receiver, tmpl, logger)
func NewReceiverIntegrations(nc alertmanagertypes.Receiver, tmpl *template.Template, logger *slog.Logger, templater alertmanagertypes.Templater) ([]notify.Integration, error) {
upstreamIntegrations, err := receiver.BuildReceiverIntegrations(nc, tmpl, logger)
if err != nil {
return nil, err
}

View File

@@ -275,11 +275,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
server.logger.InfoContext(ctx, "skipping creation of receiver not referenced by any route", slog.String("receiver", rcv.Name))
continue
}
extendedRcv, err := alertmanagerConfig.GetReceiver(rcv.Name)
if err != nil {
return err
}
integrations, err := alertmanagernotify.NewReceiverIntegrations(extendedRcv, server.tmpl, server.logger, server.templater)
integrations, err := alertmanagernotify.NewReceiverIntegrations(rcv, server.tmpl, server.logger, server.templater)
if err != nil {
return err
}
@@ -354,7 +350,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
return nil
}
func (server *Server) TestReceiver(ctx context.Context, receiver *alertmanagertypes.Receiver) error {
func (server *Server) TestReceiver(ctx context.Context, receiver alertmanagertypes.Receiver) error {
testAlert := alertmanagertypes.NewTestAlert(receiver, time.Now(), time.Now())
return alertmanagertypes.TestReceiver(ctx, receiver, alertmanagernotify.NewReceiverIntegrations, server.alertmanagerConfig, server.tmpl, server.logger, server.templater, testAlert.Labels, testAlert)
}

View File

@@ -75,7 +75,7 @@ func TestServerTestReceiverTypeWebhook(t *testing.T) {
webhookURL, err := url.Parse("http://" + webhookListener.Addr().String() + "/webhook")
require.NoError(t, err)
err = server.TestReceiver(context.Background(), &alertmanagertypes.Receiver{Receiver: &config.Receiver{
err = server.TestReceiver(context.Background(), alertmanagertypes.Receiver{
Name: "test-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
@@ -83,7 +83,7 @@ func TestServerTestReceiverTypeWebhook(t *testing.T) {
URL: config.SecretTemplateURL(webhookURL.String()),
},
},
}})
})
assert.NoError(t, err)
assert.Contains(t, requestBody.String(), "test-receiver")
@@ -101,7 +101,7 @@ func TestServerPutAlerts(t *testing.T) {
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
require.NoError(t, err)
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{Receiver: &config.Receiver{
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "test-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
@@ -109,7 +109,7 @@ func TestServerPutAlerts(t *testing.T) {
URL: config.SecretTemplateURL("http://localhost/test-receiver"),
},
},
}}))
}))
require.NoError(t, server.SetConfig(context.Background(), amConfig))
@@ -181,7 +181,7 @@ func TestServerTestAlert(t *testing.T) {
webhook2URL, err := url.Parse("http://" + webhook2Listener.Addr().String() + "/webhook")
require.NoError(t, err)
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{Receiver: &config.Receiver{
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "receiver-1",
WebhookConfigs: []*config.WebhookConfig{
{
@@ -189,9 +189,9 @@ func TestServerTestAlert(t *testing.T) {
URL: config.SecretTemplateURL(webhook1URL.String()),
},
},
}}))
}))
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{Receiver: &config.Receiver{
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "receiver-2",
WebhookConfigs: []*config.WebhookConfig{
{
@@ -199,7 +199,7 @@ func TestServerTestAlert(t *testing.T) {
URL: config.SecretTemplateURL(webhook2URL.String()),
},
},
}}))
}))
require.NoError(t, server.SetConfig(context.Background(), amConfig))
defer func() {
@@ -273,7 +273,7 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
webhookURL, err := url.Parse("http://" + webhookListener.Addr().String() + "/webhook")
require.NoError(t, err)
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{Receiver: &config.Receiver{
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "working-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
@@ -281,9 +281,9 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
URL: config.SecretTemplateURL(webhookURL.String()),
},
},
}}))
}))
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{Receiver: &config.Receiver{
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
Name: "failing-receiver",
WebhookConfigs: []*config.WebhookConfig{
{
@@ -291,7 +291,7 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
URL: config.SecretTemplateURL("http://localhost:1/webhook"),
},
},
}}))
}))
require.NoError(t, server.SetConfig(context.Background(), amConfig))
defer func() {

View File

@@ -110,7 +110,7 @@ func (_c *MockAlertmanager_Collect_Call) RunAndReturn(run func(context1 context.
}
// CreateChannel provides a mock function for the type MockAlertmanager
func (_mock *MockAlertmanager) CreateChannel(context1 context.Context, s string, v *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error) {
func (_mock *MockAlertmanager) CreateChannel(context1 context.Context, s string, v alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error) {
ret := _mock.Called(context1, s, v)
if len(ret) == 0 {
@@ -119,17 +119,17 @@ func (_mock *MockAlertmanager) CreateChannel(context1 context.Context, s string,
var r0 *alertmanagertypes.Channel
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)); ok {
return returnFunc(context1, s, v)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *alertmanagertypes.Receiver) *alertmanagertypes.Channel); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alertmanagertypes.Receiver) *alertmanagertypes.Channel); ok {
r0 = returnFunc(context1, s, v)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*alertmanagertypes.Channel)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, *alertmanagertypes.Receiver) error); ok {
if returnFunc, ok := ret.Get(1).(func(context.Context, string, alertmanagertypes.Receiver) error); ok {
r1 = returnFunc(context1, s, v)
} else {
r1 = ret.Error(1)
@@ -145,12 +145,12 @@ type MockAlertmanager_CreateChannel_Call struct {
// CreateChannel is a helper method to define mock.On call
// - context1 context.Context
// - s string
// - v *alertmanagertypes.Receiver
// - v alertmanagertypes.Receiver
func (_e *MockAlertmanager_Expecter) CreateChannel(context1 interface{}, s interface{}, v interface{}) *MockAlertmanager_CreateChannel_Call {
return &MockAlertmanager_CreateChannel_Call{Call: _e.mock.On("CreateChannel", context1, s, v)}
}
func (_c *MockAlertmanager_CreateChannel_Call) Run(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver)) *MockAlertmanager_CreateChannel_Call {
func (_c *MockAlertmanager_CreateChannel_Call) Run(run func(context1 context.Context, s string, v alertmanagertypes.Receiver)) *MockAlertmanager_CreateChannel_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -160,9 +160,9 @@ func (_c *MockAlertmanager_CreateChannel_Call) Run(run func(context1 context.Con
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 *alertmanagertypes.Receiver
var arg2 alertmanagertypes.Receiver
if args[2] != nil {
arg2 = args[2].(*alertmanagertypes.Receiver)
arg2 = args[2].(alertmanagertypes.Receiver)
}
run(
arg0,
@@ -178,7 +178,7 @@ func (_c *MockAlertmanager_CreateChannel_Call) Return(channel *alertmanagertypes
return _c
}
func (_c *MockAlertmanager_CreateChannel_Call) RunAndReturn(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)) *MockAlertmanager_CreateChannel_Call {
func (_c *MockAlertmanager_CreateChannel_Call) RunAndReturn(run func(context1 context.Context, s string, v alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)) *MockAlertmanager_CreateChannel_Call {
_c.Call.Return(run)
return _c
}
@@ -1579,7 +1579,7 @@ func (_c *MockAlertmanager_TestAlert_Call) RunAndReturn(run func(ctx context.Con
}
// TestReceiver provides a mock function for the type MockAlertmanager
func (_mock *MockAlertmanager) TestReceiver(context1 context.Context, s string, v *alertmanagertypes.Receiver) error {
func (_mock *MockAlertmanager) TestReceiver(context1 context.Context, s string, v alertmanagertypes.Receiver) error {
ret := _mock.Called(context1, s, v)
if len(ret) == 0 {
@@ -1587,7 +1587,7 @@ func (_mock *MockAlertmanager) TestReceiver(context1 context.Context, s string,
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *alertmanagertypes.Receiver) error); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alertmanagertypes.Receiver) error); ok {
r0 = returnFunc(context1, s, v)
} else {
r0 = ret.Error(0)
@@ -1603,12 +1603,12 @@ type MockAlertmanager_TestReceiver_Call struct {
// TestReceiver is a helper method to define mock.On call
// - context1 context.Context
// - s string
// - v *alertmanagertypes.Receiver
// - v alertmanagertypes.Receiver
func (_e *MockAlertmanager_Expecter) TestReceiver(context1 interface{}, s interface{}, v interface{}) *MockAlertmanager_TestReceiver_Call {
return &MockAlertmanager_TestReceiver_Call{Call: _e.mock.On("TestReceiver", context1, s, v)}
}
func (_c *MockAlertmanager_TestReceiver_Call) Run(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver)) *MockAlertmanager_TestReceiver_Call {
func (_c *MockAlertmanager_TestReceiver_Call) Run(run func(context1 context.Context, s string, v alertmanagertypes.Receiver)) *MockAlertmanager_TestReceiver_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -1618,9 +1618,9 @@ func (_c *MockAlertmanager_TestReceiver_Call) Run(run func(context1 context.Cont
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 *alertmanagertypes.Receiver
var arg2 alertmanagertypes.Receiver
if args[2] != nil {
arg2 = args[2].(*alertmanagertypes.Receiver)
arg2 = args[2].(alertmanagertypes.Receiver)
}
run(
arg0,
@@ -1636,7 +1636,7 @@ func (_c *MockAlertmanager_TestReceiver_Call) Return(err error) *MockAlertmanage
return _c
}
func (_c *MockAlertmanager_TestReceiver_Call) RunAndReturn(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver) error) *MockAlertmanager_TestReceiver_Call {
func (_c *MockAlertmanager_TestReceiver_Call) RunAndReturn(run func(context1 context.Context, s string, v alertmanagertypes.Receiver) error) *MockAlertmanager_TestReceiver_Call {
_c.Call.Return(run)
return _c
}
@@ -1705,7 +1705,7 @@ func (_c *MockAlertmanager_UpdateAllRoutePoliciesByRuleId_Call) RunAndReturn(run
}
// UpdateChannelByReceiverAndID provides a mock function for the type MockAlertmanager
func (_mock *MockAlertmanager) UpdateChannelByReceiverAndID(context1 context.Context, s string, v *alertmanagertypes.Receiver, uUID valuer.UUID) error {
func (_mock *MockAlertmanager) UpdateChannelByReceiverAndID(context1 context.Context, s string, v alertmanagertypes.Receiver, uUID valuer.UUID) error {
ret := _mock.Called(context1, s, v, uUID)
if len(ret) == 0 {
@@ -1713,7 +1713,7 @@ func (_mock *MockAlertmanager) UpdateChannelByReceiverAndID(context1 context.Con
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *alertmanagertypes.Receiver, valuer.UUID) error); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alertmanagertypes.Receiver, valuer.UUID) error); ok {
r0 = returnFunc(context1, s, v, uUID)
} else {
r0 = ret.Error(0)
@@ -1729,13 +1729,13 @@ type MockAlertmanager_UpdateChannelByReceiverAndID_Call struct {
// UpdateChannelByReceiverAndID is a helper method to define mock.On call
// - context1 context.Context
// - s string
// - v *alertmanagertypes.Receiver
// - v alertmanagertypes.Receiver
// - uUID valuer.UUID
func (_e *MockAlertmanager_Expecter) UpdateChannelByReceiverAndID(context1 interface{}, s interface{}, v interface{}, uUID interface{}) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
return &MockAlertmanager_UpdateChannelByReceiverAndID_Call{Call: _e.mock.On("UpdateChannelByReceiverAndID", context1, s, v, uUID)}
}
func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) Run(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver, uUID valuer.UUID)) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) Run(run func(context1 context.Context, s string, v alertmanagertypes.Receiver, uUID valuer.UUID)) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -1745,9 +1745,9 @@ func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) Run(run func(conte
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 *alertmanagertypes.Receiver
var arg2 alertmanagertypes.Receiver
if args[2] != nil {
arg2 = args[2].(*alertmanagertypes.Receiver)
arg2 = args[2].(alertmanagertypes.Receiver)
}
var arg3 valuer.UUID
if args[3] != nil {
@@ -1768,7 +1768,7 @@ func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) Return(err error)
return _c
}
func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) RunAndReturn(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver, uUID valuer.UUID) error) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) RunAndReturn(run func(context1 context.Context, s string, v alertmanagertypes.Receiver, uUID valuer.UUID) error) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
_c.Call.Return(run)
return _c
}

View File

@@ -138,7 +138,7 @@ func (service *Service) PutAlerts(ctx context.Context, orgID string, alerts aler
return server.PutAlerts(ctx, alerts)
}
func (service *Service) TestReceiver(ctx context.Context, orgID string, receiver *alertmanagertypes.Receiver) error {
func (service *Service) TestReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
service.serversMtx.RLock()
defer service.serversMtx.RUnlock()

View File

@@ -110,7 +110,7 @@ func (provider *provider) PutAlerts(ctx context.Context, orgID string, alerts al
return provider.service.PutAlerts(ctx, orgID, alerts)
}
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 {
return provider.service.TestReceiver(ctx, orgID, receiver)
}
@@ -151,7 +151,7 @@ func (provider *provider) GetChannelByID(ctx context.Context, orgID string, chan
return provider.configStore.GetChannelByID(ctx, orgID, channelID)
}
func (provider *provider) UpdateChannelByReceiverAndID(ctx context.Context, orgID string, receiver *alertmanagertypes.Receiver, id valuer.UUID) error {
func (provider *provider) UpdateChannelByReceiverAndID(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver, id valuer.UUID) error {
channel, err := provider.configStore.GetChannelByID(ctx, orgID, id)
if err != nil {
return err
@@ -210,7 +210,7 @@ func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, c
}))
}
func (provider *provider) CreateChannel(ctx context.Context, orgID string, receiver *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error) {
func (provider *provider) CreateChannel(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error) {
config, err := provider.configStore.Get(ctx, orgID)
if err != nil {
return nil, err

View File

@@ -269,7 +269,7 @@ func (migration *addAlertmanager) msTeamsChannelToMSTeamsV2Channel(c *alertmanag
return nil
}
func (migration *addAlertmanager) msTeamsReceiverToMSTeamsV2Receiver(receiver *alertmanagertypes.Receiver) *alertmanagertypes.Receiver {
func (migration *addAlertmanager) msTeamsReceiverToMSTeamsV2Receiver(receiver alertmanagertypes.Receiver) alertmanagertypes.Receiver {
if receiver.MSTeamsConfigs == nil {
return receiver
}

View File

@@ -134,7 +134,7 @@ func NewAlertsFromPostableAlerts(ctx context.Context, postableAlerts PostableAle
return validAlerts, errs
}
func NewTestAlert(receiver *Receiver, startsAt time.Time, updatedAt time.Time) *Alert {
func NewTestAlert(receiver Receiver, startsAt time.Time, updatedAt time.Time) *Alert {
return &Alert{
Alert: model.Alert{
StartsAt: startsAt,

View File

@@ -56,7 +56,7 @@ type Channel struct {
// NewChannelFromReceiver creates a new Channel from a Receiver.
// It can return nil if the receiver is the default receiver.
func NewChannelFromReceiver(receiver *Receiver, orgID string) (*Channel, error) {
func NewChannelFromReceiver(receiver config.Receiver, orgID string) (*Channel, error) {
if receiver.Name == DefaultReceiverName {
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeAlertmanagerChannelInvalid, "cannot use %s name as a channel name", receiver.Name)
}
@@ -74,61 +74,51 @@ func NewChannelFromReceiver(receiver *Receiver, orgID string) (*Channel, error)
OrgID: orgID,
}
// The embedded *config.Receiver marshals inline, so json.Marshal(receiver)
// emits every upstream notifier config plus any SigNoz-native ones in a
// single pass.
data, err := json.Marshal(receiver)
if err != nil {
return nil, err
}
channel.Data = string(data)
// Use reflection to examine receiver struct fields
receiverType := reflect.TypeOf(receiver)
receiverVal := reflect.ValueOf(receiver)
channel.Type = receiverChannelType(receiver)
if channel.Type == "" {
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeAlertmanagerChannelInvalid, "channel '%s' must have at least one notification configuration (e.g., email_configs, webhook_configs, slack_configs)", receiver.Name)
}
return &channel, nil
}
// receiverChannelType derives the channel.Type discriminator from the
// configured notifier by reflecting over the receiver. It walks both the
// SigNoz Receiver's own fields (for native notifiers) and the embedded
// config.Receiver's fields (for upstream notifiers); the first non-empty
// `*_configs` slice wins.
func receiverChannelType(receiver *Receiver) string {
if t := nonEmptyConfigsField(reflect.ValueOf(*receiver)); t != "" {
return t
}
if t := nonEmptyConfigsField(reflect.ValueOf(*receiver.Receiver)); t != "" {
return t
}
return ""
}
func nonEmptyConfigsField(v reflect.Value) string {
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldVal := v.Field(i)
// Iterate through fields looking for *Config fields
for i := 0; i < receiverType.NumField(); i++ {
field := receiverType.Field(i)
fieldVal := receiverVal.Field(i)
// Skip if not a slice or is empty
if fieldVal.Kind() != reflect.Slice || fieldVal.Len() == 0 {
continue
}
// Get channel type from yaml tag
yamlTag := field.Tag.Get("yaml")
if yamlTag == "" {
continue
}
// Extract the base type name (e.g., "email_configs" -> "email").
// Extract the base type name (e.g., "email_configs" -> "email")
matches := receiverTypeRegex.FindStringSubmatch(yamlTag)
if len(matches) != 2 {
continue
}
return matches[1]
channelType := matches[1]
// Marshal config data to JSON
configData, err := json.Marshal(receiver)
if err != nil {
continue
}
channel.Type = channelType
channel.Data = string(configData)
break
}
return ""
// If we were unable to find the channel type, return an error
if channel.Type == "" {
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeAlertmanagerChannelInvalid, "channel '%s' must have at least one notification configuration (e.g., email_configs, webhook_configs, slack_configs)", receiver.Name)
}
return &channel, nil
}
func NewConfigFromChannels(globalConfig GlobalConfig, routeConfig RouteConfig, channels Channels, orgID string) (*Config, error) {
@@ -192,7 +182,7 @@ func NewStatsFromChannels(channels Channels) map[string]any {
return stats
}
func (c *Channel) Update(receiver *Receiver) error {
func (c *Channel) Update(receiver Receiver) error {
channel, err := NewChannelFromReceiver(receiver, c.OrgID)
if err != nil {
return err
@@ -202,7 +192,6 @@ func (c *Channel) Update(receiver *Receiver) error {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAlertmanagerChannelNameMismatch, "cannot update channel name")
}
c.Type = channel.Type
c.Data = channel.Data
c.UpdatedAt = time.Now()
@@ -221,21 +210,15 @@ func (PostableChannel) JSONSchema() (jsonschema.Schema, error) {
schema.WithRequired("name")
var oneOf []jsonschema.SchemaOrBool
// Walk both the SigNoz Receiver's own fields (for native notifiers) and the
// embedded config.Receiver's fields (for upstream notifiers) — together
// they make up PostableChannel's JSON shape.
collect := func(t reflect.Type) {
for i := 0; i < t.NumField(); i++ {
jsonTag := strings.Split(t.Field(i).Tag.Get("json"), ",")[0]
if !strings.HasSuffix(jsonTag, "_configs") {
continue
}
branch := (&jsonschema.Schema{}).WithRequired(jsonTag)
oneOf = append(oneOf, branch.ToSchemaOrBool())
receiverType := reflect.TypeOf(Receiver{})
for i := 0; i < receiverType.NumField(); i++ {
jsonTag := strings.Split(receiverType.Field(i).Tag.Get("json"), ",")[0]
if !strings.HasSuffix(jsonTag, "_configs") {
continue
}
branch := (&jsonschema.Schema{}).WithRequired(jsonTag)
oneOf = append(oneOf, branch.ToSchemaOrBool())
}
collect(reflect.TypeOf(Receiver{}))
collect(reflect.TypeOf(config.Receiver{}))
schema.WithOneOf(oneOf...)

View File

@@ -285,8 +285,7 @@ func TestNewChannelFromReceiver(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
receiver := testCase.receiver
channel, err := NewChannelFromReceiver(&Receiver{Receiver: &receiver}, "1")
channel, err := NewChannelFromReceiver(testCase.receiver, "1")
if !testCase.pass {
assert.Error(t, err)
return
@@ -300,33 +299,3 @@ func TestNewChannelFromReceiver(t *testing.T) {
}
}
// TestNewChannelFromReceiverGoogleChat covers the SigNoz-native side of the
// receiver: the Type discriminator and the per-row Data both come from the
// embed's googlechat_configs field — no upstream notifier config is present.
func TestNewChannelFromReceiverGoogleChat(t *testing.T) {
webhookURL, err := url.Parse("https://chat.googleapis.com/v1/spaces/test/messages")
if err != nil {
t.Fatal(err)
}
receiver := &Receiver{
Receiver: &config.Receiver{Name: "googlechat-receiver"},
GoogleChatConfigs: []*GoogleChatReceiverConfig{
{
WebhookURL: &config.SecretURL{URL: webhookURL},
Title: "Alert",
Text: "Body",
},
},
}
channel, err := NewChannelFromReceiver(receiver, "1")
assert.NoError(t, err)
assert.Equal(t, "googlechat-receiver", channel.Name)
assert.Equal(t, "googlechat", channel.Type)
assert.JSONEq(t,
`{"name":"googlechat-receiver","googlechat_configs":[{"send_resolved":false,"webhook_url":"https://chat.googleapis.com/v1/spaces/test/messages","title":"Alert","text":"Body"}]}`,
channel.Data,
)
}

View File

@@ -59,55 +59,12 @@ type Config struct {
// storeableConfig is the representation of the config in the store
storeableConfig *StoreableConfig
// customConfigs holds the SigNoz-native notifier configs (which the upstream
// config.Receiver cannot carry) keyed by receiver name. It runs in parallel
// with alertmanagerConfig.Receivers (which holds the upstream base); the two
// halves are zipped back together by GetReceiver and serialized together by
// newRawFromConfig.
customConfigs map[string]customReceiverConfigs
}
// customReceiverConfigs is the per-receiver sidecar for SigNoz-native notifier
// configs that the upstream config.Receiver cannot carry. To add another
// native notifier, mirror the GoogleChat field below: declare a typed slice
// here, add the matching field on Receiver, and extend customConfigsOf and
// isEmpty. The serialization (storedConfig) and in-memory plumbing
// (CreateReceiver / GetReceiver / newRawFromConfig) need no further changes.
type customReceiverConfigs struct {
GoogleChat []*GoogleChatReceiverConfig
}
func (c customReceiverConfigs) isEmpty() bool {
return len(c.GoogleChat) == 0
}
// customConfigsOf extracts the SigNoz-native configs carried on a Receiver.
func customConfigsOf(receiver *Receiver) customReceiverConfigs {
return customReceiverConfigs{
GoogleChat: receiver.GoogleChatConfigs,
}
}
// storedConfig is the serialization unit persisted to StoreableConfig.Config.
// Embedding *config.Config emits every upstream field (global, route,
// inhibit_rules, templates, ...); the outer Receivers field shadows the
// embedded config.Config.Receivers — both marshal to the JSON key "receivers"
// and the shallower outer field dominates per encoding/json's visibility
// rules — so the receivers are emitted as the extended *Receiver. When the
// first SigNoz-native notifier field is added to Receiver, it round-trips
// through this unit automatically, with no further changes here.
type storedConfig struct {
*config.Config
Receivers []*Receiver `json:"receivers"`
}
func NewConfig(c *config.Config, orgID string) *Config {
customConfigs := make(map[string]customReceiverConfigs)
raw := string(newRawFromConfig(c, customConfigs))
raw := string(newRawFromConfig(c))
return &Config{
alertmanagerConfig: c,
customConfigs: customConfigs,
storeableConfig: &StoreableConfig{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
@@ -124,14 +81,13 @@ func NewConfig(c *config.Config, orgID string) *Config {
}
func NewConfigFromStoreableConfig(sc *StoreableConfig) (*Config, error) {
alertmanagerConfig, customConfigs, err := newConfigFromString(sc.Config)
alertmanagerConfig, err := newConfigFromString(sc.Config)
if err != nil {
return nil, err
}
return &Config{
alertmanagerConfig: alertmanagerConfig,
customConfigs: customConfigs,
storeableConfig: sc,
}, nil
}
@@ -157,49 +113,32 @@ func NewDefaultConfig(globalConfig GlobalConfig, routeConfig RouteConfig, orgID
}, orgID), nil
}
func newConfigFromString(s string) (*config.Config, map[string]customReceiverConfigs, error) {
stored := storedConfig{Config: new(config.Config)}
if err := json.Unmarshal([]byte(s), &stored); err != nil {
return nil, nil, err
func newConfigFromString(s string) (*config.Config, error) {
config := new(config.Config)
err := json.Unmarshal([]byte(s), config)
if err != nil {
return nil, err
}
amConfig := stored.Config
amConfig.Receivers = make([]config.Receiver, len(stored.Receivers))
customConfigs := make(map[string]customReceiverConfigs)
for i, receiver := range config.Receivers {
bytes, err := json.Marshal(receiver)
if err != nil {
return nil, err
}
// Re-run each receiver through NewReceiver so upstream defaults are
// applied (mirrors the create path) and native fields are pulled off the
// embed at the same time.
for i, rcv := range stored.Receivers {
rcvJSON, err := json.Marshal(rcv)
receiver, err := NewReceiver(string(bytes))
if err != nil {
return nil, nil, err
}
parsed, err := NewReceiver(string(rcvJSON))
if err != nil {
return nil, nil, err
}
amConfig.Receivers[i] = *parsed.Receiver
if custom := customConfigsOf(parsed); !custom.isEmpty() {
customConfigs[parsed.Name] = custom
return nil, err
}
config.Receivers[i] = receiver
}
return amConfig, customConfigs, nil
return config, nil
}
func newRawFromConfig(c *config.Config, customConfigs map[string]customReceiverConfigs) []byte {
receivers := make([]*Receiver, len(c.Receivers))
for i := range c.Receivers {
base := c.Receivers[i]
custom := customConfigs[base.Name]
receivers[i] = &Receiver{
Receiver: &base,
GoogleChatConfigs: custom.GoogleChat,
}
}
b, err := json.Marshal(storedConfig{Config: c, Receivers: receivers})
func newRawFromConfig(c *config.Config) []byte {
b, err := json.Marshal(c)
if err != nil {
// Taking inspiration from the upstream. This is never expected to happen.
return []byte(fmt.Sprintf("<error creating config string: %s>", err))
@@ -212,16 +151,6 @@ func newConfigHash(s string) [16]byte {
return md5.Sum([]byte(s))
}
// flush re-serializes the config into the storeable representation and
// refreshes its hash and timestamp. Call it after every mutation of
// alertmanagerConfig or customConfigs.
func (c *Config) flush() {
raw := string(newRawFromConfig(c.alertmanagerConfig, c.customConfigs))
c.storeableConfig.Config = raw
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(raw))
c.storeableConfig.UpdatedAt = time.Now()
}
func (c *Config) CopyWithReset() (*Config, error) {
newConfig, err := NewDefaultConfig(
*c.alertmanagerConfig.Global,
@@ -250,7 +179,9 @@ func (c *Config) SetGlobalConfig(globalConfig GlobalConfig) error {
globalConfig.SMTPRequireTLS = smtpRequireTLS
c.alertmanagerConfig.Global = &globalConfig
c.flush()
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
c.storeableConfig.UpdatedAt = time.Now()
return nil
}
@@ -262,7 +193,9 @@ func (c *Config) SetRouteConfig(routeConfig RouteConfig) error {
}
c.alertmanagerConfig.Route = route
c.flush()
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
c.storeableConfig.UpdatedAt = time.Now()
return nil
}
@@ -274,7 +207,9 @@ func (c *Config) AddInhibitRules(rules []config.InhibitRule) error {
c.alertmanagerConfig.InhibitRules = append(c.alertmanagerConfig.InhibitRules, rules...)
c.flush()
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
c.storeableConfig.UpdatedAt = time.Now()
return nil
}
@@ -287,7 +222,7 @@ func (c *Config) StoreableConfig() *StoreableConfig {
return c.storeableConfig
}
func (c *Config) CreateReceiver(receiver *Receiver) error {
func (c *Config) CreateReceiver(receiver config.Receiver) error {
// check that receiver name is not already used
for _, existingReceiver := range c.alertmanagerConfig.Receivers {
if existingReceiver.Name == receiver.Name {
@@ -301,41 +236,33 @@ func (c *Config) CreateReceiver(receiver *Receiver) error {
}
c.alertmanagerConfig.Route.Routes = append(c.alertmanagerConfig.Route.Routes, route)
c.alertmanagerConfig.Receivers = append(c.alertmanagerConfig.Receivers, *receiver.Receiver)
c.setCustomConfigs(receiver)
c.alertmanagerConfig.Receivers = append(c.alertmanagerConfig.Receivers, receiver)
if err := c.alertmanagerConfig.UnmarshalYAML(func(i interface{}) error { return nil }); err != nil {
return err
}
c.applyNativeDefaults()
c.flush()
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) GetReceiver(name string) (*Receiver, error) {
for i := range c.alertmanagerConfig.Receivers {
if c.alertmanagerConfig.Receivers[i].Name == name {
// Copy out of the slice to avoid handing the caller a pointer
// into a slice that may later be reallocated by append.
base := c.alertmanagerConfig.Receivers[i]
custom := c.customConfigs[name]
return &Receiver{
Receiver: &base,
GoogleChatConfigs: custom.GoogleChat,
}, nil
func (c *Config) GetReceiver(name string) (Receiver, error) {
for _, receiver := range c.alertmanagerConfig.Receivers {
if receiver.Name == name {
return receiver, nil
}
}
return nil, errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerChannelNotFound, "channel with name %q not found", name)
return Receiver{}, errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerChannelNotFound, "channel with name %q not found", name)
}
func (c *Config) UpdateReceiver(receiver *Receiver) error {
func (c *Config) UpdateReceiver(receiver config.Receiver) error {
// find and update receiver
for i, existingReceiver := range c.alertmanagerConfig.Receivers {
if existingReceiver.Name == receiver.Name {
c.alertmanagerConfig.Receivers[i] = *receiver.Receiver
c.setCustomConfigs(receiver)
c.alertmanagerConfig.Receivers[i] = receiver
break
}
}
@@ -343,9 +270,10 @@ func (c *Config) UpdateReceiver(receiver *Receiver) error {
if err := c.alertmanagerConfig.UnmarshalYAML(func(i interface{}) error { return nil }); err != nil {
return err
}
c.applyNativeDefaults()
c.flush()
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
c.storeableConfig.UpdatedAt = time.Now()
return nil
}
@@ -370,48 +298,13 @@ func (c *Config) DeleteReceiver(name string) error {
}
}
delete(c.customConfigs, name)
c.flush()
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
c.storeableConfig.UpdatedAt = time.Now()
return nil
}
// setCustomConfigs records (or clears) the SigNoz-native configs for a
// receiver in the in-memory sidecar.
func (c *Config) setCustomConfigs(receiver *Receiver) {
if custom := customConfigsOf(receiver); !custom.isEmpty() {
c.customConfigs[receiver.Name] = custom
} else {
delete(c.customConfigs, receiver.Name)
}
}
// applyNativeDefaults threads global-scoped defaults (currently:
// Global.HTTPConfig) into each SigNoz-native notifier config that has not
// supplied its own. This mirrors the corresponding loop in upstream's
// config.Config.UnmarshalYAML, which does
//
// wh.HTTPConfig = cmp.Or(wh.HTTPConfig, c.Global.HTTPConfig)
//
// for each upstream notifier config. Call it from mutation paths after the
// upstream UnmarshalYAML pass has run. Extend it when adding another native
// notifier type that needs anything threaded from Global.
func (c *Config) applyNativeDefaults() {
if c.alertmanagerConfig.Global == nil {
return
}
httpDefault := c.alertmanagerConfig.Global.HTTPConfig
for _, custom := range c.customConfigs {
for _, gc := range custom.GoogleChat {
if gc.HTTPConfig == nil {
gc.HTTPConfig = httpDefault
}
}
}
}
func (c *Config) CreateRuleIDMatcher(ruleID string, receiverNames []string) error {
if c.alertmanagerConfig.Route == nil {
return errors.New(errors.TypeInvalidInput, ErrCodeAlertmanagerConfigInvalid, "route is nil")
@@ -425,7 +318,9 @@ func (c *Config) CreateRuleIDMatcher(ruleID string, receiverNames []string) erro
}
}
c.flush()
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
c.storeableConfig.UpdatedAt = time.Now()
return nil
}
@@ -444,7 +339,9 @@ func (c *Config) DeleteRuleIDInhibitor(ruleID string) error {
}
}
c.alertmanagerConfig.InhibitRules = filteredRules
c.flush()
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
c.storeableConfig.UpdatedAt = time.Now()
return nil
}
@@ -465,7 +362,9 @@ func (c *Config) DeleteRuleIDMatcher(ruleID string) error {
}
}
c.flush()
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
c.storeableConfig.UpdatedAt = time.Now()
return nil
}

View File

@@ -108,7 +108,7 @@ func TestCreateRuleIDMatcher(t *testing.T) {
require.NoError(t, err)
for _, receiver := range tc.receivers {
err := cfg.CreateReceiver(&Receiver{Receiver: &receiver})
err := cfg.CreateReceiver(receiver)
require.NoError(t, err)
}
@@ -203,7 +203,7 @@ func TestDeleteRuleIDMatcher(t *testing.T) {
require.NoError(t, err)
for _, receiver := range tc.receivers {
err := cfg.CreateReceiver(&Receiver{Receiver: &receiver})
err := cfg.CreateReceiver(receiver)
require.NoError(t, err)
}
@@ -329,68 +329,3 @@ func TestSetGlobalConfigPreservesSMTPRequireTLS(t *testing.T) {
})
}
}
// TestConfigPreservesGoogleChatConfigs is the round-trip proof for the
// native-notifier seam: create a Config with a GoogleChat receiver, serialize
// to the storeable blob, reload via NewConfigFromStoreableConfig, and verify
// the GoogleChatConfigs survive the trip — through both the blob and the
// in-memory sidecar — and that GetReceiver zips them back into a *Receiver.
func TestConfigPreservesGoogleChatConfigs(t *testing.T) {
webhookURL, err := url.Parse("https://chat.googleapis.com/v1/spaces/test/messages")
require.NoError(t, err)
cfg, err := NewDefaultConfig(
GlobalConfig{SMTPSmarthost: config.HostPort{Host: "localhost", Port: "25"}, SMTPFrom: "test@example.com"},
RouteConfig{GroupInterval: time.Minute, GroupWait: time.Minute, RepeatInterval: time.Minute},
"1",
)
require.NoError(t, err)
receiver := &Receiver{
Receiver: &config.Receiver{Name: "googlechat-receiver"},
GoogleChatConfigs: []*GoogleChatReceiverConfig{
{
WebhookURL: &config.SecretURL{URL: webhookURL},
Title: "Alert",
Text: "Body",
},
},
}
require.NoError(t, cfg.CreateReceiver(receiver))
// In-memory: GetReceiver zips the base and the native sidecar back together.
got, err := cfg.GetReceiver("googlechat-receiver")
require.NoError(t, err)
require.Len(t, got.GoogleChatConfigs, 1)
assert.Equal(t, "Alert", got.GoogleChatConfigs[0].Title)
assert.Equal(t, "Body", got.GoogleChatConfigs[0].Text)
// Global threading: HTTPConfig was nil on input, so applyNativeDefaults
// should have filled it in from Config.Global.HTTPConfig. This mirrors
// upstream's webhook/email/slack defaulting in config.Config.UnmarshalYAML.
require.NotNil(t, got.GoogleChatConfigs[0].HTTPConfig, "HTTPConfig should be threaded from Global")
assert.Same(t, cfg.alertmanagerConfig.Global.HTTPConfig, got.GoogleChatConfigs[0].HTTPConfig)
// Persisted blob: reload it and confirm the same.
reloaded, err := NewConfigFromStoreableConfig(cfg.StoreableConfig())
require.NoError(t, err)
reloadedReceiver, err := reloaded.GetReceiver("googlechat-receiver")
require.NoError(t, err)
require.Len(t, reloadedReceiver.GoogleChatConfigs, 1)
assert.Equal(t, "Alert", reloadedReceiver.GoogleChatConfigs[0].Title)
assert.Equal(t, "Body", reloadedReceiver.GoogleChatConfigs[0].Text)
assert.Equal(t, "https://chat.googleapis.com/v1/spaces/test/messages", reloadedReceiver.GoogleChatConfigs[0].WebhookURL.String())
// HTTPConfig persisted into the blob and re-hydrated on load.
require.NotNil(t, reloadedReceiver.GoogleChatConfigs[0].HTTPConfig)
// Update path keeps the sidecar in sync.
receiver.GoogleChatConfigs[0].Title = "Updated"
require.NoError(t, cfg.UpdateReceiver(receiver))
updated, err := cfg.GetReceiver("googlechat-receiver")
require.NoError(t, err)
require.Len(t, updated.GoogleChatConfigs, 1)
assert.Equal(t, "Updated", updated.GoogleChatConfigs[0].Title)
}

View File

@@ -1,53 +0,0 @@
package alertmanagertypes
import (
"github.com/prometheus/alertmanager/config"
commoncfg "github.com/prometheus/common/config"
)
// GoogleChatReceiverConfig is a SigNoz-native notifier config that upstream
// alertmanager does not know about. It is carried on Receiver alongside the
// embedded *config.Receiver and round-trips through JSON via that embed's
// struct tags — Channel.Data and the stored config blob preserve it
// automatically without any separate registry or marshalling.
//
// The shape mirrors upstream's notifier configs (e.g. SlackConfig): the
// inline-embedded NotifierConfig contributes send_resolved + the
// SendResolved() method that the notify pipeline uses to gate resolved
// notifications, and HTTPConfig is filled in from Config.Global.HTTPConfig
// when omitted (see Config.applyNativeDefaults). Only the config shape is
// defined here; a future notifier package would consume these fields, POST
// to the webhook, and implement notify.Notifier.
type GoogleChatReceiverConfig struct {
config.NotifierConfig `yaml:",inline" json:",inline"`
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
WebhookURL *config.SecretURL `yaml:"webhook_url,omitempty" json:"webhook_url,omitempty"`
Title string `yaml:"title,omitempty" json:"title,omitempty"`
Text string `yaml:"text,omitempty" json:"text,omitempty"`
}
// DefaultGoogleChatReceiverConfig holds the defaults applied by
// GoogleChatReceiverConfig.UnmarshalYAML before user-specified fields are
// overlaid. Mirrors upstream's DefaultSlackConfig / DefaultPagerdutyConfig.
var DefaultGoogleChatReceiverConfig = GoogleChatReceiverConfig{
NotifierConfig: config.NotifierConfig{
VSendResolved: false,
},
Title: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }}`,
Text: `{{ range .Alerts -}}
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} ({{ .Labels.severity }}){{ end }}{{ if .Annotations.summary }}
*Summary:* {{ .Annotations.summary }}{{ end }}{{ if .Annotations.description }}
*Description:* {{ .Annotations.description }}{{ end }}
{{ end }}`,
}
// UnmarshalYAML implements the per-config defaulting pattern used by every
// upstream notifier config: install the defaults first, then overlay the
// user-specified fields. Triggered by the yaml round-trip in NewReceiver.
func (c *GoogleChatReceiverConfig) UnmarshalYAML(unmarshal func(any) error) error {
*c = DefaultGoogleChatReceiverConfig
type plain GoogleChatReceiverConfig
return unmarshal((*plain)(c))
}

View File

@@ -22,7 +22,7 @@ func TestAddRuleIDToRoute(t *testing.T) {
{
name: "Simple",
route: func() *config.Route {
route, err := NewRouteFromReceiver(&Receiver{Receiver: &config.Receiver{Name: "test"}})
route, err := NewRouteFromReceiver(Receiver{Name: "test"})
require.NoError(t, err)
return route
@@ -33,7 +33,7 @@ func TestAddRuleIDToRoute(t *testing.T) {
{
name: "AlreadyExists",
route: func() *config.Route {
route, err := NewRouteFromReceiver(&Receiver{Receiver: &config.Receiver{Name: "test"}})
route, err := NewRouteFromReceiver(Receiver{Name: "test"})
require.NoError(t, err)
err = addRuleIDToRoute(route, "1")
@@ -84,7 +84,7 @@ func TestRemoveRuleIDFromRoute(t *testing.T) {
{
name: "Simple",
route: func() *config.Route {
route, err := NewRouteFromReceiver(&Receiver{Receiver: &config.Receiver{Name: "test"}})
route, err := NewRouteFromReceiver(Receiver{Name: "test"})
require.NoError(t, err)
err = addRuleIDToRoute(route, "1")
@@ -98,7 +98,7 @@ func TestRemoveRuleIDFromRoute(t *testing.T) {
{
name: "DoesNotExist",
route: func() *config.Route {
route, err := NewRouteFromReceiver(&Receiver{Receiver: &config.Receiver{Name: "test"}})
route, err := NewRouteFromReceiver(Receiver{Name: "test"})
require.NoError(t, err)
return route
@@ -109,7 +109,7 @@ func TestRemoveRuleIDFromRoute(t *testing.T) {
{
name: "DeleteMatcher",
route: func() *config.Route {
route, err := NewRouteFromReceiver(&Receiver{Receiver: &config.Receiver{Name: "test"}})
route, err := NewRouteFromReceiver(Receiver{Name: "test"})
require.NoError(t, err)
return route

View File

@@ -17,4 +17,4 @@ type Templater interface {
// ReceiverIntegrationsFunc constructs the notify.Integration list for a
// configured receiver.
type ReceiverIntegrationsFunc = func(nc *Receiver, tmpl *template.Template, logger *slog.Logger, templater Templater) ([]notify.Integration, error)
type ReceiverIntegrationsFunc = func(nc Receiver, tmpl *template.Template, logger *slog.Logger, templater Templater) ([]notify.Integration, error)

View File

@@ -17,97 +17,40 @@ import (
"github.com/prometheus/alertmanager/config"
)
// Receiver is the SigNoz receiver type. It embeds the upstream alertmanager
// config.Receiver so every upstream notifier field (slack_configs,
// webhook_configs, email_configs, ...) is promoted inline and round-trips
// through encoding/json transparently.
//
// The struct is also the home for SigNoz-native notifier configs that upstream
// alertmanager does not know about (currently: GoogleChatConfigs). Because
// config.Receiver has no custom (Un)MarshalJSON, declaring a sibling field on
// this struct with matching json/yaml tags lets json.Marshal and json.Unmarshal
// carry that field alongside the upstream ones in a single pass — no
// allow-list, no post-marshal patching.
//
// To add another native notifier, mirror GoogleChatConfigs: add a typed slice
// field here, add the same field on customReceiverConfigs in config.go, and
// extend customConfigsOf / isEmpty. The serialization, in-memory storage, and
// channel-type detection all flow from there without further changes.
type Receiver struct {
*config.Receiver
GoogleChatConfigs []*GoogleChatReceiverConfig `json:"googlechat_configs,omitempty" yaml:"googlechat_configs,omitempty"`
}
type (
// Receiver is the type for the receiver configuration.
Receiver = config.Receiver
)
// NewReceiver builds a Receiver from its JSON representation, applying the
// per-config defaults from each notifier's UnmarshalYAML (mirrors upstream:
// every notifier config sets `*c = DefaultXxxConfig` first, then overlays the
// user-specified fields). Global-scoped defaulting (HTTPConfig from
// Config.Global.HTTPConfig) happens later, in Config.applyNativeDefaults.
//
// The only default missed is `send_resolved` (a bool) which, if absent from
// the input, stays false.
func NewReceiver(input string) (*Receiver, error) {
receiver := &Receiver{Receiver: &config.Receiver{}}
if err := json.Unmarshal([]byte(input), receiver); err != nil {
return nil, err
}
// Default the embedded upstream base. Each upstream *Config's UnmarshalYAML
// runs during the yaml roundtrip and applies its DefaultXxxConfig.
withDefaults, err := defaultedBaseReceiver(receiver.Receiver)
// Creates a new receiver from a string. The input is initialized with the default values from the upstream alertmanager.
// The only default value which is missed is `send_resolved` (as it is a bool) which if not set in the input will always be set to `false`.
func NewReceiver(input string) (Receiver, error) {
receiver := Receiver{}
err := json.Unmarshal([]byte(input), &receiver)
if err != nil {
return nil, err
}
receiver.Receiver = withDefaults
// Default each SigNoz-native notifier config the same way. Extend this
// block when adding another native notifier type.
for i, gc := range receiver.GoogleChatConfigs {
defaulted, err := defaultedNotifierConfig(gc)
if err != nil {
return nil, err
}
receiver.GoogleChatConfigs[i] = defaulted
return Receiver{}, err
}
return receiver, nil
}
func defaultedBaseReceiver(base *config.Receiver) (*config.Receiver, error) {
bytes, err := yaml.Marshal(base)
// We marshal and unmarshal the receiver to ensure that the receiver is
// initialized with defaults from the upstream alertmanager.
bytes, err := yaml.Marshal(receiver)
if err != nil {
return nil, err
return Receiver{}, err
}
withDefaults := &config.Receiver{}
if err := yaml.Unmarshal(bytes, withDefaults); err != nil {
return nil, err
receiverWithDefaults := Receiver{}
if err := yaml.Unmarshal(bytes, &receiverWithDefaults); err != nil {
return Receiver{}, err
}
if err := withDefaults.UnmarshalYAML(func(i interface{}) error { return nil }); err != nil {
return nil, err
if err := receiverWithDefaults.UnmarshalYAML(func(i interface{}) error { return nil }); err != nil {
return Receiver{}, err
}
return withDefaults, nil
return receiverWithDefaults, nil
}
// defaultedNotifierConfig applies a single notifier config's per-config defaults
// by round-tripping it through yaml. UnmarshalYAML on the config type runs
// during the unmarshal step and installs DefaultXxxConfig before re-overlaying
// the user-specified fields (the standard upstream defaulting pattern).
func defaultedNotifierConfig[T any](cfg *T) (*T, error) {
bytes, err := yaml.Marshal(cfg)
if err != nil {
return nil, err
}
out := new(T)
if err := yaml.Unmarshal(bytes, out); err != nil {
return nil, err
}
return out, nil
}
func TestReceiver(ctx context.Context, receiver *Receiver, receiverIntegrationsFunc ReceiverIntegrationsFunc, config *Config, tmpl *template.Template, logger *slog.Logger, templater Templater, lSet model.LabelSet, alert ...*Alert) error {
func TestReceiver(ctx context.Context, receiver Receiver, receiverIntegrationsFunc ReceiverIntegrationsFunc, config *Config, tmpl *template.Template, logger *slog.Logger, templater Templater, lSet model.LabelSet, alert ...*Alert) error {
ctx = notify.WithGroupKey(ctx, fmt.Sprintf("%s-%s-%d", receiver.Name, lSet.Fingerprint(), time.Now().Unix()))
ctx = notify.WithGroupLabels(ctx, lSet)
ctx = notify.WithReceiverName(ctx, receiver.Name)
@@ -124,12 +67,12 @@ func TestReceiver(ctx context.Context, receiver *Receiver, receiverIntegrationsF
return err
}
defaultedReceiver, err := testConfig.GetReceiver(receiver.Name)
receiver, err = testConfig.GetReceiver(receiver.Name)
if err != nil {
return err
}
integrations, err := receiverIntegrationsFunc(defaultedReceiver, tmpl, logger, templater)
integrations, err := receiverIntegrationsFunc(receiver, tmpl, logger, templater)
if err != nil {
return err
}

View File

@@ -21,18 +21,6 @@ func TestNewReceiver(t *testing.T) {
expected: `{"name":"telegram","telegram_configs":[{"send_resolved":false,"token":"1234567890","chat":12345,"message":"{{ template \"telegram.default.message\" . }}","parse_mode":"HTML"}]}`,
pass: true,
},
{
// GoogleChatConfig exercises the SigNoz-native side of the
// Receiver embed: googlechat_configs is unmarshalled into the
// sibling field and re-marshalled alongside the upstream fields
// in a single pass. send_resolved is contributed by the embedded
// NotifierConfig and is always emitted (no omitempty), matching
// upstream's behaviour for every other notifier config.
name: "GoogleChatConfig",
input: `{"name":"googlechat","googlechat_configs":[{"webhook_url":"https://chat.googleapis.com/v1/spaces/test/messages","title":"Alert","text":"Body"}]}`,
expected: `{"name":"googlechat","googlechat_configs":[{"send_resolved":false,"webhook_url":"https://chat.googleapis.com/v1/spaces/test/messages","title":"Alert","text":"Body"}]}`,
pass: true,
},
}
for _, tc := range testCases {
@@ -51,35 +39,3 @@ func TestNewReceiver(t *testing.T) {
})
}
}
// TestNewReceiverGoogleChatAppliesDefaults verifies the per-config defaulting
// mechanism for SigNoz-native configs: when the user omits Title / Text /
// send_resolved, GoogleChatReceiverConfig.UnmarshalYAML installs the values
// from DefaultGoogleChatReceiverConfig before any user-specified fields are
// overlaid. This mirrors how every upstream notifier config defaults itself
// (e.g. DefaultSlackConfig).
func TestNewReceiverGoogleChatAppliesDefaults(t *testing.T) {
receiver, err := NewReceiver(`{"name":"googlechat","googlechat_configs":[{"webhook_url":"https://chat.googleapis.com/v1/spaces/test/messages"}]}`)
require.NoError(t, err)
require.Len(t, receiver.GoogleChatConfigs, 1)
got := receiver.GoogleChatConfigs[0]
assert.Equal(t, DefaultGoogleChatReceiverConfig.Title, got.Title, "Title should fall back to the default template")
assert.Equal(t, DefaultGoogleChatReceiverConfig.Text, got.Text, "Text should fall back to the default template")
assert.Equal(t, DefaultGoogleChatReceiverConfig.VSendResolved, got.SendResolved(), "send_resolved should fall back to the default")
}
// TestNewReceiverGoogleChatPreservesUserOverrides verifies that user-specified
// values survive the defaulting pass — the default is installed first, then
// the user's fields are overlaid. send_resolved=true from the input must win
// over the default's false.
func TestNewReceiverGoogleChatPreservesUserOverrides(t *testing.T) {
receiver, err := NewReceiver(`{"name":"googlechat","googlechat_configs":[{"webhook_url":"https://chat.googleapis.com/v1/spaces/test/messages","title":"X","text":"Y","send_resolved":true}]}`)
require.NoError(t, err)
require.Len(t, receiver.GoogleChatConfigs, 1)
got := receiver.GoogleChatConfigs[0]
assert.Equal(t, "X", got.Title)
assert.Equal(t, "Y", got.Text)
assert.True(t, got.SendResolved())
}

View File

@@ -28,7 +28,7 @@ func NewRouteFromRouteConfig(route *config.Route, cfg RouteConfig) (*config.Rout
return route, nil
}
func NewRouteFromReceiver(receiver *Receiver) (*config.Route, error) {
func NewRouteFromReceiver(receiver Receiver) (*config.Route, error) {
route := &config.Route{Receiver: receiver.Name, Continue: true, Matchers: config.Matchers{noRuleIDMatcher}}
if err := route.UnmarshalYAML(func(i interface{}) error { return nil }); err != nil {
return nil, err