This commit is contained in:
Aliaksandr Valialkin 2024-05-27 16:48:34 +02:00
parent c01bc0282a
commit fc6a923c5e
No known key found for this signature in database
GPG key ID: 52C003EE2BCDB9EB
10 changed files with 166 additions and 20 deletions

View file

@ -19,6 +19,8 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
## tip ## tip
* FEATURE: allow omitting `stats` prefix in [`stats` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#stats-pipe). For example, `_time:5m | count() rows` is a valid query now. It is equivalent to `_time:5m | stats count() as rows`.
* FEATURE: allow omitting `filter` prefix in [`filter` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#filter-pipe) if the filter doesn't clash with [pipe names](#https://docs.victoriametrics.com/victorialogs/logsql/#pipes). For example, `_time:5m | stats by (host) count() rows | rows:>1000` is a valid query now. It is equivalent to `_time:5m | stats by (host) count() rows | filter rows:>1000`.
* FEATURE: allow [`head` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#limit-pipe) without number. For example, `error | head`. In this case 10 last values are returned as `head` Unix command does by default. * FEATURE: allow [`head` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#limit-pipe) without number. For example, `error | head`. In this case 10 last values are returned as `head` Unix command does by default.
* FEATURE: allow using [comparison filters](https://docs.victoriametrics.com/victorialogs/logsql/#range-comparison-filters) with strings. For example, `some_text_field:>="foo"` matches [log entries](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) with `some_text_field` field values bigger or equal to `foo`. * FEATURE: allow using [comparison filters](https://docs.victoriametrics.com/victorialogs/logsql/#range-comparison-filters) with strings. For example, `some_text_field:>="foo"` matches [log entries](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) with `some_text_field` field values bigger or equal to `foo`.

View file

@ -1391,8 +1391,7 @@ See also:
### filter pipe ### filter pipe
Sometimes it is needed to apply additional filters on the calculated results. This can be done with `| filter ...` [pipe](#pipes). The `| filter ...` [pipe](#pipes) allows filtering the selected logs entries with arbitrary [filters](#filters).
The `filter` pipe can contain arbitrary [filters](#filters).
For example, the following query returns `host` [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) values For example, the following query returns `host` [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) values
if the number of log messages with the `error` [word](#word) for them over the last hour exceeds `1_000`: if the number of log messages with the `error` [word](#word) for them over the last hour exceeds `1_000`:
@ -1401,6 +1400,13 @@ if the number of log messages with the `error` [word](#word) for them over the l
_time:1h error | stats by (host) count() logs_count | filter logs_count:> 1_000 _time:1h error | stats by (host) count() logs_count | filter logs_count:> 1_000
``` ```
It is allowed to omit `filter` prefix if the used filters do not clash with [pipe names](#pipes).
So the following query is equivalent to the previous one:
```logsql
_time:1h error | stats by (host) count() logs_count | logs_count:> 1_000
```
See also: See also:
- [`stats` pipe](#stats-pipe) - [`stats` pipe](#stats-pipe)
@ -1761,6 +1767,12 @@ For example, the following query calculates the following stats for logs over th
_time:5m | stats count() logs_total, count_uniq(_stream) streams_total _time:5m | stats count() logs_total, count_uniq(_stream) streams_total
``` ```
It is allowed to omit `stats` prefix for convenience. So the following query is equivalent to the previous one:
```logsql
_time:5m | count() logs_total, count_uniq(_stream) streams_total
```
See also: See also:
- [stats by fields](#stats-by-fields) - [stats by fields](#stats-by-fields)

View file

@ -252,7 +252,7 @@ func (q *Query) AddCountByTimePipe(step, off int64, fields []string) {
s := fmt.Sprintf("stats by (%s) count() hits", byFieldsStr) s := fmt.Sprintf("stats by (%s) count() hits", byFieldsStr)
lex := newLexer(s) lex := newLexer(s)
ps, err := parsePipeStats(lex) ps, err := parsePipeStats(lex, true)
if err != nil { if err != nil {
logger.Panicf("BUG: unexpected error when parsing [%s]: %s", s, err) logger.Panicf("BUG: unexpected error when parsing [%s]: %s", s, err)
} }

View file

@ -1094,7 +1094,7 @@ func TestParseQueryFailure(t *testing.T) {
f("") f("")
f("|") f("|")
f("foo|") f("foo|")
f("foo|bar") f("foo|bar(")
f("foo and") f("foo and")
f("foo OR ") f("foo OR ")
f("not") f("not")
@ -1163,7 +1163,7 @@ func TestParseQueryFailure(t *testing.T) {
f(`very long query with error aaa ffdfd fdfdfd fdfd:( ffdfdfdfdfd`) f(`very long query with error aaa ffdfd fdfdfd fdfd:( ffdfdfdfdfd`)
// query with unexpected tail // query with unexpected tail
f(`foo | bar`) f(`foo | bar(`)
// unexpected comma // unexpected comma
f(`foo,bar`) f(`foo,bar`)
@ -1284,9 +1284,9 @@ func TestParseQueryFailure(t *testing.T) {
// missing pipe keyword // missing pipe keyword
f(`foo |`) f(`foo |`)
// unknown pipe keyword // invlaid pipe
f(`foo | bar`) f(`foo | bar(`)
f(`foo | fields bar | baz`) f(`foo | fields bar | baz(`)
// missing field in fields pipe // missing field in fields pipe
f(`foo | fields`) f(`foo | fields`)

View file

@ -86,6 +86,8 @@ func parsePipes(lex *lexer) ([]pipe, error) {
lex.nextToken() lex.nextToken()
case lex.isKeyword(")", ""): case lex.isKeyword(")", ""):
return pipes, nil return pipes, nil
default:
return nil, fmt.Errorf("unexpected token after [%s]: %q; expecting '|' or ')'", pipes[len(pipes)-1], lex.token)
} }
} }
} }
@ -123,7 +125,7 @@ func parsePipe(lex *lexer) (pipe, error) {
} }
return pf, nil return pf, nil
case lex.isKeyword("filter"): case lex.isKeyword("filter"):
pf, err := parsePipeFilter(lex) pf, err := parsePipeFilter(lex, true)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot parse 'filter' pipe: %w", err) return nil, fmt.Errorf("cannot parse 'filter' pipe: %w", err)
} }
@ -177,7 +179,7 @@ func parsePipe(lex *lexer) (pipe, error) {
} }
return ps, nil return ps, nil
case lex.isKeyword("stats"): case lex.isKeyword("stats"):
ps, err := parsePipeStats(lex) ps, err := parsePipeStats(lex, true)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot parse 'stats' pipe: %w", err) return nil, fmt.Errorf("cannot parse 'stats' pipe: %w", err)
} }
@ -207,6 +209,22 @@ func parsePipe(lex *lexer) (pipe, error) {
} }
return pu, nil return pu, nil
default: default:
lexState := lex.backupState()
// Try parsing stats pipe without 'stats' keyword
ps, err := parsePipeStats(lex, false)
if err == nil {
return ps, nil
}
lex.restoreState(lexState)
// Try parsing filter pipe without 'filter' keyword
pf, err := parsePipeFilter(lex, false)
if err == nil {
return pf, nil
}
lex.restoreState(lexState)
return nil, fmt.Errorf("unexpected pipe %q", lex.token) return nil, fmt.Errorf("unexpected pipe %q", lex.token)
} }
} }

View file

@ -108,11 +108,13 @@ func (pfp *pipeFilterProcessor) flush() error {
return nil return nil
} }
func parsePipeFilter(lex *lexer) (*pipeFilter, error) { func parsePipeFilter(lex *lexer, needFilterKeyword bool) (*pipeFilter, error) {
if !lex.isKeyword("filter") { if needFilterKeyword {
return nil, fmt.Errorf("expecting 'filter'; got %q", lex.token) if !lex.isKeyword("filter") {
return nil, fmt.Errorf("expecting 'filter'; got %q", lex.token)
}
lex.nextToken()
} }
lex.nextToken()
f, err := parseFilter(lex) f, err := parseFilter(lex)
if err != nil { if err != nil {

View file

@ -32,6 +32,14 @@ func TestPipeFilter(t *testing.T) {
expectPipeResults(t, pipeStr, rows, rowsExpected) expectPipeResults(t, pipeStr, rows, rowsExpected)
} }
// filter mismatch, missing 'filter' prefix
f("abc", [][]Field{
{
{"_msg", `{"foo":"bar"}`},
{"a", `test`},
},
}, [][]Field{})
// filter mismatch // filter mismatch
f("filter abc", [][]Field{ f("filter abc", [][]Field{
{ {
@ -40,6 +48,19 @@ func TestPipeFilter(t *testing.T) {
}, },
}, [][]Field{}) }, [][]Field{})
// filter match, missing 'filter' prefix
f("foo", [][]Field{
{
{"_msg", `{"foo":"bar"}`},
{"a", `test`},
},
}, [][]Field{
{
{"_msg", `{"foo":"bar"}`},
{"a", `test`},
},
})
// filter match // filter match
f("filter foo", [][]Field{ f("filter foo", [][]Field{
{ {

View file

@ -62,7 +62,7 @@ func (pr *pipeRename) hasFilterInWithQuery() bool {
return false return false
} }
func (pr *pipeRename) initFilterInValues(cache map[string][]string, getFieldValuesFunc getFieldValuesFunc) (pipe, error) { func (pr *pipeRename) initFilterInValues(_ map[string][]string, _ getFieldValuesFunc) (pipe, error) {
return pr, nil return pr, nil
} }

View file

@ -537,13 +537,14 @@ func (psp *pipeStatsProcessor) flush() error {
return nil return nil
} }
func parsePipeStats(lex *lexer) (*pipeStats, error) { func parsePipeStats(lex *lexer, needStatsKeyword bool) (*pipeStats, error) {
if !lex.isKeyword("stats") { if needStatsKeyword {
return nil, fmt.Errorf("expecting 'stats'; got %q", lex.token) if !lex.isKeyword("stats") {
return nil, fmt.Errorf("expecting 'stats'; got %q", lex.token)
}
lex.nextToken()
} }
lex.nextToken()
var ps pipeStats var ps pipeStats
if lex.isKeyword("by", "(") { if lex.isKeyword("by", "(") {
if lex.isKeyword("by") { if lex.isKeyword("by") {

View file

@ -39,6 +39,70 @@ func TestPipeStats(t *testing.T) {
expectPipeResults(t, pipeStr, rows, rowsExpected) expectPipeResults(t, pipeStr, rows, rowsExpected)
} }
// missing 'stats' keyword
f("count(*) as rows", [][]Field{
{
{"_msg", `abc`},
{"a", `2`},
{"b", `3`},
},
{
{"_msg", `def`},
{"a", `1`},
},
{
{"a", `2`},
{"b", `54`},
},
}, [][]Field{
{
{"rows", "3"},
},
})
// missing 'stats' keyword
f("count() as rows, count() if (a:2) rows2", [][]Field{
{
{"_msg", `abc`},
{"a", `2`},
{"b", `3`},
},
{
{"_msg", `def`},
{"a", `1`},
},
{
{"a", `2`},
{"b", `54`},
},
}, [][]Field{
{
{"rows", "3"},
{"rows2", "2"},
},
})
f("stats count() as rows, count() if (a:2) rows2", [][]Field{
{
{"_msg", `abc`},
{"a", `2`},
{"b", `3`},
},
{
{"_msg", `def`},
{"a", `1`},
},
{
{"a", `2`},
{"b", `54`},
},
}, [][]Field{
{
{"rows", "3"},
{"rows2", "2"},
},
})
f("stats count(*) as rows", [][]Field{ f("stats count(*) as rows", [][]Field{
{ {
{"_msg", `abc`}, {"_msg", `abc`},
@ -141,6 +205,32 @@ func TestPipeStats(t *testing.T) {
}, },
}) })
// missing 'stats' keyword
f("by (a) count(*) as rows", [][]Field{
{
{"_msg", `abc`},
{"a", `2`},
{"b", `3`},
},
{
{"_msg", `def`},
{"a", `1`},
},
{
{"a", `2`},
{"b", `54`},
},
}, [][]Field{
{
{"a", "1"},
{"rows", "1"},
},
{
{"a", "2"},
{"rows", "2"},
},
})
f("stats by (a) count(*) as rows", [][]Field{ f("stats by (a) count(*) as rows", [][]Field{
{ {
{"_msg", `abc`}, {"_msg", `abc`},