app/vlselect: properly return live tailing results

This commit is contained in:
Aliaksandr Valialkin 2024-06-27 15:05:57 +02:00
parent dd62a2b9d6
commit b26acec9a8
No known key found for this signature in database
GPG Key ID: 52C003EE2BCDB9EB
3 changed files with 41 additions and 24 deletions

View File

@ -16,6 +16,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil" "github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver" "github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage" "github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
) )
@ -346,6 +347,10 @@ func ProcessLiveTailRequest(ctx context.Context, w http.ResponseWriter, r *http.
end := time.Now().UnixNano() end := time.Now().UnixNano()
doneCh := ctxWithCancel.Done() doneCh := ctxWithCancel.Done()
flusher, ok := w.(http.Flusher)
if !ok {
logger.Panicf("BUG: it is expected that http.ResponseWriter (%T) supports http.Flusher interface", w)
}
for { for {
start := end - tailOffsetNsecs start := end - tailOffsetNsecs
end = time.Now().UnixNano() end = time.Now().UnixNano()
@ -361,7 +366,10 @@ func ProcessLiveTailRequest(ctx context.Context, w http.ResponseWriter, r *http.
httpserver.Errorf(w, r, "cannot get tail results for query [%q]: %s", q, err) httpserver.Errorf(w, r, "cannot get tail results for query [%q]: %s", q, err)
return return
} }
WriteJSONRows(w, resultRows) if len(resultRows) > 0 {
WriteJSONRows(w, resultRows)
flusher.Flush()
}
select { select {
case <-doneCh: case <-doneCh:
@ -418,16 +426,12 @@ func (tp *tailProcessor) writeBlock(_ uint, timestamps []int64, columns []logsto
return return
} }
// Make sure columns contain _time and _stream_id fields. // Make sure columns contain _time field, since it is needed for proper tail work.
// These fields are needed for proper tail work.
hasTime := false hasTime := false
hasStreamID := false
for _, c := range columns { for _, c := range columns {
if c.Name == "_time" { if c.Name == "_time" {
hasTime = true hasTime = true
} break
if c.Name == "_stream_id" {
hasStreamID = true
} }
} }
if !hasTime { if !hasTime {
@ -435,11 +439,6 @@ func (tp *tailProcessor) writeBlock(_ uint, timestamps []int64, columns []logsto
tp.cancel() tp.cancel()
return return
} }
if !hasStreamID {
tp.err = fmt.Errorf("missing _stream_id field")
tp.cancel()
return
}
// Copy block rows to tp.perStreamRows // Copy block rows to tp.perStreamRows
for i, timestamp := range timestamps { for i, timestamp := range timestamps {
@ -458,6 +457,7 @@ func (tp *tailProcessor) writeBlock(_ uint, timestamps []int64, columns []logsto
streamID = value streamID = value
} }
} }
tp.perStreamRows[streamID] = append(tp.perStreamRows[streamID], logRow{ tp.perStreamRows[streamID] = append(tp.perStreamRows[streamID], logRow{
timestamp: timestamp, timestamp: timestamp,
fields: fields, fields: fields,
@ -477,15 +477,14 @@ func (tp *tailProcessor) getTailRows() ([][]logstorage.Field, error) {
lastTimestamp, ok := tp.lastTimestamps[streamID] lastTimestamp, ok := tp.lastTimestamps[streamID]
if ok { if ok {
// Skip already written rows // Skip already written rows
for i := range rows { for len(rows) > 0 && rows[0].timestamp <= lastTimestamp {
if rows[i].timestamp > lastTimestamp { rows = rows[1:]
rows = rows[i:]
break
}
} }
} }
resultRows = append(resultRows, rows...) if len(rows) > 0 {
tp.lastTimestamps[streamID] = rows[len(rows)-1].timestamp resultRows = append(resultRows, rows...)
tp.lastTimestamps[streamID] = rows[len(rows)-1].timestamp
}
} }
clear(tp.perStreamRows) clear(tp.perStreamRows)

View File

@ -132,7 +132,7 @@ VictoriaLogs provides `/select/logsql/tail?query=<query>` HTTP endpoint, which r
e.g. it works in the way similar to `tail -f` unix command. For example, the following command returns live tailing logs with the `error` word: e.g. it works in the way similar to `tail -f` unix command. For example, the following command returns live tailing logs with the `error` word:
```sh ```sh
curl http://localhost:9428/select/logsql/tail -d 'query=error' curl -N http://localhost:9428/select/logsql/tail -d 'query=error'
``` ```
The `<query>` must conform the following restrictions: The `<query>` must conform the following restrictions:
@ -143,16 +143,18 @@ The `<query>` must conform the following restrictions:
[`uniq`](https://docs.victoriametrics.com/victorialogs/logsql/#uniq-pipe), [`top`](https://docs.victoriametrics.com/victorialogs/logsql/#top-pipe), [`uniq`](https://docs.victoriametrics.com/victorialogs/logsql/#uniq-pipe), [`top`](https://docs.victoriametrics.com/victorialogs/logsql/#top-pipe),
[`unroll`](https://docs.victoriametrics.com/victorialogs/logsql/#unroll-pipe), etc. pipes. [`unroll`](https://docs.victoriametrics.com/victorialogs/logsql/#unroll-pipe), etc. pipes.
- It must return [`_time`](https://docs.victoriametrics.com/victorialogs/keyconcepts/#time-field) and [`_stream_id`](https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields) - It must return [`_time`](https://docs.victoriametrics.com/victorialogs/keyconcepts/#time-field) field. For example, this fields must be mentioned
fields, e.g. these fields must be left when using [`fields`](https://docs.victoriametrics.com/victorialogs/logsql/#fields-pipe), in [`fields`](https://docs.victoriametrics.com/victorialogs/logsql/#fields-pipe) pipe if this pipe is used.
[`delete`](https://docs.victoriametrics.com/victorialogs/logsql/#delete-pipe) or [`rename`](https://docs.victoriametrics.com/victorialogs/logsql/#rename-pipe) pipes.
- It is recommended to return [`_stream_id`](https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields) field for more accurate live tailing
across multiple streams.
By default the `(AccountID=0, ProjectID=0)` [tenant](https://docs.victoriametrics.com/victorialogs/#multitenancy) is queried. By default the `(AccountID=0, ProjectID=0)` [tenant](https://docs.victoriametrics.com/victorialogs/#multitenancy) is queried.
If you need querying other tenant, then specify it via `AccountID` and `ProjectID` http request headers. For example, the following query performs live tailing If you need querying other tenant, then specify it via `AccountID` and `ProjectID` http request headers. For example, the following query performs live tailing
for `(AccountID=12, ProjectID=34)` tenant: for `(AccountID=12, ProjectID=34)` tenant:
```sh ```sh
curl http://localhost:9428/select/logsql/tail -H 'AccountID: 12' -H 'ProjectID: 34' -d 'query=error' curl -N http://localhost:9428/select/logsql/tail -H 'AccountID: 12' -H 'ProjectID: 34' -d 'query=error'
``` ```
The number of currently executed live tailing requests to `/select/logsql/tail` can be [monitored](https://docs.victoriametrics.com/victorialogs/#monitoring) The number of currently executed live tailing requests to `/select/logsql/tail` can be [monitored](https://docs.victoriametrics.com/victorialogs/#monitoring)

View File

@ -568,6 +568,21 @@ func (rwa *responseWriterWithAbort) WriteHeader(statusCode int) {
rwa.sentHeaders = true rwa.sentHeaders = true
} }
// Flush implements net/http.Flusher interface
func (rwa *responseWriterWithAbort) Flush() {
if rwa.aborted {
return
}
if !rwa.sentHeaders {
rwa.sentHeaders = true
}
flusher, ok := rwa.ResponseWriter.(http.Flusher)
if !ok {
logger.Panicf("BUG: it is expected http.ResponseWriter (%T) supports http.Flusher interface", rwa.ResponseWriter)
}
flusher.Flush()
}
// abort aborts the client connection associated with rwa. // abort aborts the client connection associated with rwa.
// //
// The last http chunk in the response stream is intentionally written incorrectly, // The last http chunk in the response stream is intentionally written incorrectly,
@ -618,6 +633,7 @@ func Errorf(w http.ResponseWriter, r *http.Request, format string, args ...inter
break break
} }
} }
if rwa, ok := w.(*responseWriterWithAbort); ok && rwa.sentHeaders { if rwa, ok := w.(*responseWriterWithAbort); ok && rwa.sentHeaders {
// HTTP status code has been already sent to client, so it cannot be sent again. // HTTP status code has been already sent to client, so it cannot be sent again.
// Just write errStr to the response and abort the client connection, so the client could notice the error. // Just write errStr to the response and abort the client connection, so the client could notice the error.