mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 07:26:20 +00:00
feat(alertmanager): deprecate legacy alertmanager (#9046)
- Deprecate legacy alertmanager. Are the new alert improvements compatible with legacy? I don't think they are. More importantly, I don't think they should be. It will be a pain to manage it at both places. - Improve msteamsv2 experience. I have taken alertmanager's upstream and merged it with our custom implementation. Note the use of `titleLink` field to propagate the url of the alert. - Delete the private http server needed for alertmanager. It's cleanup as part of 1.
This commit is contained in:
parent
f23000831c
commit
31e042adf7
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
265
pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2.go
Normal file
265
pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2.go
Normal file
@ -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
|
||||
}
|
||||
220
pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2_test.go
Normal file
220
pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2_test.go
Normal file
@ -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())
|
||||
}
|
||||
51
pkg/alertmanager/alertmanagernotify/receiver.go
Normal file
51
pkg/alertmanager/alertmanagernotify/receiver.go
Normal file
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.")
|
||||
}
|
||||
|
||||
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user