diff --git a/conf/example.yaml b/conf/example.yaml index 7fd1fb9e976c..d22fa37cab0e 100644 --- a/conf/example.yaml +++ b/conf/example.yaml @@ -137,10 +137,7 @@ prometheus: ##################### Alertmanager ##################### alertmanager: # Specifies the alertmanager provider to use. - provider: legacy - legacy: - # The API URL (with prefix) of the legacy Alertmanager instance. - api_url: http://localhost:9093/api + provider: signoz signoz: # The poll interval for periodically syncing the alertmanager with the config in the store. poll_interval: 1m diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index fc516f49e95a..b83dd4f51b10 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -44,19 +44,6 @@ import ( "go.uber.org/zap" ) -type ServerOptions struct { - Config signoz.Config - SigNoz *signoz.SigNoz - HTTPHostPort string - PrivateHostPort string - PreferSpanMetrics bool - FluxInterval string - FluxIntervalForTraceDetail string - Cluster string - GatewayUrl string - Jwt *authtypes.JWT -} - // Server runs HTTP, Mux and a grpc server type Server struct { config signoz.Config @@ -69,11 +56,6 @@ type Server struct { httpServer *http.Server httpHostPort string - // private http - privateConn net.Listener - privateHTTP *http.Server - privateHostPort string - opampServer *opamp.Server // Usage manager @@ -183,7 +165,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT) jwt: jwt, ruleManager: rm, httpHostPort: baseconst.HTTPHostPort, - privateHostPort: baseconst.PrivateHostPort, unavailableChannel: make(chan healthcheck.Status), usageManager: usageManager, } @@ -196,13 +177,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT) s.httpServer = httpServer - privateServer, err := s.createPrivateServer(apiHandler) - if err != nil { - return nil, err - } - - s.privateHTTP = privateServer - s.opampServer = opamp.InitializeServer( &opAmpModel.AllAgents, agentConfMgr, signoz.Instrumentation, ) @@ -215,36 +189,6 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status { return s.unavailableChannel } -func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, error) { - r := baseapp.NewRouter() - - r.Use(middleware.NewAuth(s.jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap) - r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap) - r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(), - s.config.APIServer.Timeout.ExcludedRoutes, - s.config.APIServer.Timeout.Default, - s.config.APIServer.Timeout.Max, - ).Wrap) - r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap) - - apiHandler.RegisterPrivateRoutes(r) - - c := cors.New(cors.Options{ - //todo(amol): find out a way to add exact domain or - // ip here for alert manager - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "SIGNOZ-API-KEY", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"}, - }) - - handler := c.Handler(r) - handler = handlers.CompressHandler(handler) - - return &http.Server{ - Handler: handler, - }, nil -} - func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*http.Server, error) { r := baseapp.NewRouter() am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger()) @@ -310,19 +254,6 @@ func (s *Server) initListeners() error { zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.httpHostPort)) - // listen on private port to support internal services - privateHostPort := s.privateHostPort - - if privateHostPort == "" { - return fmt.Errorf("baseconst.PrivateHostPort is required") - } - - s.privateConn, err = net.Listen("tcp", privateHostPort) - if err != nil { - return err - } - zap.L().Info(fmt.Sprintf("Query server started listening on private port %s...", s.privateHostPort)) - return nil } @@ -361,26 +292,6 @@ func (s *Server) Start(ctx context.Context) error { } }() - var privatePort int - if port, err := utils.GetPort(s.privateConn.Addr()); err == nil { - privatePort = port - } - - go func() { - zap.L().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.privateHostPort)) - - switch err := s.privateHTTP.Serve(s.privateConn); err { - case nil, http.ErrServerClosed, cmux.ErrListenerClosed: - // normal exit, nothing to do - zap.L().Info("private http server closed") - default: - zap.L().Error("Could not start private HTTP server", zap.Error(err)) - } - - s.unavailableChannel <- healthcheck.Unavailable - - }() - go func() { zap.L().Info("Starting OpAmp Websocket server", zap.String("addr", baseconst.OpAmpWsEndpoint)) err := s.opampServer.Start(baseconst.OpAmpWsEndpoint) @@ -400,12 +311,6 @@ func (s *Server) Stop(ctx context.Context) error { } } - if s.privateHTTP != nil { - if err := s.privateHTTP.Shutdown(ctx); err != nil { - return err - } - } - s.opampServer.Stop() if s.ruleManager != nil { diff --git a/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest/test.go b/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest/test.go new file mode 100644 index 000000000000..3b983c4fee7e --- /dev/null +++ b/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest/test.go @@ -0,0 +1,179 @@ +package test + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/SigNoz/signoz/pkg/types/alertmanagertypes" + "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" + + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" +) + +// RetryTests returns a map of HTTP status codes to bool indicating whether the notifier should retry or not. +func RetryTests(retryCodes []int) map[int]bool { + tests := map[int]bool{ + // 1xx + http.StatusContinue: false, + http.StatusSwitchingProtocols: false, + http.StatusProcessing: false, + + // 2xx + http.StatusOK: false, + http.StatusCreated: false, + http.StatusAccepted: false, + http.StatusNonAuthoritativeInfo: false, + http.StatusNoContent: false, + http.StatusResetContent: false, + http.StatusPartialContent: false, + http.StatusMultiStatus: false, + http.StatusAlreadyReported: false, + http.StatusIMUsed: false, + + // 3xx + http.StatusMultipleChoices: false, + http.StatusMovedPermanently: false, + http.StatusFound: false, + http.StatusSeeOther: false, + http.StatusNotModified: false, + http.StatusUseProxy: false, + http.StatusTemporaryRedirect: false, + http.StatusPermanentRedirect: false, + + // 4xx + http.StatusBadRequest: false, + http.StatusUnauthorized: false, + http.StatusPaymentRequired: false, + http.StatusForbidden: false, + http.StatusNotFound: false, + http.StatusMethodNotAllowed: false, + http.StatusNotAcceptable: false, + http.StatusProxyAuthRequired: false, + http.StatusRequestTimeout: false, + http.StatusConflict: false, + http.StatusGone: false, + http.StatusLengthRequired: false, + http.StatusPreconditionFailed: false, + http.StatusRequestEntityTooLarge: false, + http.StatusRequestURITooLong: false, + http.StatusUnsupportedMediaType: false, + http.StatusRequestedRangeNotSatisfiable: false, + http.StatusExpectationFailed: false, + http.StatusTeapot: false, + http.StatusUnprocessableEntity: false, + http.StatusLocked: false, + http.StatusFailedDependency: false, + http.StatusUpgradeRequired: false, + http.StatusPreconditionRequired: false, + http.StatusTooManyRequests: false, + http.StatusRequestHeaderFieldsTooLarge: false, + http.StatusUnavailableForLegalReasons: false, + + // 5xx + http.StatusInternalServerError: false, + http.StatusNotImplemented: false, + http.StatusBadGateway: false, + http.StatusServiceUnavailable: false, + http.StatusGatewayTimeout: false, + http.StatusHTTPVersionNotSupported: false, + http.StatusVariantAlsoNegotiates: false, + http.StatusInsufficientStorage: false, + http.StatusLoopDetected: false, + http.StatusNotExtended: false, + http.StatusNetworkAuthenticationRequired: false, + } + + for _, statusCode := range retryCodes { + tests[statusCode] = true + } + + return tests +} + +// DefaultRetryCodes returns the list of HTTP status codes that need to be retried. +func DefaultRetryCodes() []int { + return []int{ + http.StatusInternalServerError, + http.StatusNotImplemented, + http.StatusBadGateway, + http.StatusServiceUnavailable, + http.StatusGatewayTimeout, + http.StatusHTTPVersionNotSupported, + http.StatusVariantAlsoNegotiates, + http.StatusInsufficientStorage, + http.StatusLoopDetected, + http.StatusNotExtended, + http.StatusNetworkAuthenticationRequired, + } +} + +// CreateTmpl returns a ready-to-use template. +func CreateTmpl(t *testing.T) *template.Template { + tmpl, err := alertmanagertypes.FromGlobs([]string{}) + require.NoError(t, err) + tmpl.ExternalURL, _ = url.Parse("http://am") + return tmpl +} + +// AssertNotifyLeaksNoSecret calls the Notify() method of the notifier, expects +// it to fail because the context is canceled by the server and checks that no +// secret data is leaked in the error message returned by Notify(). +func AssertNotifyLeaksNoSecret(ctx context.Context, t *testing.T, n notify.Notifier, secret ...string) { + t.Helper() + require.NotEmpty(t, secret) + + ctx = notify.WithGroupKey(ctx, "1") + ok, err := n.Notify(ctx, []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{ + "lbl1": "val1", + }, + StartsAt: time.Now(), + EndsAt: time.Now().Add(time.Hour), + }, + }, + }...) + + require.Error(t, err) + require.Contains(t, err.Error(), context.Canceled.Error()) + for _, s := range secret { + require.NotContains(t, err.Error(), s) + } + require.True(t, ok) +} + +// GetContextWithCancelingURL returns a context that gets canceled when a +// client does a GET request to the returned URL. +// Handlers passed to the function will be invoked in order before the context gets canceled. +// The last argument is a function that needs to be called before the caller returns. +func GetContextWithCancelingURL(h ...func(w http.ResponseWriter, r *http.Request)) (context.Context, *url.URL, func()) { + done := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) + i := 0 + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if i < len(h) { + h[i](w, r) + } else { + cancel() + <-done + } + i++ + })) + + // No need to check the error since httptest.NewServer always return a valid URL. + u, _ := url.Parse(srv.URL) + + return ctx, u, func() { + close(done) + srv.Close() + } +} diff --git a/pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2.go b/pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2.go new file mode 100644 index 000000000000..d2be7ed19975 --- /dev/null +++ b/pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2.go @@ -0,0 +1,265 @@ +package msteamsv2 + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "os" + "slices" + "strings" + + commoncfg "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" +) + +const ( + colorRed = "Attention" + colorGreen = "Good" + colorGrey = "Warning" +) + +type Notifier struct { + conf *config.MSTeamsV2Config + titleLink string + tmpl *template.Template + logger *slog.Logger + client *http.Client + retrier *notify.Retrier + webhookURL *config.SecretURL + postJSONFunc func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) +} + +// https://learn.microsoft.com/en-us/connectors/teams/?tabs=text1#adaptivecarditemschema +type Content struct { + Schema string `json:"$schema"` + Type string `json:"type"` + Version string `json:"version"` + Body []Body `json:"body"` + Msteams Msteams `json:"msteams,omitempty"` + Actions []Action `json:"actions"` +} + +type Body struct { + Type string `json:"type"` + Text string `json:"text"` + Weight string `json:"weight,omitempty"` + Size string `json:"size,omitempty"` + Wrap bool `json:"wrap,omitempty"` + Style string `json:"style,omitempty"` + Color string `json:"color,omitempty"` + Facts []Fact `json:"facts,omitempty"` +} + +type Action struct { + Type string `json:"type"` + Title string `json:"title"` + URL string `json:"url"` +} + +type Fact struct { + Title string `json:"title"` + Value string `json:"value"` +} + +type Msteams struct { + Width string `json:"width"` +} + +type Attachment struct { + ContentType string `json:"contentType"` + ContentURL *string `json:"contentUrl"` // Use a pointer to handle null values + Content Content `json:"content"` +} + +type teamsMessage struct { + Type string `json:"type"` + Attachments []Attachment `json:"attachments"` +} + +// New returns a new notifier that uses the Microsoft Teams Power Platform connector. +func New(c *config.MSTeamsV2Config, t *template.Template, titleLink string, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) { + client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "msteamsv2", httpOpts...) + if err != nil { + return nil, err + } + + n := &Notifier{ + conf: c, + titleLink: titleLink, + tmpl: t, + logger: l, + client: client, + retrier: ¬ify.Retrier{}, + webhookURL: c.WebhookURL, + postJSONFunc: notify.PostJSON, + } + + return n, nil +} + +func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { + key, err := notify.ExtractGroupKey(ctx) + if err != nil { + return false, err + } + + n.logger.DebugContext(ctx, "extracted group key", "key", key) + + data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger) + tmpl := notify.TmplText(n.tmpl, data, &err) + if err != nil { + return false, err + } + + title := tmpl(n.conf.Title) + if err != nil { + return false, err + } + + titleLink := tmpl(n.titleLink) + if err != nil { + return false, err + } + + alerts := types.Alerts(as...) + color := colorGrey + switch alerts.Status() { + case model.AlertFiring: + color = colorRed + case model.AlertResolved: + color = colorGreen + } + + var url string + if n.conf.WebhookURL != nil { + url = n.conf.WebhookURL.String() + } else { + content, err := os.ReadFile(n.conf.WebhookURLFile) + if err != nil { + return false, fmt.Errorf("read webhook_url_file: %w", err) + } + url = strings.TrimSpace(string(content)) + } + + // A message as referenced in https://learn.microsoft.com/en-us/connectors/teams/?tabs=text1%2Cdotnet#request-body-schema + t := teamsMessage{ + Type: "message", + Attachments: []Attachment{ + { + ContentType: "application/vnd.microsoft.card.adaptive", + ContentURL: nil, + Content: Content{ + Schema: "http://adaptivecards.io/schemas/adaptive-card.json", + Type: "AdaptiveCard", + Version: "1.2", + Body: []Body{ + { + Type: "TextBlock", + Text: title, + Weight: "Bolder", + Size: "Medium", + Wrap: true, + Style: "heading", + Color: color, + }, + }, + Actions: []Action{ + { + Type: "Action.OpenUrl", + Title: "View Alert", + URL: titleLink, + }, + }, + Msteams: Msteams{ + Width: "full", + }, + }, + }, + }, + } + + // add labels and annotations to the body of all alerts + for _, alert := range as { + t.Attachments[0].Content.Body = append(t.Attachments[0].Content.Body, Body{ + Type: "TextBlock", + Text: "Alerts", + Weight: "Bolder", + Size: "Medium", + Wrap: true, + Color: color, + }) + + t.Attachments[0].Content.Body = append(t.Attachments[0].Content.Body, n.createLabelsAndAnnotationsBody(alert)...) + } + + var payload bytes.Buffer + if err = json.NewEncoder(&payload).Encode(t); err != nil { + return false, err + } + + resp, err := n.postJSONFunc(ctx, n.client, url, &payload) //nolint:bodyclose + if err != nil { + return true, notify.RedactURL(err) + } + defer notify.Drain(resp) //drain is used to close the body of the response hence the nolint directive + + // https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#rate-limiting-for-connectors + shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body) + if err != nil { + return shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err) + } + return shouldRetry, err +} + +func (*Notifier) createLabelsAndAnnotationsBody(alert *types.Alert) []Body { + bodies := []Body{} + bodies = append(bodies, Body{ + Type: "TextBlock", + Text: "Labels", + Weight: "Bolder", + Size: "Medium", + }) + + facts := []Fact{} + for k, v := range alert.Labels { + if slices.Contains([]string{"alertname", "severity", "ruleId", "ruleSource"}, string(k)) { + continue + } + facts = append(facts, Fact{Title: string(k), Value: string(v)}) + } + bodies = append(bodies, Body{ + Type: "FactSet", + Facts: facts, + }) + + bodies = append(bodies, Body{ + Type: "TextBlock", + Text: "Annotations", + Weight: "Bolder", + Size: "Medium", + }) + + annotationsFacts := []Fact{} + for k, v := range alert.Annotations { + if slices.Contains([]string{"summary", "related_logs", "related_traces"}, string(k)) { + continue + } + annotationsFacts = append(annotationsFacts, Fact{Title: string(k), Value: string(v)}) + } + + bodies = append(bodies, Body{ + Type: "FactSet", + Facts: annotationsFacts, + }) + + return bodies +} diff --git a/pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2_test.go b/pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2_test.go new file mode 100644 index 000000000000..2a0d884bb1e1 --- /dev/null +++ b/pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2_test.go @@ -0,0 +1,220 @@ +package msteamsv2 + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + "time" + + commoncfg "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + "github.com/prometheus/common/promslog" + "github.com/stretchr/testify/require" + + test "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest" + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/types" +) + +// This is a test URL that has been modified to not be valid. +var testWebhookURL, _ = url.Parse("https://example.westeurope.logic.azure.com:443/workflows/xxx/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=xxx") + +func TestMSTeamsV2Retry(t *testing.T) { + notifier, err := New( + &config.MSTeamsV2Config{ + WebhookURL: &config.SecretURL{URL: testWebhookURL}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + `{{ template "msteamsv2.default.titleLink" . }}`, + promslog.NewNopLogger(), + ) + require.NoError(t, err) + + for statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) { + actual, _ := notifier.retrier.Check(statusCode, nil) + require.Equal(t, expected, actual, "retry - error on status %d", statusCode) + } +} + +func TestNotifier_Notify_WithReason(t *testing.T) { + tests := []struct { + name string + statusCode int + responseContent string + expectedReason notify.Reason + noError bool + }{ + { + name: "with a 2xx status code and response 1", + statusCode: http.StatusOK, + responseContent: "1", + noError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + notifier, err := New( + &config.MSTeamsV2Config{ + WebhookURL: &config.SecretURL{URL: testWebhookURL}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + `{{ template "msteamsv2.default.titleLink" . }}`, + promslog.NewNopLogger(), + ) + require.NoError(t, err) + + notifier.postJSONFunc = func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) { + resp := httptest.NewRecorder() + _, err := resp.WriteString(tt.responseContent) + require.NoError(t, err) + resp.WriteHeader(tt.statusCode) + return resp.Result(), nil + } + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + + alert1 := &types.Alert{ + Alert: model.Alert{ + StartsAt: time.Now(), + EndsAt: time.Now().Add(time.Hour), + }, + } + _, err = notifier.Notify(ctx, alert1) + if tt.noError { + require.NoError(t, err) + } else { + var reasonError *notify.ErrorWithReason + require.ErrorAs(t, err, &reasonError) + require.Equal(t, tt.expectedReason, reasonError.Reason) + } + }) + } +} + +func TestMSTeamsV2Templating(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + dec := json.NewDecoder(r.Body) + out := make(map[string]any) + err := dec.Decode(&out) + if err != nil { + panic(err) + } + })) + defer srv.Close() + u, _ := url.Parse(srv.URL) + + for _, tc := range []struct { + title string + cfg *config.MSTeamsV2Config + titleLink string + + retry bool + errMsg string + }{ + { + title: "full-blown message", + cfg: &config.MSTeamsV2Config{ + Title: `{{ template "msteams.default.title" . }}`, + Text: `{{ template "msteams.default.text" . }}`, + }, + titleLink: `{{ template "msteamsv2.default.titleLink" . }}`, + retry: false, + }, + { + title: "title with templating errors", + cfg: &config.MSTeamsV2Config{ + Title: "{{ ", + }, + titleLink: `{{ template "msteamsv2.default.titleLink" . }}`, + errMsg: "template: :1: unclosed action", + }, + { + title: "message with title link templating errors", + cfg: &config.MSTeamsV2Config{ + Title: `{{ template "msteams.default.title" . }}`, + Text: `{{ template "msteams.default.text" . }}`, + }, + titleLink: `{{ `, + errMsg: "template: :1: unclosed action", + }, + } { + t.Run(tc.title, func(t *testing.T) { + tc.cfg.WebhookURL = &config.SecretURL{URL: u} + tc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{} + pd, err := New(tc.cfg, test.CreateTmpl(t), tc.titleLink, promslog.NewNopLogger()) + require.NoError(t, err) + + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + + ok, err := pd.Notify(ctx, []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{ + "lbl1": "val1", + }, + StartsAt: time.Now(), + EndsAt: time.Now().Add(time.Hour), + }, + }, + }...) + if tc.errMsg == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMsg) + } + require.Equal(t, tc.retry, ok) + }) + } +} + +func TestMSTeamsV2RedactedURL(t *testing.T) { + ctx, u, fn := test.GetContextWithCancelingURL() + defer fn() + + secret := "secret" + notifier, err := New( + &config.MSTeamsV2Config{ + WebhookURL: &config.SecretURL{URL: u}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + `{{ template "msteamsv2.default.titleLink" . }}`, + promslog.NewNopLogger(), + ) + require.NoError(t, err) + + test.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret) +} + +func TestMSTeamsV2ReadingURLFromFile(t *testing.T) { + ctx, u, fn := test.GetContextWithCancelingURL() + defer fn() + + f, err := os.CreateTemp("", "webhook_url") + require.NoError(t, err, "creating temp file failed") + _, err = f.WriteString(u.String() + "\n") + require.NoError(t, err, "writing to temp file failed") + + notifier, err := New( + &config.MSTeamsV2Config{ + WebhookURLFile: f.Name(), + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + `{{ template "msteamsv2.default.titleLink" . }}`, + promslog.NewNopLogger(), + ) + require.NoError(t, err) + + test.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String()) +} diff --git a/pkg/alertmanager/alertmanagernotify/receiver.go b/pkg/alertmanager/alertmanagernotify/receiver.go new file mode 100644 index 000000000000..33fd63f82454 --- /dev/null +++ b/pkg/alertmanager/alertmanagernotify/receiver.go @@ -0,0 +1,51 @@ +package alertmanagernotify + +import ( + "log/slog" + + "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/msteamsv2" + "github.com/SigNoz/signoz/pkg/types/alertmanagertypes" + "github.com/prometheus/alertmanager/config/receiver" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" +) + +func NewReceiverIntegrations(nc alertmanagertypes.Receiver, tmpl *template.Template, logger *slog.Logger) ([]notify.Integration, error) { + upstreamIntegrations, err := receiver.BuildReceiverIntegrations(nc, tmpl, logger) + if err != nil { + return nil, err + } + + var ( + errs types.MultiError + integrations []notify.Integration + add = func(name string, i int, rs notify.ResolvedSender, f func(l *slog.Logger) (notify.Notifier, error)) { + n, err := f(logger.With("integration", name)) + if err != nil { + errs.Add(err) + return + } + integrations = append(integrations, notify.NewIntegration(n, rs, name, i, nc.Name)) + } + ) + + for _, integration := range upstreamIntegrations { + // skip upstream msteamsv2 integration + if integration.Name() != "msteamsv2" { + integrations = append(integrations, integration) + } + } + + for i, c := range nc.MSTeamsV2Configs { + add("msteamsv2", i, c, func(l *slog.Logger) (notify.Notifier, error) { + return msteamsv2.New(c, tmpl, `{{ template "msteamsv2.default.titleLink" . }}`, l) + }) + } + + if errs.Len() > 0 { + return nil, &errs + } + + return integrations, nil +} diff --git a/pkg/alertmanager/alertmanagerserver/server.go b/pkg/alertmanager/alertmanagerserver/server.go index 55662340f528..86ea94570be0 100644 --- a/pkg/alertmanager/alertmanagerserver/server.go +++ b/pkg/alertmanager/alertmanagerserver/server.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/types/alertmanagertypes" "github.com/prometheus/alertmanager/dispatch" @@ -243,7 +244,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma server.logger.InfoContext(ctx, "skipping creation of receiver not referenced by any route", "receiver", rcv.Name) continue } - integrations, err := alertmanagertypes.NewReceiverIntegrations(rcv, server.tmpl, server.logger) + integrations, err := alertmanagernotify.NewReceiverIntegrations(rcv, server.tmpl, server.logger) if err != nil { return err } @@ -316,7 +317,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma } func (server *Server) TestReceiver(ctx context.Context, receiver alertmanagertypes.Receiver) error { - return alertmanagertypes.TestReceiver(ctx, receiver, server.alertmanagerConfig, server.tmpl, server.logger, alertmanagertypes.NewTestAlert(receiver, time.Now(), time.Now())) + return alertmanagertypes.TestReceiver(ctx, receiver, alertmanagernotify.NewReceiverIntegrations, server.alertmanagerConfig, server.tmpl, server.logger, alertmanagertypes.NewTestAlert(receiver, time.Now(), time.Now())) } func (server *Server) TestAlert(ctx context.Context, postableAlert *alertmanagertypes.PostableAlert, receivers []string) error { @@ -337,7 +338,7 @@ func (server *Server) TestAlert(ctx context.Context, postableAlert *alertmanager ch <- err return } - ch <- alertmanagertypes.TestReceiver(ctx, receiver, server.alertmanagerConfig, server.tmpl, server.logger, alerts[0]) + ch <- alertmanagertypes.TestReceiver(ctx, receiver, alertmanagernotify.NewReceiverIntegrations, server.alertmanagerConfig, server.tmpl, server.logger, alerts[0]) }(receiverName) } diff --git a/pkg/alertmanager/config.go b/pkg/alertmanager/config.go index 2b47c7bc8fff..d21bfb185108 100644 --- a/pkg/alertmanager/config.go +++ b/pkg/alertmanager/config.go @@ -2,9 +2,11 @@ package alertmanager import ( "net/url" + "strings" "time" "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerserver" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" ) @@ -14,9 +16,6 @@ type Config struct { // Internal is the internal alertmanager configuration. Signoz Signoz `mapstructure:"signoz" yaml:"signoz"` - - // Legacy is the legacy alertmanager configuration. - Legacy Legacy `mapstructure:"legacy"` } type Signoz struct { @@ -38,14 +37,7 @@ func NewConfigFactory() factory.ConfigFactory { func newConfig() factory.Config { return Config{ - Provider: "legacy", - Legacy: Legacy{ - ApiURL: &url.URL{ - Scheme: "http", - Host: "alertmanager:9093", - Path: "/api", - }, - }, + Provider: "signoz", Signoz: Signoz{ PollInterval: 1 * time.Minute, Config: alertmanagerserver.NewConfig(), @@ -54,5 +46,9 @@ func newConfig() factory.Config { } func (c Config) Validate() error { + if c.Provider != "signoz" { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "provider must be one of [%s], got %s", strings.Join([]string{"signoz"}, ", "), c.Provider) + } + return nil } diff --git a/pkg/alertmanager/config_test.go b/pkg/alertmanager/config_test.go index 30b35aac3df9..4ea9033c6284 100644 --- a/pkg/alertmanager/config_test.go +++ b/pkg/alertmanager/config_test.go @@ -15,7 +15,7 @@ import ( ) func TestNewWithEnvProvider(t *testing.T) { - t.Setenv("SIGNOZ_ALERTMANAGER_PROVIDER", "legacy") + t.Setenv("SIGNOZ_ALERTMANAGER_PROVIDER", "signoz") t.Setenv("SIGNOZ_ALERTMANAGER_LEGACY_API__URL", "http://localhost:9093/api") t.Setenv("SIGNOZ_ALERTMANAGER_SIGNOZ_ROUTE_REPEAT__INTERVAL", "5m") t.Setenv("SIGNOZ_ALERTMANAGER_SIGNOZ_EXTERNAL__URL", "https://example.com/test") @@ -49,15 +49,8 @@ func TestNewWithEnvProvider(t *testing.T) { } expected := &Config{ - Provider: "legacy", - Legacy: Legacy{ - ApiURL: &url.URL{ - Scheme: "http", - Host: "localhost:9093", - Path: "/api", - }, - }, - Signoz: def.Signoz, + Provider: "signoz", + Signoz: def.Signoz, } assert.Equal(t, expected, actual) diff --git a/pkg/alertmanager/legacyalertmanager/provider.go b/pkg/alertmanager/legacyalertmanager/provider.go deleted file mode 100644 index 13137e196c08..000000000000 --- a/pkg/alertmanager/legacyalertmanager/provider.go +++ /dev/null @@ -1,482 +0,0 @@ -package legacyalertmanager - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/SigNoz/signoz/pkg/alertmanager" - "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerbatcher" - "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore" - "github.com/SigNoz/signoz/pkg/factory" - "github.com/SigNoz/signoz/pkg/modules/organization" - "github.com/SigNoz/signoz/pkg/sqlstore" - "github.com/SigNoz/signoz/pkg/types/alertmanagertypes" - "github.com/SigNoz/signoz/pkg/valuer" - "github.com/tidwall/gjson" -) - -type postableAlert struct { - *alertmanagertypes.PostableAlert - Receivers []string `json:"receivers"` -} - -func (pa *postableAlert) MarshalJSON() ([]byte, error) { - // Marshal the embedded PostableAlert to get its JSON representation. - alertJSON, err := json.Marshal(pa.PostableAlert) - if err != nil { - return nil, err - } - - // Unmarshal that JSON into a map so we can add extra fields. - var m map[string]interface{} - if err := json.Unmarshal(alertJSON, &m); err != nil { - return nil, err - } - - // Add the Receivers field. - m["receivers"] = pa.Receivers - - return json.Marshal(m) -} - -const ( - alertsPath string = "/v1/alerts" - routesPath string = "/v1/routes" - testReceiverPath string = "/v1/testReceiver" -) - -type provider struct { - config alertmanager.Config - settings factory.ScopedProviderSettings - client *http.Client - configStore alertmanagertypes.ConfigStore - batcher *alertmanagerbatcher.Batcher - url *url.URL - orgGetter organization.Getter - orgID string -} - -func NewFactory(sqlstore sqlstore.SQLStore, orgGetter organization.Getter) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] { - return factory.NewProviderFactory(factory.MustNewName("legacy"), func(ctx context.Context, settings factory.ProviderSettings, config alertmanager.Config) (alertmanager.Alertmanager, error) { - return New(ctx, settings, config, sqlstore, orgGetter) - }) -} - -func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore, orgGetter organization.Getter) (*provider, error) { - settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/alertmanager/legacyalertmanager") - configStore := sqlalertmanagerstore.NewConfigStore(sqlstore) - - return &provider{ - config: config, - settings: settings, - client: &http.Client{ - Timeout: 30 * time.Second, - }, - configStore: configStore, - batcher: alertmanagerbatcher.New(settings.Logger(), alertmanagerbatcher.NewConfig()), - url: config.Legacy.ApiURL, - orgGetter: orgGetter, - }, nil -} - -func (provider *provider) Start(ctx context.Context) error { - err := provider.batcher.Start(ctx) - if err != nil { - return err - } - - for alerts := range provider.batcher.C { - // For the first time, we need to get the orgID from the config store. - // Since this is the legacy alertmanager, we get the first org from the store. - if provider.orgID == "" { - orgIDs, err := provider.orgGetter.ListByOwnedKeyRange(ctx) - if err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to send alerts to alertmanager", "error", err) - continue - } - - if len(orgIDs) == 0 { - provider.settings.Logger().ErrorContext(ctx, "failed to send alerts to alertmanager", "error", "no orgs found") - continue - } - - provider.orgID = orgIDs[0].ID.String() - } - - if err := provider.putAlerts(ctx, provider.orgID, alerts); err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to send alerts to alertmanager", "error", err) - } - } - - return nil -} - -func (provider *provider) GetAlerts(ctx context.Context, orgID string, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.DeprecatedGettableAlerts, error) { - url := provider.url.JoinPath(alertsPath) - url.RawQuery = params.RawQuery - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil) - if err != nil { - return nil, err - } - req.Header.Add("Content-Type", "application/json") - - resp, err := provider.client.Do(req) - if err != nil { - return nil, err - } - - defer resp.Body.Close() //nolint:errcheck - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode/100 != 2 { - 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 alerts, nil -} - -func (provider *provider) PutAlerts(ctx context.Context, orgID string, alerts alertmanagertypes.PostableAlerts) error { - provider.batcher.Add(ctx, alerts...) - return nil -} - -func (provider *provider) putAlerts(ctx context.Context, orgID string, alerts alertmanagertypes.PostableAlerts) error { - cfg, err := provider.configStore.Get(ctx, orgID) - if err != nil { - return err - } - - var legacyAlerts []postableAlert - for _, alert := range alerts { - ruleID, ok := alert.Alert.Labels[alertmanagertypes.RuleIDMatcherName] - if !ok { - provider.settings.Logger().WarnContext(ctx, "cannot find ruleID for alert, skipping sending alert to alertmanager", "alert", alert) - continue - } - - receivers := cfg.ReceiverNamesFromRuleID(ruleID) - if len(receivers) == 0 { - provider.settings.Logger().WarnContext(ctx, "cannot find receivers for alert, skipping sending alert to alertmanager", "rule_id", ruleID, "alert", alert) - continue - } - - legacyAlerts = append(legacyAlerts, postableAlert{ - PostableAlert: alert, - Receivers: receivers, - }) - } - - url := provider.url.JoinPath(alertsPath) - - body, err := json.Marshal(legacyAlerts) - if err != nil { - return err - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewBuffer(body)) - if err != nil { - return err - } - req.Header.Add("Content-Type", "application/json") - - resp, err := provider.client.Do(req) - if err != nil { - return err - } - - 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) TestReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error { - url := provider.url.JoinPath(testReceiverPath) - - body, err := json.Marshal(alertmanagertypes.MSTeamsV2ReceiverToMSTeamsReceiver(receiver)) - if err != nil { - return err - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewBuffer(body)) - if err != nil { - return err - } - req.Header.Add("Content-Type", "application/json") - - resp, err := provider.client.Do(req) - if err != nil { - return err - } - - 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) TestAlert(ctx context.Context, orgID string, alert *alertmanagertypes.PostableAlert, receivers []string) error { - url := provider.url.JoinPath(alertsPath) - - legacyAlerts := make([]postableAlert, 1) - legacyAlerts[0] = postableAlert{ - PostableAlert: alert, - Receivers: receivers, - } - - body, err := json.Marshal(legacyAlerts) - if err != nil { - return err - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewBuffer(body)) - if err != nil { - return err - } - req.Header.Add("Content-Type", "application/json") - - resp, err := provider.client.Do(req) - if err != nil { - return err - } - - 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) { - channels, err := provider.configStore.ListAllChannels(ctx) - if err != nil { - return nil, err - } - - for _, channel := range channels { - if err := channel.MSTeamsV2ToMSTeams(); err != nil { - return nil, err - } - } - - return channels, nil -} - -func (provider *provider) GetChannelByID(ctx context.Context, orgID string, channelID valuer.UUID) (*alertmanagertypes.Channel, error) { - return provider.configStore.GetChannelByID(ctx, orgID, channelID) -} - -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 - } - - err = channel.Update(receiver) - if err != nil { - return err - } - - config, err := provider.configStore.Get(ctx, orgID) - if err != nil { - return err - } - - if err := config.UpdateReceiver(receiver); err != nil { - return err - } - - err = provider.configStore.UpdateChannel(ctx, orgID, channel, alertmanagertypes.WithCb(func(ctx context.Context) error { - url := provider.url.JoinPath(routesPath) - - body, err := json.Marshal(alertmanagertypes.MSTeamsV2ReceiverToMSTeamsReceiver(receiver)) - if err != nil { - return err - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPut, url.String(), bytes.NewBuffer(body)) - if err != nil { - return err - } - req.Header.Add("Content-Type", "application/json") - - resp, err := provider.client.Do(req) - if err != nil { - return err - } - - 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) - } - - if err := provider.configStore.Set(ctx, config); err != nil { - return err - } - - return nil - })) - if err != nil { - return err - } - - return nil -} - -func (provider *provider) CreateChannel(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error { - channel := alertmanagertypes.NewChannelFromReceiver(receiver, orgID) - - config, err := provider.configStore.Get(ctx, orgID) - if err != nil { - return err - } - - if err := config.CreateReceiver(receiver); err != nil { - return err - } - - return provider.configStore.CreateChannel(ctx, channel, alertmanagertypes.WithCb(func(ctx context.Context) error { - url := provider.url.JoinPath(routesPath) - - body, err := json.Marshal(alertmanagertypes.MSTeamsV2ReceiverToMSTeamsReceiver(receiver)) - if err != nil { - return err - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewBuffer(body)) - if err != nil { - return err - } - req.Header.Add("Content-Type", "application/json") - - resp, err := provider.client.Do(req) - if err != nil { - return err - } - - 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) - } - - if err := provider.configStore.Set(ctx, config); err != nil { - return err - } - - return nil - })) -} - -func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, channelID valuer.UUID) error { - channel, err := provider.configStore.GetChannelByID(ctx, orgID, channelID) - if err != nil { - return err - } - - config, err := provider.configStore.Get(ctx, orgID) - if err != nil { - return err - } - - if err := config.DeleteReceiver(channel.Name); err != nil { - return err - } - - return provider.configStore.DeleteChannelByID(ctx, orgID, channelID, alertmanagertypes.WithCb(func(ctx context.Context) error { - url := provider.url.JoinPath(routesPath) - - body, err := json.Marshal(map[string]string{"name": channel.Name}) - if err != nil { - return err - } - - req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url.String(), bytes.NewBuffer(body)) - if err != nil { - return err - } - req.Header.Add("Content-Type", "application/json") - - resp, err := provider.client.Do(req) - if err != nil { - return err - } - - 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) - } - - if err := provider.configStore.Set(ctx, config); err != nil { - return err - } - - 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 { - provider.batcher.Stop(ctx) - return nil -} - -func (provider *provider) GetConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) { - return provider.configStore.Get(ctx, orgID) -} - -func (provider *provider) SetDefaultConfig(ctx context.Context, orgID string) error { - config, err := alertmanagertypes.NewDefaultConfig(provider.config.Signoz.Config.Global, provider.config.Signoz.Config.Route, orgID) - if err != nil { - return err - } - - return provider.configStore.Set(ctx, config) -} - -func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) { - channels, err := provider.configStore.ListChannels(ctx, orgID.String()) - if err != nil { - return nil, err - } - - return alertmanagertypes.NewStatsFromChannels(channels), nil -} diff --git a/pkg/alertmanager/legacyalertmanager/provider_test.go b/pkg/alertmanager/legacyalertmanager/provider_test.go deleted file mode 100644 index e6f2138be32e..000000000000 --- a/pkg/alertmanager/legacyalertmanager/provider_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package legacyalertmanager - -import ( - "encoding/json" - "testing" - - "github.com/SigNoz/signoz/pkg/types/alertmanagertypes" - "github.com/prometheus/alertmanager/api/v2/models" - "github.com/stretchr/testify/assert" -) - -func TestProvider_TestAlert(t *testing.T) { - pa := &postableAlert{ - PostableAlert: &alertmanagertypes.PostableAlert{ - Alert: models.Alert{ - Labels: models.LabelSet{ - "alertname": "test", - }, - GeneratorURL: "http://localhost:9090/graph?g0.expr=up&g0.tab=1", - }, - Annotations: models.LabelSet{ - "summary": "test", - }, - }, - Receivers: []string{"receiver1", "receiver2"}, - } - - body, err := json.Marshal(pa) - if err != nil { - t.Fatalf("failed to marshal postable alert: %v", err) - } - - assert.Contains(t, string(body), "receiver1") - assert.Contains(t, string(body), "receiver2") -} diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 67e9a9576a80..eba029961078 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -84,9 +84,8 @@ import ( type status string const ( - statusSuccess status = "success" - statusError status = "error" - defaultFluxInterval = 5 * time.Minute + statusSuccess status = "success" + statusError status = "error" ) // NewRouter creates and configures a Gorilla Router. @@ -480,11 +479,6 @@ func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) { writeHttpResponse(w, data) } -// RegisterPrivateRoutes registers routes for this handler on the given router -func (aH *APIHandler) RegisterPrivateRoutes(router *mux.Router) { - router.HandleFunc("/api/v1/channels", aH.AlertmanagerAPI.ListAllChannels).Methods(http.MethodGet) -} - // RegisterRoutes registers routes for this handler on the given router func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) { router.HandleFunc("/api/v1/query_range", am.ViewAccess(aH.queryRangeMetrics)).Methods(http.MethodGet) diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index b33073eb74fc..b81db76d1cce 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -54,11 +54,6 @@ type Server struct { httpServer *http.Server httpHostPort string - // private http - privateConn net.Listener - privateHTTP *http.Server - privateHostPort string - opampServer *opamp.Server unavailableChannel chan healthcheck.Status @@ -131,7 +126,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT) jwt: jwt, ruleManager: rm, httpHostPort: constants.HTTPHostPort, - privateHostPort: constants.PrivateHostPort, unavailableChannel: make(chan healthcheck.Status), } @@ -143,13 +137,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT) s.httpServer = httpServer - privateServer, err := s.createPrivateServer(apiHandler) - if err != nil { - return nil, err - } - - s.privateHTTP = privateServer - opAmpModel.Init(signoz.SQLStore, signoz.Instrumentation.Logger(), signoz.Modules.OrgGetter) agentConfMgr, err := agentConf.Initiate( @@ -178,37 +165,6 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status { return s.unavailableChannel } -func (s *Server) createPrivateServer(api *APIHandler) (*http.Server, error) { - - r := NewRouter() - - r.Use(middleware.NewAuth(s.jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap) - r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(), - s.config.APIServer.Timeout.ExcludedRoutes, - s.config.APIServer.Timeout.Default, - s.config.APIServer.Timeout.Max, - ).Wrap) - r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap) - r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap) - - api.RegisterPrivateRoutes(r) - - c := cors.New(cors.Options{ - //todo(amol): find out a way to add exact domain or - // ip here for alert manager - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"}, - }) - - handler := c.Handler(r) - handler = handlers.CompressHandler(handler) - - return &http.Server{ - Handler: handler, - }, nil -} - func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server, error) { r := NewRouter() @@ -275,19 +231,6 @@ func (s *Server) initListeners() error { zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.httpHostPort)) - // listen on private port to support internal services - privateHostPort := s.privateHostPort - - if privateHostPort == "" { - return fmt.Errorf("constants.PrivateHostPort is required") - } - - s.privateConn, err = net.Listen("tcp", privateHostPort) - if err != nil { - return err - } - zap.L().Info(fmt.Sprintf("Query server started listening on private port %s...", s.privateHostPort)) - return nil } @@ -326,26 +269,6 @@ func (s *Server) Start(ctx context.Context) error { } }() - var privatePort int - if port, err := utils.GetPort(s.privateConn.Addr()); err == nil { - privatePort = port - } - fmt.Println("starting private http") - go func() { - zap.L().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.privateHostPort)) - - switch err := s.privateHTTP.Serve(s.privateConn); err { - case nil, http.ErrServerClosed, cmux.ErrListenerClosed: - // normal exit, nothing to do - zap.L().Info("private http server closed") - default: - zap.L().Error("Could not start private HTTP server", zap.Error(err)) - } - - s.unavailableChannel <- healthcheck.Unavailable - - }() - go func() { zap.L().Info("Starting OpAmp Websocket server", zap.String("addr", constants.OpAmpWsEndpoint)) err := s.opampServer.Start(constants.OpAmpWsEndpoint) @@ -365,12 +288,6 @@ func (s *Server) Stop(ctx context.Context) error { } } - if s.privateHTTP != nil { - if err := s.privateHTTP.Shutdown(context.Background()); err != nil { - return err - } - } - s.opampServer.Stop() if s.ruleManager != nil { diff --git a/pkg/signoz/config.go b/pkg/signoz/config.go index 5d568dda8ae9..3e4377254701 100644 --- a/pkg/signoz/config.go +++ b/pkg/signoz/config.go @@ -239,20 +239,6 @@ func mergeAndEnsureBackwardCompatibility(ctx context.Context, logger *slog.Logge config.TelemetryStore.Connection.DialTimeout = deprecatedFlags.DialTimeout } - if os.Getenv("ALERTMANAGER_API_PREFIX") != "" { - logger.WarnContext(ctx, "[Deprecated] env ALERTMANAGER_API_PREFIX is deprecated and scheduled for removal. Please use SIGNOZ_ALERTMANAGER_LEGACY_API__URL instead.") - u, err := url.Parse(os.Getenv("ALERTMANAGER_API_PREFIX")) - if err != nil { - logger.WarnContext(ctx, "Error parsing ALERTMANAGER_API_PREFIX, using default value") - } else { - config.Alertmanager.Legacy.ApiURL = u - } - } - - if os.Getenv("ALERTMANAGER_API_CHANNEL_PATH") != "" { - logger.WarnContext(ctx, "[Deprecated] env ALERTMANAGER_API_CHANNEL_PATH is deprecated and scheduled for complete removal.") - } - if deprecatedFlags.Config != "" { logger.WarnContext(ctx, "[Deprecated] flag --config is deprecated for passing prometheus config. The flag will be used for passing the entire SigNoz config. More details can be found at https://github.com/SigNoz/signoz/issues/6805.") } diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index c307d8c050ab..878ec34a6c90 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -2,7 +2,6 @@ package signoz import ( "github.com/SigNoz/signoz/pkg/alertmanager" - "github.com/SigNoz/signoz/pkg/alertmanager/legacyalertmanager" "github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager" "github.com/SigNoz/signoz/pkg/analytics" "github.com/SigNoz/signoz/pkg/analytics/noopanalytics" @@ -156,7 +155,6 @@ func NewPrometheusProviderFactories(telemetryStore telemetrystore.TelemetryStore func NewAlertmanagerProviderFactories(sqlstore sqlstore.SQLStore, orgGetter organization.Getter) factory.NamedMap[factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config]] { return factory.MustNewNamedMap( - legacyalertmanager.NewFactory(sqlstore, orgGetter), signozalertmanager.NewFactory(sqlstore, orgGetter), ) } diff --git a/pkg/types/alertmanagertypes/channel.go b/pkg/types/alertmanagertypes/channel.go index 1cb873ecb15d..bbb50f24adf8 100644 --- a/pkg/types/alertmanagertypes/channel.go +++ b/pkg/types/alertmanagertypes/channel.go @@ -177,26 +177,3 @@ func (c *Channel) Update(receiver Receiver) error { return nil } - -// This is needed by the legacy alertmanager to convert the MSTeamsV2Configs to MSTeamsConfigs -func (c *Channel) MSTeamsV2ToMSTeams() error { - if c.Type != "msteamsv2" { - return nil - } - - receiver, err := NewReceiver(c.Data) - if err != nil { - return err - } - - receiver = MSTeamsV2ReceiverToMSTeamsReceiver(receiver) - data, err := json.Marshal(receiver) - if err != nil { - return err - } - - c.Type = "msteams" - c.Data = string(data) - - return nil -} diff --git a/pkg/types/alertmanagertypes/receiver.go b/pkg/types/alertmanagertypes/receiver.go index bbdfaf28d7a8..83cae2931b8d 100644 --- a/pkg/types/alertmanagertypes/receiver.go +++ b/pkg/types/alertmanagertypes/receiver.go @@ -13,12 +13,12 @@ import ( "gopkg.in/yaml.v2" "github.com/prometheus/alertmanager/config" - "github.com/prometheus/alertmanager/config/receiver" ) type ( // Receiver is the type for the receiver configuration. - Receiver = config.Receiver + Receiver = config.Receiver + ReceiverIntegrationsFunc = func(nc Receiver, tmpl *template.Template, logger *slog.Logger) ([]notify.Integration, error) ) // Creates a new receiver from a string. The input is initialized with the default values from the upstream alertmanager. @@ -49,11 +49,7 @@ func NewReceiver(input string) (Receiver, error) { return receiverWithDefaults, nil } -func NewReceiverIntegrations(nc Receiver, tmpl *template.Template, logger *slog.Logger) ([]notify.Integration, error) { - return receiver.BuildReceiverIntegrations(nc, tmpl, logger) -} - -func TestReceiver(ctx context.Context, receiver Receiver, config *Config, tmpl *template.Template, logger *slog.Logger, alert *Alert) error { +func TestReceiver(ctx context.Context, receiver Receiver, receiverIntegrationsFunc ReceiverIntegrationsFunc, config *Config, tmpl *template.Template, logger *slog.Logger, alert *Alert) error { ctx = notify.WithGroupKey(ctx, fmt.Sprintf("%s-%s-%d", receiver.Name, alert.Labels.Fingerprint(), time.Now().Unix())) ctx = notify.WithGroupLabels(ctx, alert.Labels) ctx = notify.WithReceiverName(ctx, receiver.Name) @@ -75,7 +71,7 @@ func TestReceiver(ctx context.Context, receiver Receiver, config *Config, tmpl * return err } - integrations, err := NewReceiverIntegrations(receiver, tmpl, logger) + integrations, err := receiverIntegrationsFunc(receiver, tmpl, logger) if err != nil { return err } @@ -90,27 +86,3 @@ func TestReceiver(ctx context.Context, receiver Receiver, config *Config, tmpl * return nil } - -// This is needed by the legacy alertmanager to convert the MSTeamsV2Configs to MSTeamsConfigs -func MSTeamsV2ReceiverToMSTeamsReceiver(receiver Receiver) Receiver { - if receiver.MSTeamsV2Configs == nil { - return receiver - } - - var msTeamsConfigs []*config.MSTeamsConfig - for _, cfg := range receiver.MSTeamsV2Configs { - msTeamsConfigs = append(msTeamsConfigs, &config.MSTeamsConfig{ - NotifierConfig: cfg.NotifierConfig, - HTTPConfig: cfg.HTTPConfig, - WebhookURL: cfg.WebhookURL, - WebhookURLFile: cfg.WebhookURLFile, - Title: cfg.Title, - Text: cfg.Text, - }) - } - - receiver.MSTeamsV2Configs = nil - receiver.MSTeamsConfigs = msTeamsConfigs - - return receiver -} diff --git a/pkg/types/alertmanagertypes/template.go b/pkg/types/alertmanagertypes/template.go index 7de8a36acf64..8767eb7bd802 100644 --- a/pkg/types/alertmanagertypes/template.go +++ b/pkg/types/alertmanagertypes/template.go @@ -21,6 +21,7 @@ func FromGlobs(paths []string) (*alertmanagertemplate.Template, error) { {{ define "__ruleIdPath" }}{{ range .CommonLabels.SortedPairs }}{{ if eq .Name "ruleId" }}{{ if match "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" .Value }}/edit?ruleId={{ .Value | urlquery }}{{ end }}{{ end }}{{ end }}{{ end }} {{ define "__alertmanagerURL" }}{{ .ExternalURL }}/alerts{{ template "__ruleIdPath" . }}{{ end }} + {{ define "msteamsv2.default.titleLink" }}{{ template "__alertmanagerURL" . }}{{ end }} `))); err != nil { return nil, fmt.Errorf("error parsing alertmanager templates: %w", err) }