diff --git a/lib/logstorage/filter_range.go b/lib/logstorage/filter_range.go index cf45e314d..d982e364c 100644 --- a/lib/logstorage/filter_range.go +++ b/lib/logstorage/filter_range.go @@ -11,6 +11,7 @@ import ( // Example LogsQL: `fieldName:range(minValue, maxValue]` type filterRange struct { fieldName string + minValue float64 maxValue float64 @@ -18,7 +19,7 @@ type filterRange struct { } func (fr *filterRange) String() string { - return quoteFieldNameIfNeeded(fr.fieldName) + "range" + fr.stringRepr + return quoteFieldNameIfNeeded(fr.fieldName) + fr.stringRepr } func (fr *filterRange) updateNeededFields(neededFields fieldsSet) { diff --git a/lib/logstorage/parser.go b/lib/logstorage/parser.go index 5b851b418..1ade23ec1 100644 --- a/lib/logstorage/parser.go +++ b/lib/logstorage/parser.go @@ -406,6 +406,10 @@ func parseGenericFilter(lex *lexer, fieldName string) (filter, error) { return nil, fmt.Errorf("missing whitespace before the search word %q", lex.prevToken) } return parseParensFilter(lex, fieldName) + case lex.isKeyword(">"): + return parseFilterGT(lex, fieldName) + case lex.isKeyword("<"): + return parseFilterLT(lex, fieldName) case lex.isKeyword("not", "!"): return parseFilterNot(lex, fieldName) case lex.isKeyword("exact"): @@ -754,6 +758,70 @@ func parseFilterRegexp(lex *lexer, fieldName string) (filter, error) { }) } +func parseFilterGT(lex *lexer, fieldName string) (filter, error) { + if fieldName == "" { + return nil, fmt.Errorf("'>' and '>=' must be prefixed with the field name") + } + lex.nextToken() + + includeMinValue := false + op := ">" + if lex.isKeyword("=") { + lex.nextToken() + includeMinValue = true + op = ">=" + } + + minValue, fStr, err := parseFloat64(lex) + if err != nil { + return nil, fmt.Errorf("cannot parse number after '%s': %w", op, err) + } + + if !includeMinValue { + minValue = nextafter(minValue, inf) + } + fr := &filterRange{ + fieldName: fieldName, + minValue: minValue, + maxValue: inf, + + stringRepr: op + fStr, + } + return fr, nil +} + +func parseFilterLT(lex *lexer, fieldName string) (filter, error) { + if fieldName == "" { + return nil, fmt.Errorf("'<' and '<=' must be prefixed with the field name") + } + lex.nextToken() + + includeMaxValue := false + op := "<" + if lex.isKeyword("=") { + lex.nextToken() + includeMaxValue = true + op = "<=" + } + + maxValue, fStr, err := parseFloat64(lex) + if err != nil { + return nil, fmt.Errorf("cannot parse number after '%s': %w", op, err) + } + + if !includeMaxValue { + maxValue = nextafter(maxValue, -inf) + } + fr := &filterRange{ + fieldName: fieldName, + minValue: -inf, + maxValue: maxValue, + + stringRepr: op + fStr, + } + return fr, nil +} + func parseFilterRange(lex *lexer, fieldName string) (filter, error) { funcName := lex.token lex.nextToken() @@ -801,19 +869,19 @@ func parseFilterRange(lex *lexer, fieldName string) (filter, error) { } lex.nextToken() - stringRepr := "" + stringRepr := "range" if includeMinValue { stringRepr += "[" } else { stringRepr += "(" - minValue = math.Nextafter(minValue, inf) + minValue = nextafter(minValue, inf) } stringRepr += minValueStr + ", " + maxValueStr if includeMaxValue { stringRepr += "]" } else { stringRepr += ")" - maxValue = math.Nextafter(maxValue, -inf) + maxValue = nextafter(maxValue, -inf) } fr := &filterRange{ @@ -1216,3 +1284,10 @@ func parseInt(s string) (int64, error) { } return nn, nil } + +func nextafter(f, xInf float64) float64 { + if math.IsInf(f, 0) { + return f + } + return math.Nextafter(f, xInf) +} diff --git a/lib/logstorage/parser_test.go b/lib/logstorage/parser_test.go index 32c621421..6dac4caf5 100644 --- a/lib/logstorage/parser_test.go +++ b/lib/logstorage/parser_test.go @@ -1,7 +1,6 @@ package logstorage import ( - "math" "reflect" "strings" "testing" @@ -492,15 +491,25 @@ func TestParseRangeFilter(t *testing.T) { f(`range:range["-1.234e5", "-2e-5"]`, `range`, -1.234e5, -2e-5) f(`_msg:range[1, 2]`, `_msg`, 1, 2) - f(`:range(1, 2)`, ``, math.Nextafter(1, inf), math.Nextafter(2, -inf)) - f(`range[1, 2)`, ``, 1, math.Nextafter(2, -inf)) - f(`range("1", 2]`, ``, math.Nextafter(1, inf), 2) + f(`:range(1, 2)`, ``, nextafter(1, inf), nextafter(2, -inf)) + f(`range[1, 2)`, ``, 1, nextafter(2, -inf)) + f(`range("1", 2]`, ``, nextafter(1, inf), 2) f(`response_size:range[1KB, 10MiB]`, `response_size`, 1_000, 10*(1<<20)) f(`response_size:range[1G, 10Ti]`, `response_size`, 1_000_000_000, 10*(1<<40)) f(`response_size:range[10, inf]`, `response_size`, 10, inf) f(`duration:range[100ns, 1y2w2.5m3s5ms]`, `duration`, 100, 1*nsecsPerYear+2*nsecsPerWeek+2.5*nsecsPerMinute+3*nsecsPerSecond+5*nsecsPerMillisecond) + + f(`foo:>10.43`, `foo`, nextafter(10.43, inf), inf) + f(`foo: > -10.43`, `foo`, nextafter(-10.43, inf), inf) + f(`foo:>=10.43`, `foo`, 10.43, inf) + f(`foo: >= -10.43`, `foo`, -10.43, inf) + + f(`foo:<10.43`, `foo`, -inf, nextafter(10.43, -inf)) + f(`foo: < -10.43`, `foo`, -inf, nextafter(-10.43, -inf)) + f(`foo:<=10.43`, `foo`, -inf, 10.43) + f(`foo: <= 10.43`, `foo`, -inf, 10.43) } func TestParseQuerySuccess(t *testing.T) { @@ -723,6 +732,13 @@ func TestParseQuerySuccess(t *testing.T) { f(`range(0x1ff, inf)`, `range(0x1ff, inf)`) f(`range(-INF,+inF)`, `range(-INF, +inF)`) f(`range(1.5K, 22.5GiB)`, `range(1.5K, 22.5GiB)`) + f(`foo:range(5,inf)`, `foo:range(5, inf)`) + + // >, >=, < and <= filter + f(`foo: > 10.5M`, `foo:>10.5M`) + f(`foo: >= 10.5M`, `foo:>=10.5M`) + f(`foo: < 10.5M`, `foo:<10.5M`) + f(`foo: <= 10.5M`, `foo:<=10.5M`) // re filter f("re('foo|ba(r.+)')", `re("foo|ba(r.+)")`)