mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-01-20 15:16:42 +00:00
wip
This commit is contained in:
parent
51b869d458
commit
2270c42c82
4 changed files with 150 additions and 17 deletions
|
@ -22,6 +22,7 @@ according to [these docs](https://docs.victoriametrics.com/VictoriaLogs/QuickSta
|
||||||
* FEATURE: return all the log fields by default in query results. Previously only [`_stream`](https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields), [`_time`](https://docs.victoriametrics.com/victorialogs/keyconcepts/#time-field) and [`_msg`](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field) fields were returned by default.
|
* FEATURE: return all the log fields by default in query results. Previously only [`_stream`](https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields), [`_time`](https://docs.victoriametrics.com/victorialogs/keyconcepts/#time-field) and [`_msg`](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field) fields were returned by default.
|
||||||
* FEATURE: add support for returning only the requested log [fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model). See [these docs](https://docs.victoriametrics.com/victorialogs/logsql/#querying-specific-fields).
|
* FEATURE: add support for returning only the requested log [fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model). See [these docs](https://docs.victoriametrics.com/victorialogs/logsql/#querying-specific-fields).
|
||||||
* FEATURE: add support for calculating the number of matching logs and the number of logs with non-empty [fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model). Grouping by arbitrary set of [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) is supported. See [these docs](https://docs.victoriametrics.com/victorialogs/logsql/#stats) for details.
|
* FEATURE: add support for calculating the number of matching logs and the number of logs with non-empty [fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model). Grouping by arbitrary set of [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) is supported. See [these docs](https://docs.victoriametrics.com/victorialogs/logsql/#stats) for details.
|
||||||
|
* FEATURE: add support for returning the first `N` results. See [these docs](https://docs.victoriametrics.com/victorialogs/logsql/#limiters).
|
||||||
* FEATURE: optimize performance for [LogsQL query](https://docs.victoriametrics.com/victorialogs/logsql/), which contains multiple filters for [words](https://docs.victoriametrics.com/victorialogs/logsql/#word-filter) or [phrases](https://docs.victoriametrics.com/victorialogs/logsql/#phrase-filter) delimited with [`AND` operator](https://docs.victoriametrics.com/victorialogs/logsql/#logical-filter). For example, `foo AND bar` query must find [log messages](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field) with `foo` and `bar` words at faster speed.
|
* FEATURE: optimize performance for [LogsQL query](https://docs.victoriametrics.com/victorialogs/logsql/), which contains multiple filters for [words](https://docs.victoriametrics.com/victorialogs/logsql/#word-filter) or [phrases](https://docs.victoriametrics.com/victorialogs/logsql/#phrase-filter) delimited with [`AND` operator](https://docs.victoriametrics.com/victorialogs/logsql/#logical-filter). For example, `foo AND bar` query must find [log messages](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field) with `foo` and `bar` words at faster speed.
|
||||||
|
|
||||||
* BUGFIX: prevent from additional CPU usage for up to a few seconds after canceling the query.
|
* BUGFIX: prevent from additional CPU usage for up to a few seconds after canceling the query.
|
||||||
|
|
|
@ -1092,12 +1092,15 @@ See the [Roadmap](https://docs.victoriametrics.com/VictoriaLogs/Roadmap.html) fo
|
||||||
|
|
||||||
## Limiters
|
## Limiters
|
||||||
|
|
||||||
|
LogsQL provides the following functionality for limiting the amounts of returned log entries:
|
||||||
|
|
||||||
|
- `error | head 10` - returns up to 10 log entries with the `error` [word](#word).
|
||||||
|
|
||||||
|
LogsQL will support the ability to page the returned results.
|
||||||
|
|
||||||
It is possible to limit the returned results with `head`, `tail`, `less`, etc. Unix commands
|
It is possible to limit the returned results with `head`, `tail`, `less`, etc. Unix commands
|
||||||
according to [these docs](https://docs.victoriametrics.com/VictoriaLogs/querying/#command-line).
|
according to [these docs](https://docs.victoriametrics.com/VictoriaLogs/querying/#command-line).
|
||||||
|
|
||||||
LogsQL will support the ability to limit the number of returned results alongside the ability to page the returned results.
|
|
||||||
Additionally, LogsQL will provide the ability to select fields, which must be returned in the response.
|
|
||||||
|
|
||||||
See the [Roadmap](https://docs.victoriametrics.com/VictoriaLogs/Roadmap.html) for details.
|
See the [Roadmap](https://docs.victoriametrics.com/VictoriaLogs/Roadmap.html) for details.
|
||||||
|
|
||||||
## Querying specific fields
|
## Querying specific fields
|
||||||
|
|
|
@ -813,10 +813,20 @@ func TestParseQuerySuccess(t *testing.T) {
|
||||||
// multiple fields pipes
|
// multiple fields pipes
|
||||||
f(`foo | fields bar | fields baz, abc`, `foo | fields bar | fields baz, abc`)
|
f(`foo | fields bar | fields baz, abc`, `foo | fields bar | fields baz, abc`)
|
||||||
|
|
||||||
|
// head pipe
|
||||||
|
f(`foo | head 10`, `foo | head 10`)
|
||||||
|
f(`foo | HEAD 1123432`, `foo | head 1123432`)
|
||||||
|
|
||||||
|
// multiple head pipes
|
||||||
|
f(`foo | head 100 | head 10 | head 234`, `foo | head 100 | head 10 | head 234`)
|
||||||
|
|
||||||
// stats count pipe
|
// stats count pipe
|
||||||
f(`* | Stats count() AS foo`, `* | stats count() as foo`)
|
f(`* | Stats count() AS foo`, `* | stats count() as foo`)
|
||||||
f(`* | STATS bY (foo, b.a/r, "b az") count(*) as XYz`, `* | stats by (foo, "b.a/r", "b az") count(*) as XYz`)
|
f(`* | STATS bY (foo, b.a/r, "b az") count(*) as XYz`, `* | stats by (foo, "b.a/r", "b az") count(*) as XYz`)
|
||||||
f(`* | stats by() COUNT(x, 'a).b,c|d') as qwert`, `* | stats count(x, "a).b,c|d") as qwert`)
|
f(`* | stats by() COUNT(x, 'a).b,c|d') as qwert`, `* | stats count(x, "a).b,c|d") as qwert`)
|
||||||
|
|
||||||
|
// multiple different pipes
|
||||||
|
f(`* | fields foo, bar | head 100 | stats by(foo,bar) count(baz) as qwert`, `* | fields foo, bar | head 100 | stats by (foo, bar) count(baz) as qwert`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseQueryFailure(t *testing.T) {
|
func TestParseQueryFailure(t *testing.T) {
|
||||||
|
@ -1028,4 +1038,35 @@ func TestParseQueryFailure(t *testing.T) {
|
||||||
f(`foo | fields ,`)
|
f(`foo | fields ,`)
|
||||||
f(`foo | fields bar,`)
|
f(`foo | fields bar,`)
|
||||||
f(`foo | fields bar,,`)
|
f(`foo | fields bar,,`)
|
||||||
|
|
||||||
|
// missing head pipe value
|
||||||
|
f(`foo | head`)
|
||||||
|
|
||||||
|
// invalid head pipe value
|
||||||
|
f(`foo | head bar`)
|
||||||
|
f(`foo | head -123`)
|
||||||
|
|
||||||
|
// missing stats
|
||||||
|
f(`foo | stats`)
|
||||||
|
|
||||||
|
// invalid stats
|
||||||
|
f(`foo | stats bar`)
|
||||||
|
|
||||||
|
// invalid count
|
||||||
|
f(`foo | stats count`)
|
||||||
|
f(`foo | stats count(`)
|
||||||
|
f(`foo | stats count bar`)
|
||||||
|
f(`foo | stats count(bar`)
|
||||||
|
f(`foo | stats count(bar)`)
|
||||||
|
f(`foo | stats count() bar`)
|
||||||
|
f(`foo | stats count() as`)
|
||||||
|
f(`foo | stats count() as |`)
|
||||||
|
|
||||||
|
// invalid by clause
|
||||||
|
f(`foo | stats by`)
|
||||||
|
f(`foo | stats by bar`)
|
||||||
|
f(`foo | stats by(`)
|
||||||
|
f(`foo | stats by(bar`)
|
||||||
|
f(`foo | stats by(bar,`)
|
||||||
|
f(`foo | stats by(bar)`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||||
|
@ -23,7 +24,7 @@ type pipe interface {
|
||||||
// If stopCh is closed, the returned pipeProcessor must stop performing CPU-intensive tasks which take more than a few milliseconds.
|
// If stopCh is closed, the returned pipeProcessor must stop performing CPU-intensive tasks which take more than a few milliseconds.
|
||||||
// It is OK to continue processing pipeProcessor calls if they take less than a few milliseconds.
|
// It is OK to continue processing pipeProcessor calls if they take less than a few milliseconds.
|
||||||
//
|
//
|
||||||
// The returned pipeProcessor may call cancel() at any time in order to stop ppBase.
|
// The returned pipeProcessor may call cancel() at any time in order to notify writeBlock callers that it doesn't accept more data.
|
||||||
newPipeProcessor(workersCount int, stopCh <-chan struct{}, cancel func(), ppBase pipeProcessor) pipeProcessor
|
newPipeProcessor(workersCount int, stopCh <-chan struct{}, cancel func(), ppBase pipeProcessor) pipeProcessor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +40,7 @@ type pipeProcessor interface {
|
||||||
|
|
||||||
// flush must flush all the data accumulated in the pipeProcessor to the base pipeProcessor.
|
// flush must flush all the data accumulated in the pipeProcessor to the base pipeProcessor.
|
||||||
//
|
//
|
||||||
// The pipeProcessor must call ppBase.flush() and cancel(), which has been passed to newPipeProcessor, before returning from the flush.
|
// The pipeProcessor must call cancel() and ppBase.flush(), which has been passed to newPipeProcessor, before returning from the flush.
|
||||||
flush()
|
flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +80,12 @@ func parsePipes(lex *lexer) ([]pipe, error) {
|
||||||
return nil, fmt.Errorf("cannot parse 'stats' pipe: %w", err)
|
return nil, fmt.Errorf("cannot parse 'stats' pipe: %w", err)
|
||||||
}
|
}
|
||||||
pipes = append(pipes, sp)
|
pipes = append(pipes, sp)
|
||||||
|
case lex.isKeyword("head"):
|
||||||
|
hp, err := parseHeadPipe(lex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse 'head' pipe: %w", err)
|
||||||
|
}
|
||||||
|
pipes = append(pipes, hp)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unexpected pipe %q", lex.token)
|
return nil, fmt.Errorf("unexpected pipe %q", lex.token)
|
||||||
}
|
}
|
||||||
|
@ -135,8 +142,8 @@ func (fpp *fieldsPipeProcessor) writeBlock(workerID uint, timestamps []int64, co
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fpp *fieldsPipeProcessor) flush() {
|
func (fpp *fieldsPipeProcessor) flush() {
|
||||||
fpp.ppBase.flush()
|
|
||||||
fpp.cancel()
|
fpp.cancel()
|
||||||
|
fpp.ppBase.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFieldsPipe(lex *lexer) (*fieldsPipe, error) {
|
func parseFieldsPipe(lex *lexer) (*fieldsPipe, error) {
|
||||||
|
@ -148,7 +155,10 @@ func parseFieldsPipe(lex *lexer) (*fieldsPipe, error) {
|
||||||
if lex.isKeyword(",") {
|
if lex.isKeyword(",") {
|
||||||
return nil, fmt.Errorf("unexpected ','; expecting field name")
|
return nil, fmt.Errorf("unexpected ','; expecting field name")
|
||||||
}
|
}
|
||||||
field := parseFieldName(lex)
|
field, err := parseFieldName(lex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse field name: %w", err)
|
||||||
|
}
|
||||||
fields = append(fields, field)
|
fields = append(fields, field)
|
||||||
switch {
|
switch {
|
||||||
case lex.isKeyword("|", ")", ""):
|
case lex.isKeyword("|", ")", ""):
|
||||||
|
@ -320,8 +330,8 @@ func (spp *statsPipeProcessor) writeBlock(workerID uint, timestamps []int64, col
|
||||||
|
|
||||||
func (spp *statsPipeProcessor) flush() {
|
func (spp *statsPipeProcessor) flush() {
|
||||||
defer func() {
|
defer func() {
|
||||||
spp.ppBase.flush()
|
|
||||||
spp.cancel()
|
spp.cancel()
|
||||||
|
spp.ppBase.flush()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Merge states across shards
|
// Merge states across shards
|
||||||
|
@ -560,7 +570,10 @@ func parseStatsFuncCount(lex *lexer) (*statsFuncCount, error) {
|
||||||
if !lex.mustNextToken() {
|
if !lex.mustNextToken() {
|
||||||
return nil, fmt.Errorf("missing token after 'as' keyword")
|
return nil, fmt.Errorf("missing token after 'as' keyword")
|
||||||
}
|
}
|
||||||
resultName := parseFieldName(lex)
|
resultName, err := parseFieldName(lex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse 'as' field name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
sfc := &statsFuncCount{
|
sfc := &statsFuncCount{
|
||||||
fields: fields,
|
fields: fields,
|
||||||
|
@ -569,6 +582,80 @@ func parseStatsFuncCount(lex *lexer) (*statsFuncCount, error) {
|
||||||
return sfc, nil
|
return sfc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type headPipe struct {
|
||||||
|
n uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp *headPipe) String() string {
|
||||||
|
return fmt.Sprintf("head %d", hp.n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp *headPipe) newPipeProcessor(_ int, _ <-chan struct{}, cancel func(), ppBase pipeProcessor) pipeProcessor {
|
||||||
|
return &headPipeProcessor{
|
||||||
|
hp: hp,
|
||||||
|
cancel: cancel,
|
||||||
|
ppBase: ppBase,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type headPipeProcessor struct {
|
||||||
|
hp *headPipe
|
||||||
|
cancel func()
|
||||||
|
ppBase pipeProcessor
|
||||||
|
|
||||||
|
rowsWritten atomic.Uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hpp *headPipeProcessor) writeBlock(workerID uint, timestamps []int64, columns []BlockColumn) {
|
||||||
|
rowsWritten := hpp.rowsWritten.Add(uint64(len(timestamps)))
|
||||||
|
if rowsWritten <= hpp.hp.n {
|
||||||
|
// Fast path - write all the rows to ppBase.
|
||||||
|
hpp.ppBase.writeBlock(workerID, timestamps, columns)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow path - overflow. Write the remaining rows if needed.
|
||||||
|
rowsWritten -= uint64(len(timestamps))
|
||||||
|
if rowsWritten >= hpp.hp.n {
|
||||||
|
// Nothing to write. There is no need in cancel() call, since it has been called by another goroutine.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write remaining rows.
|
||||||
|
rowsRemaining := hpp.hp.n - rowsWritten
|
||||||
|
cs := make([]BlockColumn, len(columns))
|
||||||
|
for i, c := range columns {
|
||||||
|
cDst := &cs[i]
|
||||||
|
cDst.Name = c.Name
|
||||||
|
cDst.Values = c.Values[:rowsRemaining]
|
||||||
|
}
|
||||||
|
timestamps = timestamps[:rowsRemaining]
|
||||||
|
hpp.ppBase.writeBlock(workerID, timestamps, cs)
|
||||||
|
|
||||||
|
// Notify the caller that it should stop passing more data to writeBlock().
|
||||||
|
hpp.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hpp *headPipeProcessor) flush() {
|
||||||
|
hpp.cancel()
|
||||||
|
hpp.ppBase.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHeadPipe(lex *lexer) (*headPipe, error) {
|
||||||
|
if !lex.mustNextToken() {
|
||||||
|
return nil, fmt.Errorf("missing the number of head rows to return")
|
||||||
|
}
|
||||||
|
n, err := strconv.ParseUint(lex.token, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse %q: %w", lex.token, err)
|
||||||
|
}
|
||||||
|
lex.nextToken()
|
||||||
|
hp := &headPipe{
|
||||||
|
n: n,
|
||||||
|
}
|
||||||
|
return hp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseFieldNamesInParens(lex *lexer) ([]string, error) {
|
func parseFieldNamesInParens(lex *lexer) ([]string, error) {
|
||||||
if !lex.isKeyword("(") {
|
if !lex.isKeyword("(") {
|
||||||
return nil, fmt.Errorf("missing `(`")
|
return nil, fmt.Errorf("missing `(`")
|
||||||
|
@ -585,7 +672,10 @@ func parseFieldNamesInParens(lex *lexer) ([]string, error) {
|
||||||
if lex.isKeyword(",") {
|
if lex.isKeyword(",") {
|
||||||
return nil, fmt.Errorf("unexpected `,`")
|
return nil, fmt.Errorf("unexpected `,`")
|
||||||
}
|
}
|
||||||
field := parseFieldName(lex)
|
field, err := parseFieldName(lex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse field name: %w", err)
|
||||||
|
}
|
||||||
fields = append(fields, field)
|
fields = append(fields, field)
|
||||||
switch {
|
switch {
|
||||||
case lex.isKeyword(")"):
|
case lex.isKeyword(")"):
|
||||||
|
@ -598,14 +688,12 @@ func parseFieldNamesInParens(lex *lexer) ([]string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFieldName(lex *lexer) string {
|
func parseFieldName(lex *lexer) (string, error) {
|
||||||
s := lex.token
|
if lex.isKeyword(",", "(", ")", "[", "]", "|", "") {
|
||||||
lex.nextToken()
|
return "", fmt.Errorf("unexpected token: %q", lex.token)
|
||||||
for !lex.isSkippedSpace && !lex.isKeyword(",", "|", ")", "") {
|
|
||||||
s += lex.rawToken
|
|
||||||
lex.nextToken()
|
|
||||||
}
|
}
|
||||||
return s
|
token := getCompoundToken(lex)
|
||||||
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fieldNamesString(fields []string) string {
|
func fieldNamesString(fields []string) string {
|
||||||
|
|
Loading…
Reference in a new issue