mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-08 21:20:26 +01:00
Compare commits
2 Commits
refactor/c
...
cursor/ale
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f02458ef8 | ||
|
|
8bfadbc197 |
@@ -90,8 +90,9 @@ func (r *ThresholdRule) prepareQueryRange(ctx context.Context, ts time.Time) (*q
|
||||
},
|
||||
NoCache: true,
|
||||
}
|
||||
req.CompositeQuery.Queries = make([]qbtypes.QueryEnvelope, len(r.Condition().CompositeQuery.Queries))
|
||||
copy(req.CompositeQuery.Queries, r.Condition().CompositeQuery.Queries)
|
||||
queries := r.Condition().CompositeQuery.QueriesIncludingFormulas()
|
||||
req.CompositeQuery.Queries = make([]qbtypes.QueryEnvelope, len(queries))
|
||||
copy(req.CompositeQuery.Queries, queries)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,75 @@ func TestThresholdRuleEvalWithoutRecoveryTarget(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareQueryRangeIncludesQueryFormulas(t *testing.T) {
|
||||
target := 10.0
|
||||
postableRule := ruletypes.PostableRule{
|
||||
AlertName: "prepare query range includes formulas",
|
||||
AlertType: ruletypes.AlertTypeMetric,
|
||||
RuleType: ruletypes.RuleTypeThreshold,
|
||||
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
|
||||
EvalWindow: valuer.MustParseTextDuration("5m"),
|
||||
Frequency: valuer.MustParseTextDuration("1m"),
|
||||
}},
|
||||
RuleCondition: &ruletypes.RuleCondition{
|
||||
CompositeQuery: &ruletypes.AlertCompositeQuery{
|
||||
QueryType: ruletypes.QueryTypeBuilder,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
StepInterval: qbtypes.Step{Duration: time.Minute},
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "signoz_calls_total",
|
||||
Temporality: metrictypes.Delta,
|
||||
TimeAggregation: metrictypes.TimeAggregationRate,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
QueryFormulas: []qbtypes.QueryBuilderFormula{
|
||||
{
|
||||
Name: "F1",
|
||||
Expression: "A",
|
||||
},
|
||||
},
|
||||
},
|
||||
Target: &target,
|
||||
CompareOperator: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
Thresholds: &ruletypes.RuleThresholdData{
|
||||
Kind: ruletypes.BasicThresholdKind,
|
||||
Spec: ruletypes.BasicRuleThresholds{
|
||||
{
|
||||
Name: ruletypes.CriticalThresholdName,
|
||||
TargetValue: &target,
|
||||
CompareOperator: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Version: "v5",
|
||||
}
|
||||
|
||||
logger := instrumentationtest.New().Logger()
|
||||
rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := rule.prepareQueryRange(context.Background(), time.Now())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, req.CompositeQuery.Queries, 2)
|
||||
|
||||
assert.Equal(t, "A", req.CompositeQuery.Queries[0].GetQueryName())
|
||||
assert.Equal(t, "F1", req.CompositeQuery.Queries[1].GetQueryName())
|
||||
assert.Equal(t, qbtypes.QueryTypeFormula, req.CompositeQuery.Queries[1].Type)
|
||||
}
|
||||
|
||||
func TestNormalizeLabelName(t *testing.T) {
|
||||
cases := []struct {
|
||||
labelName string
|
||||
|
||||
@@ -818,9 +818,9 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon
|
||||
case "has":
|
||||
cond = fmt.Sprintf("has(%s, %s)", fieldName, v.builder.Var(value[0]))
|
||||
case "hasAny":
|
||||
cond = fmt.Sprintf("hasAny(%s, %s)", fieldName, v.builder.Var(value))
|
||||
cond = fmt.Sprintf("hasAny(%s, %s)", fieldName, v.builder.Var(value[0]))
|
||||
case "hasAll":
|
||||
cond = fmt.Sprintf("hasAll(%s, %s)", fieldName, v.builder.Var(value))
|
||||
cond = fmt.Sprintf("hasAll(%s, %s)", fieldName, v.builder.Var(value[0]))
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
|
||||
@@ -631,7 +631,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
|
||||
filter: "hasAll(body.user.permissions, ['read', 'write'])",
|
||||
expected: TestExpected{
|
||||
WhereClause: "hasAll(dynamicElement(body_v2.`user.permissions`, 'Array(Nullable(String))'), ?)",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), []any{[]any{"read", "write"}}, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), []any{"read", "write"}, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -757,7 +757,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
|
||||
filter: "hasAny(education[].awards[].participated[].members, ['Piyush', 'Tushar'])",
|
||||
expected: TestExpected{
|
||||
WhereClause: "hasAny(arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->arrayConcat(arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?)",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), []any{[]any{"Piyush", "Tushar"}}, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), []any{"Piyush", "Tushar"}, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -85,7 +85,8 @@ var (
|
||||
)
|
||||
|
||||
type AlertCompositeQuery struct {
|
||||
Queries []qbtypes.QueryEnvelope `json:"queries"`
|
||||
Queries []qbtypes.QueryEnvelope `json:"queries"`
|
||||
QueryFormulas []qbtypes.QueryBuilderFormula `json:"queryFormulas,omitempty"`
|
||||
|
||||
PanelType PanelType `json:"panelType"`
|
||||
QueryType QueryType `json:"queryType"`
|
||||
@@ -94,6 +95,22 @@ type AlertCompositeQuery struct {
|
||||
Unit string `json:"unit,omitempty"`
|
||||
}
|
||||
|
||||
func (acq *AlertCompositeQuery) QueriesIncludingFormulas() []qbtypes.QueryEnvelope {
|
||||
if acq == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
queries := make([]qbtypes.QueryEnvelope, 0, len(acq.Queries)+len(acq.QueryFormulas))
|
||||
queries = append(queries, acq.Queries...)
|
||||
for _, formula := range acq.QueryFormulas {
|
||||
queries = append(queries, qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeFormula,
|
||||
Spec: formula,
|
||||
})
|
||||
}
|
||||
return queries
|
||||
}
|
||||
|
||||
type RuleCondition struct {
|
||||
CompositeQuery *AlertCompositeQuery `json:"compositeQuery"`
|
||||
CompareOperator CompareOperator `json:"op"`
|
||||
@@ -114,7 +131,7 @@ func (rc *RuleCondition) SelectedQueryName() string {
|
||||
|
||||
queryNames := map[string]struct{}{}
|
||||
|
||||
for _, query := range rc.CompositeQuery.Queries {
|
||||
for _, query := range rc.CompositeQuery.QueriesIncludingFormulas() {
|
||||
switch spec := query.Spec.(type) {
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
||||
if !spec.Disabled {
|
||||
|
||||
@@ -406,19 +406,20 @@ func (r *PostableRule) Validate() error {
|
||||
if r.RuleCondition.CompositeQuery == nil {
|
||||
errs = append(errs, errors.NewInvalidInputf(errors.CodeInvalidInput, "condition.compositeQuery: field is required"))
|
||||
} else {
|
||||
if len(r.RuleCondition.CompositeQuery.Queries) == 0 {
|
||||
queries := r.RuleCondition.CompositeQuery.QueriesIncludingFormulas()
|
||||
if len(queries) == 0 {
|
||||
errs = append(errs, errors.NewInvalidInputf(errors.CodeInvalidInput, "condition.compositeQuery.queries: must have at least one query"))
|
||||
} else {
|
||||
cq := &qbtypes.CompositeQuery{Queries: r.RuleCondition.CompositeQuery.Queries}
|
||||
cq := &qbtypes.CompositeQuery{Queries: queries}
|
||||
if err := cq.Validate(qbtypes.GetValidationOptions(qbtypes.RequestTypeTimeSeries)...); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.RuleCondition.SelectedQuery != "" && r.RuleCondition.CompositeQuery != nil && len(r.RuleCondition.CompositeQuery.Queries) > 0 {
|
||||
if r.RuleCondition.SelectedQuery != "" && r.RuleCondition.CompositeQuery != nil {
|
||||
found := false
|
||||
for _, query := range r.RuleCondition.CompositeQuery.Queries {
|
||||
for _, query := range r.RuleCondition.CompositeQuery.QueriesIncludingFormulas() {
|
||||
if query.GetQueryName() == r.RuleCondition.SelectedQuery {
|
||||
found = true
|
||||
break
|
||||
|
||||
@@ -296,6 +296,21 @@ func TestValidate_PostableRule_Common(t *testing.T) {
|
||||
wantErr: true,
|
||||
errSubstr: "selectedQueryName",
|
||||
},
|
||||
{
|
||||
name: "selectedQueryName matches queryFormulas entry",
|
||||
json: `{
|
||||
"alert": "Test", "version": "v5",
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queryType": "builder",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}],
|
||||
"queryFormulas": [{"name": "F1", "expression": "A", "disabled": false}]
|
||||
},
|
||||
"target": 10.0, "matchType": "1", "op": "1",
|
||||
"selectedQueryName": "F1"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "empty selectedQueryName is ok (optional)",
|
||||
json: validV1Builder(),
|
||||
|
||||
Reference in New Issue
Block a user