package app import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.signoz.io/signoz/pkg/query-service/common" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" ) func TestParseAggregateAttrReques(t *testing.T) { reqCases := []struct { desc string queryString string expectedOperator v3.AggregateOperator expectedDataSource v3.DataSource expectedLimit int expectedSearchText string expectErr bool errMsg string }{ { desc: "valid operator and data source", queryString: "aggregateOperator=sum&dataSource=metrics&searchText=abc", expectedOperator: v3.AggregateOperatorSum, expectedDataSource: v3.DataSourceMetrics, expectedLimit: 50, expectedSearchText: "abc", }, { desc: "different valid operator and data source as logs", queryString: "aggregateOperator=avg&dataSource=logs&searchText=abc", expectedOperator: v3.AggregateOperatorAvg, expectedDataSource: v3.DataSourceLogs, expectedLimit: 50, expectedSearchText: "abc", }, { desc: "different valid operator and with default search text and limit", queryString: "aggregateOperator=avg&dataSource=metrics", expectedOperator: v3.AggregateOperatorAvg, expectedDataSource: v3.DataSourceMetrics, expectedLimit: 50, expectedSearchText: "", }, { desc: "valid operator and data source with limit", queryString: "aggregateOperator=avg&dataSource=traces&limit=10", expectedOperator: v3.AggregateOperatorAvg, expectedDataSource: v3.DataSourceTraces, expectedLimit: 10, expectedSearchText: "", }, { desc: "invalid operator", queryString: "aggregateOperator=avg1&dataSource=traces&limit=10", expectErr: true, errMsg: "invalid operator", }, { desc: "invalid data source", queryString: "aggregateOperator=avg&dataSource=traces1&limit=10", expectErr: true, errMsg: "invalid data source", }, { desc: "invalid limit", queryString: "aggregateOperator=avg&dataSource=traces&limit=abc", expectedOperator: v3.AggregateOperatorAvg, expectedDataSource: v3.DataSourceTraces, expectedLimit: 50, }, } for _, reqCase := range reqCases { r := httptest.NewRequest("GET", "/api/v3/autocomplete/aggregate_attributes?"+reqCase.queryString, nil) aggregateAttrRequest, err := parseAggregateAttributeRequest(r) if reqCase.expectErr { if err == nil { t.Errorf("expected error: %s", reqCase.errMsg) } if !strings.Contains(err.Error(), reqCase.errMsg) { t.Errorf("expected error to contain: %s, got: %s", reqCase.errMsg, err.Error()) } continue } if err != nil { t.Errorf("unexpected error: %v", err) } assert.Equal(t, reqCase.expectedOperator, aggregateAttrRequest.Operator) assert.Equal(t, reqCase.expectedDataSource, aggregateAttrRequest.DataSource) assert.Equal(t, reqCase.expectedLimit, aggregateAttrRequest.Limit) assert.Equal(t, reqCase.expectedSearchText, aggregateAttrRequest.SearchText) } } func TestParseFilterAttributeKeyRequest(t *testing.T) { reqCases := []struct { desc string queryString string expectedOperator v3.AggregateOperator expectedDataSource v3.DataSource expectedAggAttr string expectedLimit int expectedSearchText string expectErr bool errMsg string }{ { desc: "valid operator and data source", queryString: "aggregateOperator=sum&dataSource=metrics&aggregateAttribute=metric_name&searchText=abc", expectedOperator: v3.AggregateOperatorSum, expectedDataSource: v3.DataSourceMetrics, expectedAggAttr: "metric_name", expectedLimit: 50, expectedSearchText: "abc", }, { desc: "different valid operator and data source as logs", queryString: "aggregateOperator=avg&dataSource=logs&aggregateAttribute=bytes&searchText=abc", expectedOperator: v3.AggregateOperatorAvg, expectedDataSource: v3.DataSourceLogs, expectedAggAttr: "bytes", expectedLimit: 50, expectedSearchText: "abc", }, { desc: "different valid operator and with default search text and limit", queryString: "aggregateOperator=avg&dataSource=metrics&aggregateAttribute=metric_name", expectedOperator: v3.AggregateOperatorAvg, expectedDataSource: v3.DataSourceMetrics, expectedAggAttr: "metric_name", expectedLimit: 50, expectedSearchText: "", }, { desc: "valid operator and data source with limit", queryString: "aggregateOperator=avg&dataSource=traces&aggregateAttribute=http.req.duration&limit=10", expectedOperator: v3.AggregateOperatorAvg, expectedAggAttr: "http.req.duration", expectedDataSource: v3.DataSourceTraces, expectedLimit: 10, expectedSearchText: "", }, { desc: "invalid operator", queryString: "aggregateOperator=avg1&dataSource=traces&limit=10", expectErr: true, errMsg: "invalid operator", }, { desc: "invalid data source", queryString: "aggregateOperator=avg&dataSource=traces1&limit=10", expectErr: true, errMsg: "invalid data source", }, { desc: "invalid limit", queryString: "aggregateOperator=avg&dataSource=traces&limit=abc", expectedOperator: v3.AggregateOperatorAvg, expectedDataSource: v3.DataSourceTraces, expectedLimit: 50, }, } for _, reqCase := range reqCases { r := httptest.NewRequest("GET", "/api/v3/autocomplete/filter_attributes?"+reqCase.queryString, nil) filterAttrRequest, err := parseFilterAttributeKeyRequest(r) if reqCase.expectErr { if err == nil { t.Errorf("expected error: %s", reqCase.errMsg) } if !strings.Contains(err.Error(), reqCase.errMsg) { t.Errorf("expected error to contain: %s, got: %s", reqCase.errMsg, err.Error()) } continue } if err != nil { t.Errorf("unexpected error: %v", err) } assert.Equal(t, reqCase.expectedOperator, filterAttrRequest.AggregateOperator) assert.Equal(t, reqCase.expectedDataSource, filterAttrRequest.DataSource) assert.Equal(t, reqCase.expectedAggAttr, filterAttrRequest.AggregateAttribute) assert.Equal(t, reqCase.expectedLimit, filterAttrRequest.Limit) assert.Equal(t, reqCase.expectedSearchText, filterAttrRequest.SearchText) } } func TestParseFilterAttributeValueRequest(t *testing.T) { reqCases := []struct { desc string queryString string expectedOperator v3.AggregateOperator expectedDataSource v3.DataSource expectedAggAttr string expectedFilterAttr string expectedLimit int expectedSearchText string expectErr bool errMsg string }{ { desc: "valid operator and data source", queryString: "aggregateOperator=sum&dataSource=metrics&aggregateAttribute=metric_name&attributeKey=service_name&searchText=abc", expectedOperator: v3.AggregateOperatorSum, expectedDataSource: v3.DataSourceMetrics, expectedAggAttr: "metric_name", expectedFilterAttr: "service_name", expectedLimit: 50, expectedSearchText: "abc", }, { desc: "different valid operator and data source as logs", queryString: "aggregateOperator=avg&dataSource=logs&aggregateAttribute=bytes&attributeKey=service_name&searchText=abc", expectedOperator: v3.AggregateOperatorAvg, expectedDataSource: v3.DataSourceLogs, expectedAggAttr: "bytes", expectedFilterAttr: "service_name", expectedLimit: 50, expectedSearchText: "abc", }, { desc: "different valid operator and with default search text and limit", queryString: "aggregateOperator=avg&dataSource=metrics&aggregateAttribute=metric_name&attributeKey=service_name", expectedOperator: v3.AggregateOperatorAvg, expectedDataSource: v3.DataSourceMetrics, expectedAggAttr: "metric_name", expectedFilterAttr: "service_name", expectedLimit: 50, expectedSearchText: "", }, { desc: "valid operator and data source with limit", queryString: "aggregateOperator=avg&dataSource=traces&aggregateAttribute=http.req.duration&attributeKey=service_name&limit=10", expectedOperator: v3.AggregateOperatorAvg, expectedAggAttr: "http.req.duration", expectedFilterAttr: "service_name", expectedDataSource: v3.DataSourceTraces, expectedLimit: 10, expectedSearchText: "", }, { desc: "invalid operator", queryString: "aggregateOperator=avg1&dataSource=traces&limit=10", expectErr: true, errMsg: "invalid operator", }, { desc: "invalid data source", queryString: "aggregateOperator=avg&dataSource=traces1&limit=10", expectErr: true, errMsg: "invalid data source", }, { desc: "invalid limit", queryString: "aggregateOperator=avg&dataSource=traces&limit=abc", expectedOperator: v3.AggregateOperatorAvg, expectedDataSource: v3.DataSourceTraces, expectedLimit: 50, }, } for _, reqCase := range reqCases { r := httptest.NewRequest("GET", "/api/v3/autocomplete/filter_attribute_values?"+reqCase.queryString, nil) filterAttrRequest, err := parseFilterAttributeValueRequest(r) if reqCase.expectErr { if err == nil { t.Errorf("expected error: %s", reqCase.errMsg) } if !strings.Contains(err.Error(), reqCase.errMsg) { t.Errorf("expected error to contain: %s, got: %s", reqCase.errMsg, err.Error()) } continue } if err != nil { t.Errorf("unexpected error: %v", err) } assert.Equal(t, reqCase.expectedOperator, filterAttrRequest.AggregateOperator) assert.Equal(t, reqCase.expectedDataSource, filterAttrRequest.DataSource) assert.Equal(t, reqCase.expectedAggAttr, filterAttrRequest.AggregateAttribute) assert.Equal(t, reqCase.expectedFilterAttr, filterAttrRequest.FilterAttributeKey) assert.Equal(t, reqCase.expectedLimit, filterAttrRequest.Limit) assert.Equal(t, reqCase.expectedSearchText, filterAttrRequest.SearchText) } } func TestParseQueryRangeParamsCompositeQuery(t *testing.T) { reqCases := []struct { desc string compositeQuery v3.CompositeQuery expectErr bool errMsg string }{ { desc: "no query in request", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeClickHouseSQL, }, expectErr: true, errMsg: "composite query must contain at least one query", }, { desc: "invalid panel type", compositeQuery: v3.CompositeQuery{ PanelType: "invalid", QueryType: v3.QueryTypeClickHouseSQL, ClickHouseQueries: map[string]*v3.ClickHouseQuery{ "A": { Query: "query", Disabled: false, }, }, }, expectErr: true, errMsg: "panel type is invalid", }, { desc: "invalid query type", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: "invalid", ClickHouseQueries: map[string]*v3.ClickHouseQuery{ "A": { Query: "query", Disabled: false, }, }, }, expectErr: true, errMsg: "query type is invalid", }, { desc: "invalid prometheus query", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": { Query: "", Disabled: false, }, }, }, expectErr: true, errMsg: "query is empty", }, { desc: "invalid clickhouse query", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeClickHouseSQL, ClickHouseQueries: map[string]*v3.ClickHouseQuery{ "A": { Query: "", Disabled: false, }, }, }, expectErr: true, errMsg: "query is empty", }, { desc: "invalid prometheus query with disabled query", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": { Query: "", Disabled: true, }, }, }, expectErr: true, errMsg: "query is empty", }, { desc: "invalid clickhouse query with disabled query", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeClickHouseSQL, ClickHouseQueries: map[string]*v3.ClickHouseQuery{ "A": { Query: "", Disabled: true, }, }, }, expectErr: true, errMsg: "query is empty", }, { desc: "valid prometheus query", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": { Query: "http_calls_total", Disabled: false, }, }, }, expectErr: false, }, { desc: "invalid builder query without query name", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "": { QueryName: "", Expression: "A", }, }, }, expectErr: true, errMsg: "query name is required", }, { desc: "invalid data source for builder query", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: "invalid", Expression: "A", }, }, }, expectErr: true, errMsg: "data source is invalid", }, // { // desc: "invalid aggregate operator for builder query", // compositeQuery: v3.CompositeQuery{ // PanelType: v3.PanelTypeGraph, // QueryType: v3.QueryTypeBuilder, // BuilderQueries: map[string]*v3.BuilderQuery{ // "A": { // QueryName: "A", // DataSource: "metrics", // AggregateOperator: "invalid", // Expression: "A", // }, // }, // }, // expectErr: true, // errMsg: "aggregate operator is invalid", // }, { desc: "invalid aggregate attribute for builder query", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: "metrics", AggregateOperator: "sum", AggregateAttribute: v3.AttributeKey{}, Expression: "A", }, }, }, expectErr: true, errMsg: "aggregate attribute is required", }, { desc: "invalid group by attribute for builder query", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: "logs", AggregateOperator: "sum", AggregateAttribute: v3.AttributeKey{Key: "attribute"}, GroupBy: []v3.AttributeKey{{Key: ""}}, Expression: "A", }, }, }, expectErr: true, errMsg: "builder query A is invalid: group by is invalid", }, } for _, tc := range reqCases { t.Run(tc.desc, func(t *testing.T) { queryRangeParams := &v3.QueryRangeParamsV3{ Start: time.Now().Add(-time.Hour).UnixMilli(), End: time.Now().UnixMilli(), Step: time.Minute.Microseconds(), CompositeQuery: &tc.compositeQuery, Variables: map[string]interface{}{}, } body := &bytes.Buffer{} err := json.NewEncoder(body).Encode(queryRangeParams) require.NoError(t, err) req := httptest.NewRequest(http.MethodPost, "/api/v3/query_range", body) _, apiErr := ParseQueryRangeParams(req) if tc.expectErr { require.Error(t, apiErr) require.Contains(t, apiErr.Error(), tc.errMsg) } else { require.Nil(t, apiErr) } }) } } func TestParseQueryRangeParamsExpressions(t *testing.T) { reqCases := []struct { desc string compositeQuery v3.CompositeQuery expectErr bool errMsg string }{ { desc: "invalid expression", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "attribute_metrics"}, Expression: "A +", }, }, }, expectErr: true, errMsg: "Unexpected end of expression", }, { desc: "invalid expression", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceLogs, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "attribute_logs"}, Expression: "A", }, "F1": { QueryName: "F1", Expression: "A + B", }, }, }, expectErr: true, errMsg: "unknown variable B", }, { desc: "invalid expression", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceLogs, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "attribute_logs"}, Expression: "A", }, "F1": { QueryName: "F1", Expression: "A + B + C", }, }, }, expectErr: true, errMsg: "unknown variable B; unknown variable C", }, } for _, tc := range reqCases { t.Run(tc.desc, func(t *testing.T) { queryRangeParams := &v3.QueryRangeParamsV3{ Start: time.Now().Add(-time.Hour).UnixMilli(), End: time.Now().UnixMilli(), Step: time.Minute.Microseconds(), CompositeQuery: &tc.compositeQuery, Variables: map[string]interface{}{}, } body := &bytes.Buffer{} err := json.NewEncoder(body).Encode(queryRangeParams) require.NoError(t, err) req := httptest.NewRequest(http.MethodPost, "/api/v3/query_range", body) _, apiErr := ParseQueryRangeParams(req) if tc.expectErr { if apiErr == nil { t.Fatalf("expected error %s, got nil", tc.errMsg) } require.Error(t, apiErr) require.Contains(t, apiErr.Error(), tc.errMsg) } else { require.Nil(t, apiErr) } }) } } func TestParseQueryRangeParamsDashboardVarsSubstitution(t *testing.T) { reqCases := []struct { desc string compositeQuery v3.CompositeQuery variables map[string]interface{} expectErr bool errMsg string expectedValue []interface{} }{ { desc: "valid builder query with dashboard variables", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "attribute_metrics"}, Expression: "A", Filters: &v3.FilterSet{ Operator: "AND", Items: []v3.FilterItem{ { Key: v3.AttributeKey{Key: "service_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: v3.FilterOperatorEqual, Value: "{{.service_name}}", }, { Key: v3.AttributeKey{Key: "operation_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: v3.FilterOperatorIn, Value: "{{.operation_name}}", }, }, }, }, }, }, variables: map[string]interface{}{ "service_name": "route", "operation_name": []interface{}{ "GET /route", "POST /route", }, }, expectErr: false, expectedValue: []interface{}{"route", []interface{}{"GET /route", "POST /route"}}, }, { desc: "valid builder query with dashboard variables {{service_name}} and {{operation_name}}", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "attribute_metrics"}, Expression: "A", Filters: &v3.FilterSet{ Operator: "AND", Items: []v3.FilterItem{ { Key: v3.AttributeKey{Key: "service_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: v3.FilterOperatorEqual, Value: "{{service_name}}", }, { Key: v3.AttributeKey{Key: "operation_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: v3.FilterOperatorIn, Value: "{{operation_name}}", }, }, }, }, }, }, variables: map[string]interface{}{ "service_name": "route", "operation_name": []interface{}{ "GET /route", "POST /route", }, }, expectErr: false, expectedValue: []interface{}{"route", []interface{}{"GET /route", "POST /route"}}, }, { desc: "valid builder query with dashboard variables [[service_name]] and [[operation_name]]", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "attribute_metrics"}, Expression: "A", Filters: &v3.FilterSet{ Operator: "AND", Items: []v3.FilterItem{ { Key: v3.AttributeKey{Key: "service_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: v3.FilterOperatorEqual, Value: "[[service_name]]", }, { Key: v3.AttributeKey{Key: "operation_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: v3.FilterOperatorIn, Value: "[[operation_name]]", }, }, }, }, }, }, variables: map[string]interface{}{ "service_name": "route", "operation_name": []interface{}{ "GET /route", "POST /route", }, }, expectErr: false, expectedValue: []interface{}{"route", []interface{}{"GET /route", "POST /route"}}, }, { desc: "valid builder query with dashboard variables $service_name and $operation_name", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "attribute_metrics"}, Expression: "A", Filters: &v3.FilterSet{ Operator: "AND", Items: []v3.FilterItem{ { Key: v3.AttributeKey{Key: "service_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: v3.FilterOperatorEqual, Value: "$service_name", }, { Key: v3.AttributeKey{Key: "operation_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: v3.FilterOperatorIn, Value: "$operation_name", }, }, }, }, }, }, variables: map[string]interface{}{ "service_name": "route", "operation_name": []interface{}{ "GET /route", "POST /route", }, }, expectErr: false, expectedValue: []interface{}{"route", []interface{}{"GET /route", "POST /route"}}, }, { desc: "multiple values for single select operator", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "attribute_metrics"}, Expression: "A", Filters: &v3.FilterSet{ Operator: "AND", Items: []v3.FilterItem{ { Key: v3.AttributeKey{Key: "operation_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Operator: v3.FilterOperatorEqual, Value: "{{.operation_name}}", }, }, }, }, }, }, variables: map[string]interface{}{ "service_name": "route", "operation_name": []interface{}{ "GET /route", "POST /route", }, }, expectErr: true, errMsg: "multiple values [GET /route POST /route] are not allowed for operator `=` for key `operation_name`", }, } for _, tc := range reqCases { t.Run(tc.desc, func(t *testing.T) { queryRangeParams := &v3.QueryRangeParamsV3{ Start: time.Now().Add(-time.Hour).UnixMilli(), End: time.Now().UnixMilli(), Step: time.Minute.Microseconds(), CompositeQuery: &tc.compositeQuery, Variables: tc.variables, } body := &bytes.Buffer{} err := json.NewEncoder(body).Encode(queryRangeParams) require.NoError(t, err) req := httptest.NewRequest(http.MethodPost, "/api/v3/query_range", body) parsedQueryRangeParams, apiErr := ParseQueryRangeParams(req) if tc.expectErr { require.Error(t, apiErr) require.Contains(t, apiErr.Error(), tc.errMsg) } else { require.Nil(t, apiErr) require.Equal(t, parsedQueryRangeParams.CompositeQuery.BuilderQueries["A"].Filters.Items[0].Value, tc.expectedValue[0]) require.Equal(t, parsedQueryRangeParams.CompositeQuery.BuilderQueries["A"].Filters.Items[1].Value, tc.expectedValue[1]) } }) } } func TestParseQueryRangeParamsPromQLVars(t *testing.T) { reqCases := []struct { desc string compositeQuery v3.CompositeQuery variables map[string]interface{} expectErr bool errMsg string expectedQuery string }{ { desc: "valid prom query with dashboard variables", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": { Query: "http_calls_total{service_name=\"{{.service_name}}\", operation_name=~\"{{.operation_name}}\"}", Disabled: false, }, }, }, variables: map[string]interface{}{ "service_name": "route", "operation_name": []interface{}{ "GET /route", "POST /route", }, }, expectErr: false, expectedQuery: "http_calls_total{service_name=\"route\", operation_name=~\"GET /route|POST /route\"}", }, { desc: "valid prom query with dashboard variables", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": { Query: "http_calls_total{service_name=\"{{.service_name}}\", status_code=~\"{{.status_code}}\"}", Disabled: false, }, }, }, variables: map[string]interface{}{ "service_name": "route", "status_code": []interface{}{ 200, 505, }, }, expectErr: false, expectedQuery: "http_calls_total{service_name=\"route\", status_code=~\"200|505\"}", }, { desc: "valid prom query with dashboard variables {{service_name}} and {{status_code}}", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": { Query: "http_calls_total{service_name=\"{{service_name}}\", status_code=~\"{{status_code}}\"}", Disabled: false, }, }, }, variables: map[string]interface{}{ "service_name": "route", "status_code": []interface{}{ 200, 505, }, }, expectErr: false, expectedQuery: "http_calls_total{service_name=\"route\", status_code=~\"200|505\"}", }, { desc: "valid prom query with dashboard variables [[service_name]] and [[status_code]]", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": { Query: "http_calls_total{service_name=\"[[service_name]]\", status_code=~\"[[status_code]]\"}", Disabled: false, }, }, }, variables: map[string]interface{}{ "service_name": "route", "status_code": []interface{}{ 200, 505, }, }, expectErr: false, expectedQuery: "http_calls_total{service_name=\"route\", status_code=~\"200|505\"}", }, { desc: "valid prom query with dashboard variables $service_name and $status_code", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": { Query: "http_calls_total{service_name=\"$service_name\", status_code=~\"$status_code\"}", Disabled: false, }, }, }, variables: map[string]interface{}{ "service_name": "route", "status_code": []interface{}{ 200, 505, }, }, expectErr: false, expectedQuery: "http_calls_total{service_name=\"route\", status_code=~\"200|505\"}", }, { desc: "valid prom query with dashboard variables", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": { Query: "http_calls_total{service_name=\"{{.service_name}}\", quantity=~\"{{.quantity}}\"}", Disabled: false, }, }, }, variables: map[string]interface{}{ "service_name": "route", "quantity": []interface{}{ 4.5, 4.6, }, }, expectErr: false, expectedQuery: "http_calls_total{service_name=\"route\", quantity=~\"4.5|4.6\"}", }, } for _, tc := range reqCases { t.Run(tc.desc, func(t *testing.T) { queryRangeParams := &v3.QueryRangeParamsV3{ Start: time.Now().Add(-time.Hour).UnixMilli(), End: time.Now().UnixMilli(), Step: time.Minute.Microseconds(), CompositeQuery: &tc.compositeQuery, Variables: tc.variables, } body := &bytes.Buffer{} err := json.NewEncoder(body).Encode(queryRangeParams) require.NoError(t, err) req := httptest.NewRequest(http.MethodPost, "/api/v3/query_range", body) parsedQueryRangeParams, apiErr := ParseQueryRangeParams(req) if tc.expectErr { require.Error(t, apiErr) require.Contains(t, apiErr.Error(), tc.errMsg) } else { require.Nil(t, apiErr) require.Equal(t, parsedQueryRangeParams.CompositeQuery.PromQueries["A"].Query, tc.expectedQuery) } }) } } func TestQueryRangeFormula(t *testing.T) { reqCases := []struct { desc string compositeQuery v3.CompositeQuery variables map[string]interface{} expectErr bool errMsg string }{ { desc: "disjoint group by keys", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}}, Expression: "A", }, "B": { QueryName: "B", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "operation_name"}}, Expression: "B", }, "F1": { QueryName: "F1", Expression: "B/A", }, }, }, expectErr: true, errMsg: "Group keys must match or be a subset of the other but found left: [operation_name], right: [service_name]", }, { desc: "identical single group by key", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}}, Expression: "A", }, "B": { QueryName: "B", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}}, Expression: "B", }, "F1": { QueryName: "F1", Expression: "B/A", }, }, }, expectErr: false, }, { desc: "identical multiple group by keys", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}}, Expression: "A", }, "B": { QueryName: "B", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}}, Expression: "B", }, "F1": { QueryName: "F1", Expression: "B/A", }, }, }, expectErr: false, }, { desc: "identical multiple group by keys with different order", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}}, Expression: "A", }, "B": { QueryName: "B", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "operation_name"}, {Key: "service_name"}}, Expression: "B", }, "F1": { QueryName: "F1", Expression: "B/A", }, }, }, expectErr: false, }, { desc: "subset group by keys", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}}, Expression: "A", }, "B": { QueryName: "B", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}}, Expression: "B", }, "F1": { QueryName: "F1", Expression: "A/B", }, }, }, expectErr: false, }, { desc: "empty keys on one side", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}}, Expression: "A", }, "B": { QueryName: "B", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, Expression: "B", }, "F1": { QueryName: "F1", Expression: "A/B", }, }, }, expectErr: false, }, { desc: "empty keys on both sides", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, Expression: "A", }, "B": { QueryName: "B", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, Expression: "B", }, "F1": { QueryName: "F1", Expression: "A/B", }, }, }, expectErr: false, }, { desc: "multiple group by keys with partial overlap", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}}, Expression: "A", }, "B": { QueryName: "B", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "operation_name"}, {Key: "status_code"}}, Expression: "B", }, "F1": { QueryName: "F1", Expression: "A/B", }, }, }, expectErr: true, errMsg: "Group keys must match or be a subset of the other but found left: [service_name operation_name], right: [operation_name status_code]", }, { desc: "Nested Expressions with Matching Keys - Testing expressions that involve operations (e.g., addition, division) with series whose keys match or are subsets.", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}}, Expression: "A", }, "B": { QueryName: "B", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}}, Expression: "B", }, "F1": { QueryName: "F1", Expression: "A + B", }, }, }, expectErr: false, }, { desc: "Nested Expressions with Matching Keys - Testing expressions that involve operations (e.g., addition, division) with series whose keys match or are subsets.", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}}, Expression: "A", }, "B": { QueryName: "B", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}}, Expression: "B", }, "C": { QueryName: "C", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}, {Key: "status_code"}}, Expression: "C", }, "F1": { QueryName: "F1", Expression: "C/(A + B)", }, }, }, expectErr: false, }, { desc: "Unknow variable in expression", compositeQuery: v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "F1": { QueryName: "F1", Expression: "A + B", }, }, }, expectErr: true, errMsg: "unknown variable", }, } for _, tc := range reqCases { t.Run(tc.desc, func(t *testing.T) { queryRangeParams := &v3.QueryRangeParamsV3{ Start: time.Now().Add(-time.Hour).UnixMilli(), End: time.Now().UnixMilli(), Step: time.Minute.Microseconds(), CompositeQuery: &tc.compositeQuery, Variables: tc.variables, } body := &bytes.Buffer{} err := json.NewEncoder(body).Encode(queryRangeParams) require.NoError(t, err) req := httptest.NewRequest(http.MethodPost, "/api/v4/query_range", body) _, apiErr := ParseQueryRangeParams(req) if tc.expectErr { require.Error(t, apiErr) require.Contains(t, apiErr.Error(), tc.errMsg) } else { if apiErr != nil { if apiErr.Err != nil { t.Fatalf("unexpected error for case: %s - %v", tc.desc, apiErr.Err) } } require.Nil(t, apiErr) } }) } } func TestParseQueryRangeParamsStepIntervalAdjustment(t *testing.T) { reqCases := []struct { desc string start int64 end int64 step int64 }{ { desc: "30 minutes and 60 seconds step", start: time.Now().Add(-30 * time.Minute).UnixMilli(), end: time.Now().UnixMilli(), step: 60, // no update }, { desc: "1 hour and 1 second step", start: time.Now().Add(-time.Hour).UnixMilli(), end: time.Now().UnixMilli(), step: 1, // gets updated }, { desc: "1 week and 1 minute step", start: time.Now().Add(-7 * 24 * time.Hour).UnixMilli(), end: time.Now().UnixMilli(), step: 60, // gets updated }, { desc: "1 day and 1 hour step", start: time.Now().Add(-24 * time.Hour).UnixMilli(), end: time.Now().UnixMilli(), step: 3600, // no update }, { desc: "1 day and 1 minute step", start: time.Now().Add(-24 * time.Hour).UnixMilli(), end: time.Now().UnixMilli(), step: 60, // gets updated }, { desc: "1 day and 2 minutes step", start: time.Now().Add(-24 * time.Hour).UnixMilli(), end: time.Now().UnixMilli(), step: 120, // gets updated }, { desc: "1 day and 5 minutes step", start: time.Now().Add(-24 * time.Hour).UnixMilli(), end: time.Now().UnixMilli(), step: 300, // no update }, { desc: "1 week and 10 minutes step", start: time.Now().Add(-7 * 24 * time.Hour).UnixMilli(), end: time.Now().UnixMilli(), step: 600, // get updated }, { desc: "1 week and 45 minutes step", start: time.Now().Add(-7 * 24 * time.Hour).UnixMilli(), end: time.Now().UnixMilli(), step: 2700, // no update }, } for _, tc := range reqCases { t.Run(tc.desc, func(t *testing.T) { queryRangeParams := &v3.QueryRangeParamsV3{ Start: tc.start, End: tc.end, Step: tc.step, CompositeQuery: &v3.CompositeQuery{ PanelType: v3.PanelTypeGraph, QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", DataSource: v3.DataSourceMetrics, AggregateOperator: v3.AggregateOperatorSum, AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}}, Expression: "A", StepInterval: tc.step, }, }, }, Variables: map[string]interface{}{}, } body := &bytes.Buffer{} err := json.NewEncoder(body).Encode(queryRangeParams) require.NoError(t, err) req := httptest.NewRequest(http.MethodPost, "/api/v3/query_range", body) p, apiErr := ParseQueryRangeParams(req) if apiErr != nil && apiErr.Err != nil { t.Fatalf("unexpected error %s", apiErr.Err) } require.True(t, p.CompositeQuery.BuilderQueries["A"].StepInterval >= common.MinAllowedStepInterval(p.Start, p.End)) }) } }