logs-analyzer/signoz/pkg/query-service/app/logs/parser_test.go

485 lines
13 KiB
Go
Raw Normal View History

2024-09-02 22:47:30 +03:00
package logs
import (
"net/http"
"net/http/httptest"
"testing"
. "github.com/smartystreets/goconvey/convey"
"go.signoz.io/signoz/pkg/query-service/model"
)
var correctQueriesTest = []struct {
Name string
InputQuery string
WantSqlTokens []string
}{
{
`filter with fulltext`,
`OPERATION in ('bcd') AND FULLTEXT contains 'searchstring'`,
[]string{`OPERATION IN ('bcd') `, `AND body ILIKE '%searchstring%' `},
},
{
`fulltext`,
`searchstring`,
[]string{`body ILIKE '%searchstring%' `},
},
{
`fulltext with quotes and space`,
`FULLTEXT contains 'Hello, "World"'`,
[]string{`body ILIKE '%Hello, "World"%' `},
},
{
`contains search with a different attributes`,
`resource contains 'Hello, "World" and user\'s'`,
[]string{`resource ILIKE '%Hello, "World" and user\'s%' `},
},
{
`more than one continas`,
`resource contains 'Hello, "World"' and myresource contains 'abcd'`,
[]string{`resource ILIKE '%Hello, "World"%' `, `AND myresource ILIKE '%abcd%' `},
},
{
"contains with or",
`id in ('2CkBCauK8m3nkyKR19YhCw6WbdY') or fulltext contains 'OPTIONS /api/v1/logs'`,
[]string{`id IN ('2CkBCauK8m3nkyKR19YhCw6WbdY') `, `OR body ILIKE '%OPTIONS /api/v1/logs%' `},
},
{
"mixing and or",
`id in ('2CkBCauK8m3nkyKR19YhCw6WbdY') and id in ('2CkBCauK8m3nkyKR19YhCw6WbdY','2CkBCauK8m3nkyKR19YhCw6WbdY') or fulltext contains 'OPTIONS /api/v1/logs'`,
[]string{`id IN ('2CkBCauK8m3nkyKR19YhCw6WbdY') `, `and id IN ('2CkBCauK8m3nkyKR19YhCw6WbdY','2CkBCauK8m3nkyKR19YhCw6WbdY') `, `OR body ILIKE '%OPTIONS /api/v1/logs%' `},
},
{
`filters with lt,gt,lte,gte operators`,
`id lt 100 and id gt 50 and code lte 500 and code gte 400`,
[]string{`id < 100 `, `and id > 50 `, `and code <= 500 `, `and code >= 400 `},
},
{
`filters with lt,gt,lte,gte operators seprated by OR`,
`id lt 100 or id gt 50 or code lte 500 or code gte 400`,
[]string{`id < 100 `, `or id > 50 `, `or code <= 500 `, `or code >= 400 `},
},
{
`filter with number`,
`status gte 200 AND FULLTEXT ncontains '"key"'`,
[]string{`status >= 200 `, `AND body NOT ILIKE '%"key"%' `},
},
{
`characters inside string`,
`service NIN ('name > 100') AND length gt 100`,
[]string{`service NOT IN ('name > 100') `, `AND length > 100 `},
},
{
`fulltext with in`,
`key in 2`,
[]string{`body ILIKE '%key in 2%' `},
},
{
`not valid fulltext but a filter`,
`key in (2,3)`,
[]string{`key IN (2,3) `},
},
{
`filters with extra spaces`,
`service IN ('name > 100') AND length gt 100`,
[]string{`service IN ('name > 100') `, `AND length > 100 `},
},
{
`Extra space within a filter expression`,
`service IN ('name > 100')`,
[]string{`service IN ('name > 100') `},
},
{
`Extra space between a query filter`,
`data contains 'hello world .'`,
[]string{`data ILIKE '%hello world .%' `},
},
{
`filters with special characters in key name`,
`id.userid in (100) and id_userid gt 50`,
[]string{`id.userid IN (100) `, `and id_userid > 50 `},
},
{
`filters with case sensitive key name`,
`userIdentifier in ('user') and userIdentifier contains 'user'`,
[]string{`userIdentifier IN ('user') `, `AND userIdentifier ILIKE '%user%' `},
},
{
`filters with for exists`,
`userIdentifier exists and user nexists`,
[]string{`userIdentifier exists `, `and user nexists`},
},
}
func TestParseLogQueryCorrect(t *testing.T) {
for _, test := range correctQueriesTest {
Convey(test.Name, t, func() {
query, err := parseLogQuery(test.InputQuery)
So(err, ShouldBeNil)
So(query, ShouldResemble, test.WantSqlTokens)
})
}
}
var incorrectQueries = []struct {
Name string
Query string
}{
{
"filter without a key",
"OPERATION in ('bcd') AND 'abcd' FULLTEXT contains 'helloxyz'",
},
{
"fulltext without fulltext keyword",
"OPERATION in ('bcd') AND 'searchstring'",
},
{
"fulltext in the beginning without keyword",
"'searchstring and OPERATION in ('bcd')",
},
}
func TestParseLogQueryInCorrect(t *testing.T) {
for _, test := range incorrectQueries {
Convey(test.Name, t, func() {
_, err := parseLogQuery(test.Query)
So(err, ShouldBeError)
})
}
}
var parseCorrectColumns = []struct {
Name string
Filter string
Column string
}{
{
"column with IN operator",
"id.userid IN (100) ",
"id.userid",
},
{
"column with NOT IN operator",
"service NOT IN ('name > 100') ",
"service",
},
{
"column with > operator",
"and id_userid > 50 ",
"id_userid",
},
{
"column with < operator",
"and id_userid < 50 ",
"id_userid",
},
{
"column with <= operator",
"and id_userid <= 50 ",
"id_userid",
},
{
"column with >= operator",
"and id_userid >= 50 ",
"id_userid",
},
{
"column starting with and",
"andor = 1",
"andor",
},
{
"column starting with and after an 'and'",
"and andor = 1",
"andor",
},
{
"column starting with And",
"Andor = 1",
"Andor",
},
{
"column starting with and after an 'and'",
"and Andor = 1",
"Andor",
},
{
"column with ilike",
`AND body ILIKE '%searchstring%' `,
"body",
},
{
"column with not ilike",
`AND body ILIKE '%searchstring%' `,
"body",
},
{
"column with exists",
`AND user exists`,
"user",
},
{
"column with nexists",
`AND user nexists `,
"user",
},
}
func TestParseColumn(t *testing.T) {
for _, test := range parseCorrectColumns {
Convey(test.Name, t, func() {
column, err := parseColumn(test.Filter)
So(err, ShouldBeNil)
So(*column, ShouldEqual, test.Column)
})
}
}
func TestReplaceInterestingFields(t *testing.T) {
queryTokens := []string{"id.userid IN (100) ", "and id_key >= 50 ", `AND body ILIKE '%searchstring%'`}
allFields := model.GetFieldsResponse{
Selected: []model.LogField{
{
Name: "id_key",
DataType: "int64",
Type: "attributes",
},
},
Interesting: []model.LogField{
{
Name: "id.userid",
DataType: "int64",
Type: "attributes",
},
},
}
expectedTokens := []string{"attributes_int64_value[indexOf(attributes_int64_key, 'id.userid')] IN (100) ", "and `attribute_int64_id_key` >= 50 ", `AND body ILIKE '%searchstring%'`}
Convey("testInterestingFields", t, func() {
tokens, err := replaceInterestingFields(&allFields, queryTokens)
So(err, ShouldBeNil)
So(tokens, ShouldResemble, expectedTokens)
})
}
var previousPaginateTestCases = []struct {
Name string
Filter model.LogsFilterParams
IsPaginatePrev bool
Order string
}{
{
Name: "empty",
Filter: model.LogsFilterParams{},
IsPaginatePrev: false,
},
{
Name: "next ordery by asc",
Filter: model.LogsFilterParams{
OrderBy: TIMESTAMP,
Order: ASC,
IdGt: "myid",
},
IsPaginatePrev: false,
Order: ASC,
},
{
Name: "next ordery by desc",
Filter: model.LogsFilterParams{
OrderBy: TIMESTAMP,
Order: DESC,
IdLT: "myid",
},
IsPaginatePrev: false,
Order: DESC,
},
{
Name: "prev ordery by desc",
Filter: model.LogsFilterParams{
OrderBy: TIMESTAMP,
Order: DESC,
IdGt: "myid",
},
IsPaginatePrev: true,
Order: ASC,
},
{
Name: "prev ordery by asc",
Filter: model.LogsFilterParams{
OrderBy: TIMESTAMP,
Order: ASC,
IdLT: "myid",
},
IsPaginatePrev: true,
Order: DESC,
},
}
func TestCheckIfPrevousPaginateAndModifyOrder(t *testing.T) {
for _, test := range previousPaginateTestCases {
Convey(test.Name, t, func() {
isPrevPaginate := CheckIfPrevousPaginateAndModifyOrder(&test.Filter)
So(isPrevPaginate, ShouldEqual, test.IsPaginatePrev)
So(test.Order, ShouldEqual, test.Filter.Order)
})
}
}
var generateSQLQueryFields = model.GetFieldsResponse{
Selected: []model.LogField{
{
Name: "field1",
DataType: "int64",
Type: "attributes",
},
{
Name: "Field2",
DataType: "double64",
Type: "attributes",
},
{
Name: "field2",
DataType: "string",
Type: "attributes",
},
{
Name: "severity_number",
DataType: "string",
Type: "static",
},
},
Interesting: []model.LogField{
{
Name: "FielD1",
DataType: "int64",
Type: "attributes",
},
{
Name: "code",
DataType: "int64",
Type: "attributes",
},
},
}
var generateSQLQueryTestCases = []struct {
Name string
Filter model.LogsFilterParams
SqlFilter string
}{
{
Name: "first query with more than 1 compulsory filters",
Filter: model.LogsFilterParams{
Query: "field1 lt 100 and field1 gt 50 and code lte 500 and code gte 400",
TimestampStart: uint64(1657689292000),
TimestampEnd: uint64(1657689294000),
IdGt: "2BsKLKv8cZrLCn6rkOcRGkdjBdM",
IdLT: "2BsKG6tRpFWjYMcWsAGKfSxoQdU",
},
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' and id > '2BsKLKv8cZrLCn6rkOcRGkdjBdM' and id < '2BsKG6tRpFWjYMcWsAGKfSxoQdU' ) and ( `attribute_int64_field1` < 100 and `attribute_int64_field1` > 50 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] <= 500 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] >= 400 ) ",
},
{
Name: "second query with only timestamp range",
Filter: model.LogsFilterParams{
Query: "field1 lt 100 and field1 gt 50 and code lte 500 and code gte 400",
TimestampStart: uint64(1657689292000),
TimestampEnd: uint64(1657689294000),
},
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( `attribute_int64_field1` < 100 and `attribute_int64_field1` > 50 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] <= 500 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] >= 400 ) ",
},
{
Name: "generate case sensitive query",
Filter: model.LogsFilterParams{
Query: "field1 lt 100 and FielD1 gt 50 and Field2 gt 10 and code lte 500 and code gte 400",
TimestampStart: uint64(1657689292000),
TimestampEnd: uint64(1657689294000),
},
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( `attribute_int64_field1` < 100 and attributes_int64_value[indexOf(attributes_int64_key, 'FielD1')] > 50 and `attribute_double64_Field2` > 10 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] <= 500 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] >= 400 ) ",
},
{
Name: "Check exists and not exists",
Filter: model.LogsFilterParams{
Query: "field1 exists and Field2 nexists and Field2 gt 10",
TimestampStart: uint64(1657689292000),
TimestampEnd: uint64(1657689294000),
},
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( has(attributes_int64_key, 'field1') and NOT has(attributes_double64_key, 'Field2') and `attribute_double64_Field2` > 10 ) ",
},
{
Name: "Check top level key filter",
Filter: model.LogsFilterParams{
Query: "severity_number in (1)",
TimestampStart: uint64(1657689292000),
TimestampEnd: uint64(1657689294000),
},
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( severity_number IN (1) ) ",
},
{
Name: "Check exists and not exists on top level keys",
Filter: model.LogsFilterParams{
Query: "trace_id exists and span_id nexists and trace_flags exists and severity_number nexists",
TimestampStart: uint64(1657689292000),
TimestampEnd: uint64(1657689294000),
},
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( trace_id != '' and span_id = '' and trace_flags != 0 and severity_number = 0) ",
},
}
func TestGenerateSQLQuery(t *testing.T) {
for _, test := range generateSQLQueryTestCases {
Convey("testGenerateSQL", t, func() {
res, _, err := GenerateSQLWhere(&generateSQLQueryFields, &test.Filter)
So(err, ShouldBeNil)
So(res, ShouldEqual, test.SqlFilter)
})
}
}
var parseLogFilterParams = []struct {
Name string
ReqParams string
ExpectedLogFilterParams *model.LogsFilterParams
}{
{
Name: "test with proper order by",
ReqParams: "order=desc&q=service.name='myservice'&limit=10",
ExpectedLogFilterParams: &model.LogsFilterParams{
Limit: 10,
OrderBy: "timestamp",
Order: DESC,
Query: "service.name='myservice'",
},
},
{
Name: "test with proper order by asc",
ReqParams: "order=asc&q=service.name='myservice'&limit=10",
ExpectedLogFilterParams: &model.LogsFilterParams{
Limit: 10,
OrderBy: "timestamp",
Order: ASC,
Query: "service.name='myservice'",
},
},
{
Name: "test with incorrect order by",
ReqParams: "order=undefined&q=service.name='myservice'&limit=10",
ExpectedLogFilterParams: &model.LogsFilterParams{
Limit: 10,
OrderBy: "timestamp",
Order: DESC,
Query: "service.name='myservice'",
},
},
}
func TestParseLogFilterParams(t *testing.T) {
for _, test := range parseLogFilterParams {
Convey(test.Name, t, func() {
req := httptest.NewRequest(http.MethodGet, "/logs?"+test.ReqParams, nil)
params, err := ParseLogFilterParams(req)
So(err, ShouldBeNil)
So(params, ShouldEqual, test.ExpectedLogFilterParams)
})
}
}