mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-19 16:40:42 +01:00
Compare commits
8 Commits
fix/dynami
...
refactor/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43a0f5ab5f | ||
|
|
ed9488b7e4 | ||
|
|
0e7af3c785 | ||
|
|
8667678c96 | ||
|
|
316a75787b | ||
|
|
c2f5f86583 | ||
|
|
c622e13a4f | ||
|
|
7a96011b1e |
@@ -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> = {
|
||||
|
||||
@@ -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 }}`;
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -262,6 +262,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddRuleViewFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddFieldTuplesFactory(sqlstore),
|
||||
sqlmigration.NewKeepRelevantLLMPricingRulesFactory(),
|
||||
sqlmigration.NewMigrateRelatedLinksTemplateFactory(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
510
pkg/sqlmigration/136_migrate_related_links_template.go
Normal file
510
pkg/sqlmigration/136_migrate_related_links_template.go
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
59
pkg/types/alertmanagertypes/relatedlinks.go
Normal file
59
pkg/types/alertmanagertypes/relatedlinks.go
Normal 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 ""
|
||||
}
|
||||
100
pkg/types/alertmanagertypes/relatedlinks_test.go
Normal file
100
pkg/types/alertmanagertypes/relatedlinks_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
6
tests/fixtures/notification_channel.py
vendored
6
tests/fixtures/notification_channel.py
vendored
@@ -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 }}'
|
||||
},
|
||||
|
||||
@@ -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 }}'
|
||||
),
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -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\?")],
|
||||
),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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\?")],
|
||||
),
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user