mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-14 14:11:11 +01:00
Compare commits
4 Commits
issue_6021
...
nv/context
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a9246bfe3 | ||
|
|
e59afbec38 | ||
|
|
b1e2164739 | ||
|
|
140498cb11 |
@@ -3,42 +3,51 @@ 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"
|
||||
)
|
||||
|
||||
// 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 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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, whereClause string) url.Values {
|
||||
func prepareExplorerParams(dataSource string, queryType qbtypes.QueryType, start, end int64, labels map[string]string, queries []BuilderQuery) 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: []LinkQuery{{
|
||||
DataSource: dataSource,
|
||||
BuilderQueryType: builderQueryType,
|
||||
Filter: &FilterExpression{Expression: whereClause},
|
||||
}},
|
||||
QueryData: queryData,
|
||||
QueryFormulas: make([]string, 0),
|
||||
},
|
||||
}
|
||||
@@ -52,40 +61,56 @@ func prepareExplorerParams(dataSource string, queryType qbtypes.QueryType, start
|
||||
return params
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func ExtractBuilderQueries(condition *ruletypes.RuleCondition, signal telemetrytypes.Signal) []BuilderQuery {
|
||||
if condition == nil || condition.CompositeQuery == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
names := resolveSelectedQueryNames(condition)
|
||||
switch signal {
|
||||
case telemetrytypes.SignalLogs:
|
||||
return builderQueryForSignal[qbtypes.LogAggregation](queries, signal)
|
||||
return extractBuilderQueries[qbtypes.LogAggregation](condition.CompositeQuery.Queries, names, signal)
|
||||
case telemetrytypes.SignalTraces:
|
||||
return builderQueryForSignal[qbtypes.TraceAggregation](queries, signal)
|
||||
return extractBuilderQueries[qbtypes.TraceAggregation](condition.CompositeQuery.Queries, names, signal)
|
||||
}
|
||||
return "", nil, false
|
||||
return nil
|
||||
}
|
||||
|
||||
func builderQueryForSignal[T any](queries []qbtypes.QueryEnvelope, signal telemetrytypes.Signal) (string, []qbtypes.GroupByKey, bool) {
|
||||
var q qbtypes.QueryBuilderQuery[T]
|
||||
found := 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
|
||||
for _, query := range queries {
|
||||
if query.Type != qbtypes.QueryTypeBuilder && query.Type != qbtypes.QueryTypeBuilderAI {
|
||||
continue
|
||||
}
|
||||
if spec, ok := query.Spec.(qbtypes.QueryBuilderQuery[T]); ok {
|
||||
q = spec
|
||||
found = true
|
||||
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
|
||||
}
|
||||
filterExpr := ""
|
||||
if spec.Filter != nil {
|
||||
filterExpr = spec.Filter.Expression
|
||||
}
|
||||
matches = append(matches, BuilderQuery{Name: spec.Name, Filter: filterExpr, GroupBy: spec.GroupBy})
|
||||
}
|
||||
if !found || q.Signal != signal {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if q.Filter != nil {
|
||||
filterExpr = q.Filter.Expression
|
||||
}
|
||||
return filterExpr, q.GroupBy, true
|
||||
return matches
|
||||
}
|
||||
|
||||
@@ -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 TestBuilderQueryForSignal(t *testing.T) {
|
||||
func TestExtractBuilderQueries(t *testing.T) {
|
||||
logQuery := qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
@@ -19,62 +19,145 @@ func TestBuilderQueryForSignal(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: "B",
|
||||
Name: "C",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
},
|
||||
}
|
||||
promQuery := qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypePromQL,
|
||||
Spec: qbtypes.PromQuery{Name: "C"},
|
||||
Spec: qbtypes.PromQuery{Name: "D"},
|
||||
}
|
||||
aiTraceQuery := qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilderAI,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "D",
|
||||
Name: "E",
|
||||
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"},
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
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("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)
|
||||
})
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.expectedBuilderQueries, ExtractBuilderQueries(testCase.condition, testCase.signal))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package contextlinks
|
||||
|
||||
import (
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
)
|
||||
|
||||
@@ -10,9 +11,19 @@ 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,10 +17,9 @@ 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
|
||||
filterExpr string
|
||||
groupBy []qbtypes.GroupByKey
|
||||
alertType ruletypes.AlertType
|
||||
evaluation ruletypes.Evaluation
|
||||
builderQueries []contextlinks.BuilderQuery
|
||||
}
|
||||
|
||||
// relatedLinkBuilderForRule returns nil when the rule cannot be loaded or is
|
||||
@@ -65,7 +64,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.filterExpr, builder.groupBy, _ = contextlinks.BuilderQueryForSignal(rule.RuleCondition.CompositeQuery.Queries, signal)
|
||||
builder.builderQueries = contextlinks.ExtractBuilderQueries(rule.RuleCondition, signal)
|
||||
|
||||
return builder
|
||||
}
|
||||
@@ -89,15 +88,13 @@ 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, whereClause).Encode()}
|
||||
return rulestatehistorytypes.RelatedLinks{RelatedLogsLink: contextlinks.PrepareParamsForLogsV5(start, end, lbls, b.builderQueries).Encode()}
|
||||
case ruletypes.AlertTypeTraces:
|
||||
return rulestatehistorytypes.RelatedLinks{RelatedTracesLink: contextlinks.PrepareParamsForTracesV5(start, end, whereClause, qbtypes.QueryTypeBuilder).Encode()}
|
||||
return rulestatehistorytypes.RelatedLinks{RelatedTracesLink: contextlinks.PrepareParamsForTracesV5(start, end, lbls, b.builderQueries, qbtypes.QueryTypeBuilder).Encode()}
|
||||
case ruletypes.AlertTypeAITraces:
|
||||
return rulestatehistorytypes.RelatedLinks{RelatedAITracesLink: contextlinks.PrepareParamsForTracesV5(start, end, whereClause, qbtypes.QueryTypeBuilderAI).Encode()}
|
||||
return rulestatehistorytypes.RelatedLinks{RelatedAITracesLink: contextlinks.PrepareParamsForTracesV5(start, end, lbls, b.builderQueries, qbtypes.QueryTypeBuilderAI).Encode()}
|
||||
}
|
||||
return rulestatehistorytypes.RelatedLinks{}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ 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"
|
||||
@@ -802,47 +803,13 @@ 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)
|
||||
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()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,8 +86,6 @@ 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
|
||||
@@ -95,24 +93,15 @@ func (r *ThresholdRule) prepareParamsForLogs(ctx context.Context, ts time.Time,
|
||||
start := time.UnixMilli(int64(qr.Start))
|
||||
end := time.UnixMilli(int64(qr.End))
|
||||
|
||||
// TODO(srikanthccv): handle formula queries
|
||||
if selectedQuery < "A" || selectedQuery > "Z" {
|
||||
builderQueries := contextlinks.ExtractBuilderQueries(r.ruleCondition, telemetrytypes.SignalLogs)
|
||||
if len(builderQueries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
return contextlinks.PrepareParamsForLogsV5(start, end, lbls.Map(), builderQueries)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -120,19 +109,12 @@ func (r *ThresholdRule) prepareParamsForTraces(ctx context.Context, ts time.Time
|
||||
start := time.UnixMilli(int64(qr.Start))
|
||||
end := time.UnixMilli(int64(qr.End))
|
||||
|
||||
// TODO(srikanthccv): handle formula queries
|
||||
if selectedQuery < "A" || selectedQuery > "Z" {
|
||||
builderQueries := contextlinks.ExtractBuilderQueries(r.ruleCondition, telemetrytypes.SignalTraces)
|
||||
if len(builderQueries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
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())
|
||||
return contextlinks.PrepareParamsForTracesV5(start, end, lbls.Map(), builderQueries, r.typ.BuilderQueryType())
|
||||
}
|
||||
|
||||
func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) {
|
||||
|
||||
@@ -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":[{"dataSource":"logs","filter":{}}],"queryFormulas":[]}}`, compositeQuery)
|
||||
assert.JSONEq(t, `{"queryType":"builder","builder":{"queryData":[{"queryName":"A","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":[{"dataSource":"logs","filter":{"expression":"service.name EXISTS"}}],"queryFormulas":[]}}`, compositeQuery)
|
||||
assert.JSONEq(t, `{"queryType":"builder","builder":{"queryData":[{"queryName":"A","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":[{"dataSource":"traces","filter":{"expression":"service.name EXISTS"}}],"queryFormulas":[]}}`, compositeQuery)
|
||||
assert.JSONEq(t, `{"queryType":"builder","builder":{"queryData":[{"queryName":"A","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":[{"dataSource":"traces","filter":{}}],"queryFormulas":[]}}`, compositeQuery)
|
||||
assert.JSONEq(t, `{"queryType":"builder","builder":{"queryData":[{"queryName":"A","dataSource":"traces","filter":{}}],"queryFormulas":[]}}`, compositeQuery)
|
||||
}
|
||||
|
||||
func TestThresholdRuleLabelNormalization(t *testing.T) {
|
||||
|
||||
@@ -918,6 +918,10 @@ 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 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)},
|
||||
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},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
|
||||
@@ -131,6 +131,22 @@ 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.
|
||||
|
||||
13
tests/fixtures/alerts.py
vendored
13
tests/fixtures/alerts.py
vendored
@@ -150,13 +150,14 @@ def parse_related_link(link: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def assert_related_link_query(link: dict, data_source: str, expression_pieces: list[str]) -> None:
|
||||
def assert_related_link_queries(link: dict, data_source: str, expression_pieces_by_query_name: dict[str, list[str]]) -> None:
|
||||
query_data = link["composite_query"]["builder"]["queryData"]
|
||||
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}"
|
||||
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}"
|
||||
|
||||
|
||||
def wait_for_firing_timeline_entry(signoz: types.SigNoz, token: str, rule_id: str, start_ms: int, wait_seconds: int = 60) -> tuple[dict, int]:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{ "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" }
|
||||
104
tests/integration/testdata/ruler/test_scenarios/rule_state_history_formula_logs/rule.json
vendored
Normal file
104
tests/integration/testdata/ruler/test_scenarios/rule_state_history_formula_logs/rule.json
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{ "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" }
|
||||
88
tests/integration/testdata/ruler/test_scenarios/rule_state_history_multi_query_logs/rule.json
vendored
Normal file
88
tests/integration/testdata/ruler/test_scenarios/rule_state_history_multi_query_logs/rule.json
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -784,3 +784,84 @@ 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_query,
|
||||
assert_related_link_queries,
|
||||
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_query(link, "logs", ["payment success", "service.name", "payment-service"])
|
||||
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"]
|
||||
@@ -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_query(contributor_link, "logs", ["payment success", "service.name", "payment-service"])
|
||||
assert_related_link_queries(contributor_link, "logs", {"A": ["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_query(link, "traces", ["http.request.path", "/order", "service.name", "order-service"])
|
||||
assert_related_link_queries(link, "traces", {"A": ["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_query(contributor_link, "traces", ["http.request.path", "/order", "service.name", "order-service"])
|
||||
assert_related_link_queries(contributor_link, "traces", {"A": ["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_query(link, "traces", ["trace.input_tokens", "100", "service.name", "llm-gateway"])
|
||||
assert_related_link_queries(link, "traces", {"A": ["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,5 +149,87 @@ 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_query(contributor_link, "traces", ["trace.input_tokens", "100", "service.name", "llm-gateway"])
|
||||
assert_related_link_queries(contributor_link, "traces", {"A": ["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