mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-24 02:50:50 +01:00
Compare commits
1 Commits
backup/thr
...
srikanth/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb1b993c14 |
@@ -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.RelatedLinks(alerts[i].Annotations) {
|
||||
bodies[i] += htmlButton(link.Label, link.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.RelatedLinks(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
|
||||
}
|
||||
|
||||
|
||||
@@ -343,6 +343,16 @@ func TestGoogleChatLinkButtons(t *testing.T) {
|
||||
"View Related Traces": "https://signoz.example/traces",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ai traces link",
|
||||
labels: model.LabelSet{"alertname": "X"},
|
||||
annotations: model.LabelSet{
|
||||
ruletypes.AnnotationRelatedTraces: "https://signoz.example/ai-observability/explorer?q=1",
|
||||
},
|
||||
wantButtons: map[string]string{
|
||||
"View Related AI Traces": "https://signoz.example/ai-observability/explorer?q=1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no links → no buttons",
|
||||
labels: model.LabelSet{"alertname": "X"},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.RelatedLinks(a.Annotations) {
|
||||
add(link.Label, link.URL)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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.RelatedLinks(alert.Annotations) {
|
||||
actions = append(actions, config.SlackAction{Type: "button", Text: link.Label, URL: link.URL})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
@@ -157,6 +157,108 @@ Description: Request rate exceeded 10k/s`},
|
||||
" "},
|
||||
wantIsDefaultBody: true,
|
||||
},
|
||||
{
|
||||
// The current seeded default renders the related-links block via the
|
||||
// server-side define; for a plain traces link the output is byte-equal
|
||||
// to the old inline template above.
|
||||
name: "current default: related links via define, same bytes as old default",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(
|
||||
map[string]string{ruletypes.LabelAlertName: "DiskUsageHigh", ruletypes.LabelSeverityName: "critical", "instance": "db-primary-01"},
|
||||
map[string]string{
|
||||
"summary": "Disk usage high on database host",
|
||||
"description": "Disk usage is high on the database host",
|
||||
"related_logs": "https://logs.example.com/search?q=DiskUsageHigh",
|
||||
"related_traces": "https://traces.example.com/search?q=DiskUsageHigh",
|
||||
},
|
||||
true,
|
||||
),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
DefaultTitleTemplate: `{{ .CommonLabels.alertname }}`,
|
||||
DefaultBodyTemplate: `{{ 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 }}`,
|
||||
},
|
||||
wantTitle: "DiskUsageHigh",
|
||||
wantBody: []string{"*Alert:* DiskUsageHigh - critical\n" +
|
||||
"\n" +
|
||||
" *Summary:* Disk usage high on database host\n" +
|
||||
" *Description:* Disk usage is high on the database host\n" +
|
||||
" *RelatedLogs:* View in <https://logs.example.com/search?q=DiskUsageHigh|logs explorer>\n" +
|
||||
" *RelatedTraces:* View in <https://traces.example.com/search?q=DiskUsageHigh|traces explorer>\n" +
|
||||
"\n" +
|
||||
" *Details:*\n" +
|
||||
" • *alertname:* DiskUsageHigh\n" +
|
||||
" • *instance:* db-primary-01\n" +
|
||||
" • *severity:* critical\n" +
|
||||
" \n" +
|
||||
" "},
|
||||
wantIsDefaultBody: true,
|
||||
},
|
||||
{
|
||||
// An AI trace alert links to the ai-observability explorer and the
|
||||
// define labels it accordingly.
|
||||
name: "current default: ai traces link labelled ai traces explorer",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(
|
||||
map[string]string{ruletypes.LabelAlertName: "HighLLMLatency", ruletypes.LabelSeverityName: "warning"},
|
||||
map[string]string{
|
||||
"summary": "LLM latency is high",
|
||||
"description": "p99 latency of LLM calls is above threshold",
|
||||
"related_traces": "https://signoz.example.com/ai-observability/explorer?startTime=1&endTime=2",
|
||||
},
|
||||
true,
|
||||
),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
DefaultTitleTemplate: `{{ .CommonLabels.alertname }}`,
|
||||
DefaultBodyTemplate: `{{ range .Alerts -}}
|
||||
*RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|{{ if isAITracesLink .Annotations.related_traces }}ai traces explorer{{ else }}traces explorer{{ end }}> {{- end}}
|
||||
{{ end }}`,
|
||||
},
|
||||
wantTitle: "HighLLMLatency",
|
||||
wantBody: []string{"*RelatedTraces:* View in <https://signoz.example.com/ai-observability/explorer?startTime=1&endTime=2|ai traces explorer>\n "},
|
||||
wantIsDefaultBody: true,
|
||||
},
|
||||
{
|
||||
// The markdown define used by the incident.io / JSM Ops defaults,
|
||||
// exercised through the shipped JSM Ops description template.
|
||||
name: "jsmops default: ai traces link labelled view related ai traces",
|
||||
alerts: []*types.Alert{
|
||||
createAlert(
|
||||
map[string]string{ruletypes.LabelAlertName: "HighLLMLatency", ruletypes.LabelSeverityName: "warning"},
|
||||
map[string]string{
|
||||
"summary": "LLM latency is high",
|
||||
"description": "p99 latency of LLM calls is above threshold",
|
||||
"related_traces": "https://signoz.example.com/ai-observability/explorer?startTime=1&endTime=2",
|
||||
},
|
||||
true,
|
||||
),
|
||||
},
|
||||
input: alertmanagertypes.ExpandRequest{
|
||||
DefaultTitleTemplate: alertmanagertypes.DefaultJSMOpsMessageTemplate,
|
||||
DefaultBodyTemplate: alertmanagertypes.DefaultJSMOpsDescriptionTemplate,
|
||||
},
|
||||
wantTitle: "[FIRING:1] HighLLMLatency",
|
||||
wantBody: []string{"**Alert:** HighLLMLatency (warning)\n" +
|
||||
"\n" +
|
||||
"**Summary:** LLM latency is high\n" +
|
||||
"\n" +
|
||||
"**Description:** p99 latency of LLM calls is above threshold\n" +
|
||||
"\n" +
|
||||
"[View related AI traces](https://signoz.example.com/ai-observability/explorer?startTime=1&endTime=2)\n" +
|
||||
"\n"},
|
||||
wantIsDefaultBody: true,
|
||||
},
|
||||
{
|
||||
// Pod crash loop on multiple pods — body is expanded once per alert
|
||||
// and joined with "\n\n", with the pod name pulled from labels.
|
||||
|
||||
@@ -311,11 +311,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
case ruletypes.AlertTypeTraces, ruletypes.AlertTypeAITraces:
|
||||
params := r.prepareParamsForTraces(ctx, ts, smpl.Metric)
|
||||
if len(params) > 0 {
|
||||
explorerPath := "traces-explorer"
|
||||
if r.typ == ruletypes.AlertTypeAITraces {
|
||||
explorerPath = "ai-observability/explorer"
|
||||
}
|
||||
link := r.ExternalURL(explorerPath, params)
|
||||
link := r.ExternalURL(r.typ.TracesExplorerPath(), 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})
|
||||
}
|
||||
|
||||
@@ -261,6 +261,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddUserTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddRuleViewFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddFieldTuplesFactory(sqlstore),
|
||||
sqlmigration.NewUpdateChannelDefaultTemplatesFactory(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
272
pkg/sqlmigration/135_update_channel_default_templates.go
Normal file
272
pkg/sqlmigration/135_update_channel_default_templates.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
// The seeded default templates this migration replaces, byte-exact as the
|
||||
// frontend (slack / msteams) and the backend consts (incidentio / jsmops)
|
||||
// shipped them over time. A channel whose template matches none of these was
|
||||
// customized and is never touched.
|
||||
const (
|
||||
slackDefaultTextPre2024 = `{{ range .Alerts -}}
|
||||
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
|
||||
|
||||
*Summary:* {{ .Annotations.summary }}
|
||||
*Description:* {{ .Annotations.description }}
|
||||
|
||||
*Details:*
|
||||
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
|
||||
{{ end }}
|
||||
{{ end }}`
|
||||
|
||||
slackDefaultText2024 = `{{ range .Alerts -}}
|
||||
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
|
||||
|
||||
*Summary:* {{ .Annotations.summary }}
|
||||
*Description:* {{ .Annotations.description }}
|
||||
*RelatedLogs:* {{ .Annotations.related_logs }}
|
||||
*RelatedTraces:* {{ .Annotations.related_traces }}
|
||||
|
||||
*Details:*
|
||||
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
|
||||
{{ end }}
|
||||
{{ end }}`
|
||||
|
||||
slackDefaultText2026 = `{{ 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 }}`
|
||||
|
||||
slackDefaultTextCurrent = `{{ 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 }}`
|
||||
|
||||
markdownDefaultDescription2026 = `{{ range .Alerts -}}
|
||||
**Alert:** {{ .Labels.alertname }}{{ if .Labels.severity }} ({{ .Labels.severity }}){{ end }}
|
||||
|
||||
{{ if .Annotations.summary }}**Summary:** {{ .Annotations.summary }}
|
||||
|
||||
{{ end }}{{ if .Annotations.description }}**Description:** {{ .Annotations.description }}
|
||||
|
||||
{{ end }}{{ 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 }}`
|
||||
|
||||
markdownDefaultDescriptionCurrent = `{{ range .Alerts -}}
|
||||
**Alert:** {{ .Labels.alertname }}{{ if .Labels.severity }} ({{ .Labels.severity }}){{ end }}
|
||||
|
||||
{{ if .Annotations.summary }}**Summary:** {{ .Annotations.summary }}
|
||||
|
||||
{{ end }}{{ if .Annotations.description }}**Description:** {{ .Annotations.description }}
|
||||
|
||||
{{ end }}{{ if .GeneratorURL }}[View in SigNoz]({{ .GeneratorURL }})
|
||||
|
||||
{{ end }}{{ template "signoz.relatedLinks.markdown" . }}{{ end }}`
|
||||
)
|
||||
|
||||
var channelDefaultTextUpgrades = map[string]string{
|
||||
slackDefaultTextPre2024: slackDefaultTextCurrent,
|
||||
slackDefaultText2024: slackDefaultTextCurrent,
|
||||
slackDefaultText2026: slackDefaultTextCurrent,
|
||||
}
|
||||
|
||||
var channelDefaultDescriptionUpgrades = map[string]string{
|
||||
markdownDefaultDescription2026: markdownDefaultDescriptionCurrent,
|
||||
}
|
||||
|
||||
// receiver configs lists and the template field each one carries.
|
||||
var channelTemplateFields = map[string]string{
|
||||
"slack_configs": "text",
|
||||
"msteams_configs": "text",
|
||||
"msteamsv2_configs": "text",
|
||||
"incidentio_configs": "description",
|
||||
"jsmops_configs": "description",
|
||||
}
|
||||
|
||||
type updateChannelDefaultTemplates struct {
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewUpdateChannelDefaultTemplatesFactory() factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("update_channel_templates"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &updateChannelDefaultTemplates{logger: ps.Logger}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *updateChannelDefaultTemplates) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
type channelTemplateRow struct {
|
||||
bun.BaseModel `bun:"table:notification_channel"`
|
||||
|
||||
ID string `bun:"id,pk"`
|
||||
Type string `bun:"type"`
|
||||
Data string `bun:"data"`
|
||||
Spec string `bun:"spec,nullzero"`
|
||||
}
|
||||
|
||||
// Up rewrites, in both the receiver (data) and the v2 spec, channel templates
|
||||
// that are byte-equal to a seeded default of an earlier release, so untouched
|
||||
// channels keep tracking the current default. A malformed row is logged and
|
||||
// skipped, never fails the migration.
|
||||
func (migration *updateChannelDefaultTemplates) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
rows := make([]*channelTemplateRow, 0)
|
||||
err = tx.NewSelect().
|
||||
Model(&rows).
|
||||
Where("type IN (?)", bun.In([]string{"slack", "msteams", "msteamsv2", "incidentio", "jsmops"})).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
data, dataChanged, err := upgradeReceiverDefaultTemplates(row.Data)
|
||||
if err != nil {
|
||||
migration.logger.WarnContext(ctx, "skipping channel with unreadable receiver", "channel_id", row.ID, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
spec, specChanged, err := upgradeSpecDefaultTemplate(row.Type, row.Spec)
|
||||
if err != nil {
|
||||
migration.logger.WarnContext(ctx, "skipping channel with unreadable spec", "channel_id", row.ID, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !dataChanged && !specChanged {
|
||||
continue
|
||||
}
|
||||
|
||||
row.Data, row.Spec = data, spec
|
||||
if _, err := tx.NewUpdate().Model(row).Column("data", "spec").WherePK().Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *updateChannelDefaultTemplates) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func upgradeReceiverDefaultTemplates(data string) (string, bool, error) {
|
||||
receiver := map[string]any{}
|
||||
if err := json.Unmarshal([]byte(data), &receiver); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
changed := false
|
||||
for configsKey, field := range channelTemplateFields {
|
||||
configs, ok := receiver[configsKey].([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, c := range configs {
|
||||
config, ok := c.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
value, ok := config[field].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if upgraded, ok := upgradeChannelTemplate(field, value); ok {
|
||||
config[field] = upgraded
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return data, false, nil
|
||||
}
|
||||
|
||||
out, err := marshalJSONNoHTMLEscape(receiver)
|
||||
return out, true, err
|
||||
}
|
||||
|
||||
func upgradeSpecDefaultTemplate(rowType, spec string) (string, bool, error) {
|
||||
if spec == "" {
|
||||
return spec, false, nil
|
||||
}
|
||||
|
||||
field := "text"
|
||||
if rowType == "incidentio" || rowType == "jsmops" {
|
||||
field = "description"
|
||||
}
|
||||
|
||||
parsed := map[string]any{}
|
||||
if err := json.Unmarshal([]byte(spec), &parsed); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
value, ok := parsed[field].(string)
|
||||
if !ok {
|
||||
return spec, false, nil
|
||||
}
|
||||
upgraded, ok := upgradeChannelTemplate(field, value)
|
||||
if !ok {
|
||||
return spec, false, nil
|
||||
}
|
||||
|
||||
parsed[field] = upgraded
|
||||
out, err := marshalJSONNoHTMLEscape(parsed)
|
||||
return out, true, err
|
||||
}
|
||||
|
||||
func upgradeChannelTemplate(field, value string) (string, bool) {
|
||||
upgrades := channelDefaultTextUpgrades
|
||||
if field == "description" {
|
||||
upgrades = channelDefaultDescriptionUpgrades
|
||||
}
|
||||
upgraded, ok := upgrades[value]
|
||||
return upgraded, ok
|
||||
}
|
||||
|
||||
// marshalJSONNoHTMLEscape keeps < and > literal: slack templates contain
|
||||
// <url|label> links and the stored JSON stays readable.
|
||||
func marshalJSONNoHTMLEscape(v any) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
if err := enc.Encode(v); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSuffix(buf.String(), "\n"), nil
|
||||
}
|
||||
150
pkg/sqlmigration/135_update_channel_default_templates_test.go
Normal file
150
pkg/sqlmigration/135_update_channel_default_templates_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// The migration duplicates the current defaults (migrations stay import-free);
|
||||
// this pins the copies to the shipped consts.
|
||||
func TestChannelDefaultTemplatesMatchShippedDefaults(t *testing.T) {
|
||||
assert.Equal(t, alertmanagertypes.DefaultJSMOpsDescriptionTemplate, markdownDefaultDescriptionCurrent)
|
||||
assert.Equal(t, alertmanagertypes.DefaultIncidentIODescriptionTemplate, markdownDefaultDescriptionCurrent)
|
||||
}
|
||||
|
||||
func TestUpgradeReceiverDefaultTemplates(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
receiver map[string]any
|
||||
wantChanged bool
|
||||
wantText string
|
||||
}{
|
||||
{
|
||||
name: "SlackSeededDefault2026_Upgraded",
|
||||
receiver: map[string]any{"name": "slack-prod", "slack_configs": []any{map[string]any{"api_url": "https://hooks.slack.com/services/x", "text": slackDefaultText2026}}},
|
||||
wantChanged: true,
|
||||
wantText: slackDefaultTextCurrent,
|
||||
},
|
||||
{
|
||||
name: "SlackSeededDefault2024_Upgraded",
|
||||
receiver: map[string]any{"name": "slack-staging", "slack_configs": []any{map[string]any{"text": slackDefaultText2024}}},
|
||||
wantChanged: true,
|
||||
wantText: slackDefaultTextCurrent,
|
||||
},
|
||||
{
|
||||
name: "SlackSeededDefaultPre2024_Upgraded",
|
||||
receiver: map[string]any{"name": "slack-oldest", "slack_configs": []any{map[string]any{"text": slackDefaultTextPre2024}}},
|
||||
wantChanged: true,
|
||||
wantText: slackDefaultTextCurrent,
|
||||
},
|
||||
{
|
||||
name: "CustomizedText_Untouched",
|
||||
receiver: map[string]any{"name": "slack-custom", "slack_configs": []any{map[string]any{"text": "my own template {{ .Annotations.summary }}"}}},
|
||||
wantChanged: false,
|
||||
wantText: "my own template {{ .Annotations.summary }}",
|
||||
},
|
||||
{
|
||||
name: "MSTeamsSharesSlackDefault_Upgraded",
|
||||
receiver: map[string]any{"name": "teams-prod", "msteamsv2_configs": []any{map[string]any{"text": slackDefaultText2026}}},
|
||||
wantChanged: true,
|
||||
wantText: slackDefaultTextCurrent,
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
data, err := marshalJSONNoHTMLEscape(testCase.receiver)
|
||||
require.NoError(t, err)
|
||||
|
||||
upgraded, changed, err := upgradeReceiverDefaultTemplates(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testCase.wantChanged, changed)
|
||||
|
||||
parsed := map[string]any{}
|
||||
require.NoError(t, json.Unmarshal([]byte(upgraded), &parsed))
|
||||
for _, key := range []string{"slack_configs", "msteamsv2_configs"} {
|
||||
configs, ok := parsed[key].([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase.wantText, configs[0].(map[string]any)["text"])
|
||||
}
|
||||
assert.Equal(t, testCase.receiver["name"], parsed["name"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeReceiverDefaultTemplates_MarkdownDescription(t *testing.T) {
|
||||
data, err := marshalJSONNoHTMLEscape(map[string]any{"name": "incidentio-prod", "incidentio_configs": []any{map[string]any{"url": "https://api.incident.io/v2/alert_events/http/x", "description": markdownDefaultDescription2026}}})
|
||||
require.NoError(t, err)
|
||||
|
||||
upgraded, changed, err := upgradeReceiverDefaultTemplates(data)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, changed)
|
||||
|
||||
parsed := map[string]any{}
|
||||
require.NoError(t, json.Unmarshal([]byte(upgraded), &parsed))
|
||||
config := parsed["incidentio_configs"].([]any)[0].(map[string]any)
|
||||
assert.Equal(t, markdownDefaultDescriptionCurrent, config["description"])
|
||||
assert.Equal(t, "https://api.incident.io/v2/alert_events/http/x", config["url"])
|
||||
}
|
||||
|
||||
func TestUpgradeSpecDefaultTemplate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
rowType string
|
||||
spec map[string]any
|
||||
wantChanged bool
|
||||
wantField string
|
||||
wantValue string
|
||||
}{
|
||||
{
|
||||
name: "SlackSpecSeededDefault_Upgraded",
|
||||
rowType: "slack",
|
||||
spec: map[string]any{"send_resolved": true, "text": slackDefaultText2026},
|
||||
wantChanged: true,
|
||||
wantField: "text",
|
||||
wantValue: slackDefaultTextCurrent,
|
||||
},
|
||||
{
|
||||
name: "JSMOpsSpecSeededDefault_Upgraded",
|
||||
rowType: "jsmops",
|
||||
spec: map[string]any{"description": markdownDefaultDescription2026},
|
||||
wantChanged: true,
|
||||
wantField: "description",
|
||||
wantValue: markdownDefaultDescriptionCurrent,
|
||||
},
|
||||
{
|
||||
name: "CustomizedSpec_Untouched",
|
||||
rowType: "slack",
|
||||
spec: map[string]any{"text": "mine"},
|
||||
wantChanged: false,
|
||||
wantField: "text",
|
||||
wantValue: "mine",
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
spec, err := marshalJSONNoHTMLEscape(testCase.spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
upgraded, changed, err := upgradeSpecDefaultTemplate(testCase.rowType, spec)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testCase.wantChanged, changed)
|
||||
|
||||
parsed := map[string]any{}
|
||||
require.NoError(t, json.Unmarshal([]byte(upgraded), &parsed))
|
||||
assert.Equal(t, testCase.wantValue, parsed[testCase.wantField])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeSpecDefaultTemplate_EmptySpec(t *testing.T) {
|
||||
upgraded, changed, err := upgradeSpecDefaultTemplate("slack", "")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, changed)
|
||||
assert.Empty(t, upgraded)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
47
pkg/types/alertmanagertypes/relatedlinks.go
Normal file
47
pkg/types/alertmanagertypes/relatedlinks.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package alertmanagertypes
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
const (
|
||||
AnnotationRelatedLogs = "related_logs"
|
||||
AnnotationRelatedTraces = "related_traces"
|
||||
|
||||
AIObservabilityPathPrefix = "ai-observability"
|
||||
)
|
||||
|
||||
// RelatedLink is one deep link a notification renders for an alert.
|
||||
type RelatedLink struct {
|
||||
Label string
|
||||
URL string
|
||||
}
|
||||
|
||||
// RelatedLinks returns the deep links the ruler attached to an alert, in the
|
||||
// order notifiers render them. Labels are decided here so every notifier shows
|
||||
// the same text for the same link.
|
||||
func RelatedLinks(annotations model.LabelSet) []RelatedLink {
|
||||
links := make([]RelatedLink, 0, 2)
|
||||
if link := string(annotations[AnnotationRelatedLogs]); link != "" {
|
||||
links = append(links, RelatedLink{Label: "View Related Logs", URL: link})
|
||||
}
|
||||
if link := string(annotations[AnnotationRelatedTraces]); link != "" {
|
||||
label := "View Related Traces"
|
||||
if IsAITracesLink(link) {
|
||||
label = "View Related AI Traces"
|
||||
}
|
||||
links = append(links, RelatedLink{Label: label, URL: link})
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
// IsAITracesLink reports whether a related-traces link opens the AI
|
||||
// observability section of the UI. Matched on the URL path so installs served
|
||||
// under a path prefix still count. Channel templates call it as isAITracesLink.
|
||||
func IsAITracesLink(link string) bool {
|
||||
u, err := url.Parse(link)
|
||||
return err == nil && strings.Contains(u.Path, "/"+AIObservabilityPathPrefix+"/")
|
||||
}
|
||||
63
pkg/types/alertmanagertypes/relatedlinks_test.go
Normal file
63
pkg/types/alertmanagertypes/relatedlinks_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package alertmanagertypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsAITracesLink(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
link string
|
||||
want bool
|
||||
}{
|
||||
{name: "TracesExplorer", link: "https://signoz.example/traces-explorer?q=1", want: false},
|
||||
{name: "AITracesExplorer", link: "https://signoz.example/ai-observability/explorer?q=1", want: true},
|
||||
{name: "PrefixedInstall", link: "https://acme.com/signoz/ai-observability/explorer?q=1", want: true},
|
||||
{name: "Empty", link: "", want: false},
|
||||
{name: "AIObservabilityHost", link: "https://ai-observability.acme.com/traces-explorer", want: false},
|
||||
{name: "AIObservabilityPathLookalike", link: "https://acme.com/ai-observability-team/explorer", want: false},
|
||||
{name: "AIObservabilityInQuery", link: "https://acme.com/traces-explorer?q=/ai-observability/", want: false},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.want, IsAITracesLink(testCase.link))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelatedLinks(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
annotations model.LabelSet
|
||||
want []RelatedLink
|
||||
}{
|
||||
{
|
||||
name: "NoAnnotations",
|
||||
annotations: model.LabelSet{"summary": "cpu is high"},
|
||||
want: []RelatedLink{},
|
||||
},
|
||||
{
|
||||
name: "LogsAndTraces_LogsFirst",
|
||||
annotations: model.LabelSet{AnnotationRelatedLogs: "https://signoz.example/logs/logs-explorer?a=1", AnnotationRelatedTraces: "https://signoz.example/traces-explorer?a=1"},
|
||||
want: []RelatedLink{
|
||||
{Label: "View Related Logs", URL: "https://signoz.example/logs/logs-explorer?a=1"},
|
||||
{Label: "View Related Traces", URL: "https://signoz.example/traces-explorer?a=1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AITraces",
|
||||
annotations: model.LabelSet{AnnotationRelatedTraces: "https://signoz.example/ai-observability/explorer?a=1"},
|
||||
want: []RelatedLink{
|
||||
{Label: "View Related AI Traces", URL: "https://signoz.example/ai-observability/explorer?a=1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.want, RelatedLinks(testCase.annotations))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ func AdditionalFuncMap() tmpltext.FuncMap {
|
||||
"urlescape": func(value string) tmplhtml.HTML {
|
||||
return tmplhtml.HTML(url.QueryEscape(value))
|
||||
},
|
||||
"isAITracesLink": IsAITracesLink,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +46,13 @@ 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" }}*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 }}|{{ if isAITracesLink .Annotations.related_traces }}ai traces explorer{{ else }}traces explorer{{ end }}> {{- end}}{{ end }}
|
||||
{{ define "signoz.relatedLinks.markdown" }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})
|
||||
|
||||
{{ end }}{{ if .Annotations.related_traces }}[{{ if isAITracesLink .Annotations.related_traces }}View related AI traces{{ else }}View related traces{{ end }}]({{ .Annotations.related_traces }})
|
||||
|
||||
{{ end }}{{ end }}
|
||||
`))); err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error parsing alertmanager templates")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package ruletypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
|
||||
const (
|
||||
CriticalThresholdName = "critical"
|
||||
ErrorThresholdName = "error"
|
||||
@@ -32,8 +34,8 @@ const (
|
||||
const (
|
||||
AnnotationTitleTemplate = "_title_template"
|
||||
AnnotationBodyTemplate = "_body_template"
|
||||
AnnotationRelatedLogs = "related_logs"
|
||||
AnnotationRelatedTraces = "related_traces"
|
||||
AnnotationRelatedLogs = alertmanagertypes.AnnotationRelatedLogs
|
||||
AnnotationRelatedTraces = alertmanagertypes.AnnotationRelatedTraces
|
||||
AnnotationValue = "value"
|
||||
AnnotationThresholdValue = "threshold.value"
|
||||
AnnotationCompareOp = "compare_op"
|
||||
|
||||
16
pkg/types/ruletypes/related_links.go
Normal file
16
pkg/types/ruletypes/related_links.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package ruletypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
|
||||
const (
|
||||
DefaultTracesExplorerPath = "traces-explorer"
|
||||
AITracesExplorerPath = alertmanagertypes.AIObservabilityPathPrefix + "/explorer"
|
||||
)
|
||||
|
||||
// TracesExplorerPath is the UI path the alert's related-traces link opens.
|
||||
func (t AlertType) TracesExplorerPath() string {
|
||||
if t == AlertTypeAITraces {
|
||||
return AITracesExplorerPath
|
||||
}
|
||||
return DefaultTracesExplorerPath
|
||||
}
|
||||
26
pkg/types/ruletypes/related_links_test.go
Normal file
26
pkg/types/ruletypes/related_links_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTracesExplorerPath(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
alertType AlertType
|
||||
want string
|
||||
}{
|
||||
{name: "Metric", alertType: AlertTypeMetric, want: "traces-explorer"},
|
||||
{name: "Traces", alertType: AlertTypeTraces, want: "traces-explorer"},
|
||||
{name: "Logs", alertType: AlertTypeLogs, want: "traces-explorer"},
|
||||
{name: "Exceptions", alertType: AlertTypeExceptions, want: "traces-explorer"},
|
||||
{name: "AITraces", alertType: AlertTypeAITraces, want: "ai-observability/explorer"},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.want, testCase.alertType.TracesExplorerPath())
|
||||
})
|
||||
}
|
||||
}
|
||||
4
tests/fixtures/notification_channel.py
vendored
4
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 }}',
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -62,6 +62,39 @@ NOTIFIERS_TEST = [
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertManagerNotificationTestCase(
|
||||
name="slack_notifier_ai_traces_default_templating",
|
||||
rule_path="ruler/test_scenarios/rule_state_history_ai_traces/rule.json",
|
||||
alert_data=[
|
||||
types.AlertData(
|
||||
type="traces",
|
||||
data_path="ruler/test_scenarios/rule_state_history_ai_traces/alert_data.jsonl",
|
||||
),
|
||||
],
|
||||
channel_config=slack_default_config,
|
||||
notification_expectation=types.AMNotificationExpectation(
|
||||
should_notify=True,
|
||||
wait_time_seconds=120,
|
||||
notification_validations=[
|
||||
types.NotificationValidation(
|
||||
destination_type="webhook",
|
||||
validation_data={
|
||||
"path": "/services/TEAM_ID/BOT_ID/TOKEN_ID",
|
||||
"json_body": {
|
||||
"username": "Alertmanager",
|
||||
"attachments": [
|
||||
{
|
||||
"color": "danger",
|
||||
"mrkdwn_in": ["fallback", "pretext", "text"],
|
||||
"text": re.compile(r"\*RelatedTraces:\* View in <[^>]*/ai-observability/explorer\?[^>]*\|ai traces explorer>"),
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertManagerNotificationTestCase(
|
||||
name="msteams_notifier_default_templating",
|
||||
rule_path="ruler/test_scenarios/threshold_above_at_least_once/rule.json",
|
||||
|
||||
@@ -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 }}'
|
||||
),
|
||||
},
|
||||
[],
|
||||
|
||||
Reference in New Issue
Block a user