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
* 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 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
Sometimes it is needed to apply additional filters on the calculated results. This can be done with `| filter ...` [pipe](#pipes).
The `filter` pipe can contain arbitrary [filters](#filters).
The `| filter ...` [pipe](#pipes) allows filtering the selected logs entries with arbitrary [filters](#filters).
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`:
@ -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
```
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:
- [`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
```
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:
- [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)
lex := newLexer(s)
ps, err := parsePipeStats(lex)
ps, err := parsePipeStats(lex, true)
if err != nil {
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("foo|")
f("foo|bar")
f("foo|bar(")
f("foo and")
f("foo OR ")
f("not")
@ -1163,7 +1163,7 @@ func TestParseQueryFailure(t *testing.T) {
f(`very long query with error aaa ffdfd fdfdfd fdfd:( ffdfdfdfdfd`)
// query with unexpected tail
f(`foo | bar`)
f(`foo | bar(`)
// unexpected comma
f(`foo,bar`)
@ -1284,9 +1284,9 @@ func TestParseQueryFailure(t *testing.T) {
// missing pipe keyword
f(`foo |`)
// unknown pipe keyword
f(`foo | bar`)
f(`foo | fields bar | baz`)
// invlaid pipe
f(`foo | bar(`)
f(`foo | fields bar | baz(`)
// missing field in fields pipe
f(`foo | fields`)

View file

@ -86,6 +86,8 @@ func parsePipes(lex *lexer) ([]pipe, error) {
lex.nextToken()
case lex.isKeyword(")", ""):
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
case lex.isKeyword("filter"):
pf, err := parsePipeFilter(lex)
pf, err := parsePipeFilter(lex, true)
if err != nil {
return nil, fmt.Errorf("cannot parse 'filter' pipe: %w", err)
}
@ -177,7 +179,7 @@ func parsePipe(lex *lexer) (pipe, error) {
}
return ps, nil
case lex.isKeyword("stats"):
ps, err := parsePipeStats(lex)
ps, err := parsePipeStats(lex, true)
if err != nil {
return nil, fmt.Errorf("cannot parse 'stats' pipe: %w", err)
}
@ -207,6 +209,22 @@ func parsePipe(lex *lexer) (pipe, error) {
}
return pu, nil
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)
}
}

View file

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

View file

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

View file

@ -62,7 +62,7 @@ func (pr *pipeRename) hasFilterInWithQuery() bool {
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
}

View file

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

View file

@ -39,6 +39,70 @@ func TestPipeStats(t *testing.T) {
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{
{
{"_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{
{
{"_msg", `abc`},