mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-14 06:00:40 +01:00
Compare commits
12 Commits
nv/context
...
issue_6021
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b26ab83206 | ||
|
|
56e6c8e386 | ||
|
|
608e44069d | ||
|
|
4fd8bab355 | ||
|
|
3180a5c856 | ||
|
|
9d07444cef | ||
|
|
f42a1af260 | ||
|
|
be8147431d | ||
|
|
57f9774fb1 | ||
|
|
b7abfba3e1 | ||
|
|
8004b62493 | ||
|
|
631e6eefde |
@@ -20,7 +20,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}}
|
||||
*RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}}{{ if match "^https?://[^?#]*/ai-observability/" .Annotations.related_traces -}} View in <{{ .Annotations.related_traces }}|ai traces explorer> {{- else -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end }}{{- end}}
|
||||
|
||||
*Details:*
|
||||
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
|
||||
@@ -137,7 +137,7 @@ export const JsmOpsInitialConfig: Partial<JsmOpsChannel> = {
|
||||
|
||||
{{ end }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})
|
||||
|
||||
{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})
|
||||
{{ end }}{{ if .Annotations.related_traces }}{{ if match "^https?://[^?#]*/ai-observability/" .Annotations.related_traces }}[View related AI traces]{{ else }}[View related traces]{{ end }}({{ .Annotations.related_traces }})
|
||||
|
||||
{{ end }}{{ end }}`,
|
||||
priority:
|
||||
@@ -163,7 +163,7 @@ export const IncidentIOInitialConfig: Partial<IncidentIOChannel> = {
|
||||
|
||||
{{ end }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})
|
||||
|
||||
{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})
|
||||
{{ end }}{{ if .Annotations.related_traces }}{{ if match "^https?://[^?#]*/ai-observability/" .Annotations.related_traces }}[View related AI traces]{{ else }}[View related traces]{{ end }}({{ .Annotations.related_traces }})
|
||||
|
||||
{{ end }}{{ end }}`,
|
||||
};
|
||||
|
||||
@@ -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 }} *RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}} *RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}}{{ if match "^https?://[^?#]*/ai-observability/" .Annotations.related_traces -}} View in <{{ .Annotations.related_traces }}|ai traces explorer> {{- else -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end }}{{- end}} *Details:* {{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }} {{ end }} {{ end }}`;
|
||||
|
||||
export const googleChatTitleDefaultValue = `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }}`;
|
||||
|
||||
|
||||
@@ -493,7 +493,7 @@ func appendRelatedLinkButtons(alerts []*types.Alert, bodies []string) {
|
||||
bodies[i] += htmlButton("View Related Logs", string(link))
|
||||
}
|
||||
if link := alerts[i].Annotations[ruletypes.AnnotationRelatedTraces]; link != "" {
|
||||
bodies[i] += htmlButton("View Related Traces", string(link))
|
||||
bodies[i] += htmlButton(ruletypes.RelatedTracesLabel(string(link)), string(link))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,8 @@ func relatedButtons(alert *types.Alert) []button {
|
||||
}
|
||||
}
|
||||
add("View Related Logs", string(alert.Annotations[ruletypes.AnnotationRelatedLogs]))
|
||||
add("View Related Traces", string(alert.Annotations[ruletypes.AnnotationRelatedTraces]))
|
||||
traces := string(alert.Annotations[ruletypes.AnnotationRelatedTraces])
|
||||
add(ruletypes.RelatedTracesLabel(traces), traces)
|
||||
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"},
|
||||
|
||||
@@ -214,3 +214,28 @@ func TestNotifyTruncatesLongDescription(t *testing.T) {
|
||||
assert.LessOrEqual(t, len(desc), maxDescriptionLenRunes)
|
||||
assert.Equal(t, '…', desc[len(desc)-1])
|
||||
}
|
||||
|
||||
func TestNotifyRelatedTracesLabel(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
link string
|
||||
want string
|
||||
}{
|
||||
{name: "TracesExplorer", link: "https://signoz.example/traces-explorer?q=1", want: "[View related traces]"},
|
||||
{name: "AITracesExplorer", link: "https://signoz.example/ai-observability/explorer?q=1", want: "[View related AI traces]"},
|
||||
{name: "PrefixedInstall", link: "https://signoz.example/signoz/ai-observability/explorer?q=1", want: "[View related AI traces]"},
|
||||
{name: "AIObservabilityHost", link: "https://ai-observability.acme.com/traces-explorer", want: "[View related traces]"},
|
||||
{name: "AIObservabilityInQuery", link: "https://signoz.example/traces-explorer?q=/ai-observability/", want: "[View related traces]"},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
m := newMockIncidentIO(t)
|
||||
a := alert(true)
|
||||
a.Annotations["related_traces"] = model.LabelValue(testCase.link)
|
||||
_, err := newNotifier(t, m).Notify(ctx(), a)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, m.lastEvent(t).Description, testCase.want+"("+testCase.link+")")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +260,8 @@ 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]))
|
||||
traces := string(a.Annotations[ruletypes.AnnotationRelatedTraces])
|
||||
add(ruletypes.RelatedTracesLabel(traces), traces)
|
||||
if len(parts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
package jsmops
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/prometheus/alertmanager/notify/test"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
commoncfg "github.com/prometheus/common/config"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -39,3 +47,37 @@ func TestToOpsGenieConfigNilHTTPConfig(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, og.HTTPConfig)
|
||||
}
|
||||
|
||||
func TestDefaultDescriptionRelatedTracesLabel(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
link string
|
||||
want string
|
||||
}{
|
||||
{name: "TracesExplorer", link: "https://signoz.example/traces-explorer?q=1", want: "[View related traces]"},
|
||||
{name: "AITracesExplorer", link: "https://signoz.example/ai-observability/explorer?q=1", want: "[View related AI traces]"},
|
||||
{name: "PrefixedInstall", link: "https://signoz.example/signoz/ai-observability/explorer?q=1", want: "[View related AI traces]"},
|
||||
{name: "AIObservabilityHost", link: "https://ai-observability.acme.com/traces-explorer", want: "[View related traces]"},
|
||||
{name: "AIObservabilityInQuery", link: "https://signoz.example/traces-explorer?q=/ai-observability/", want: "[View related traces]"},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
tmpl := test.CreateTmpl(t)
|
||||
templater := alertmanagertemplate.New(tmpl, slog.New(slog.DiscardHandler))
|
||||
a := &types.Alert{Alert: model.Alert{
|
||||
Labels: model.LabelSet{"alertname": "HighCPU"},
|
||||
Annotations: model.LabelSet{"related_traces": model.LabelValue(testCase.link)},
|
||||
StartsAt: time.Now().Add(-time.Minute),
|
||||
EndsAt: time.Now().Add(time.Hour),
|
||||
}}
|
||||
|
||||
result, err := templater.Expand(context.Background(), alertmanagertypes.ExpandRequest{
|
||||
DefaultTitleTemplate: alertmanagertypes.DefaultJSMOpsMessageTemplate,
|
||||
DefaultBodyTemplate: alertmanagertypes.DefaultJSMOpsDescriptionTemplate,
|
||||
}, []*types.Alert{a})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, strings.Join(result.Body, ""), testCase.want+"("+testCase.link+")")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ func buildRelatedLinkActions(alert *types.Alert) []config.SlackAction {
|
||||
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)})
|
||||
actions = append(actions, config.SlackAction{Type: "button", Text: ruletypes.RelatedTracesLabel(string(link)), URL: string(link)})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ Description: Request rate exceeded 10k/s`},
|
||||
*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}}
|
||||
*RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}}{{ if match "^https?://[^?#]*/ai-observability/" .Annotations.related_traces -}} View in <{{ .Annotations.related_traces }}|ai traces explorer> {{- else -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end }}{{- end}}
|
||||
|
||||
*Details:*
|
||||
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
|
||||
@@ -157,6 +157,31 @@ Description: Request rate exceeded 10k/s`},
|
||||
" "},
|
||||
wantIsDefaultBody: true,
|
||||
},
|
||||
{
|
||||
// AI trace alerts link to the ai-observability explorer; the default
|
||||
// body template labels that link differently from a plain traces link.
|
||||
name: "old template: 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 high",
|
||||
"description": "p99 latency of LLM calls is high",
|
||||
"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 -}}{{ if match "^https?://[^?#]*/ai-observability/" .Annotations.related_traces -}} View in <{{ .Annotations.related_traces }}|ai traces explorer> {{- else -}} View in <{{ .Annotations.related_traces }}|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,
|
||||
},
|
||||
{
|
||||
// Pod crash loop on multiple pods — body is expanded once per alert
|
||||
// and joined with "\n\n", with the pod name pulled from labels.
|
||||
|
||||
@@ -3,51 +3,42 @@ package contextlinks
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
func PrepareParamsForTracesV5(start, end time.Time, labels map[string]string, queries []BuilderQuery, queryType qbtypes.QueryType) url.Values {
|
||||
// the traces explorer takes its time params in nanoseconds
|
||||
return prepareExplorerParams("traces", queryType, start.UnixNano(), end.UnixNano(), labels, queries)
|
||||
// PrepareParamsForTracesV5 returns the traces explorer query params for the
|
||||
// given range and filter; the traces explorer writes its time params in
|
||||
// nanoseconds. queryType is builder_ai_query for the AI observability explorer.
|
||||
func PrepareParamsForTracesV5(start, end time.Time, whereClause string, queryType qbtypes.QueryType) url.Values {
|
||||
return prepareExplorerParams("traces", queryType, start.UnixNano(), end.UnixNano(), whereClause)
|
||||
}
|
||||
|
||||
func PrepareParamsForLogsV5(start, end time.Time, labels map[string]string, queries []BuilderQuery) url.Values {
|
||||
// the logs explorer takes its time params in milliseconds
|
||||
return prepareExplorerParams("logs", qbtypes.QueryTypeBuilder, start.UnixMilli(), end.UnixMilli(), labels, queries)
|
||||
// PrepareParamsForLogsV5 returns the logs explorer query params for the given
|
||||
// range and filter; the logs explorer writes its time params in milliseconds.
|
||||
func PrepareParamsForLogsV5(start, end time.Time, whereClause string) url.Values {
|
||||
return prepareExplorerParams("logs", qbtypes.QueryTypeBuilder, start.UnixMilli(), end.UnixMilli(), whereClause)
|
||||
}
|
||||
|
||||
// The end link is double encoded because otherwise a filter expression with `%` somewhere in it breaks.
|
||||
func prepareExplorerParams(dataSource string, queryType qbtypes.QueryType, start, end int64, labels map[string]string, queries []BuilderQuery) url.Values {
|
||||
func prepareExplorerParams(dataSource string, queryType qbtypes.QueryType, start, end int64, whereClause string) url.Values {
|
||||
// builder_query is the explorer default, so it is left out to keep existing links unchanged
|
||||
builderQueryType := ""
|
||||
if queryType != qbtypes.QueryTypeBuilder {
|
||||
builderQueryType = queryType.StringValue()
|
||||
}
|
||||
|
||||
if len(queries) == 0 {
|
||||
queries = []BuilderQuery{{}}
|
||||
}
|
||||
|
||||
queryData := make([]LinkQuery, 0, len(queries))
|
||||
for _, query := range queries {
|
||||
queryData = append(queryData, LinkQuery{
|
||||
QueryName: query.Name,
|
||||
DataSource: dataSource,
|
||||
BuilderQueryType: builderQueryType,
|
||||
Filter: &FilterExpression{Expression: PrepareFilterExpression(labels, query.Filter, query.GroupBy)},
|
||||
})
|
||||
}
|
||||
|
||||
urlData := URLShareableCompositeQuery{
|
||||
QueryType: "builder",
|
||||
Builder: URLShareableBuilderQuery{
|
||||
QueryData: queryData,
|
||||
QueryData: []LinkQuery{{
|
||||
DataSource: dataSource,
|
||||
BuilderQueryType: builderQueryType,
|
||||
Filter: &FilterExpression{Expression: whereClause},
|
||||
}},
|
||||
QueryFormulas: make([]string, 0),
|
||||
},
|
||||
}
|
||||
@@ -61,56 +52,40 @@ func prepareExplorerParams(dataSource string, queryType qbtypes.QueryType, start
|
||||
return params
|
||||
}
|
||||
|
||||
func ExtractBuilderQueries(condition *ruletypes.RuleCondition, signal telemetrytypes.Signal) []BuilderQuery {
|
||||
if condition == nil || condition.CompositeQuery == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
names := resolveSelectedQueryNames(condition)
|
||||
// BuilderQueryForSignal returns the filter expression and group-by keys of the
|
||||
// builder query for the given signal, or found=false when the composite query
|
||||
// has no builder query for it (e.g. PromQL or ClickHouse SQL alerts). AI trace
|
||||
// queries (builder_ai_query) count as trace builder queries.
|
||||
// TODO(srikanthccv): re-visit this and support multiple queries.
|
||||
func BuilderQueryForSignal(queries []qbtypes.QueryEnvelope, signal telemetrytypes.Signal) (string, []qbtypes.GroupByKey, bool) {
|
||||
switch signal {
|
||||
case telemetrytypes.SignalLogs:
|
||||
return extractBuilderQueries[qbtypes.LogAggregation](condition.CompositeQuery.Queries, names, signal)
|
||||
return builderQueryForSignal[qbtypes.LogAggregation](queries, signal)
|
||||
case telemetrytypes.SignalTraces:
|
||||
return extractBuilderQueries[qbtypes.TraceAggregation](condition.CompositeQuery.Queries, names, signal)
|
||||
return builderQueryForSignal[qbtypes.TraceAggregation](queries, signal)
|
||||
}
|
||||
return nil
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
func resolveSelectedQueryNames(condition *ruletypes.RuleCondition) []string {
|
||||
selected := condition.SelectedQuery
|
||||
if selected == "" {
|
||||
selected = condition.SelectedQueryName()
|
||||
}
|
||||
|
||||
for _, query := range condition.CompositeQuery.Queries {
|
||||
if query.GetQueryName() != selected {
|
||||
continue
|
||||
}
|
||||
if formula, ok := query.Spec.(qbtypes.QueryBuilderFormula); ok {
|
||||
return formula.ExtractReferencedQueryNames()
|
||||
}
|
||||
return []string{selected}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractBuilderQueries[T any](queries []qbtypes.QueryEnvelope, names []string, signal telemetrytypes.Signal) []BuilderQuery {
|
||||
var matches []BuilderQuery
|
||||
func builderQueryForSignal[T any](queries []qbtypes.QueryEnvelope, signal telemetrytypes.Signal) (string, []qbtypes.GroupByKey, bool) {
|
||||
var q qbtypes.QueryBuilderQuery[T]
|
||||
found := false
|
||||
for _, query := range queries {
|
||||
if query.Type != qbtypes.QueryTypeBuilder && query.Type != qbtypes.QueryTypeBuilderAI {
|
||||
continue
|
||||
}
|
||||
spec, ok := query.Spec.(qbtypes.QueryBuilderQuery[T])
|
||||
// spec.Disabled is deliberately not checked: a formula's inputs are
|
||||
// usually hidden in the UI but must still make it into the link
|
||||
if !ok || spec.Signal != signal || !slices.Contains(names, spec.Name) {
|
||||
continue
|
||||
if spec, ok := query.Spec.(qbtypes.QueryBuilderQuery[T]); ok {
|
||||
q = spec
|
||||
found = true
|
||||
}
|
||||
filterExpr := ""
|
||||
if spec.Filter != nil {
|
||||
filterExpr = spec.Filter.Expression
|
||||
}
|
||||
matches = append(matches, BuilderQuery{Name: spec.Name, Filter: filterExpr, GroupBy: spec.GroupBy})
|
||||
}
|
||||
return matches
|
||||
if !found || q.Signal != signal {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if q.Filter != nil {
|
||||
filterExpr = q.Filter.Expression
|
||||
}
|
||||
return filterExpr, q.GroupBy, true
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractBuilderQueries(t *testing.T) {
|
||||
func TestBuilderQueryForSignal(t *testing.T) {
|
||||
logQuery := qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
@@ -19,145 +19,62 @@ func TestExtractBuilderQueries(t *testing.T) {
|
||||
GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "service.name"}}},
|
||||
},
|
||||
}
|
||||
hiddenLogQuery := qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Name: "B",
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Disabled: true,
|
||||
Filter: &qbtypes.Filter{Expression: "body CONTAINS 'timeout'"},
|
||||
},
|
||||
}
|
||||
traceQuery := qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "C",
|
||||
Name: "B",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
},
|
||||
}
|
||||
promQuery := qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypePromQL,
|
||||
Spec: qbtypes.PromQuery{Name: "D"},
|
||||
Spec: qbtypes.PromQuery{Name: "C"},
|
||||
}
|
||||
aiTraceQuery := qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilderAI,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "E",
|
||||
Name: "D",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{Expression: "trace.input_tokens > 1000"},
|
||||
GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "session.id"}}},
|
||||
},
|
||||
}
|
||||
formula := qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeFormula,
|
||||
Spec: qbtypes.QueryBuilderFormula{Name: "F1", Expression: "B / A * 100"},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
condition *ruletypes.RuleCondition
|
||||
signal telemetrytypes.Signal
|
||||
expectedBuilderQueries []BuilderQuery
|
||||
}{
|
||||
{
|
||||
name: "SelectedLogsQuery_AmongMixedQueries",
|
||||
condition: &ruletypes.RuleCondition{
|
||||
SelectedQuery: "A",
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{Queries: []qbtypes.QueryEnvelope{promQuery, logQuery, traceQuery}},
|
||||
},
|
||||
signal: telemetrytypes.SignalLogs,
|
||||
expectedBuilderQueries: []BuilderQuery{
|
||||
{Name: "A", Filter: "severity_text = 'ERROR'", GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "service.name"}}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SelectedLogsQuery_LeavesOutUnselectedQuery",
|
||||
condition: &ruletypes.RuleCondition{
|
||||
SelectedQuery: "A",
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{Queries: []qbtypes.QueryEnvelope{logQuery, hiddenLogQuery}},
|
||||
},
|
||||
signal: telemetrytypes.SignalLogs,
|
||||
expectedBuilderQueries: []BuilderQuery{
|
||||
{Name: "A", Filter: "severity_text = 'ERROR'", GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "service.name"}}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SelectedFormula_HiddenInputsInCompositeQueryOrder",
|
||||
condition: &ruletypes.RuleCondition{
|
||||
SelectedQuery: "F1",
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{Queries: []qbtypes.QueryEnvelope{hiddenLogQuery, logQuery, formula}},
|
||||
},
|
||||
signal: telemetrytypes.SignalLogs,
|
||||
expectedBuilderQueries: []BuilderQuery{
|
||||
{Name: "B", Filter: "body CONTAINS 'timeout'"},
|
||||
{Name: "A", Filter: "severity_text = 'ERROR'", GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "service.name"}}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NoSelectedQuery_FallsBackToFormula",
|
||||
condition: &ruletypes.RuleCondition{
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{Queries: []qbtypes.QueryEnvelope{logQuery, hiddenLogQuery, formula}},
|
||||
},
|
||||
signal: telemetrytypes.SignalLogs,
|
||||
expectedBuilderQueries: []BuilderQuery{
|
||||
{Name: "A", Filter: "severity_text = 'ERROR'", GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "service.name"}}}},
|
||||
{Name: "B", Filter: "body CONTAINS 'timeout'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SelectedTracesQuery_WithoutFilter",
|
||||
condition: &ruletypes.RuleCondition{
|
||||
SelectedQuery: "C",
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{Queries: []qbtypes.QueryEnvelope{logQuery, traceQuery}},
|
||||
},
|
||||
signal: telemetrytypes.SignalTraces,
|
||||
expectedBuilderQueries: []BuilderQuery{{Name: "C"}},
|
||||
},
|
||||
{
|
||||
name: "SelectedAITraceQuery_CountsAsTraces",
|
||||
condition: &ruletypes.RuleCondition{
|
||||
SelectedQuery: "E",
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{Queries: []qbtypes.QueryEnvelope{logQuery, aiTraceQuery}},
|
||||
},
|
||||
signal: telemetrytypes.SignalTraces,
|
||||
expectedBuilderQueries: []BuilderQuery{
|
||||
{Name: "E", Filter: "trace.input_tokens > 1000", GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "session.id"}}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SelectedQuery_OfAnotherSignal",
|
||||
condition: &ruletypes.RuleCondition{
|
||||
SelectedQuery: "C",
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{Queries: []qbtypes.QueryEnvelope{logQuery, traceQuery}},
|
||||
},
|
||||
signal: telemetrytypes.SignalLogs,
|
||||
},
|
||||
{
|
||||
name: "SelectedQuery_NotABuilderQuery",
|
||||
condition: &ruletypes.RuleCondition{
|
||||
SelectedQuery: "D",
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{Queries: []qbtypes.QueryEnvelope{promQuery}},
|
||||
},
|
||||
signal: telemetrytypes.SignalLogs,
|
||||
},
|
||||
{
|
||||
name: "UnsupportedSignal",
|
||||
condition: &ruletypes.RuleCondition{
|
||||
SelectedQuery: "A",
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{Queries: []qbtypes.QueryEnvelope{logQuery}},
|
||||
},
|
||||
signal: telemetrytypes.SignalMetrics,
|
||||
},
|
||||
{
|
||||
name: "NoCompositeQuery",
|
||||
condition: &ruletypes.RuleCondition{SelectedQuery: "A"},
|
||||
signal: telemetrytypes.SignalLogs,
|
||||
},
|
||||
}
|
||||
t.Run("logs query among mixed queries", func(t *testing.T) {
|
||||
filterExpr, groupBy, found := BuilderQueryForSignal([]qbtypes.QueryEnvelope{promQuery, logQuery, traceQuery}, telemetrytypes.SignalLogs)
|
||||
require.True(t, found)
|
||||
assert.Equal(t, "severity_text = 'ERROR'", filterExpr)
|
||||
require.Len(t, groupBy, 1)
|
||||
assert.Equal(t, "service.name", groupBy[0].Name)
|
||||
})
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.expectedBuilderQueries, ExtractBuilderQueries(testCase.condition, testCase.signal))
|
||||
})
|
||||
}
|
||||
t.Run("traces query without filter", func(t *testing.T) {
|
||||
filterExpr, groupBy, found := BuilderQueryForSignal([]qbtypes.QueryEnvelope{logQuery, traceQuery}, telemetrytypes.SignalTraces)
|
||||
require.True(t, found)
|
||||
assert.Empty(t, filterExpr)
|
||||
assert.Empty(t, groupBy)
|
||||
})
|
||||
|
||||
t.Run("ai trace query counts as traces", func(t *testing.T) {
|
||||
filterExpr, groupBy, found := BuilderQueryForSignal([]qbtypes.QueryEnvelope{logQuery, aiTraceQuery}, telemetrytypes.SignalTraces)
|
||||
require.True(t, found)
|
||||
assert.Equal(t, "trace.input_tokens > 1000", filterExpr)
|
||||
require.Len(t, groupBy, 1)
|
||||
assert.Equal(t, "session.id", groupBy[0].Name)
|
||||
})
|
||||
|
||||
t.Run("no builder query for signal", func(t *testing.T) {
|
||||
_, _, found := BuilderQueryForSignal([]qbtypes.QueryEnvelope{traceQuery}, telemetrytypes.SignalLogs)
|
||||
assert.False(t, found)
|
||||
})
|
||||
|
||||
t.Run("no builder queries at all", func(t *testing.T) {
|
||||
_, _, found := BuilderQueryForSignal([]qbtypes.QueryEnvelope{promQuery}, telemetrytypes.SignalLogs)
|
||||
assert.False(t, found)
|
||||
})
|
||||
|
||||
t.Run("unsupported signal", func(t *testing.T) {
|
||||
_, _, found := BuilderQueryForSignal([]qbtypes.QueryEnvelope{logQuery}, telemetrytypes.SignalMetrics)
|
||||
assert.False(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package contextlinks
|
||||
|
||||
import (
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
)
|
||||
|
||||
@@ -11,19 +10,9 @@ type FilterExpression struct {
|
||||
Expression string `json:"expression,omitempty"`
|
||||
}
|
||||
|
||||
// BuilderQuery is the part of an alert's builder query that carries over into
|
||||
// an explorer link.
|
||||
type BuilderQuery struct {
|
||||
Name string
|
||||
Filter string
|
||||
GroupBy []qbtypes.GroupByKey
|
||||
}
|
||||
|
||||
// LinkQuery carries the only fields the explorer pages read from a shared
|
||||
// link; the frontend fills in the rest of the query shape with defaults.
|
||||
type LinkQuery struct {
|
||||
// the explorer names an unnamed query A, so a link with several queries names each
|
||||
QueryName string `json:"queryName,omitempty"`
|
||||
DataSource string `json:"dataSource"`
|
||||
BuilderQueryType string `json:"builderQueryType,omitempty"`
|
||||
Filter *FilterExpression `json:"filter,omitempty"`
|
||||
|
||||
@@ -17,9 +17,10 @@ import (
|
||||
// filter and the entry's labels, so a history entry can be opened in the
|
||||
// explorer with the context that produced it.
|
||||
type relatedLinkBuilder struct {
|
||||
alertType ruletypes.AlertType
|
||||
evaluation ruletypes.Evaluation
|
||||
builderQueries []contextlinks.BuilderQuery
|
||||
alertType ruletypes.AlertType
|
||||
evaluation ruletypes.Evaluation
|
||||
filterExpr string
|
||||
groupBy []qbtypes.GroupByKey
|
||||
}
|
||||
|
||||
// relatedLinkBuilderForRule returns nil when the rule cannot be loaded or is
|
||||
@@ -64,7 +65,7 @@ func (m *module) relatedLinkBuilderForRule(ctx context.Context, orgID valuer.UUI
|
||||
|
||||
// links are still built from the labels alone when the rule has no builder
|
||||
// query for the signal (e.g. ClickHouse SQL alerts)
|
||||
builder.builderQueries = contextlinks.ExtractBuilderQueries(rule.RuleCondition, signal)
|
||||
builder.filterExpr, builder.groupBy, _ = contextlinks.BuilderQueryForSignal(rule.RuleCondition.CompositeQuery.Queries, signal)
|
||||
|
||||
return builder
|
||||
}
|
||||
@@ -88,13 +89,15 @@ func (b *relatedLinkBuilder) links(labels rulestatehistorytypes.LabelsString, st
|
||||
return rulestatehistorytypes.RelatedLinks{}
|
||||
}
|
||||
|
||||
whereClause := contextlinks.PrepareFilterExpression(lbls, b.filterExpr, b.groupBy)
|
||||
|
||||
switch b.alertType {
|
||||
case ruletypes.AlertTypeLogs:
|
||||
return rulestatehistorytypes.RelatedLinks{RelatedLogsLink: contextlinks.PrepareParamsForLogsV5(start, end, lbls, b.builderQueries).Encode()}
|
||||
return rulestatehistorytypes.RelatedLinks{RelatedLogsLink: contextlinks.PrepareParamsForLogsV5(start, end, whereClause).Encode()}
|
||||
case ruletypes.AlertTypeTraces:
|
||||
return rulestatehistorytypes.RelatedLinks{RelatedTracesLink: contextlinks.PrepareParamsForTracesV5(start, end, lbls, b.builderQueries, qbtypes.QueryTypeBuilder).Encode()}
|
||||
return rulestatehistorytypes.RelatedLinks{RelatedTracesLink: contextlinks.PrepareParamsForTracesV5(start, end, whereClause, qbtypes.QueryTypeBuilder).Encode()}
|
||||
case ruletypes.AlertTypeAITraces:
|
||||
return rulestatehistorytypes.RelatedLinks{RelatedAITracesLink: contextlinks.PrepareParamsForTracesV5(start, end, lbls, b.builderQueries, qbtypes.QueryTypeBuilderAI).Encode()}
|
||||
return rulestatehistorytypes.RelatedLinks{RelatedAITracesLink: contextlinks.PrepareParamsForTracesV5(start, end, whereClause, qbtypes.QueryTypeBuilderAI).Encode()}
|
||||
}
|
||||
return rulestatehistorytypes.RelatedLinks{}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations/messagingQueues/kafka"
|
||||
@@ -803,13 +802,47 @@ func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request
|
||||
// alerts have 2 minutes delay built in, so we need to subtract that from the start time
|
||||
// to get the correct query range
|
||||
start := end.Add(-rule.EvalWindow.Duration() - 3*time.Minute)
|
||||
switch rule.AlertType {
|
||||
case ruletypes.AlertTypeLogs:
|
||||
builderQueries := contextlinks.ExtractBuilderQueries(rule.RuleCondition, telemetrytypes.SignalLogs)
|
||||
res.Items[idx].RelatedLogsLink = contextlinks.PrepareParamsForLogsV5(start, end, lbls, builderQueries).Encode()
|
||||
case ruletypes.AlertTypeTraces, ruletypes.AlertTypeAITraces:
|
||||
builderQueries := contextlinks.ExtractBuilderQueries(rule.RuleCondition, telemetrytypes.SignalTraces)
|
||||
res.Items[idx].RelatedTracesLink = contextlinks.PrepareParamsForTracesV5(start, end, lbls, builderQueries, rule.AlertType.BuilderQueryType()).Encode()
|
||||
if rule.AlertType == ruletypes.AlertTypeLogs {
|
||||
// TODO(srikanthccv): re-visit this and support multiple queries
|
||||
var q qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
|
||||
|
||||
for _, query := range rule.RuleCondition.CompositeQuery.Queries {
|
||||
if query.Type == qbtypes.QueryTypeBuilder {
|
||||
switch spec := query.Spec.(type) {
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
|
||||
q = spec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if q.Filter != nil && q.Filter.Expression != "" {
|
||||
filterExpr = q.Filter.Expression
|
||||
}
|
||||
|
||||
whereClause := contextlinks.PrepareFilterExpression(lbls, filterExpr, q.GroupBy)
|
||||
|
||||
res.Items[idx].RelatedLogsLink = contextlinks.PrepareParamsForLogsV5(start, end, whereClause).Encode()
|
||||
} else if rule.AlertType == ruletypes.AlertTypeTraces || rule.AlertType == ruletypes.AlertTypeAITraces {
|
||||
// TODO(srikanthccv): re-visit this and support multiple queries
|
||||
var q qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]
|
||||
|
||||
for _, query := range rule.RuleCondition.CompositeQuery.Queries {
|
||||
if query.Type == qbtypes.QueryTypeBuilder || query.Type == qbtypes.QueryTypeBuilderAI {
|
||||
switch spec := query.Spec.(type) {
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
||||
q = spec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if q.Filter != nil && q.Filter.Expression != "" {
|
||||
filterExpr = q.Filter.Expression
|
||||
}
|
||||
|
||||
whereClause := contextlinks.PrepareFilterExpression(lbls, filterExpr, q.GroupBy)
|
||||
res.Items[idx].RelatedTracesLink = contextlinks.PrepareParamsForTracesV5(start, end, whereClause, rule.AlertType.BuilderQueryType()).Encode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -768,6 +768,7 @@ func TestBaseRule_ExternalURL(t *testing.T) {
|
||||
{name: "default value returned as-is", externalURL: mustParseURL(t, "http://localhost:8080"), want: "http://localhost:8080"},
|
||||
{name: "configured https host", externalURL: mustParseURL(t, "https://signoz.example.com"), want: "https://signoz.example.com"},
|
||||
{name: "configured host with port", externalURL: mustParseURL(t, "http://signoz.internal:3301"), want: "http://signoz.internal:3301"},
|
||||
{name: "configured host with path prefix", externalURL: mustParseURL(t, "https://signoz.example.com/signoz/"), want: "https://signoz.example.com/signoz"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
||||
@@ -86,6 +86,8 @@ func (r *ThresholdRule) prepareQueryRange(ctx context.Context, ts time.Time) (*q
|
||||
}
|
||||
|
||||
func (r *ThresholdRule) prepareParamsForLogs(ctx context.Context, ts time.Time, lbls ruletypes.Labels) url.Values {
|
||||
selectedQuery := r.SelectedQuery(ctx)
|
||||
|
||||
qr, err := r.prepareQueryRange(ctx, ts)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -93,15 +95,24 @@ func (r *ThresholdRule) prepareParamsForLogs(ctx context.Context, ts time.Time,
|
||||
start := time.UnixMilli(int64(qr.Start))
|
||||
end := time.UnixMilli(int64(qr.End))
|
||||
|
||||
builderQueries := contextlinks.ExtractBuilderQueries(r.ruleCondition, telemetrytypes.SignalLogs)
|
||||
if len(builderQueries) == 0 {
|
||||
// TODO(srikanthccv): handle formula queries
|
||||
if selectedQuery < "A" || selectedQuery > "Z" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return contextlinks.PrepareParamsForLogsV5(start, end, lbls.Map(), builderQueries)
|
||||
filterExpr, groupBy, found := contextlinks.BuilderQueryForSignal(r.ruleCondition.CompositeQuery.Queries, telemetrytypes.SignalLogs)
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
whereClause := contextlinks.PrepareFilterExpression(lbls.Map(), filterExpr, groupBy)
|
||||
|
||||
return contextlinks.PrepareParamsForLogsV5(start, end, whereClause)
|
||||
}
|
||||
|
||||
func (r *ThresholdRule) prepareParamsForTraces(ctx context.Context, ts time.Time, lbls ruletypes.Labels) url.Values {
|
||||
selectedQuery := r.SelectedQuery(ctx)
|
||||
|
||||
qr, err := r.prepareQueryRange(ctx, ts)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -109,12 +120,19 @@ func (r *ThresholdRule) prepareParamsForTraces(ctx context.Context, ts time.Time
|
||||
start := time.UnixMilli(int64(qr.Start))
|
||||
end := time.UnixMilli(int64(qr.End))
|
||||
|
||||
builderQueries := contextlinks.ExtractBuilderQueries(r.ruleCondition, telemetrytypes.SignalTraces)
|
||||
if len(builderQueries) == 0 {
|
||||
// TODO(srikanthccv): handle formula queries
|
||||
if selectedQuery < "A" || selectedQuery > "Z" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return contextlinks.PrepareParamsForTracesV5(start, end, lbls.Map(), builderQueries, r.typ.BuilderQueryType())
|
||||
filterExpr, groupBy, found := contextlinks.BuilderQueryForSignal(r.ruleCondition.CompositeQuery.Queries, telemetrytypes.SignalTraces)
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
whereClause := contextlinks.PrepareFilterExpression(lbls.Map(), filterExpr, groupBy)
|
||||
|
||||
return contextlinks.PrepareParamsForTracesV5(start, end, whereClause, r.typ.BuilderQueryType())
|
||||
}
|
||||
|
||||
func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) {
|
||||
@@ -293,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})
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ func TestPrepareParamsForLogs(t *testing.T) {
|
||||
assert.Contains(t, params.Encode(), "compositeQuery=%257B")
|
||||
compositeQuery, err := url.QueryUnescape(params.Get("compositeQuery"))
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, `{"queryType":"builder","builder":{"queryData":[{"queryName":"A","dataSource":"logs","filter":{}}],"queryFormulas":[]}}`, compositeQuery)
|
||||
assert.JSONEq(t, `{"queryType":"builder","builder":{"queryData":[{"dataSource":"logs","filter":{}}],"queryFormulas":[]}}`, compositeQuery)
|
||||
}
|
||||
|
||||
func TestPrepareParamsForLogsFilterExpression(t *testing.T) {
|
||||
@@ -270,7 +270,7 @@ func TestPrepareParamsForLogsFilterExpression(t *testing.T) {
|
||||
|
||||
compositeQuery, err := url.QueryUnescape(params.Get("compositeQuery"))
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, `{"queryType":"builder","builder":{"queryData":[{"queryName":"A","dataSource":"logs","filter":{"expression":"service.name EXISTS"}}],"queryFormulas":[]}}`, compositeQuery)
|
||||
assert.JSONEq(t, `{"queryType":"builder","builder":{"queryData":[{"dataSource":"logs","filter":{"expression":"service.name EXISTS"}}],"queryFormulas":[]}}`, compositeQuery)
|
||||
}
|
||||
|
||||
func TestPrepareParamsForTracesFilterExpression(t *testing.T) {
|
||||
@@ -335,7 +335,7 @@ func TestPrepareParamsForTracesFilterExpression(t *testing.T) {
|
||||
|
||||
compositeQuery, err := url.QueryUnescape(params.Get("compositeQuery"))
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, `{"queryType":"builder","builder":{"queryData":[{"queryName":"A","dataSource":"traces","filter":{"expression":"service.name EXISTS"}}],"queryFormulas":[]}}`, compositeQuery)
|
||||
assert.JSONEq(t, `{"queryType":"builder","builder":{"queryData":[{"dataSource":"traces","filter":{"expression":"service.name EXISTS"}}],"queryFormulas":[]}}`, compositeQuery)
|
||||
}
|
||||
|
||||
func TestPrepareParamsForTraces(t *testing.T) {
|
||||
@@ -401,7 +401,7 @@ func TestPrepareParamsForTraces(t *testing.T) {
|
||||
assert.Contains(t, params.Encode(), "compositeQuery=%257B")
|
||||
compositeQuery, err := url.QueryUnescape(params.Get("compositeQuery"))
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, `{"queryType":"builder","builder":{"queryData":[{"queryName":"A","dataSource":"traces","filter":{}}],"queryFormulas":[]}}`, compositeQuery)
|
||||
assert.JSONEq(t, `{"queryType":"builder","builder":{"queryData":[{"dataSource":"traces","filter":{}}],"queryFormulas":[]}}`, compositeQuery)
|
||||
}
|
||||
|
||||
func TestThresholdRuleLabelNormalization(t *testing.T) {
|
||||
|
||||
@@ -918,10 +918,6 @@ func (b *traceOperatorCTEBuilder) buildScalarQuery(ctx context.Context, selectFr
|
||||
sb.OrderBy("__result_0 DESC")
|
||||
}
|
||||
|
||||
if b.operator.Limit > 0 {
|
||||
sb.Limit(b.operator.Limit)
|
||||
}
|
||||
|
||||
combinedArgs := allAggChArgs
|
||||
|
||||
// Add HAVING clause if specified
|
||||
|
||||
@@ -343,8 +343,8 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND toFloat64(response_status_code) < ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT toString(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL)) AS `service.name`, avg(toFloat64(duration_nano)) AS __result_0 FROM A_AND_B GROUP BY `service.name` ORDER BY __result_0 desc LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), float64(400), 10},
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND toFloat64(response_status_code) < ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT toString(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL)) AS `service.name`, avg(toFloat64(duration_nano)) AS __result_0 FROM A_AND_B GROUP BY `service.name` ORDER BY __result_0 desc SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), float64(400)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
|
||||
@@ -91,7 +91,7 @@ const (
|
||||
|
||||
{{ end }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})
|
||||
|
||||
{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})
|
||||
{{ end }}{{ if .Annotations.related_traces }}{{ if match "^https?://[^?#]*/ai-observability/" .Annotations.related_traces }}[View related AI traces]{{ else }}[View related traces]{{ end }}({{ .Annotations.related_traces }})
|
||||
|
||||
{{ end }}{{ end }}`
|
||||
)
|
||||
|
||||
@@ -96,7 +96,7 @@ const (
|
||||
|
||||
{{ end }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})
|
||||
|
||||
{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})
|
||||
{{ end }}{{ if .Annotations.related_traces }}{{ if match "^https?://[^?#]*/ai-observability/" .Annotations.related_traces }}[View related AI traces]{{ else }}[View related traces]{{ end }}({{ .Annotations.related_traces }})
|
||||
|
||||
{{ end }}{{ end }}`
|
||||
)
|
||||
|
||||
@@ -131,22 +131,6 @@ func (f QueryBuilderFormula) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f QueryBuilderFormula) ExtractReferencedQueryNames() []string {
|
||||
// parsed the way the evaluator parses it, so the names match what it resolves
|
||||
expression, err := govaluate.NewEvaluableExpressionWithFunctions(f.Expression, EvalFuncs())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, variable := range expression.Vars() {
|
||||
if !slices.Contains(names, variable) {
|
||||
names = append(names, variable)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// small container to store the query name and index or alias reference
|
||||
// for a variable in the formula expression
|
||||
// read below for more details on aggregation references.
|
||||
|
||||
31
pkg/types/ruletypes/related_links.go
Normal file
31
pkg/types/ruletypes/related_links.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultTracesExplorerPath = "traces-explorer"
|
||||
AIObservabilityPathPrefix = "ai-observability"
|
||||
AITracesExplorerPath = 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
|
||||
}
|
||||
|
||||
// RelatedTracesLabel is the button text notifiers show for a related_traces link.
|
||||
// Any link whose path contains the AI observability section counts, so installs
|
||||
// served under a URL prefix still match. Mirrors the templates'
|
||||
// `match "^https?://[^?#]*/ai-observability/"` check.
|
||||
func RelatedTracesLabel(link string) string {
|
||||
if u, err := url.Parse(link); err == nil && strings.Contains(u.Path, "/"+AIObservabilityPathPrefix+"/") {
|
||||
return "View Related AI Traces"
|
||||
}
|
||||
return "View Related Traces"
|
||||
}
|
||||
47
pkg/types/ruletypes/related_links_test.go
Normal file
47
pkg/types/ruletypes/related_links_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRelatedTracesLabel(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
link string
|
||||
want string
|
||||
}{
|
||||
{name: "TracesExplorer", link: "https://signoz.example/traces-explorer?q=1", want: "View Related Traces"},
|
||||
{name: "AITracesExplorer", link: "https://signoz.example/ai-observability/explorer?q=1", want: "View Related AI Traces"},
|
||||
{name: "Empty", link: "", want: "View Related Traces"},
|
||||
{name: "AIObservabilityHost", link: "https://ai-observability.acme.com/traces-explorer", want: "View Related Traces"},
|
||||
{name: "AIObservabilityPathLookalike", link: "https://acme.com/ai-observability-team/explorer", want: "View Related Traces"},
|
||||
{name: "PrefixedInstall", link: "https://acme.com/signoz/ai-observability/explorer?q=1", want: "View Related AI Traces"},
|
||||
{name: "AIObservabilityInQuery", link: "https://acme.com/traces-explorer?q=/ai-observability/", want: "View Related Traces"},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.want, RelatedTracesLabel(testCase.link))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTracesExplorerPath(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
alertType AlertType
|
||||
want string
|
||||
}{
|
||||
{name: "Metric", alertType: AlertTypeMetric, want: DefaultTracesExplorerPath},
|
||||
{name: "Traces", alertType: AlertTypeTraces, want: DefaultTracesExplorerPath},
|
||||
{name: "Logs", alertType: AlertTypeLogs, want: DefaultTracesExplorerPath},
|
||||
{name: "Exceptions", alertType: AlertTypeExceptions, want: DefaultTracesExplorerPath},
|
||||
{name: "AITraces", alertType: AlertTypeAITraces, want: AITracesExplorerPath},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.want, testCase.alertType.TracesExplorerPath())
|
||||
})
|
||||
}
|
||||
}
|
||||
13
tests/fixtures/alerts.py
vendored
13
tests/fixtures/alerts.py
vendored
@@ -150,14 +150,13 @@ def parse_related_link(link: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def assert_related_link_queries(link: dict, data_source: str, expression_pieces_by_query_name: dict[str, list[str]]) -> None:
|
||||
def assert_related_link_query(link: dict, data_source: str, expression_pieces: list[str]) -> None:
|
||||
query_data = link["composite_query"]["builder"]["queryData"]
|
||||
assert [query["queryName"] for query in query_data] == list(expression_pieces_by_query_name)
|
||||
for query in query_data:
|
||||
assert query["dataSource"] == data_source
|
||||
expression = query["filter"]["expression"]
|
||||
for piece in expression_pieces_by_query_name[query["queryName"]]:
|
||||
assert piece in expression, f"expected {piece} in link filter expression: {expression}"
|
||||
assert len(query_data) == 1
|
||||
assert query_data[0]["dataSource"] == data_source
|
||||
expression = query_data[0]["filter"]["expression"]
|
||||
for piece in expression_pieces:
|
||||
assert piece in expression, f"expected {piece} in link filter expression: {expression}"
|
||||
|
||||
|
||||
def wait_for_firing_timeline_entry(signoz: types.SigNoz, token: str, rule_id: str, start_ms: int, wait_seconds: int = 60) -> tuple[dict, int]:
|
||||
|
||||
2
tests/fixtures/notification_channel.py
vendored
2
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 *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 -}}{{ if match "^https?://[^?#]*/ai-observability/" .Annotations.related_traces -}} View in <{{ .Annotations.related_traces }}|ai traces explorer> {{- else -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end }}{{- 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 }}',
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{ "timestamp": "2026-01-29T10:00:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:00:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:01:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:01:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:02:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:02:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:03:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:03:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
@@ -1,104 +0,0 @@
|
||||
{
|
||||
"alert": "rule_state_history_formula_logs",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "LOGS_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 0,
|
||||
"matchType": "at_least_once",
|
||||
"op": "above",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"disabled": true,
|
||||
"filter": {
|
||||
"expression": "body CONTAINS 'payment success'"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "service.name",
|
||||
"fieldContext": "resource",
|
||||
"fieldDataType": "string"
|
||||
}
|
||||
],
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count()"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "B",
|
||||
"signal": "logs",
|
||||
"disabled": true,
|
||||
"filter": {
|
||||
"expression": "severity_text = 'INFO'"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "service.name",
|
||||
"fieldContext": "resource",
|
||||
"fieldDataType": "string"
|
||||
}
|
||||
],
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count()"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builder_formula",
|
||||
"spec": {
|
||||
"name": "F1",
|
||||
"expression": "A / B * 100"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "F1"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{ "timestamp": "2026-01-29T10:00:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:00:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:01:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:01:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:02:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:02:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:03:00.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
{ "timestamp": "2026-01-29T10:03:30.000000Z", "resources": { "service.name": "payment-service" }, "attributes": { "code.file": "payment_handler.py" }, "body": "payment success", "severity_text": "INFO" }
|
||||
@@ -1,88 +0,0 @@
|
||||
{
|
||||
"alert": "rule_state_history_multi_query_logs",
|
||||
"ruleType": "threshold_rule",
|
||||
"alertType": "LOGS_BASED_ALERT",
|
||||
"condition": {
|
||||
"thresholds": {
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{
|
||||
"name": "critical",
|
||||
"target": 0,
|
||||
"matchType": "at_least_once",
|
||||
"op": "above",
|
||||
"channels": [
|
||||
"test channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"filter": {
|
||||
"expression": "body CONTAINS 'payment success'"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "service.name",
|
||||
"fieldContext": "resource",
|
||||
"fieldDataType": "string"
|
||||
}
|
||||
],
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count()"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "B",
|
||||
"signal": "logs",
|
||||
"filter": {
|
||||
"expression": "severity_text = 'INFO'"
|
||||
},
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count()"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"selectedQueryName": "A"
|
||||
},
|
||||
"evaluation": {
|
||||
"kind": "rolling",
|
||||
"spec": {
|
||||
"evalWindow": "5m0s",
|
||||
"frequency": "15s"
|
||||
}
|
||||
},
|
||||
"labels": {},
|
||||
"annotations": {
|
||||
"description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})",
|
||||
"summary": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"groupBy": [],
|
||||
"usePolicy": false,
|
||||
"renotify": {
|
||||
"enabled": false,
|
||||
"interval": "30m",
|
||||
"alertStates": []
|
||||
}
|
||||
},
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -421,7 +421,7 @@ def test_create_channel( # pylint: disable=too-many-arguments,too-many-position
|
||||
"\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"
|
||||
'{{ end }}{{ if .Annotations.related_traces }}{{ if match "^https?://[^?#]*/ai-observability/" .Annotations.related_traces }}[View related AI traces]{{ else }}[View related traces]{{ end }}({{ .Annotations.related_traces }})\n'
|
||||
"\n"
|
||||
"{{ end }}{{ end }}"
|
||||
),
|
||||
@@ -452,7 +452,7 @@ def test_create_channel( # pylint: disable=too-many-arguments,too-many-position
|
||||
"\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"
|
||||
'{{ end }}{{ if .Annotations.related_traces }}{{ if match "^https?://[^?#]*/ai-observability/" .Annotations.related_traces }}[View related AI traces]{{ else }}[View related traces]{{ end }}({{ .Annotations.related_traces }})\n'
|
||||
"\n"
|
||||
"{{ end }}{{ end }}"
|
||||
),
|
||||
|
||||
@@ -29,6 +29,8 @@ LOGS_DATA = "ruler/test_scenarios/threshold_below_at_least_once/alert_data.jsonl
|
||||
LOGS_RULE = "ruler/test_scenarios/threshold_below_at_least_once/rule.json"
|
||||
TRACES_DATA = "ruler/test_scenarios/threshold_above_average/alert_data.jsonl"
|
||||
TRACES_RULE = "ruler/test_scenarios/threshold_above_average/rule.json"
|
||||
AI_TRACES_DATA = "ruler/test_scenarios/rule_state_history_ai_traces/alert_data.jsonl"
|
||||
AI_TRACES_RULE = "ruler/test_scenarios/rule_state_history_ai_traces/rule.json"
|
||||
|
||||
|
||||
GOOGLECHAT_CASES = [
|
||||
@@ -98,6 +100,29 @@ GOOGLECHAT_CASES = [
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertManagerNotificationTestCase(
|
||||
name="googlechat_rich_card_ai_traces",
|
||||
rule_path=AI_TRACES_RULE,
|
||||
alert_data=[types.AlertData(type="traces", data_path=AI_TRACES_DATA)],
|
||||
channel_config=googlechat_config("gc-ai-traces"),
|
||||
notification_expectation=types.AMNotificationExpectation(
|
||||
should_notify=True,
|
||||
wait_time_seconds=60,
|
||||
notification_validations=[
|
||||
types.NotificationValidation(
|
||||
destination_type="webhook",
|
||||
validation_data={
|
||||
"path": "/v1/spaces/gc-ai-traces/messages",
|
||||
"count": 1,
|
||||
"json_body": googlechat_card_subset(
|
||||
"rule_state_history_ai_traces",
|
||||
[("View Related AI Traces", r"/ai-observability/explorer\?"), ("Open in SigNoz", r"/alerts/overview\?ruleId=")],
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ METRICS_DATA = "ruler/test_scenarios/threshold_above_at_least_once/alert_data.js
|
||||
METRICS_RULE = "ruler/test_scenarios/threshold_above_at_least_once/rule.json"
|
||||
LOGS_DATA = "ruler/test_scenarios/threshold_below_at_least_once/alert_data.jsonl"
|
||||
LOGS_RULE = "ruler/test_scenarios/threshold_below_at_least_once/rule.json"
|
||||
AI_TRACES_DATA = "ruler/test_scenarios/rule_state_history_ai_traces/alert_data.jsonl"
|
||||
AI_TRACES_RULE = "ruler/test_scenarios/rule_state_history_ai_traces/rule.json"
|
||||
|
||||
|
||||
INCIDENTIO_CASES = [
|
||||
@@ -74,6 +76,29 @@ INCIDENTIO_CASES = [
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertManagerNotificationTestCase(
|
||||
name="incidentio_rich_event_ai_traces",
|
||||
rule_path=AI_TRACES_RULE,
|
||||
alert_data=[types.AlertData(type="traces", data_path=AI_TRACES_DATA)],
|
||||
channel_config=incidentio_config("inc-ai-traces"),
|
||||
notification_expectation=types.AMNotificationExpectation(
|
||||
should_notify=True,
|
||||
wait_time_seconds=60,
|
||||
notification_validations=[
|
||||
types.NotificationValidation(
|
||||
destination_type="webhook",
|
||||
validation_data={
|
||||
"path": incidentio_path("inc-ai-traces"),
|
||||
"count": 1,
|
||||
"json_body": incidentio_event_subset(
|
||||
"rule_state_history_ai_traces",
|
||||
[("View in SigNoz", r"/alerts/overview\?ruleId="), ("View related AI traces", r"/ai-observability/explorer\?")],
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ METRICS_DATA = "ruler/test_scenarios/threshold_above_at_least_once/alert_data.js
|
||||
METRICS_RULE = "ruler/test_scenarios/threshold_above_at_least_once/rule.json"
|
||||
LOGS_DATA = "ruler/test_scenarios/threshold_below_at_least_once/alert_data.jsonl"
|
||||
LOGS_RULE = "ruler/test_scenarios/threshold_below_at_least_once/rule.json"
|
||||
AI_TRACES_DATA = "ruler/test_scenarios/rule_state_history_ai_traces/alert_data.jsonl"
|
||||
AI_TRACES_RULE = "ruler/test_scenarios/rule_state_history_ai_traces/rule.json"
|
||||
|
||||
|
||||
JIRA_CASES = [
|
||||
@@ -79,6 +81,29 @@ JIRA_CASES = [
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertManagerNotificationTestCase(
|
||||
name="jira_rich_issue_ai_traces",
|
||||
rule_path=AI_TRACES_RULE,
|
||||
alert_data=[types.AlertData(type="traces", data_path=AI_TRACES_DATA)],
|
||||
channel_config=jira_config(),
|
||||
notification_expectation=types.AMNotificationExpectation(
|
||||
should_notify=True,
|
||||
wait_time_seconds=60,
|
||||
notification_validations=[
|
||||
types.NotificationValidation(
|
||||
destination_type="webhook",
|
||||
validation_data={
|
||||
"path": f"{JIRA_API_BASE}/issue",
|
||||
"count": 1,
|
||||
"json_body": jira_issue_subset(
|
||||
"rule_state_history_ai_traces",
|
||||
[("Open in SigNoz", r"/alerts/overview\?ruleId="), ("View Related AI Traces", r"/ai-observability/explorer\?")],
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ METRICS_DATA = "ruler/test_scenarios/threshold_above_at_least_once/alert_data.js
|
||||
METRICS_RULE = "ruler/test_scenarios/threshold_above_at_least_once/rule.json"
|
||||
LOGS_DATA = "ruler/test_scenarios/threshold_below_at_least_once/alert_data.jsonl"
|
||||
LOGS_RULE = "ruler/test_scenarios/threshold_below_at_least_once/rule.json"
|
||||
AI_TRACES_DATA = "ruler/test_scenarios/rule_state_history_ai_traces/alert_data.jsonl"
|
||||
AI_TRACES_RULE = "ruler/test_scenarios/rule_state_history_ai_traces/rule.json"
|
||||
|
||||
|
||||
JSMOPS_CASES = [
|
||||
@@ -81,6 +83,29 @@ JSMOPS_CASES = [
|
||||
],
|
||||
),
|
||||
),
|
||||
types.AlertManagerNotificationTestCase(
|
||||
name="jsmops_rich_alert_ai_traces",
|
||||
rule_path=AI_TRACES_RULE,
|
||||
alert_data=[types.AlertData(type="traces", data_path=AI_TRACES_DATA)],
|
||||
channel_config=jsmops_config(),
|
||||
notification_expectation=types.AMNotificationExpectation(
|
||||
should_notify=True,
|
||||
wait_time_seconds=60,
|
||||
notification_validations=[
|
||||
types.NotificationValidation(
|
||||
destination_type="webhook",
|
||||
validation_data={
|
||||
"path": f"{JSMOPS_API_BASE}/v2/alerts",
|
||||
"count": 1,
|
||||
"json_body": jsmops_alert_subset(
|
||||
"rule_state_history_ai_traces",
|
||||
[("View in SigNoz", r"/alerts/overview\?ruleId="), ("View related AI traces", r"/ai-observability/explorer\?")],
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -784,84 +784,3 @@ def test_trace_operator_select_fields(
|
||||
# (events/links/attributes/resource) and the rest of the intrinsics never
|
||||
# appear because the consume-layer merge isn't triggered.
|
||||
assert set(parent_row.keys()) == set(TRACE_OPERATOR_CORE_FIELDS + ["service.name"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("attribute_backend", ["map", "json"])
|
||||
def test_trace_operator_scalar_limit(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
use_attribute_backend: Callable[[str], None],
|
||||
attribute_backend: str,
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Insert 4, 3, 2 and 1 spans for services limit-a..limit-d.
|
||||
|
||||
Tests:
|
||||
1. A scalar trace operator grouped by service.name with limit=2 returns
|
||||
only the top two groups by count.
|
||||
"""
|
||||
use_attribute_backend(attribute_backend)
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
span_counts = {"limit-a": 4, "limit-b": 3, "limit-c": 2, "limit-d": 1}
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=10),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=TraceIdGenerator.trace_id(),
|
||||
span_id=TraceIdGenerator.span_id(),
|
||||
parent_span_id="",
|
||||
name="scalar-limit",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": service},
|
||||
attributes={"operation.type": "scalar-limit"},
|
||||
)
|
||||
for service, count in span_counts.items()
|
||||
for _ in range(count)
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
request_type=RequestType.SCALAR,
|
||||
queries=[
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"disabled": True,
|
||||
"filter": {"expression": "operation.type = 'scalar-limit'"},
|
||||
"aggregations": [{"expression": "count()"}],
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "builder_trace_operator",
|
||||
"spec": {
|
||||
"name": "C",
|
||||
"expression": "A",
|
||||
"aggregations": [{"expression": "count()"}],
|
||||
"groupBy": [{"name": "service.name", "fieldContext": "resource"}],
|
||||
"limit": 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
result = find_named_result(response.json()["data"]["data"]["results"], "C")
|
||||
assert result is not None, "trace_operator result C not found"
|
||||
assert result["data"] == [["limit-a", 4], ["limit-b", 3]]
|
||||
|
||||
@@ -3,7 +3,7 @@ from datetime import UTC, datetime, timedelta
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.alerts import (
|
||||
assert_related_link_queries,
|
||||
assert_related_link_query,
|
||||
get_rule_history_top_contributors,
|
||||
labels_to_map,
|
||||
parse_related_link,
|
||||
@@ -46,7 +46,7 @@ def test_logs_rule_history_related_links(
|
||||
link = parse_related_link(item["relatedLogsLink"])
|
||||
assert link["end"] == (item["unixMilli"] // 1000) * 1000
|
||||
assert link["end"] - link["start"] == RELATED_LINK_WINDOW_SECONDS * 1000
|
||||
assert_related_link_queries(link, "logs", {"A": ["payment success", "service.name", "payment-service"]})
|
||||
assert_related_link_query(link, "logs", ["payment success", "service.name", "payment-service"])
|
||||
|
||||
contributors = get_rule_history_top_contributors(signoz, token, rule_id, query_start_ms, query_end_ms)
|
||||
contributors = [c for c in contributors if labels_to_map(c["labels"]).get("service.name") == "payment-service"]
|
||||
@@ -60,7 +60,7 @@ def test_logs_rule_history_related_links(
|
||||
contributor_link = parse_related_link(contributors[0]["relatedLogsLink"])
|
||||
assert contributor_link["start"] == query_start_ms
|
||||
assert contributor_link["end"] == query_end_ms
|
||||
assert_related_link_queries(contributor_link, "logs", {"A": ["payment success", "service.name", "payment-service"]})
|
||||
assert_related_link_query(contributor_link, "logs", ["payment success", "service.name", "payment-service"])
|
||||
|
||||
|
||||
def test_traces_rule_history_related_links(
|
||||
@@ -90,7 +90,7 @@ def test_traces_rule_history_related_links(
|
||||
link = parse_related_link(item["relatedTracesLink"])
|
||||
assert link["end"] == (item["unixMilli"] // 1000) * 1_000_000_000
|
||||
assert link["end"] - link["start"] == RELATED_LINK_WINDOW_SECONDS * 1_000_000_000
|
||||
assert_related_link_queries(link, "traces", {"A": ["http.request.path", "/order", "service.name", "order-service"]})
|
||||
assert_related_link_query(link, "traces", ["http.request.path", "/order", "service.name", "order-service"])
|
||||
|
||||
contributors = get_rule_history_top_contributors(signoz, token, rule_id, query_start_ms, query_end_ms)
|
||||
contributors = [c for c in contributors if labels_to_map(c["labels"]).get("service.name") == "order-service"]
|
||||
@@ -104,7 +104,7 @@ def test_traces_rule_history_related_links(
|
||||
contributor_link = parse_related_link(contributors[0]["relatedTracesLink"])
|
||||
assert contributor_link["start"] == query_start_ms * 1_000_000
|
||||
assert contributor_link["end"] == query_end_ms * 1_000_000
|
||||
assert_related_link_queries(contributor_link, "traces", {"A": ["http.request.path", "/order", "service.name", "order-service"]})
|
||||
assert_related_link_query(contributor_link, "traces", ["http.request.path", "/order", "service.name", "order-service"])
|
||||
|
||||
|
||||
def test_ai_traces_rule_history_related_links(
|
||||
@@ -134,7 +134,7 @@ def test_ai_traces_rule_history_related_links(
|
||||
link = parse_related_link(item["relatedAITracesLink"])
|
||||
assert link["end"] == (item["unixMilli"] // 1000) * 1_000_000_000
|
||||
assert link["end"] - link["start"] == RELATED_LINK_WINDOW_SECONDS * 1_000_000_000
|
||||
assert_related_link_queries(link, "traces", {"A": ["trace.input_tokens", "100", "service.name", "llm-gateway"]})
|
||||
assert_related_link_query(link, "traces", ["trace.input_tokens", "100", "service.name", "llm-gateway"])
|
||||
# the AI explorer only resolves trace.* fields for builder_ai_query
|
||||
assert link["composite_query"]["builder"]["queryData"][0]["builderQueryType"] == "builder_ai_query"
|
||||
|
||||
@@ -149,87 +149,5 @@ def test_ai_traces_rule_history_related_links(
|
||||
contributor_link = parse_related_link(contributors[0]["relatedAITracesLink"])
|
||||
assert contributor_link["start"] == query_start_ms * 1_000_000
|
||||
assert contributor_link["end"] == query_end_ms * 1_000_000
|
||||
assert_related_link_queries(contributor_link, "traces", {"A": ["trace.input_tokens", "100", "service.name", "llm-gateway"]})
|
||||
assert_related_link_query(contributor_link, "traces", ["trace.input_tokens", "100", "service.name", "llm-gateway"])
|
||||
assert contributor_link["composite_query"]["builder"]["queryData"][0]["builderQueryType"] == "builder_ai_query"
|
||||
|
||||
|
||||
def test_multi_query_logs_rule_history_related_links(
|
||||
signoz: types.SigNoz,
|
||||
create_alert_rule_with_channel: Callable[[str], str],
|
||||
insert_alert_data: Callable[[list[types.AlertData], datetime], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query_start_ms = int((datetime.now(tz=UTC) - timedelta(minutes=30)).timestamp() * 1000)
|
||||
|
||||
insert_alert_data(
|
||||
[types.AlertData(type="logs", data_path="ruler/test_scenarios/rule_state_history_multi_query_logs/alert_data.jsonl")],
|
||||
base_time=datetime.now(tz=UTC) - timedelta(minutes=5),
|
||||
)
|
||||
rule_id = create_alert_rule_with_channel("ruler/test_scenarios/rule_state_history_multi_query_logs/rule.json")
|
||||
|
||||
(item, query_end_ms) = wait_for_firing_timeline_entry(signoz, token, rule_id, query_start_ms)
|
||||
|
||||
assert labels_to_map(item["labels"]).get("service.name") == "payment-service"
|
||||
assert item.get("relatedLogsLink", "") != ""
|
||||
|
||||
# the rule evaluates query A alone, so query B plays no part in the alert
|
||||
# and the link leaves it out
|
||||
link = parse_related_link(item["relatedLogsLink"])
|
||||
assert_related_link_queries(link, "logs", {"A": ["payment success", "service.name", "payment-service"]})
|
||||
|
||||
contributors = get_rule_history_top_contributors(signoz, token, rule_id, query_start_ms, query_end_ms)
|
||||
contributors = [c for c in contributors if labels_to_map(c["labels"]).get("service.name") == "payment-service"]
|
||||
assert len(contributors) == 1
|
||||
assert contributors[0].get("relatedLogsLink", "") != ""
|
||||
|
||||
contributor_link = parse_related_link(contributors[0]["relatedLogsLink"])
|
||||
assert_related_link_queries(contributor_link, "logs", {"A": ["payment success", "service.name", "payment-service"]})
|
||||
|
||||
|
||||
def test_formula_logs_rule_history_related_links(
|
||||
signoz: types.SigNoz,
|
||||
create_alert_rule_with_channel: Callable[[str], str],
|
||||
insert_alert_data: Callable[[list[types.AlertData], datetime], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query_start_ms = int((datetime.now(tz=UTC) - timedelta(minutes=30)).timestamp() * 1000)
|
||||
|
||||
insert_alert_data(
|
||||
[types.AlertData(type="logs", data_path="ruler/test_scenarios/rule_state_history_formula_logs/alert_data.jsonl")],
|
||||
base_time=datetime.now(tz=UTC) - timedelta(minutes=5),
|
||||
)
|
||||
rule_id = create_alert_rule_with_channel("ruler/test_scenarios/rule_state_history_formula_logs/rule.json")
|
||||
|
||||
(item, query_end_ms) = wait_for_firing_timeline_entry(signoz, token, rule_id, query_start_ms)
|
||||
|
||||
assert labels_to_map(item["labels"]).get("service.name") == "payment-service"
|
||||
assert item.get("relatedLogsLink", "") != ""
|
||||
|
||||
# the rule evaluates formula F1 over the hidden queries A and B, so the
|
||||
# link carries both inputs, each narrowed to the firing series' labels
|
||||
link = parse_related_link(item["relatedLogsLink"])
|
||||
assert_related_link_queries(
|
||||
link,
|
||||
"logs",
|
||||
{
|
||||
"A": ["payment success", "service.name", "payment-service"],
|
||||
"B": ["severity_text", "INFO", "service.name", "payment-service"],
|
||||
},
|
||||
)
|
||||
|
||||
contributors = get_rule_history_top_contributors(signoz, token, rule_id, query_start_ms, query_end_ms)
|
||||
contributors = [c for c in contributors if labels_to_map(c["labels"]).get("service.name") == "payment-service"]
|
||||
assert len(contributors) == 1
|
||||
assert contributors[0].get("relatedLogsLink", "") != ""
|
||||
|
||||
contributor_link = parse_related_link(contributors[0]["relatedLogsLink"])
|
||||
assert_related_link_queries(
|
||||
contributor_link,
|
||||
"logs",
|
||||
{
|
||||
"A": ["payment success", "service.name", "payment-service"],
|
||||
"B": ["severity_text", "INFO", "service.name", "payment-service"],
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user