Compare commits

...

8 Commits

28 changed files with 949 additions and 75 deletions

View File

@@ -19,8 +19,7 @@ export const SlackInitialConfig: Partial<SlackChannel> = {
*Summary:* {{ .Annotations.summary }}
*Description:* {{ .Annotations.description }}
*RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}}
*RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}}
{{ template "signoz.relatedLinks.slack" . }}
*Details:*
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
@@ -135,11 +134,7 @@ export const JsmOpsInitialConfig: Partial<JsmOpsChannel> = {
{{ end }}{{ if .GeneratorURL }}[View in SigNoz]({{ .GeneratorURL }})
{{ end }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})
{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})
{{ end }}{{ end }}`,
{{ end }}{{ template "signoz.relatedLinks.markdown" . }}{{ end }}`,
priority:
'{{ if eq (index .Alerts 0).Labels.severity "critical" }}P1{{ else if eq (index .Alerts 0).Labels.severity "warning" }}P2{{ else if eq (index .Alerts 0).Labels.severity "info" }}P3{{ else }}P4{{ end }}',
tags: ['signoz-alert'],
@@ -161,11 +156,7 @@ export const IncidentIOInitialConfig: Partial<IncidentIOChannel> = {
{{ end }}{{ if .GeneratorURL }}[View in SigNoz]({{ .GeneratorURL }})
{{ end }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})
{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})
{{ end }}{{ end }}`,
{{ end }}{{ template "signoz.relatedLinks.markdown" . }}{{ end }}`,
};
export const EmailInitialConfig: Partial<EmailChannel> = {

View File

@@ -25,7 +25,7 @@ export const editAlertChannelInitialValue = {
export const slackTitleDefaultValue = `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }} {{- if gt (len .CommonLabels) (len .GroupLabels) -}} {{" "}}( {{- with .CommonLabels.Remove .GroupLabels.Names }} {{- range $index, $label := .SortedPairs -}} {{ if $index }}, {{ end }} {{- $label.Name }}="{{ $label.Value -}}" {{- end }} {{- end -}} ) {{- end }}`;
export const slackDescriptionDefaultValue = `{{ range .Alerts -}} *Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }} *Summary:* {{ .Annotations.summary }} *Description:* {{ .Annotations.description }} *RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}} *RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}} *Details:* {{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }} {{ end }} {{ end }}`;
export const slackDescriptionDefaultValue = `{{ range .Alerts -}} *Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }} *Summary:* {{ .Annotations.summary }} *Description:* {{ .Annotations.description }} {{ template "signoz.relatedLinks.slack" . }} *Details:* {{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }} {{ end }} {{ end }}`;
export const googleChatTitleDefaultValue = `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }}`;

View File

@@ -29,7 +29,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/alertmanager/config"
@@ -489,11 +488,8 @@ func appendRelatedLinkButtons(alerts []*types.Alert, bodies []string) {
if i >= len(alerts) || bodies[i] == "" {
continue
}
if link := alerts[i].Annotations[ruletypes.AnnotationRelatedLogs]; link != "" {
bodies[i] += htmlButton("View Related Logs", string(link))
}
if link := alerts[i].Annotations[ruletypes.AnnotationRelatedTraces]; link != "" {
bodies[i] += htmlButton("View Related Traces", string(link))
for _, link := range alertmanagertypes.BuildRelatedLinks(alerts[i].Annotations) {
bodies[i] += htmlButton(link.Label, link.URL)
}
}
}

View File

@@ -0,0 +1,65 @@
package email
import (
"strings"
"testing"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
)
func TestAppendRelatedLinkButtons(t *testing.T) {
testCases := []struct {
name string
annotations model.LabelSet
body string
expectedOrder []string
}{
{
name: "BothLinks_LogsBeforeTraces",
annotations: model.LabelSet{"related_traces": "https://signoz.example/traces?a=1", "related_logs": "https://signoz.example/logs?a=1"},
body: "<p>body</p>",
expectedOrder: []string{"https://signoz.example/logs?a=1", "View Related Logs", "https://signoz.example/traces?a=1", "View Related Traces"},
},
{
name: "TracesOnly_SingleButton",
annotations: model.LabelSet{"related_traces": "https://signoz.example/traces?b=2"},
body: "<p>body</p>",
expectedOrder: []string{"https://signoz.example/traces?b=2", "View Related Traces"},
},
{
name: "NoLinks_BodyUnchanged",
annotations: model.LabelSet{"summary": "s"},
body: "<p>body</p>",
expectedOrder: nil,
},
{
name: "EmptyBody_NoButtons",
annotations: model.LabelSet{"related_logs": "https://signoz.example/logs?c=3"},
body: "",
expectedOrder: nil,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
alerts := []*types.Alert{{Alert: model.Alert{Annotations: testCase.annotations}}}
bodies := []string{testCase.body}
appendRelatedLinkButtons(alerts, bodies)
if testCase.expectedOrder == nil {
assert.Equal(t, testCase.body, bodies[0])
return
}
assert.True(t, strings.HasPrefix(bodies[0], testCase.body))
last := -1
for _, needle := range testCase.expectedOrder {
idx := strings.Index(bodies[0], needle)
assert.Greater(t, idx, last, needle)
last = idx
}
})
}
}

View File

@@ -243,13 +243,9 @@ func statusLine(alerts []*types.Alert) string {
// the annotations the ruler attaches to each alert. Empty links are skipped.
func relatedButtons(alert *types.Alert) []button {
var buttons []button
add := func(text, u string) {
if u != "" {
buttons = append(buttons, button{Text: text, OnClick: onClick{OpenLink: openLink{URL: u}}})
}
for _, link := range alertmanagertypes.BuildRelatedLinks(alert.Annotations) {
buttons = append(buttons, button{Text: link.Label, OnClick: onClick{OpenLink: openLink{URL: link.URL}}})
}
add("View Related Logs", string(alert.Annotations[ruletypes.AnnotationRelatedLogs]))
add("View Related Traces", string(alert.Annotations[ruletypes.AnnotationRelatedTraces]))
return buttons
}

View File

@@ -334,8 +334,8 @@ func TestGoogleChatLinkButtons(t *testing.T) {
ruletypes.LabelRuleSource: "https://signoz.example/alerts/1",
},
annotations: model.LabelSet{
ruletypes.AnnotationRelatedLogs: "https://signoz.example/logs",
ruletypes.AnnotationRelatedTraces: "https://signoz.example/traces",
alertmanagertypes.AnnotationRelatedLogs: "https://signoz.example/logs",
alertmanagertypes.AnnotationRelatedTraces: "https://signoz.example/traces",
},
wantButtons: map[string]string{
"Open in SigNoz": "https://signoz.example/alerts/1",
@@ -416,8 +416,8 @@ func TestGoogleChatMultiAlertSections(t *testing.T) {
ruletypes.LabelRuleSource: "https://signoz.example/alerts/1",
},
Annotations: model.LabelSet{
ruletypes.AnnotationBodyTemplate: "an alert fired",
ruletypes.AnnotationRelatedLogs: model.LabelValue(logs),
ruletypes.AnnotationBodyTemplate: "an alert fired",
alertmanagertypes.AnnotationRelatedLogs: model.LabelValue(logs),
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Minute),
@@ -464,7 +464,7 @@ func TestGoogleChatDefaultBodyGrouped(t *testing.T) {
ruletypes.LabelRuleSource: "https://signoz.example/alerts/1",
},
Annotations: model.LabelSet{
ruletypes.AnnotationRelatedLogs: model.LabelValue("https://signoz.example/logs?pod=" + pod),
alertmanagertypes.AnnotationRelatedLogs: model.LabelValue("https://signoz.example/logs?pod=" + pod),
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Minute),

View File

@@ -11,10 +11,10 @@ import (
"testing"
"time"
test "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/notify/test"
"github.com/prometheus/alertmanager/types"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@@ -111,7 +111,7 @@ func TestNotifyFiringEvent(t *testing.T) {
assert.Contains(t, ev.Description, "**Alert:** HighCPU (critical)")
assert.Contains(t, ev.Description, "**Summary:** cpu high")
assert.Contains(t, ev.Description, "[View in SigNoz](https://signoz.example/alerts/edit?ruleId=1)")
assert.Contains(t, ev.Description, "[View related logs](https://signoz.example/logs?q=1)")
assert.Contains(t, ev.Description, "[View Related Logs](https://signoz.example/logs?q=1)")
assert.Equal(t, map[string]string{
"alertname": "HighCPU",
"severity": "critical",

View File

@@ -259,8 +259,9 @@ func deepLinks(alerts []*types.Alert) map[string]any {
})
}
add("Open in SigNoz", string(a.Labels[ruletypes.LabelRuleSource]))
add("View Related Logs", string(a.Annotations[ruletypes.AnnotationRelatedLogs]))
add("View Related Traces", string(a.Annotations[ruletypes.AnnotationRelatedTraces]))
for _, link := range alertmanagertypes.BuildRelatedLinks(a.Annotations) {
add(link.Label, link.URL)
}
if len(parts) == 0 {
return nil
}

View File

@@ -275,7 +275,7 @@ func TestNotifyRichDescriptionPanelAndLinks(t *testing.T) {
m := newMockJira(t)
a := alert(true)
a.Labels[ruletypes.LabelRuleSource] = model.LabelValue("https://app.signoz.io/alerts?ruleId=1")
a.Annotations[ruletypes.AnnotationRelatedLogs] = model.LabelValue("https://app.signoz.io/logs")
a.Annotations[alertmanagertypes.AnnotationRelatedLogs] = model.LabelValue("https://app.signoz.io/logs")
_, err := newNotifier(t, m).Notify(ctx(), a)
require.NoError(t, err)

View File

@@ -0,0 +1,59 @@
package jira
import (
"testing"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDeepLinks(t *testing.T) {
testCases := []struct {
name string
labels model.LabelSet
annotations model.LabelSet
expectedTexts []string
}{
{
name: "RuleSourceAndBothLinks_DotSeparatedInOrder",
labels: model.LabelSet{"ruleSource": "https://signoz.example/alerts?ruleId=1"},
annotations: model.LabelSet{"related_traces": "https://signoz.example/traces?a=1", "related_logs": "https://signoz.example/logs?a=1"},
expectedTexts: []string{"Open in SigNoz", " · ", "View Related Logs", " · ", "View Related Traces"},
},
{
name: "TracesOnly_NoRuleSource",
labels: model.LabelSet{},
annotations: model.LabelSet{"related_traces": "https://signoz.example/traces?b=2"},
expectedTexts: []string{"View Related Traces"},
},
{
name: "NoLinks_Nil",
labels: model.LabelSet{},
annotations: model.LabelSet{"summary": "s"},
expectedTexts: nil,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
alerts := []*types.Alert{{Alert: model.Alert{Labels: testCase.labels, Annotations: testCase.annotations}}}
paragraph := deepLinks(alerts)
if testCase.expectedTexts == nil {
assert.Nil(t, paragraph)
return
}
require.NotNil(t, paragraph)
content, ok := paragraph["content"].([]any)
require.True(t, ok)
texts := make([]string, 0, len(content))
for _, part := range content {
texts = append(texts, part.(map[string]any)["text"].(string))
}
assert.Equal(t, testCase.expectedTexts, texts)
})
}
}

View File

@@ -0,0 +1,44 @@
package slack
import (
"testing"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
)
func TestBuildRelatedLinkActions(t *testing.T) {
testCases := []struct {
name string
annotations model.LabelSet
expected []config.SlackAction
}{
{
name: "BothLinks_LogsBeforeTraces",
annotations: model.LabelSet{"related_traces": "https://signoz.example/traces?a=1", "related_logs": "https://signoz.example/logs?a=1"},
expected: []config.SlackAction{
{Type: "button", Text: "View Related Logs", URL: "https://signoz.example/logs?a=1"},
{Type: "button", Text: "View Related Traces", URL: "https://signoz.example/traces?a=1"},
},
},
{
name: "LogsOnly_SingleAction",
annotations: model.LabelSet{"related_logs": "https://signoz.example/logs?b=2"},
expected: []config.SlackAction{{Type: "button", Text: "View Related Logs", URL: "https://signoz.example/logs?b=2"}},
},
{
name: "NoLinks_Nil",
annotations: model.LabelSet{"summary": "s"},
expected: nil,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
alert := &types.Alert{Alert: model.Alert{Annotations: testCase.annotations}}
assert.Equal(t, testCase.expected, buildRelatedLinkActions(alert))
})
}
}

View File

@@ -18,7 +18,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/templating/markdownrenderer"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/alertmanager/config"
@@ -269,11 +268,8 @@ func (n *Notifier) prepareContent(ctx context.Context, alerts []*types.Alert, tm
// buttons for an alert, or nil when no related-link annotations are present.
func buildRelatedLinkActions(alert *types.Alert) []config.SlackAction {
var actions []config.SlackAction
if link := alert.Annotations[ruletypes.AnnotationRelatedLogs]; link != "" {
actions = append(actions, config.SlackAction{Type: "button", Text: "View Related Logs", URL: string(link)})
}
if link := alert.Annotations[ruletypes.AnnotationRelatedTraces]; link != "" {
actions = append(actions, config.SlackAction{Type: "button", Text: "View Related Traces", URL: string(link)})
for _, link := range alertmanagertypes.BuildRelatedLinks(alert.Annotations) {
actions = append(actions, config.SlackAction{Type: "button", Text: link.Label, URL: link.URL})
}
return actions
}

View File

@@ -267,8 +267,8 @@ func buildAlertData(a *types.Alert, receiver string) alertmanagertypes.AlertData
IsRecovering: labels[ruletypes.LabelIsRecovering] == "true",
},
Rule: buildRuleInfo(labels, annotations),
Log: alertmanagertypes.LinkInfo{URL: annotations[ruletypes.AnnotationRelatedLogs]},
Trace: alertmanagertypes.LinkInfo{URL: annotations[ruletypes.AnnotationRelatedTraces]},
Log: alertmanagertypes.LinkInfo{URL: annotations[alertmanagertypes.AnnotationRelatedLogs]},
Trace: alertmanagertypes.LinkInfo{URL: annotations[alertmanagertypes.AnnotationRelatedTraces]},
Labels: labels,
// Strip private annotations once the structured fields above have
// been populated from the raw map.

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/contextlinks"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
@@ -317,14 +318,14 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (int, error) {
}
link := r.ExternalURL(explorerPath, params)
r.logger.InfoContext(ctx, "adding traces link to annotations", slog.String("annotation.link", link))
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationRelatedTraces, Value: link})
annotations = append(annotations, ruletypes.Label{Name: alertmanagertypes.AnnotationRelatedTraces, Value: link})
}
case ruletypes.AlertTypeLogs:
params := r.prepareParamsForLogs(ctx, ts, smpl.Metric)
if len(params) > 0 {
link := r.ExternalURL("logs/logs-explorer", params)
r.logger.InfoContext(ctx, "adding logs link to annotations", slog.String("annotation.link", link))
annotations = append(annotations, ruletypes.Label{Name: ruletypes.AnnotationRelatedLogs, Value: link})
annotations = append(annotations, ruletypes.Label{Name: alertmanagertypes.AnnotationRelatedLogs, Value: link})
}
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
@@ -906,7 +907,7 @@ func TestThresholdRuleTracesLink(t *testing.T) {
assert.Equal(t, c.expectAlerts, alertsFound, "case %d", idx)
for _, item := range rule.Active {
for name, value := range item.Annotations.Map() {
if name == ruletypes.AnnotationRelatedTraces {
if name == alertmanagertypes.AnnotationRelatedTraces {
assert.NotEmpty(t, value, "case %d", idx)
assert.Contains(t, value, "GET")
}
@@ -1006,7 +1007,7 @@ func TestThresholdRuleAITracesLink(t *testing.T) {
assert.Equal(t, c.expectAlerts, alertsFound, "case %d", idx)
for _, item := range rule.Active {
link := item.Annotations.Map()[ruletypes.AnnotationRelatedTraces]
link := item.Annotations.Map()[alertmanagertypes.AnnotationRelatedTraces]
assert.True(t, strings.HasPrefix(link, "http://localhost:8080/ai-observability/explorer?"), "case %d: %s", idx, link)
assert.Contains(t, link, "builder_ai_query", "case %d", idx)
assert.Contains(t, link, "llm-gateway", "case %d", idx)
@@ -1122,7 +1123,7 @@ func TestThresholdRuleLogsLink(t *testing.T) {
assert.Equal(t, c.expectAlerts, alertsFound, "case %d", idx)
for _, item := range rule.Active {
for name, value := range item.Annotations.Map() {
if name == ruletypes.AnnotationRelatedLogs {
if name == alertmanagertypes.AnnotationRelatedLogs {
assert.NotEmpty(t, value, "case %d", idx)
assert.Contains(t, value, "testcontainer")
}

View File

@@ -262,6 +262,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddRuleViewFactory(sqlstore, sqlschema),
sqlmigration.NewAddFieldTuplesFactory(sqlstore),
sqlmigration.NewKeepRelevantLLMPricingRulesFactory(),
sqlmigration.NewMigrateRelatedLinksTemplateFactory(),
)
}

View File

@@ -0,0 +1,510 @@
package sqlmigration
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"log/slog"
"regexp"
"strings"
"text/template/parse"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
const (
relatedLinksSlackHeader = "*RelatedLogs:*"
)
var (
relatedLinksActionPattern = regexp.MustCompile(`(?s)\{\{-?(.*?)-?\}\}`)
relatedLinksSlackKind = relatedLinksChannelKind{
configsKey: "slack_configs",
field: "text",
replacement: `{{ template "signoz.relatedLinks.slack" . }}`,
defaults: []string{
"*RelatedLogs:* {{ .Annotations.related_logs }}\n *RelatedTraces:* {{ .Annotations.related_traces }}",
"*RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}}\n *RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}}",
},
}
relatedLinksMSTeamsKind = relatedLinksChannelKind{
configsKey: "msteamsv2_configs",
field: "text",
replacement: relatedLinksSlackKind.replacement,
defaults: relatedLinksSlackKind.defaults,
}
relatedLinksIncidentIOKind = relatedLinksChannelKind{
configsKey: "incidentio_configs",
field: "description",
replacement: `{{ template "signoz.relatedLinks.markdown" . }}`,
defaults: []string{
"{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})\n\n{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})\n\n{{ end }}",
},
}
relatedLinksJSMOpsKind = relatedLinksChannelKind{
configsKey: "jsmops_configs",
field: "description",
replacement: relatedLinksIncidentIOKind.replacement,
defaults: relatedLinksIncidentIOKind.defaults,
}
relatedLinksKindsByChannelType = map[string]relatedLinksChannelKind{
"slack": relatedLinksSlackKind,
"msteams": relatedLinksMSTeamsKind,
"incidentio": relatedLinksIncidentIOKind,
"jsmops": relatedLinksJSMOpsKind,
}
errRelatedLinksCustomised = errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "related links section differs from the shipped defaults")
errRelatedLinksUnrecognised = errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "related links section not in the expected shape")
)
type migrateRelatedLinksTemplate struct {
logger *slog.Logger
}
func NewMigrateRelatedLinksTemplateFactory() factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("migrate_related_links_template"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &migrateRelatedLinksTemplate{logger: ps.Logger}, nil
},
)
}
func (migration *migrateRelatedLinksTemplate) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *migrateRelatedLinksTemplate) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
if err := migration.migrateConfigs(ctx, tx); err != nil {
return err
}
if err := migration.migrateChannels(ctx, tx); err != nil {
return err
}
return tx.Commit()
}
func (migration *migrateRelatedLinksTemplate) Down(context.Context, *bun.DB) error {
return nil
}
func (migration *migrateRelatedLinksTemplate) migrateConfigs(ctx context.Context, tx bun.Tx) error {
rows := make([]*relatedLinksConfigRow, 0)
if err := tx.NewSelect().Model(&rows).Scan(ctx); err != nil {
return err
}
for _, row := range rows {
cfg := make(map[string]json.RawMessage)
if err := json.Unmarshal([]byte(row.Config), &cfg); err != nil {
migration.logger.WarnContext(ctx, "skipping alertmanager config with unreadable config", slog.String("config_id", row.ID), errors.Attr(err))
continue
}
receiversRaw, ok := cfg["receivers"]
if !ok || string(receiversRaw) == "null" {
continue
}
receivers := make([]map[string]json.RawMessage, 0)
if err := json.Unmarshal(receiversRaw, &receivers); err != nil {
migration.logger.WarnContext(ctx, "skipping alertmanager config with unreadable receivers", slog.String("config_id", row.ID), errors.Attr(err))
continue
}
changed := false
for _, receiver := range receivers {
for _, kind := range []relatedLinksChannelKind{relatedLinksSlackKind, relatedLinksMSTeamsKind, relatedLinksIncidentIOKind, relatedLinksJSMOpsKind} {
rewritten, err := rewriteRelatedLinksInReceiver(receiver, kind)
if err != nil {
migration.logger.WarnContext(ctx, "leaving receiver related links template as is", slog.String("config_id", row.ID), slog.String("receiver", string(receiver["name"])), slog.String("configs_key", kind.configsKey), errors.Attr(err))
continue
}
changed = changed || rewritten
}
}
if !changed {
continue
}
newReceivers, err := json.Marshal(receivers)
if err != nil {
return err
}
cfg["receivers"] = newReceivers
newConfig, err := json.Marshal(cfg)
if err != nil {
return err
}
if _, err := tx.NewUpdate().
Model((*relatedLinksConfigRow)(nil)).
Set("config = ?", string(newConfig)).
Set("hash = ?", fmt.Sprintf("%x", md5.Sum(newConfig))).
Where("id = ?", row.ID).
Exec(ctx); err != nil {
return err
}
}
return nil
}
func (migration *migrateRelatedLinksTemplate) migrateChannels(ctx context.Context, tx bun.Tx) error {
channelTypes := make([]string, 0, len(relatedLinksKindsByChannelType))
for channelType := range relatedLinksKindsByChannelType {
channelTypes = append(channelTypes, channelType)
}
rows := make([]*relatedLinksChannelRow, 0)
if err := tx.NewSelect().Model(&rows).Where("type IN (?)", bun.In(channelTypes)).Scan(ctx); err != nil {
return err
}
for _, row := range rows {
kind := relatedLinksKindsByChannelType[row.Type]
query := tx.NewUpdate().Model((*relatedLinksChannelRow)(nil)).Where("id = ?", row.ID)
changed := false
receiver := make(map[string]json.RawMessage)
if err := json.Unmarshal([]byte(row.Data), &receiver); err != nil {
migration.logger.WarnContext(ctx, "skipping notification channel with unreadable data", slog.String("org_id", row.OrgID), slog.String("channel_id", row.ID), errors.Attr(err))
continue
}
dataChanged, err := rewriteRelatedLinksInReceiver(receiver, kind)
if err != nil {
migration.logger.WarnContext(ctx, "leaving notification channel data template as is", slog.String("org_id", row.OrgID), slog.String("channel_id", row.ID), errors.Attr(err))
}
if dataChanged {
newData, err := json.Marshal(receiver)
if err != nil {
return err
}
query = query.Set("data = ?", string(newData))
changed = true
}
if row.Spec != "" {
spec := make(map[string]json.RawMessage)
if err := json.Unmarshal([]byte(row.Spec), &spec); err != nil {
migration.logger.WarnContext(ctx, "skipping notification channel spec with unreadable json", slog.String("org_id", row.OrgID), slog.String("channel_id", row.ID), errors.Attr(err))
} else {
specChanged, err := rewriteRelatedLinksInField(spec, kind)
if err != nil {
migration.logger.WarnContext(ctx, "leaving notification channel spec template as is", slog.String("org_id", row.OrgID), slog.String("channel_id", row.ID), errors.Attr(err))
}
if specChanged {
newSpec, err := json.Marshal(spec)
if err != nil {
return err
}
query = query.Set("spec = ?", string(newSpec))
changed = true
}
}
}
if !changed {
continue
}
if _, err := query.Exec(ctx); err != nil {
return err
}
}
return nil
}
type relatedLinksChannelKind struct {
configsKey string
field string
replacement string
defaults []string
}
type relatedLinksConfigRow struct {
bun.BaseModel `bun:"table:alertmanager_config"`
ID string `bun:"id"`
Config string `bun:"config"`
}
type relatedLinksChannelRow struct {
bun.BaseModel `bun:"table:notification_channel"`
ID string `bun:"id"`
OrgID string `bun:"org_id"`
Type string `bun:"type"`
Data string `bun:"data"`
Spec string `bun:"spec,nullzero"`
}
// relatedLinksChunk is one text run or one {{...}} action of a template, with
// its byte span in the source and the action's trimmed inner text.
type relatedLinksChunk struct {
start int
end int
action bool
inner string
}
func rewriteRelatedLinksInReceiver(receiver map[string]json.RawMessage, kind relatedLinksChannelKind) (bool, error) {
configsRaw, ok := receiver[kind.configsKey]
if !ok || string(configsRaw) == "null" {
return false, nil
}
configs := make([]map[string]json.RawMessage, 0)
if err := json.Unmarshal(configsRaw, &configs); err != nil {
return false, err
}
changed := false
var firstErr error
for _, config := range configs {
fieldChanged, err := rewriteRelatedLinksInField(config, kind)
if err != nil && firstErr == nil {
firstErr = err
}
changed = changed || fieldChanged
}
if !changed {
return false, firstErr
}
newConfigs, err := json.Marshal(configs)
if err != nil {
return false, err
}
receiver[kind.configsKey] = newConfigs
return true, firstErr
}
func rewriteRelatedLinksInField(object map[string]json.RawMessage, kind relatedLinksChannelKind) (bool, error) {
fieldRaw, ok := object[kind.field]
if !ok || string(fieldRaw) == "null" {
return false, nil
}
var tmpl string
if err := json.Unmarshal(fieldRaw, &tmpl); err != nil {
return false, err
}
rewritten, changed, err := rewriteRelatedLinks(tmpl, kind)
if err != nil || !changed {
return false, err
}
newField, err := json.Marshal(rewritten)
if err != nil {
return false, err
}
object[kind.field] = newField
return true, nil
}
// rewriteRelatedLinks swaps the related links section of tmpl for the kind's
// block call. It returns the input unchanged when no section is present, and an
// error when a section is present but was customised or does not parse.
func rewriteRelatedLinks(tmpl string, kind relatedLinksChannelKind) (string, bool, error) {
chunks := lexRelatedLinksChunks(tmpl)
start, end, found := findRelatedLinksSection(tmpl, chunks)
if !found {
if indexRelatedLinksAction(chunks, 0, "related_logs") >= 0 || indexRelatedLinksAction(chunks, 0, "related_traces") >= 0 {
return tmpl, false, errRelatedLinksUnrecognised
}
return tmpl, false, nil
}
section, err := normalizeRelatedLinksTemplate(tmpl[start:end])
if err != nil {
return tmpl, false, err
}
matched := false
for _, shipped := range kind.defaults {
normalized, err := normalizeRelatedLinksTemplate(shipped)
if err != nil {
return tmpl, false, err
}
if section == normalized {
matched = true
break
}
}
if !matched {
return tmpl, false, errRelatedLinksCustomised
}
rewritten := tmpl[:start] + kind.replacement + tmpl[end:]
if _, err := normalizeRelatedLinksTemplate(rewritten); err != nil {
return tmpl, false, err
}
return rewritten, true, nil
}
func lexRelatedLinksChunks(tmpl string) []relatedLinksChunk {
var chunks []relatedLinksChunk
pos := 0
for _, match := range relatedLinksActionPattern.FindAllStringSubmatchIndex(tmpl, -1) {
if match[0] > pos {
chunks = append(chunks, relatedLinksChunk{start: pos, end: match[0]})
}
chunks = append(chunks, relatedLinksChunk{start: match[0], end: match[1], action: true, inner: strings.TrimSpace(tmpl[match[2]:match[3]])})
pos = match[1]
}
if pos < len(tmpl) {
chunks = append(chunks, relatedLinksChunk{start: pos, end: len(tmpl)})
}
return chunks
}
// findRelatedLinksSection spans from the first action naming related_logs (or
// the Slack header text before it) to the end of the first action naming
// related_traces that follows.
func findRelatedLinksSection(tmpl string, chunks []relatedLinksChunk) (int, int, bool) {
logsIdx := indexRelatedLinksAction(chunks, 0, "related_logs")
if logsIdx < 0 {
return 0, 0, false
}
logsEnd := indexRelatedLinksBlockEnd(chunks, logsIdx)
if logsEnd < 0 {
return 0, 0, false
}
tracesIdx := indexRelatedLinksAction(chunks, logsEnd+1, "related_traces")
if tracesIdx < 0 {
return 0, 0, false
}
tracesEnd := indexRelatedLinksBlockEnd(chunks, tracesIdx)
if tracesEnd < 0 {
return 0, 0, false
}
start := chunks[logsIdx].start
if logsIdx > 0 && !chunks[logsIdx-1].action {
previous := tmpl[chunks[logsIdx-1].start:chunks[logsIdx-1].end]
if header := strings.LastIndex(previous, relatedLinksSlackHeader); header >= 0 {
start = chunks[logsIdx-1].start + header
}
}
return start, chunks[tracesEnd].end, true
}
func indexRelatedLinksAction(chunks []relatedLinksChunk, from int, annotation string) int {
for i := from; i < len(chunks); i++ {
if chunks[i].action && strings.Contains(chunks[i].inner, annotation) {
return i
}
}
return -1
}
// indexRelatedLinksBlockEnd returns the chunk closing the block opened at idx,
// or idx itself for a bare action.
func indexRelatedLinksBlockEnd(chunks []relatedLinksChunk, idx int) int {
if !opensRelatedLinksBlock(chunks[idx].inner) {
return idx
}
depth := 0
for i := idx; i < len(chunks); i++ {
if !chunks[i].action {
continue
}
if opensRelatedLinksBlock(chunks[i].inner) {
depth++
continue
}
if chunks[i].inner == "end" {
depth--
if depth == 0 {
return i
}
}
}
return -1
}
func opensRelatedLinksBlock(inner string) bool {
for _, keyword := range []string{"if ", "range ", "with ", "define ", "block "} {
if strings.HasPrefix(inner, keyword) {
return true
}
}
return false
}
// normalizeRelatedLinksTemplate re-emits a parsed template with action spacing
// canonicalised and text runs collapsed to single spaces, so two snippets that
// differ only in whitespace compare equal.
func normalizeRelatedLinksTemplate(tmpl string) (string, error) {
tree := parse.New("related_links")
tree.Mode = parse.SkipFuncCheck
parsed, err := tree.Parse(tmpl, "{{", "}}", map[string]*parse.Tree{})
if err != nil {
return "", err
}
var builder strings.Builder
writeNormalizedRelatedLinksNode(&builder, parsed.Root)
return builder.String(), nil
}
func writeNormalizedRelatedLinksNode(builder *strings.Builder, node parse.Node) {
switch node := node.(type) {
case *parse.ListNode:
for _, child := range node.Nodes {
writeNormalizedRelatedLinksNode(builder, child)
}
case *parse.TextNode:
builder.WriteString(strings.Join(strings.Fields(string(node.Text)), " "))
builder.WriteString("|")
case *parse.IfNode:
builder.WriteString("{{if ")
builder.WriteString(node.Pipe.String())
builder.WriteString("}}")
writeNormalizedRelatedLinksNode(builder, node.List)
if node.ElseList != nil {
builder.WriteString("{{else}}")
writeNormalizedRelatedLinksNode(builder, node.ElseList)
}
builder.WriteString("{{end}}")
default:
builder.WriteString(node.String())
}
}

View File

@@ -89,11 +89,7 @@ const (
{{ end }}{{ if .GeneratorURL }}[View in SigNoz]({{ .GeneratorURL }})
{{ end }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})
{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})
{{ end }}{{ end }}`
{{ end }}{{ template "signoz.relatedLinks.markdown" . }}{{ end }}`
)
// IncidentIOReceiverConfig is the SigNoz incident.io receiver, backed by an

View File

@@ -94,11 +94,7 @@ const (
{{ end }}{{ if .GeneratorURL }}[View in SigNoz]({{ .GeneratorURL }})
{{ end }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})
{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})
{{ end }}{{ end }}`
{{ end }}{{ template "signoz.relatedLinks.markdown" . }}{{ end }}`
)
// JSMOpsReceiverConfig is the SigNoz Jira Service Management Ops receiver. It is

View File

@@ -0,0 +1,59 @@
package alertmanagertypes
import (
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/common/model"
)
const (
AnnotationRelatedLogs = "related_logs"
AnnotationRelatedTraces = "related_traces"
)
const (
relatedLogsLabel = "View Related Logs"
relatedTracesLabel = "View Related Traces"
)
type RelatedLink struct {
Label string
URL string
}
func BuildRelatedLinks(annotations model.LabelSet) []RelatedLink {
return newRelatedLinks(string(annotations[AnnotationRelatedLogs]), string(annotations[AnnotationRelatedTraces]))
}
// buildRelatedLinksFromTemplateData backs the relatedLinks template func, where
// .Annotations is template.KV in channel templates and map[string]any after
// custom template preprocessing.
func buildRelatedLinksFromTemplateData(annotations any) []RelatedLink {
return newRelatedLinks(lookupTemplateAnnotation(annotations, AnnotationRelatedLogs), lookupTemplateAnnotation(annotations, AnnotationRelatedTraces))
}
func newRelatedLinks(logsURL, tracesURL string) []RelatedLink {
var links []RelatedLink
if logsURL != "" {
links = append(links, RelatedLink{Label: relatedLogsLabel, URL: logsURL})
}
if tracesURL != "" {
links = append(links, RelatedLink{Label: relatedTracesLabel, URL: tracesURL})
}
return links
}
func lookupTemplateAnnotation(annotations any, key string) string {
switch m := annotations.(type) {
case template.KV:
return m[key]
case map[string]string:
return m[key]
case map[string]any:
value, _ := m[key].(string)
return value
}
return ""
}

View File

@@ -0,0 +1,100 @@
package alertmanagertypes
import (
"testing"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
)
func TestBuildRelatedLinks(t *testing.T) {
testCases := []struct {
name string
annotations model.LabelSet
expected []RelatedLink
}{
{
name: "Both_LogsFirst",
annotations: model.LabelSet{"related_traces": "https://signoz.io/traces-explorer?a=1", "related_logs": "https://signoz.io/logs/logs-explorer?a=1"},
expected: []RelatedLink{
{Label: "View Related Logs", URL: "https://signoz.io/logs/logs-explorer?a=1"},
{Label: "View Related Traces", URL: "https://signoz.io/traces-explorer?a=1"},
},
},
{
name: "LogsOnly",
annotations: model.LabelSet{"related_logs": "https://signoz.io/logs/logs-explorer?b=2", "summary": "s"},
expected: []RelatedLink{{Label: "View Related Logs", URL: "https://signoz.io/logs/logs-explorer?b=2"}},
},
{
name: "TracesOnly",
annotations: model.LabelSet{"related_traces": "https://signoz.io/traces-explorer?c=3"},
expected: []RelatedLink{{Label: "View Related Traces", URL: "https://signoz.io/traces-explorer?c=3"}},
},
{
name: "EmptyValues_NoLinks",
annotations: model.LabelSet{"related_logs": "", "related_traces": ""},
expected: nil,
},
{
name: "Nil_NoLinks",
annotations: nil,
expected: nil,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.expected, BuildRelatedLinks(testCase.annotations))
})
}
}
func TestBuildRelatedLinksFromTemplateData(t *testing.T) {
testCases := []struct {
name string
annotations any
expected []RelatedLink
}{
{
name: "KV_Both_LogsFirst",
annotations: template.KV{"related_traces": "https://signoz.io/traces-explorer?d=4", "related_logs": "https://signoz.io/logs/logs-explorer?d=4"},
expected: []RelatedLink{
{Label: "View Related Logs", URL: "https://signoz.io/logs/logs-explorer?d=4"},
{Label: "View Related Traces", URL: "https://signoz.io/traces-explorer?d=4"},
},
},
{
name: "MapStringString_TracesOnly",
annotations: map[string]string{"related_traces": "https://signoz.io/traces-explorer?e=5"},
expected: []RelatedLink{{Label: "View Related Traces", URL: "https://signoz.io/traces-explorer?e=5"}},
},
{
name: "MapStringAny_StringValue",
annotations: map[string]any{"related_logs": "https://signoz.io/logs/logs-explorer?f=6"},
expected: []RelatedLink{{Label: "View Related Logs", URL: "https://signoz.io/logs/logs-explorer?f=6"}},
},
{
name: "MapStringAny_NonStringValue_Ignored",
annotations: map[string]any{"related_logs": 42, "related_traces": nil},
expected: nil,
},
{
name: "Nil_NoLinks",
annotations: nil,
expected: nil,
},
{
name: "UnsupportedShape_NoLinks",
annotations: "related_logs=https://signoz.io",
expected: nil,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.expected, buildRelatedLinksFromTemplateData(testCase.annotations))
})
}
}

View File

@@ -19,6 +19,7 @@ func AdditionalFuncMap() tmpltext.FuncMap {
"urlescape": func(value string) tmplhtml.HTML {
return tmplhtml.HTML(url.QueryEscape(value))
},
"relatedLinks": buildRelatedLinksFromTemplateData,
}
}
@@ -45,6 +46,10 @@ func FromGlobs(paths []string) (*alertmanagertemplate.Template, error) {
{{ define "__ruleIdPath" }}{{- $isTestAlert := "" -}}{{- range .CommonLabels.SortedPairs -}}{{- if eq .Name "testalert" -}}{{- if eq .Value "true" -}}{{- $isTestAlert = "true" -}}{{- end -}}{{- end -}}{{- end -}}{{- range .CommonLabels.SortedPairs -}}{{- if eq .Name "ruleId" -}}{{- if ne .Value "" -}}/edit?ruleId={{ .Value | urlescape }}{{- if $isTestAlert -}}&isTestAlert=true{{- end -}}{{- end -}}{{- end -}}{{- end -}}{{- end }}
{{ define "__alertmanagerURL" }}{{ .ExternalURL }}/alerts{{ template "__ruleIdPath" . }}{{ end }}
{{ define "msteamsv2.default.titleLink" }}{{ template "__alertmanagerURL" . }}{{ end }}
{{ define "signoz.relatedLinks.slack" }}{{ range $i, $link := relatedLinks .Annotations }}{{ if $i }} · {{ end }}<{{ $link.URL }}|{{ $link.Label }}>{{ end }}{{ end }}
{{ define "signoz.relatedLinks.markdown" }}{{ range relatedLinks .Annotations }}[{{ .Label }}]({{ .URL }})
{{ end }}{{ end }}
`))); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error parsing alertmanager templates")
}

View File

@@ -201,3 +201,70 @@ func TestFromGlobs(t *testing.T) {
})
}
}
func TestFromGlobsRelatedLinks(t *testing.T) {
tmpl, err := FromGlobs([]string{})
require.NoError(t, err)
tmpl.ExternalURL = &url.URL{Scheme: "http", Host: "localhost:8080"}
testCases := []struct {
name string
template string
annotations model.LabelSet
expected string
}{
{
name: "Slack_Both_DotSeparated",
template: `{{ template "signoz.relatedLinks.slack" . }}`,
annotations: model.LabelSet{"related_logs": "https://signoz.io/logs/logs-explorer?a=1", "related_traces": "https://signoz.io/traces-explorer?a=1"},
expected: "<https://signoz.io/logs/logs-explorer?a=1|View Related Logs> · <https://signoz.io/traces-explorer?a=1|View Related Traces>",
},
{
name: "Slack_LogsOnly_NoSeparator",
template: `{{ template "signoz.relatedLinks.slack" . }}`,
annotations: model.LabelSet{"related_logs": "https://signoz.io/logs/logs-explorer?b=2"},
expected: "<https://signoz.io/logs/logs-explorer?b=2|View Related Logs>",
},
{
name: "Slack_NoLinks_Empty",
template: `{{ template "signoz.relatedLinks.slack" . }}`,
annotations: model.LabelSet{"summary": "s"},
expected: "",
},
{
name: "Markdown_Both_BlankLineAfterEach",
template: `{{ template "signoz.relatedLinks.markdown" . }}`,
annotations: model.LabelSet{"related_logs": "https://signoz.io/logs/logs-explorer?c=3", "related_traces": "https://signoz.io/traces-explorer?c=3"},
expected: "[View Related Logs](https://signoz.io/logs/logs-explorer?c=3)\n\n[View Related Traces](https://signoz.io/traces-explorer?c=3)\n\n",
},
{
name: "Markdown_TracesOnly",
template: `{{ template "signoz.relatedLinks.markdown" . }}`,
annotations: model.LabelSet{"related_traces": "https://signoz.io/traces-explorer?d=4"},
expected: "[View Related Traces](https://signoz.io/traces-explorer?d=4)\n\n",
},
{
name: "Markdown_NoLinks_Empty",
template: `{{ template "signoz.relatedLinks.markdown" . }}`,
annotations: model.LabelSet{},
expected: "",
},
{
name: "Function_DirectRange",
template: `{{ range relatedLinks .Annotations }}{{ .Label }}={{ .URL }};{{ end }}`,
annotations: model.LabelSet{"related_logs": "https://signoz.io/logs/logs-explorer?e=5", "related_traces": "https://signoz.io/traces-explorer?e=5"},
expected: "View Related Logs=https://signoz.io/logs/logs-explorer?e=5;View Related Traces=https://signoz.io/traces-explorer?e=5;",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
alert := &types.Alert{Alert: model.Alert{Labels: model.LabelSet{"alertname": "a"}, Annotations: testCase.annotations}, UpdatedAt: time.Now()}
data := tmpl.Data("__receiver", model.LabelSet{}, alert)
got, err := tmpl.ExecuteTextString(`{{ range .Alerts }}`+testCase.template+`{{ end }}`, data)
require.NoError(t, err)
assert.Equal(t, testCase.expected, got)
})
}
}

View File

@@ -32,8 +32,6 @@ const (
const (
AnnotationTitleTemplate = "_title_template"
AnnotationBodyTemplate = "_body_template"
AnnotationRelatedLogs = "related_logs"
AnnotationRelatedTraces = "related_traces"
AnnotationValue = "value"
AnnotationThresholdValue = "threshold.value"
AnnotationCompareOp = "compare_op"

View File

@@ -101,7 +101,7 @@ slack_default_config = {
{
"api_url": "services/TEAM_ID/BOT_ID/TOKEN_ID", # base_url configured on runtime
"title": '[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}\n {{- if gt (len .CommonLabels) (len .GroupLabels) -}}\n {{" "}}(\n {{- with .CommonLabels.Remove .GroupLabels.Names }}\n {{- range $index, $label := .SortedPairs -}}\n {{ if $index }}, {{ end }}\n {{- $label.Name }}="{{ $label.Value -}}"\n {{- end }}\n {{- end -}}\n )\n {{- end }}',
"text": '{{ range .Alerts -}}\r\n *Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}\r\n\r\n *Summary:* {{ .Annotations.summary }}\r\n *Description:* {{ .Annotations.description }}\r\n *RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}}\r\n *RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}}\r\n\r\n *Details:*\r\n {{ range .Labels.SortedPairs -}}\r\n {{- if ne .Name "ruleId" -}}\r\n \u2022 *{{ .Name }}:* {{ .Value }}\r\n {{ end -}}\r\n {{ end -}}\r\n{{ end }}',
"text": '{{ range .Alerts -}}\r\n *Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}\r\n\r\n *Summary:* {{ .Annotations.summary }}\r\n *Description:* {{ .Annotations.description }}\r\n {{ template "signoz.relatedLinks.slack" . }}\r\n\r\n *Details:*\r\n {{ range .Labels.SortedPairs -}}\r\n {{- if ne .Name "ruleId" -}}\r\n \u2022 *{{ .Name }}:* {{ .Value }}\r\n {{ end -}}\r\n {{ end -}}\r\n{{ end }}',
}
],
}
@@ -111,7 +111,7 @@ msteams_default_config = {
{
"webhook_url": "msteams/webhook_url", # base_url configured on runtime
"title": '[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}\n {{- if gt (len .CommonLabels) (len .GroupLabels) -}}\n {{" "}}(\n {{- with .CommonLabels.Remove .GroupLabels.Names }}\n {{- range $index, $label := .SortedPairs -}}\n {{ if $index }}, {{ end }}\n {{- $label.Name }}="{{ $label.Value -}}"\n {{- end }}\n {{- end -}}\n )\n {{- end }}',
"text": '{{ range .Alerts -}}\r\n *Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}\r\n\r\n *Summary:* {{ .Annotations.summary }}\r\n *Description:* {{ .Annotations.description }}\r\n *RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}}\r\n *RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}}\r\n\r\n *Details:*\r\n {{ range .Labels.SortedPairs -}}\r\n {{- if ne .Name "ruleId" -}}\r\n \u2022 *{{ .Name }}:* {{ .Value }}\r\n {{ end -}}\r\n {{ end -}}\r\n{{ end }}',
"text": '{{ range .Alerts -}}\r\n *Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}\r\n\r\n *Summary:* {{ .Annotations.summary }}\r\n *Description:* {{ .Annotations.description }}\r\n {{ template "signoz.relatedLinks.slack" . }}\r\n\r\n *Details:*\r\n {{ range .Labels.SortedPairs -}}\r\n {{- if ne .Name "ruleId" -}}\r\n \u2022 *{{ .Name }}:* {{ .Value }}\r\n {{ end -}}\r\n {{ end -}}\r\n{{ end }}',
}
],
}
@@ -161,7 +161,7 @@ email_default_config = {
"email_configs": [
{
"to": "test@example.com",
"html": '<html><body>{{ range .Alerts -}}\r\n *Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}\r\n\r\n *Summary:* {{ .Annotations.summary }}\r\n *Description:* {{ .Annotations.description }}\r\n *RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}}\r\n *RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}}\r\n\r\n *Details:*\r\n {{ range .Labels.SortedPairs -}}\r\n {{- if ne .Name "ruleId" -}}\r\n \u2022 *{{ .Name }}:* {{ .Value }}\r\n {{ end -}}\r\n {{ end -}}\r\n{{ end }}</body></html>',
"html": '<html><body>{{ range .Alerts -}}\r\n *Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}\r\n\r\n *Summary:* {{ .Annotations.summary }}\r\n *Description:* {{ .Annotations.description }}\r\n {{ template "signoz.relatedLinks.slack" . }}\r\n\r\n *Details:*\r\n {{ range .Labels.SortedPairs -}}\r\n {{- if ne .Name "ruleId" -}}\r\n \u2022 *{{ .Name }}:* {{ .Value }}\r\n {{ end -}}\r\n {{ end -}}\r\n{{ end }}</body></html>',
"headers": {
"Subject": '[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}\n {{- if gt (len .CommonLabels) (len .GroupLabels) -}}\n {{" "}}(\n {{- with .CommonLabels.Remove .GroupLabels.Names }}\n {{- range $index, $label := .SortedPairs -}}\n {{ if $index }}, {{ end }}\n {{- $label.Name }}="{{ $label.Value -}}"\n {{- end }}\n {{- end -}}\n )\n {{- end }}'
},

View File

@@ -419,11 +419,7 @@ def test_create_channel( # pylint: disable=too-many-arguments,too-many-position
"\n"
"{{ end }}{{ if .GeneratorURL }}[View in SigNoz]({{ .GeneratorURL }})\n"
"\n"
"{{ end }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})\n"
"\n"
"{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})\n"
"\n"
"{{ end }}{{ end }}"
'{{ end }}{{ template "signoz.relatedLinks.markdown" . }}{{ end }}'
),
"tags": "signoz",
},
@@ -450,11 +446,7 @@ def test_create_channel( # pylint: disable=too-many-arguments,too-many-position
"\n"
"{{ end }}{{ if .GeneratorURL }}[View in SigNoz]({{ .GeneratorURL }})\n"
"\n"
"{{ end }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})\n"
"\n"
"{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})\n"
"\n"
"{{ end }}{{ end }}"
'{{ end }}{{ template "signoz.relatedLinks.markdown" . }}{{ end }}'
),
},
[],

View File

@@ -66,7 +66,7 @@ INCIDENTIO_CASES = [
"count": 1,
"json_body": incidentio_event_subset(
"threshold_below_at_least_once",
[("View in SigNoz", r"/alerts/overview\?ruleId="), ("View related logs", r"/logs/logs-explorer\?")],
[("View in SigNoz", r"/alerts/overview\?ruleId="), ("View Related Logs", r"/logs/logs-explorer\?")],
),
},
),

View File

@@ -73,7 +73,7 @@ JSMOPS_CASES = [
"count": 1,
"json_body": jsmops_alert_subset(
"threshold_below_at_least_once",
[("View in SigNoz", r"/alerts/overview\?ruleId="), ("View related logs", r"/logs/logs-explorer\?")],
[("View in SigNoz", r"/alerts/overview\?ruleId="), ("View Related Logs", r"/logs/logs-explorer\?")],
),
},
),