Compare commits

..

3 Commits

Author SHA1 Message Date
Zakhar Bessarab
1d6340fe83 deps: go mod vendor after 1334eee4
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-10 18:29:25 +04:00
Zakhar Bessarab
1334eee433 make/tools: switch only qtc to "go tool"
Restore other tools to migrate step by step.

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-10 18:28:31 +04:00
Zakhar Bessarab
2d67e14d59 make/tools: switch tooling to use go tool for external tools
Go 1.24 added first class support for management of tools via built-in `go tool` commands - https://tip.golang.org/doc/go1.24#tools

Switching tools management to go tooling addresses the following:
- version drift - previously make commands did not verify specific version of the tool
- global installation of dependencies - dependencies are now well scoped and do not require any globally installed parts

This commit sets up tooling to use `go tool` pattern for the same make targets as previously so there are no changes in workflow needed for existing users.

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-06 15:04:44 +04:00
213 changed files with 5528 additions and 7233 deletions

View File

@@ -528,8 +528,6 @@ vet:
check-all: fmt vet golangci-lint govulncheck
clean-checkers: remove-golangci-lint remove-govulncheck
test:
GOEXPERIMENT=synctest go test ./lib/... ./app/...
@@ -545,7 +543,7 @@ test-full:
test-full-386:
GOEXPERIMENT=synctest GOARCH=386 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
integration-test: victoria-metrics vmagent vmalert vmauth vmctl vmbackup vmrestore victoria-logs
integration-test: victoria-metrics vmagent vmalert vmauth vmctl
go test ./apptest/... -skip="^TestCluster.*"
benchmark:
@@ -574,12 +572,11 @@ app-local-goos-goarch:
app-local-windows-goarch:
CGO_ENABLED=0 GOOS=windows GOARCH=$(GOARCH) go build $(RACE) -ldflags "$(GO_BUILDINFO)" -o bin/$(APP_NAME)-windows-$(GOARCH)$(RACE).exe $(PKG_PREFIX)/app/$(APP_NAME)
quicktemplate-gen: install-qtc
qtc
install-qtc:
which qtc || go install github.com/valyala/quicktemplate/qtc@latest
quicktemplate-gen:
go tool qtc
golangci-lint:
GOEXPERIMENT=synctest go tool golangci-lint run
golangci-lint: install-golangci-lint
GOEXPERIMENT=synctest golangci-lint run

View File

@@ -8,7 +8,6 @@ import (
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlselect"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
@@ -45,8 +44,6 @@ func main() {
vlstorage.Init()
vlselect.Init()
insertutil.SetLogRowsStorage(&vlstorage.Storage{})
vlinsert.Init()
go httpserver.Serve(listenAddrs, requestHandler, httpserver.ServeOptions{

View File

@@ -11,6 +11,7 @@ import (
"github.com/valyala/fastjson"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
@@ -32,10 +33,10 @@ var parserPool fastjson.ParserPool
// RequestHandler processes Datadog insert requests
func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
switch path {
case "/insert/datadog/api/v1/validate":
case "/api/v1/validate":
fmt.Fprintf(w, `{}`)
return true
case "/insert/datadog/api/v2/logs":
case "/api/v2/logs":
return datadogLogsIngestion(w, r)
default:
return false
@@ -73,7 +74,7 @@ func datadogLogsIngestion(w http.ResponseWriter, r *http.Request) bool {
cp.IgnoreFields = *datadogIgnoreFields
}
if err := insertutil.CanWriteData(); err != nil {
if err := vlstorage.CanWriteData(); err != nil {
httpserver.Errorf(w, r, "%s", err)
return true
}
@@ -101,7 +102,7 @@ func datadogLogsIngestion(w http.ResponseWriter, r *http.Request) bool {
var (
v2LogsRequestsTotal = metrics.NewCounter(`vl_http_requests_total{path="/insert/datadog/api/v2/logs"}`)
v2LogsRequestDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/insert/datadog/api/v2/logs"}`)
v2LogsRequestDuration = metrics.NewHistogram(`vl_http_request_duration_seconds{path="/insert/datadog/api/v2/logs"}`)
)
// datadog message field has two formats:

View File

@@ -11,6 +11,7 @@ import (
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bufferedwriter"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
@@ -30,38 +31,36 @@ func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
// This header is needed for Logstash
w.Header().Set("X-Elastic-Product", "Elasticsearch")
if strings.HasPrefix(path, "/insert/elasticsearch/_ilm/policy") {
if strings.HasPrefix(path, "/_ilm/policy") {
// Return fake response for Elasticsearch ilm request.
fmt.Fprintf(w, `{}`)
return true
}
if strings.HasPrefix(path, "/insert/elasticsearch/_index_template") {
if strings.HasPrefix(path, "/_index_template") {
// Return fake response for Elasticsearch index template request.
fmt.Fprintf(w, `{}`)
return true
}
if strings.HasPrefix(path, "/insert/elasticsearch/_ingest") {
if strings.HasPrefix(path, "/_ingest") {
// Return fake response for Elasticsearch ingest pipeline request.
// See: https://www.elastic.co/guide/en/elasticsearch/reference/8.8/put-pipeline-api.html
fmt.Fprintf(w, `{}`)
return true
}
if strings.HasPrefix(path, "/insert/elasticsearch/_nodes") {
if strings.HasPrefix(path, "/_nodes") {
// Return fake response for Elasticsearch nodes discovery request.
// See: https://www.elastic.co/guide/en/elasticsearch/reference/8.8/cluster.html
fmt.Fprintf(w, `{}`)
return true
}
if strings.HasPrefix(path, "/insert/elasticsearch/logstash") || strings.HasPrefix(path, "/insert/elasticsearch/_logstash") {
if strings.HasPrefix(path, "/logstash") || strings.HasPrefix(path, "/_logstash") {
// Return fake response for Logstash APIs requests.
// See: https://www.elastic.co/guide/en/elasticsearch/reference/8.8/logstash-apis.html
fmt.Fprintf(w, `{}`)
return true
}
switch path {
// some clients may omit trailing slash
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8353
case "/insert/elasticsearch/", "/insert/elasticsearch":
case "/", "":
switch r.Method {
case http.MethodGet:
// Return fake response for Elasticsearch ping request.
@@ -76,7 +75,7 @@ func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
}
return true
case "/insert/elasticsearch/_license":
case "/_license":
// Return fake response for Elasticsearch license request.
fmt.Fprintf(w, `{
"license": {
@@ -87,7 +86,7 @@ func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
}
}`)
return true
case "/insert/elasticsearch/_bulk":
case "/_bulk":
startTime := time.Now()
bulkRequestsTotal.Inc()
@@ -96,7 +95,7 @@ func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
httpserver.Errorf(w, r, "%s", err)
return true
}
if err := insertutil.CanWriteData(); err != nil {
if err := vlstorage.CanWriteData(); err != nil {
httpserver.Errorf(w, r, "%s", err)
return true
}
@@ -129,7 +128,7 @@ func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
var (
bulkRequestsTotal = metrics.NewCounter(`vl_http_requests_total{path="/insert/elasticsearch/_bulk"}`)
bulkRequestDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/insert/elasticsearch/_bulk"}`)
bulkRequestDuration = metrics.NewHistogram(`vl_http_request_duration_seconds{path="/insert/elasticsearch/_bulk"}`)
)
func readBulkRequest(streamName string, r io.Reader, encoding string, timeFields, msgFields []string, lmp insertutil.LogMessageProcessor) (int, error) {

View File

@@ -11,6 +11,7 @@ import (
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
@@ -35,7 +36,6 @@ type CommonParams struct {
DecolorizeFields []string
ExtraFields []logstorage.Field
IsTimeFieldSet bool
Debug bool
DebugRequestURI string
DebugRemoteAddr string
@@ -49,10 +49,8 @@ func GetCommonParams(r *http.Request) (*CommonParams, error) {
return nil, err
}
var isTimeFieldSet bool
timeFields := []string{"_time"}
if tfs := httputil.GetArray(r, "_time_field", "VL-Time-Field"); len(tfs) > 0 {
isTimeFieldSet = true
timeFields = tfs
}
@@ -88,11 +86,9 @@ func GetCommonParams(r *http.Request) (*CommonParams, error) {
IgnoreFields: ignoreFields,
DecolorizeFields: decolorizeFields,
ExtraFields: extraFields,
IsTimeFieldSet: isTimeFieldSet,
Debug: debug,
DebugRequestURI: debugRequestURI,
DebugRemoteAddr: debugRemoteAddr,
Debug: debug,
DebugRequestURI: debugRequestURI,
DebugRemoteAddr: debugRemoteAddr,
}
return cp, nil
@@ -145,29 +141,6 @@ func GetCommonParamsForSyslog(tenantID logstorage.TenantID, streamFields, ignore
return cp
}
// LogRowsStorage is an interface for ingesting logs into the storage.
type LogRowsStorage interface {
// MustAddRows must add lr to the underlying storage.
MustAddRows(lr *logstorage.LogRows)
// CanWriteData must returns non-nil error if logs cannot be added to the underlying storage.
CanWriteData() error
}
var logRowsStorage LogRowsStorage
// SetLogRowsStorage sets the storage for writing data to via LogMessageProcessor.
//
// This function must be called before using LogMessageProcessor and CanWriteData from this package.
func SetLogRowsStorage(storage LogRowsStorage) {
logRowsStorage = storage
}
// CanWriteData returns non-nil error if data cannot be written to the underlying storage.
func CanWriteData() error {
return logRowsStorage.CanWriteData()
}
// LogMessageProcessor is an interface for log message processors.
type LogMessageProcessor interface {
// AddRow must add row to the LogMessageProcessor with the given timestamp and fields.
@@ -291,7 +264,7 @@ func (lmp *logMessageProcessor) AddInsertRow(r *logstorage.InsertRow) {
// flushLocked must be called under locked lmp.mu.
func (lmp *logMessageProcessor) flushLocked() {
lmp.lastFlushTime = time.Now()
logRowsStorage.MustAddRows(lmp.lr)
vlstorage.MustAddRows(lmp.lr)
lmp.lr.ResetKeepSettings()
}

View File

@@ -56,7 +56,6 @@ func NewLineReader(name string, r io.Reader) *LineReader {
// Check for Err in this case.
func (lr *LineReader) NextLine() bool {
for {
lr.Line = nil
if lr.bufOffset >= len(lr.buf) {
if lr.err != nil || lr.eofReached {
return false

View File

@@ -24,9 +24,6 @@ func TestLineReader_Success(t *testing.T) {
if lr.NextLine() {
t.Fatalf("expecting error on the second call to NextLine()")
}
if len(lr.Line) > 0 {
t.Fatalf("unexpected non-empty line after failed NextLine(): %q", lr.Line)
}
if !reflect.DeepEqual(lines, linesExpected) {
t.Fatalf("unexpected lines\ngot\n%q\nwant\n%q", lines, linesExpected)
}

View File

@@ -38,10 +38,7 @@ func ExtractTimestampFromFields(timeFields []string, fields []logstorage.Field)
}
func parseTimestamp(s string) (int64, error) {
// "-" is a nil timestamp value, if the syslog
// application is incapable of obtaining system time
// https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.3
if s == "" || s == "0" || s == "-" {
if s == "" || s == "0" {
return time.Now().UnixNano(), nil
}
if len(s) <= len("YYYY") || s[len("YYYY")] != '-' {

View File

@@ -133,33 +133,6 @@ func TestExtractTimestampFromFields_Success(t *testing.T) {
}, 1718773640000000000)
}
func TestExtractTimestampFromFields_Now(t *testing.T) {
f := func(timeField string, fields []logstorage.Field) {
t.Helper()
nsecs, err := ExtractTimestampFromFields([]string{timeField}, fields)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if nsecs < 1 {
t.Fatalf("expected generated timestamp, got error: %s", err)
}
}
// RFC5424 allows `-` for nil timestamp (log ingestion time)
f("time", []logstorage.Field{
{Name: "time", Value: "-"},
})
f("time", []logstorage.Field{
{Name: "time", Value: ""},
})
f("time", []logstorage.Field{
{Name: "time", Value: "0"},
})
}
func TestExtractTimestampFromFields_Error(t *testing.T) {
f := func(s string) {
t.Helper()

View File

@@ -8,6 +8,7 @@ import (
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage/netinsert"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
@@ -39,7 +40,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) {
httpserver.Errorf(w, r, "%s", err)
return
}
if err := insertutil.CanWriteData(); err != nil {
if err := vlstorage.CanWriteData(); err != nil {
httpserver.Errorf(w, r, "%s", err)
return
}
@@ -84,5 +85,5 @@ var (
requestsTotal = metrics.NewCounter(`vl_http_requests_total{path="/internal/insert"}`)
errorsTotal = metrics.NewCounter(`vl_http_errors_total{path="/internal/insert"}`)
requestDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/internal/insert"}`)
requestDuration = metrics.NewHistogram(`vl_http_request_duration_seconds{path="/internal/insert"}`)
)

View File

@@ -3,30 +3,29 @@ package journald
import (
"bytes"
"encoding/binary"
"errors"
"flag"
"fmt"
"io"
"net/http"
"regexp"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/writeconcurrencylimiter"
"github.com/VictoriaMetrics/metrics"
)
// See https://github.com/systemd/systemd/blob/main/src/libsystemd/sd-journal/journal-file.c#L1703
const maxFieldNameLen = 64
const journaldEntryMaxNameLen = 64
var allowedJournaldEntryNameChars = regexp.MustCompile(`^[A-Z_][A-Z0-9_]*`)
var (
journaldStreamFields = flagutil.NewArrayString("journald.streamFields", "Comma-separated list of fields to use as log stream fields for logs ingested over journald protocol. "+
@@ -37,7 +36,9 @@ var (
"See https://docs.victoriametrics.com/victorialogs/data-ingestion/journald/#time-field")
journaldTenantID = flag.String("journald.tenantID", "0:0", "TenantID for logs ingested via the Journald endpoint. "+
"See https://docs.victoriametrics.com/victorialogs/data-ingestion/journald/#multitenancy")
journaldIncludeEntryMetadata = flag.Bool("journald.includeEntryMetadata", false, "Include Journald fields with double underscore prefixes")
journaldIncludeEntryMetadata = flag.Bool("journald.includeEntryMetadata", false, "Include journal entry fields, which with double underscores.")
maxRequestSize = flagutil.NewBytes("journald.maxRequestSize", 64*1024*1024, "The maximum size in bytes of a single journald request")
)
func getCommonParams(r *http.Request) (*insertutil.CommonParams, error) {
@@ -52,12 +53,11 @@ func getCommonParams(r *http.Request) (*insertutil.CommonParams, error) {
}
cp.TenantID = tenantID
}
if !cp.IsTimeFieldSet {
if len(cp.TimeFields) == 0 {
cp.TimeFields = []string{*journaldTimeField}
}
if len(cp.StreamFields) == 0 {
cp.StreamFields = getStreamFields()
cp.StreamFields = *journaldStreamFields
}
if len(cp.IgnoreFields) == 0 {
cp.IgnoreFields = *journaldIgnoreFields
@@ -66,23 +66,10 @@ func getCommonParams(r *http.Request) (*insertutil.CommonParams, error) {
return cp, nil
}
func getStreamFields() []string {
if len(*journaldStreamFields) > 0 {
return *journaldStreamFields
}
return defaultStreamFields
}
var defaultStreamFields = []string{
"_MACHINE_ID",
"_HOSTNAME",
"_SYSTEMD_UNIT",
}
// RequestHandler processes Journald Export insert requests
func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
switch path {
case "/insert/journald/upload":
case "/upload":
if r.Header.Get("Content-Type") != "application/vnd.fdo.journal" {
httpserver.Errorf(w, r, "only application/vnd.fdo.journal encoding is supported for Journald")
return true
@@ -97,7 +84,7 @@ func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
// handleJournald parses Journal binary entries
func handleJournald(r *http.Request, w http.ResponseWriter) {
startTime := time.Now()
requestsTotal.Inc()
requestsJournaldTotal.Inc()
cp, err := getCommonParams(r)
if err != nil {
@@ -106,25 +93,19 @@ func handleJournald(r *http.Request, w http.ResponseWriter) {
return
}
if err := insertutil.CanWriteData(); err != nil {
if err := vlstorage.CanWriteData(); err != nil {
errorsTotal.Inc()
httpserver.Errorf(w, r, "%s", err)
return
}
encoding := r.Header.Get("Content-Encoding")
reader, err := protoparserutil.GetUncompressedReader(r.Body, encoding)
if err != nil {
errorsTotal.Inc()
logger.Errorf("cannot decode journald request: %s", err)
return
}
lmp := cp.NewLogMessageProcessor("journald", true)
streamName := fmt.Sprintf("remoteAddr=%s, requestURI=%q", httpserver.GetQuotedRemoteAddr(r), r.RequestURI)
err = processStreamInternal(streamName, reader, lmp, cp)
protoparserutil.PutUncompressedReader(reader)
lmp.MustClose()
err = protoparserutil.ReadUncompressedData(r.Body, encoding, maxRequestSize, func(data []byte) error {
lmp := cp.NewLogMessageProcessor("journald", false)
err := parseJournaldRequest(data, lmp, cp)
lmp.MustClose()
return err
})
if err != nil {
errorsTotal.Inc()
httpserver.Errorf(w, r, "cannot read journald protocol data: %s", err)
@@ -136,185 +117,102 @@ func handleJournald(r *http.Request, w http.ResponseWriter) {
// See https://github.com/systemd/systemd/pull/34822
w.Header().Set("Accept-Encoding", "zstd")
// update requestDuration only for successfully parsed requests
// There is no need in updating requestDuration for request errors,
// update requestJournaldDuration only for successfully parsed requests
// There is no need in updating requestJournaldDuration for request errors,
// since their timings are usually much smaller than the timing for successful request parsing.
requestDuration.UpdateDuration(startTime)
requestJournaldDuration.UpdateDuration(startTime)
}
var (
requestsTotal = metrics.NewCounter(`vl_http_requests_total{path="/insert/journald/upload"}`)
errorsTotal = metrics.NewCounter(`vl_http_errors_total{path="/insert/journald/upload"}`)
requestDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/insert/journald/upload"}`)
requestsJournaldTotal = metrics.NewCounter(`vl_http_requests_total{path="/insert/journald/upload"}`)
errorsTotal = metrics.NewCounter(`vl_http_errors_total{path="/insert/journald/upload"}`)
requestJournaldDuration = metrics.NewHistogram(`vl_http_request_duration_seconds{path="/insert/journald/upload"}`)
)
func processStreamInternal(streamName string, r io.Reader, lmp insertutil.LogMessageProcessor, cp *insertutil.CommonParams) error {
wcr := writeconcurrencylimiter.GetReader(r)
defer writeconcurrencylimiter.PutReader(wcr)
lr := insertutil.NewLineReader("journald", wcr)
for {
err := readJournaldLogEntry(streamName, lr, lmp, cp)
wcr.DecConcurrency()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return fmt.Errorf("%s: %w", streamName, err)
}
}
}
type fieldsBuf struct {
fields []logstorage.Field
buf []byte
name []byte
value []byte
}
func (fb *fieldsBuf) reset() {
fb.fields = fb.fields[:0]
fb.buf = fb.buf[:0]
fb.name = fb.name[:0]
fb.value = fb.value[:0]
}
func (fb *fieldsBuf) addField(name, value string) {
bufLen := len(fb.buf)
fb.buf = append(fb.buf, name...)
nameCopy := bytesutil.ToUnsafeString(fb.buf[bufLen:])
bufLen = len(fb.buf)
fb.buf = append(fb.buf, value...)
valueCopy := bytesutil.ToUnsafeString(fb.buf[bufLen:])
fb.fields = append(fb.fields, logstorage.Field{
Name: nameCopy,
Value: valueCopy,
})
}
func (fb *fieldsBuf) appendNextLineToValue(lr *insertutil.LineReader) error {
if !lr.NextLine() {
if err := lr.Err(); err != nil {
return err
}
return fmt.Errorf("unexpected end of stream")
}
fb.value = append(fb.value, lr.Line...)
fb.value = append(fb.value, '\n')
return nil
}
func getFieldsBuf() *fieldsBuf {
fb := fieldsBufPool.Get()
if fb == nil {
return &fieldsBuf{}
}
return fb.(*fieldsBuf)
}
func putFieldsBuf(fb *fieldsBuf) {
fb.reset()
fieldsBufPool.Put(fb)
}
var fieldsBufPool sync.Pool
// readJournaldLogEntry reads a single log entry in Journald format.
//
// See https://systemd.io/JOURNAL_EXPORT_FORMATS/#journal-export-format
func readJournaldLogEntry(streamName string, lr *insertutil.LineReader, lmp insertutil.LogMessageProcessor, cp *insertutil.CommonParams) error {
func parseJournaldRequest(data []byte, lmp insertutil.LogMessageProcessor, cp *insertutil.CommonParams) error {
var fields []logstorage.Field
var ts int64
var size uint64
var name, value string
var line []byte
fb := getFieldsBuf()
defer putFieldsBuf(fb)
currentTimestamp := time.Now().UnixNano()
if !lr.NextLine() {
if err := lr.Err(); err != nil {
return fmt.Errorf("cannot read the first field: %w", err)
}
return io.EOF
}
for {
line := lr.Line
if len(line) == 0 {
// The end of a single log entry. Write it to the storage
if len(fb.fields) > 0 {
for len(data) > 0 {
idx := bytes.IndexByte(data, '\n')
switch {
case idx > 0:
// process fields
line = data[:idx]
data = data[idx+1:]
case idx == 0:
// next message or end of file
// double new line is a separator for the next message
if len(fields) > 0 {
if ts == 0 {
ts = time.Now().UnixNano()
ts = currentTimestamp
}
lmp.AddRow(ts, fb.fields, nil)
lmp.AddRow(ts, fields, nil)
fields = fields[:0]
}
return nil
// skip newline separator
data = data[1:]
continue
case idx < 0:
return fmt.Errorf("missing new line separator, unread data left=%d", len(data))
}
// line could be either "key=value" or "key"
// according to https://systemd.io/JOURNAL_EXPORT_FORMATS/#journal-export-format
if n := bytes.IndexByte(line, '='); n >= 0 {
// line = "key=value"
fb.name = append(fb.name[:0], line[:n]...)
name = bytesutil.ToUnsafeString(fb.name)
fb.value = append(fb.value[:0], line[n+1:]...)
value = bytesutil.ToUnsafeString(fb.value)
idx = bytes.IndexByte(line, '=')
// could b either e key=value\n pair
// or just key\n
// with binary data at the buffer
if idx > 0 {
name = bytesutil.ToUnsafeString(line[:idx])
value = bytesutil.ToUnsafeString(line[idx+1:])
} else {
// line = "key"
// Parse the binary-encoded value from the next line according to "key\n<little_endian_size_64>value\n" format
fb.name = append(fb.name[:0], line...)
name = bytesutil.ToUnsafeString(fb.name)
fb.value = fb.value[:0]
for len(fb.value) < 8 {
if err := fb.appendNextLineToValue(lr); err != nil {
return fmt.Errorf("cannot read value size: %w", err)
}
name = bytesutil.ToUnsafeString(line)
if len(data) == 0 {
return fmt.Errorf("unexpected zero data for binary field value of key=%s", name)
}
size := binary.LittleEndian.Uint64(fb.value[:8])
// Read the value until its lenth exceeds the given size - the last char in the read value will always be '\n'
// because it is appended by appendNextLineToValue().
for uint64(len(fb.value[8:])) <= size {
if err := fb.appendNextLineToValue(lr); err != nil {
return fmt.Errorf("cannot read %q value with size %d bytes; read only %d bytes: %w", fb.name, size, len(fb.value[8:]), err)
}
}
value = bytesutil.ToUnsafeString(fb.value[8 : len(fb.value)-1])
if uint64(len(value)) != size {
return fmt.Errorf("unexpected %q value size; got %d bytes; want %d bytes; value: %q", fb.name, len(value), size, value)
}
}
if !lr.NextLine() {
if err := lr.Err(); err != nil {
return fmt.Errorf("cannot read the next log field: %w", err)
}
// add the last log field below before the return
}
if len(name) > maxFieldNameLen {
logger.Errorf("%s: field name size should not exceed %d bytes; got %d bytes: %q; skipping this field", streamName, maxFieldNameLen, len(name), name)
continue
}
if !isValidFieldName(name) {
logger.Errorf("%s: invalid field name %q; it must consist of `A-Z0-9_` chars and must start from non-digit char; skipping this field", streamName, name)
continue
}
if slices.Contains(cp.TimeFields, name) {
t, err := strconv.ParseInt(value, 10, 64)
// size of binary data encoded as le i64 at the begging
idx, err := binary.Decode(data, binary.LittleEndian, &size)
if err != nil {
logger.Errorf("%s: cannot parse timestamp from the field %q: %w; using the current timestamp", streamName, name, err)
ts = 0
} else {
// Convert journald microsecond timestamp to nanoseconds
ts = t * 1e3
return fmt.Errorf("failed to extract binary field %q value size: %w", name, err)
}
// skip binary data size
data = data[idx:]
if size == 0 {
return fmt.Errorf("unexpected zero binary data size decoded %d", size)
}
if int(size) > len(data) {
return fmt.Errorf("binary data size=%d cannot exceed size of the data at buffer=%d", size, len(data))
}
value = bytesutil.ToUnsafeString(data[:size])
data = data[int(size):]
// binary data must has new line separator for the new line or next field
if len(data) == 0 {
return fmt.Errorf("unexpected empty buffer after binary field=%s read", name)
}
lastB := data[0]
if lastB != '\n' {
return fmt.Errorf("expected new line separator after binary field=%s, got=%s", name, string(lastB))
}
data = data[1:]
}
if len(name) > journaldEntryMaxNameLen {
return fmt.Errorf("journald entry name should not exceed %d symbols, got: %q", journaldEntryMaxNameLen, name)
}
if !allowedJournaldEntryNameChars.MatchString(name) {
return fmt.Errorf("journald entry name should consist of `A-Z0-9_` characters and must start from non-digit symbol")
}
if slices.Contains(cp.TimeFields, name) {
n, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return fmt.Errorf("failed to parse Journald timestamp, %w", err)
}
ts = n * 1e3
continue
}
@@ -322,56 +220,18 @@ func readJournaldLogEntry(streamName string, lr *insertutil.LineReader, lmp inse
name = "_msg"
}
if name == "PRIORITY" {
priority := journaldPriorityToLevel(value)
fb.addField("level", priority)
}
if !strings.HasPrefix(name, "__") || *journaldIncludeEntryMetadata {
fb.addField(name, value)
if *journaldIncludeEntryMetadata || !strings.HasPrefix(name, "__") {
fields = append(fields, logstorage.Field{
Name: name,
Value: value,
})
}
}
}
func journaldPriorityToLevel(priority string) string {
// See https://wiki.archlinux.org/title/Systemd/Journal#Priority_level
// and https://grafana.com/docs/grafana/latest/explore/logs-integration/#log-level
switch priority {
case "0":
return "emerg"
case "1":
return "alert"
case "2":
return "critical"
case "3":
return "error"
case "4":
return "warning"
case "5":
return "notice"
case "6":
return "info"
case "7":
return "debug"
default:
return priority
}
}
func isValidFieldName(s string) bool {
if len(s) == 0 {
return false
}
c := s[0]
if !(c >= 'A' && c <= 'Z' || c == '_') {
return false
}
for i := 1; i < len(s); i++ {
c := s[i]
if !(c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_') {
return false
if len(fields) > 0 {
if ts == 0 {
ts = currentTimestamp
}
lmp.AddRow(ts, fields, nil)
}
return true
return nil
}

View File

@@ -1,81 +1,20 @@
package journald
import (
"bytes"
"net/http"
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
)
func TestIsValidFieldName(t *testing.T) {
f := func(name string, resultExpected bool) {
t.Helper()
result := isValidFieldName(name)
if result != resultExpected {
t.Fatalf("unexpected result for isValidJournaldFieldName(%q); got %v; want %v", name, result, resultExpected)
}
}
f("", false)
f("a", false)
f("1", false)
f("_", true)
f("X", true)
f("Xa", false)
f("X_343", true)
f("X_0123456789_AZ", true)
f("SDDFD sdf", false)
}
func TestGetCommonParams_TimeField(t *testing.T) {
f := func(timeFieldHeader, expectedTimeField string) {
t.Helper()
req, err := http.NewRequest("POST", "/insert/journald/upload", nil)
if err != nil {
t.Fatalf("unexpected error creating request: %s", err)
}
if timeFieldHeader != "" {
req.Header.Set("VL-Time-Field", timeFieldHeader)
}
cp, err := getCommonParams(req)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if len(cp.TimeFields) != 1 || cp.TimeFields[0] != expectedTimeField {
t.Fatalf("unexpected TimeFields; got %v; want [%s]", cp.TimeFields, expectedTimeField)
}
}
// Test default behavior - when no custom time field is specified, journald uses __REALTIME_TIMESTAMP
f("", "__REALTIME_TIMESTAMP")
// Test custom time field - when a custom time field is specified via HTTP header, it's respected
f("custom_time", "custom_time")
}
func TestPushJournald_Success(t *testing.T) {
func TestPushJournaldOk(t *testing.T) {
f := func(src string, timestampsExpected []int64, resultExpected string) {
t.Helper()
tlp := &insertutil.TestLogMessageProcessor{}
r, err := http.NewRequest("GET", "https://foo.bar/baz", nil)
if err != nil {
t.Fatalf("cannot create request: %s", err)
cp := &insertutil.CommonParams{
TimeFields: []string{"__REALTIME_TIMESTAMP"},
MsgFields: []string{"MESSAGE"},
}
cp, err := getCommonParams(r)
if err != nil {
t.Fatalf("cannot create commonParams: %s", err)
}
buf := bytes.NewBufferString(src)
if err := processStreamInternal("test", buf, tlp, cp); err != nil {
if err := parseJournaldRequest([]byte(src), tlp, cp); err != nil {
t.Fatalf("unexpected error: %s", err)
}
@@ -83,17 +22,16 @@ func TestPushJournald_Success(t *testing.T) {
t.Fatal(err)
}
}
// Single event
f("__REALTIME_TIMESTAMP=91723819283\nMESSAGE=Test message\n\n",
f("__REALTIME_TIMESTAMP=91723819283\nMESSAGE=Test message\n",
[]int64{91723819283000},
"{\"_msg\":\"Test message\"}",
)
// Multiple events
f("__REALTIME_TIMESTAMP=91723819283\nPRIORITY=3\nMESSAGE=Test message\n\n__REALTIME_TIMESTAMP=91723819284\nMESSAGE=Test message2\n",
f("__REALTIME_TIMESTAMP=91723819283\nMESSAGE=Test message\n\n__REALTIME_TIMESTAMP=91723819284\nMESSAGE=Test message2\n",
[]int64{91723819283000, 91723819284000},
"{\"level\":\"error\",\"PRIORITY\":\"3\",\"_msg\":\"Test message\"}\n{\"_msg\":\"Test message2\"}",
"{\"_msg\":\"Test message\"}\n{\"_msg\":\"Test message2\"}",
)
// Parse binary data
@@ -101,66 +39,30 @@ func TestPushJournald_Success(t *testing.T) {
[]int64{1729698775704404000},
"{\"E\":\"JobStateChanged\",\"_BOOT_ID\":\"f778b6e2f7584a77b991a2366612a7b5\",\"_UID\":\"0\",\"_GID\":\"0\",\"_MACHINE_ID\":\"a4a970370c30a925df02a13c67167847\",\"_HOSTNAME\":\"ecd5e4555787\",\"_RUNTIME_SCOPE\":\"system\",\"_TRANSPORT\":\"journal\",\"_CAP_EFFECTIVE\":\"1ffffffffff\",\"_SYSTEMD_CGROUP\":\"/init.scope\",\"_SYSTEMD_UNIT\":\"init.scope\",\"_SYSTEMD_SLICE\":\"-.slice\",\"CODE_FILE\":\"\\u003cstdin>\",\"CODE_LINE\":\"1\",\"CODE_FUNC\":\"\\u003cmodule>\",\"SYSLOG_IDENTIFIER\":\"python3\",\"_COMM\":\"python3\",\"_EXE\":\"/usr/bin/python3.12\",\"_CMDLINE\":\"python3\",\"_msg\":\"foo\\nbar\\n\\n\\nasda\\nasda\",\"_PID\":\"2763\",\"_SOURCE_REALTIME_TIMESTAMP\":\"1729698775704375\"}",
)
// Parse binary data with trailing newline
f("__REALTIME_TIMESTAMP=1729698775704404\n_CMDLINE=python3\nMESSAGE\n\x14\x00\x00\x00\x00\x00\x00\x00foo\nbar\n\n\nasda\nasda\n\n_PID=2763\n\n",
[]int64{1729698775704404000},
`{"_CMDLINE":"python3","_msg":"foo\nbar\n\n\nasda\nasda\n","_PID":"2763"}`,
)
f("__REALTIME_TIMESTAMP=1729698775704404\n_CMDLINE=python3\nMESSAGE\n\x00\x00\x00\x00\x00\x00\x00\x00\n_PID=2763\n\n",
[]int64{1729698775704404000},
`{"_CMDLINE":"python3","_PID":"2763"}`,
)
f("__REALTIME_TIMESTAMP=1729698775704404\n_CMDLINE=python3\nMESSAGE\n\x0A\x00\x00\x00\x00\x00\x00\x00123456789\n\n_PID=2763\n\n",
[]int64{1729698775704404000},
`{"_CMDLINE":"python3","_msg":"123456789\n","_PID":"2763"}`,
)
f("__REALTIME_TIMESTAMP=1729698775704404\n_CMDLINE=python3\nMESSAGE\n\x0A\x00\x00\x00\x00\x00\x00\x001234567890\n_PID=2763\n\n",
[]int64{1729698775704404000},
`{"_CMDLINE":"python3","_msg":"1234567890","_PID":"2763"}`,
)
// Empty field name must be ignored
f("__REALTIME_TIMESTAMP=91723819283\na=b\n=Test message", nil, "")
f("__REALTIME_TIMESTAMP=91723819284\nMESSAGE=Test message2\n\n__REALTIME_TIMESTAMP=91723819283\n=Test message\n", []int64{91723819284000}, `{"_msg":"Test message2"}`)
// field name starting with number must be ignored
f("__REALTIME_TIMESTAMP=91723819283\n1incorrect=Test message\n\n__REALTIME_TIMESTAMP=91723819284\nMESSAGE=Test message2\n\n", []int64{91723819284000}, `{"_msg":"Test message2"}`)
// field name exceeding 64 bytes limit must be ignored
f("__REALTIME_TIMESTAMP=91723819283\ntoolooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongcorrecooooooooooooong=Test message\n", nil, "")
// field name with invalid chars must be ignored
f("__REALTIME_TIMESTAMP=91723819283\nbadC!@$!@$as=Test message\n", nil, "")
}
func TestPushJournald_Failure(t *testing.T) {
f := func(data string) {
t.Helper()
tlp := &insertutil.TestLogMessageProcessor{}
r, err := http.NewRequest("GET", "https://foo.bar/baz", nil)
if err != nil {
t.Fatalf("cannot create request: %s", err)
cp := &insertutil.CommonParams{
TimeFields: []string{"__REALTIME_TIMESTAMP"},
MsgFields: []string{"MESSAGE"},
}
cp, err := getCommonParams(r)
if err != nil {
t.Fatalf("cannot create commonParams: %s", err)
}
buf := bytes.NewBufferString(data)
if err := processStreamInternal("test", buf, tlp, cp); err == nil {
t.Fatalf("expecting non-nil error")
if err := parseJournaldRequest([]byte(data), tlp, cp); err == nil {
t.Fatalf("expected non nil error")
}
}
// too short binary encoded message
f("__CURSOR=s=e0afe8412a6a49d2bfcf66aa7927b588;i=1f06;b=f778b6e2f7584a77b991a2366612a7b5;m=300bdfd420;t=62526e1182354;x=930dc44b370963b7\n__REALTIME_TIMESTAMP=1729698775704404\nMESSAGE\n\x13\x00\x00\x00\x00\x00\x00\x00foo\nbar\n\n\nasdaasd")
f("__REALTIME_TIMESTAMP=1729698775704404\n_CMDLINE=python3\nMESSAGE\n\x00\x00\x00\x00\x00\x00\x00\x00_PID=2763\n\n")
f("__REALTIME_TIMESTAMP=1729698775704404\n_CMDLINE=python3\nMESSAGE\n\x0A\x00\x00\x00\x00\x00\x00\x001234567890_PID=2763\n\n")
f("__REALTIME_TIMESTAMP=1729698775704404\n_CMDLINE=python3\nMESSAGE\n\x0A\x00\x00\x00\x00\x00\x00\x00123456789\n_PID=2763\n\n")
// too long binary encoded message
f("__CURSOR=s=e0afe8412a6a49d2bfcf66aa7927b588;i=1f06;b=f778b6e2f7584a77b991a2366612a7b5;m=300bdfd420;t=62526e1182354;x=930dc44b370963b7\n__REALTIME_TIMESTAMP=1729698775704404\nMESSAGE\n\x13\x00\x00\x00\x00\x00\x00\x00foo\nbar\n\n\nasdaasdakljlsfd")
// missing new line terminator for binary encoded message
f("__CURSOR=s=e0afe8412a6a49d2bfcf66aa7927b588;i=1f06;b=f778b6e2f7584a77b991a2366612a7b5;m=300bdfd420;t=62526e1182354;x=930dc44b370963b7\n__REALTIME_TIMESTAMP=1729698775704404\nMESSAGE\n\x13\x00\x00\x00\x00\x00\x00\x00foo\nbar\n\n\nasdaasda2")
// missing new line terminator
f("__REALTIME_TIMESTAMP=91723819283\n=Test message")
// empty field name
f("__REALTIME_TIMESTAMP=91723819283\n=Test message\n")
// field name starting with number
f("__REALTIME_TIMESTAMP=91723819283\n1incorrect=Test message\n")
// field name exceeds 64 limit
f("__REALTIME_TIMESTAMP=91723819283\ntoolooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongcorrecooooooooooooong=Test message\n")
// Only allow A-Z0-9 and '_'
f("__REALTIME_TIMESTAMP=91723819283\nbadC!@$!@$as=Test message\n")
}

View File

@@ -1,82 +0,0 @@
package journald
import (
"bytes"
"encoding/binary"
"fmt"
"strings"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
)
func BenchmarkIsValidFieldName(b *testing.B) {
b.ReportAllocs()
b.SetBytes(int64(len(benchmarkFields)))
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
for _, field := range benchmarkFields {
if !isValidFieldName(field) {
panic(fmt.Errorf("cannot validate field %q", field))
}
}
}
})
}
var benchmarkFields = strings.Split(
"E,_BOOT_ID,_UID,_GID,_MACHINE_ID,_HOSTNAME,_RUNTIME_SCOPE,_TRANSPORT,_CAP_EFFECTIVE,_SYSTEMD_CGROUP,_SYSTEMD_UNIT,"+
"_SYSTEMD_SLICE,CODE_FILE,CODE_LINE,CODE_FUNC,SYSLOG_IDENTIFIER,_COMM,_EXE,_CMDLINE,MESSAGE,_PID,_SOURCE_REALTIME_TIMESTAMP,_REALTIME_TIMESTAMP",
",")
func BenchmarkPushJournaldPerformance(b *testing.B) {
cp := &insertutil.CommonParams{
TimeFields: []string{"__REALTIME_TIMESTAMP"},
MsgFields: []string{"MESSAGE"},
}
const dataChunkSize = 1024 * 1024
data := generateJournaldData(dataChunkSize)
b.ReportAllocs()
b.SetBytes(int64(len(data)))
b.RunParallel(func(pb *testing.PB) {
r := &bytes.Reader{}
blp := &insertutil.BenchmarkLogMessageProcessor{}
for pb.Next() {
r.Reset(data)
if err := processStreamInternal("performance_test", r, blp, cp); err != nil {
panic(fmt.Errorf("unexpected error: %w", err))
}
}
})
}
func generateJournaldData(size int) []byte {
var buf []byte
timestamp := time.Now().UnixMicro()
binaryMsg := []byte("binary message data for performance test")
var sizeBuf [8]byte
for len(buf) < size {
timestamp++
var entry string
// Generate a mix of simple and binary messages
if timestamp%10 == 0 {
// Generate binary message
binary.LittleEndian.PutUint64(sizeBuf[:], uint64(len(binaryMsg)))
entry = fmt.Sprintf("__REALTIME_TIMESTAMP=%d\nMESSAGE\n%s%s\n\n",
timestamp,
sizeBuf[:],
binaryMsg,
)
} else {
// Generate simple message
entry = fmt.Sprintf("__REALTIME_TIMESTAMP=%d\nMESSAGE=Performance test message %d\n\n", timestamp, timestamp)
}
buf = append(buf, entry...)
}
return buf
}

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
@@ -32,7 +33,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) {
httpserver.Errorf(w, r, "%s", err)
return
}
if err := insertutil.CanWriteData(); err != nil {
if err := vlstorage.CanWriteData(); err != nil {
httpserver.Errorf(w, r, "%s", err)
return
}
@@ -119,5 +120,5 @@ var (
requestsTotal = metrics.NewCounter(`vl_http_requests_total{path="/insert/jsonline"}`)
errorsTotal = metrics.NewCounter(`vl_http_errors_total{path="/insert/jsonline"}`)
requestDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/insert/jsonline"}`)
requestDuration = metrics.NewHistogram(`vl_http_request_duration_seconds{path="/insert/jsonline"}`)
)

View File

@@ -16,10 +16,10 @@ var disableMessageParsing = flag.Bool("loki.disableMessageParsing", false, "Whet
// RequestHandler processes Loki insert requests
func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
switch path {
case "/insert/loki/api/v1/push":
case "/api/v1/push":
handleInsert(r, w)
return true
case "/insert/loki/ready":
case "/ready":
// See https://grafana.com/docs/loki/latest/api/#identify-ready-loki-instance
w.WriteHeader(http.StatusOK)
w.Write([]byte("ready"))

View File

@@ -9,6 +9,7 @@ import (
"github.com/valyala/fastjson"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
@@ -29,7 +30,7 @@ func handleJSON(r *http.Request, w http.ResponseWriter) {
httpserver.Errorf(w, r, "cannot parse common params from request: %s", err)
return
}
if err := insertutil.CanWriteData(); err != nil {
if err := vlstorage.CanWriteData(); err != nil {
httpserver.Errorf(w, r, "%s", err)
return
}
@@ -58,7 +59,7 @@ func handleJSON(r *http.Request, w http.ResponseWriter) {
var (
requestsJSONTotal = metrics.NewCounter(`vl_http_requests_total{path="/insert/loki/api/v1/push",format="json"}`)
requestJSONDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/insert/loki/api/v1/push",format="json"}`)
requestJSONDuration = metrics.NewHistogram(`vl_http_request_duration_seconds{path="/insert/loki/api/v1/push",format="json"}`)
)
func parseJSONRequest(data []byte, lmp insertutil.LogMessageProcessor, msgFields []string, useDefaultStreamFields, parseMessage bool) error {

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
@@ -28,7 +29,7 @@ func handleProtobuf(r *http.Request, w http.ResponseWriter) {
httpserver.Errorf(w, r, "cannot parse common params from request: %s", err)
return
}
if err := insertutil.CanWriteData(); err != nil {
if err := vlstorage.CanWriteData(); err != nil {
httpserver.Errorf(w, r, "%s", err)
return
}
@@ -62,7 +63,7 @@ func handleProtobuf(r *http.Request, w http.ResponseWriter) {
var (
requestsProtobufTotal = metrics.NewCounter(`vl_http_requests_total{path="/insert/loki/api/v1/push",format="protobuf"}`)
requestProtobufDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/insert/loki/api/v1/push",format="protobuf"}`)
requestProtobufDuration = metrics.NewHistogram(`vl_http_request_duration_seconds{path="/insert/loki/api/v1/push",format="protobuf"}`)
)
func parseProtobufRequest(data []byte, lmp insertutil.LogMessageProcessor, msgFields []string, useDefaultStreamFields, parseMessage bool) error {

View File

@@ -58,29 +58,35 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
}
func insertHandler(w http.ResponseWriter, r *http.Request, path string) bool {
path = strings.TrimPrefix(path, "/insert")
switch path {
case "/insert/jsonline":
case "/jsonline":
jsonline.RequestHandler(w, r)
return true
case "/insert/ready":
case "/ready":
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
fmt.Fprintf(w, `{"status":"ok"}`)
return true
}
switch {
// some clients may omit trailing slash at elasticsearch protocol.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8353
case strings.HasPrefix(path, "/insert/elasticsearch"):
case strings.HasPrefix(path, "/elasticsearch"):
// some clients may omit trailing slash
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8353
path = strings.TrimPrefix(path, "/elasticsearch")
return elasticsearch.RequestHandler(path, w, r)
case strings.HasPrefix(path, "/insert/loki/"):
case strings.HasPrefix(path, "/loki/"):
path = strings.TrimPrefix(path, "/loki")
return loki.RequestHandler(path, w, r)
case strings.HasPrefix(path, "/insert/opentelemetry/"):
case strings.HasPrefix(path, "/opentelemetry/"):
path = strings.TrimPrefix(path, "/opentelemetry")
return opentelemetry.RequestHandler(path, w, r)
case strings.HasPrefix(path, "/insert/journald/"):
case strings.HasPrefix(path, "/journald/"):
path = strings.TrimPrefix(path, "/journald")
return journald.RequestHandler(path, w, r)
case strings.HasPrefix(path, "/insert/datadog/"):
case strings.HasPrefix(path, "/datadog/"):
path = strings.TrimPrefix(path, "/datadog")
return datadog.RequestHandler(path, w, r)
}

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
@@ -21,7 +22,7 @@ func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
switch path {
// use the same path as opentelemetry collector
// https://opentelemetry.io/docs/specs/otlp/#otlphttp-request
case "/insert/opentelemetry/v1/logs":
case "/v1/logs":
if r.Header.Get("Content-Type") == "application/json" {
httpserver.Errorf(w, r, "json encoding isn't supported for opentelemetry format. Use protobuf encoding")
return true
@@ -42,7 +43,7 @@ func handleProtobuf(r *http.Request, w http.ResponseWriter) {
httpserver.Errorf(w, r, "cannot parse common params from request: %s", err)
return
}
if err := insertutil.CanWriteData(); err != nil {
if err := vlstorage.CanWriteData(); err != nil {
httpserver.Errorf(w, r, "%s", err)
return
}
@@ -70,7 +71,7 @@ var (
requestsProtobufTotal = metrics.NewCounter(`vl_http_requests_total{path="/insert/opentelemetry/v1/logs",format="protobuf"}`)
errorsTotal = metrics.NewCounter(`vl_http_errors_total{path="/insert/opentelemetry/v1/logs",format="protobuf"}`)
requestProtobufDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/insert/opentelemetry/v1/logs",format="protobuf"}`)
requestProtobufDuration = metrics.NewHistogram(`vl_http_request_duration_seconds{path="/insert/opentelemetry/v1/logs",format="protobuf"}`)
)
func pushProtobufRequest(data []byte, lmp insertutil.LogMessageProcessor, msgFields []string, useDefaultStreamFields bool) error {

View File

@@ -17,6 +17,7 @@ import (
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
@@ -384,7 +385,7 @@ func serveTCP(ln net.Listener, tenantID logstorage.TenantID, encoding string, us
// processStream parses a stream of syslog messages from r and ingests them into vlstorage.
func processStream(protocol string, r io.Reader, encoding string, useLocalTimestamp bool, cp *insertutil.CommonParams) error {
if err := insertutil.CanWriteData(); err != nil {
if err := vlstorage.CanWriteData(); err != nil {
return err
}

View File

@@ -101,8 +101,8 @@ func TestProcessStreamInternal_Success(t *testing.T) {
currentYear := 2023
timestampsExpected := []int64{1685794113000000000, 1685880513000000000, 1685814132345000000}
resultExpected := `{"format":"rfc3164","hostname":"abcd","app_name":"systemd","_msg":"Starting Update the local ESM caches..."}
{"priority":"165","facility_keyword":"local4","level":"notice","facility":"20","severity":"5","format":"rfc3164","hostname":"abcd","app_name":"systemd","proc_id":"345","_msg":"abc defg"}
{"priority":"123","facility_keyword":"solaris-cron","level":"error","facility":"15","severity":"3","format":"rfc5424","hostname":"mymachine.example.com","app_name":"appname","proc_id":"12345","msg_id":"ID47","exampleSDID@32473.iut":"3","exampleSDID@32473.eventSource":"Application 123 = ] 56","exampleSDID@32473.eventID":"11211","_msg":"This is a test message with structured data."}`
{"priority":"165","facility":"20","severity":"5","format":"rfc3164","hostname":"abcd","app_name":"systemd","proc_id":"345","_msg":"abc defg"}
{"priority":"123","facility":"15","severity":"3","format":"rfc5424","hostname":"mymachine.example.com","app_name":"appname","proc_id":"12345","msg_id":"ID47","exampleSDID@32473.iut":"3","exampleSDID@32473.eventSource":"Application 123 = ] 56","exampleSDID@32473.eventID":"11211","_msg":"This is a test message with structured data."}`
f(data, currentYear, timestampsExpected, resultExpected)
}

View File

@@ -742,23 +742,7 @@ Metric names are stripped from the resulting rollups. Add [keep_metric_names](#k
This function is supported by PromQL.
See also [irate](#irate), [rollup_rate](#rollup_rate) and [rate_prometheus](#rate_prometheus).
#### rate_prometheus
`rate_prometheus(series_selector[d])` {{% available_from "#" %}} is a [rollup function](#rollup-functions), which calculates the average per-second
increase rate over the given lookbehind window `d` per each time series returned from the given [series_selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering).
The resulting calculation is equivalent to `increase_prometheus(series_selector[d]) / d`.
It doesn't take into account the last sample before the given lookbehind window `d` when calculating the result in the same way as Prometheus does.
See [this article](https://medium.com/@romanhavronenko/victoriametrics-promql-compliance-d4318203f51e) for details.
Metric names are stripped from the resulting rollups. Add [keep_metric_names](#keep_metric_names) modifier in order to keep metric names.
This function is usually applied to [counters](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#counter).
See also [increase_prometheus](#increase_prometheus) and [rate](#rate).
See also [irate](#irate) and [rollup_rate](#rollup_rate).
#### rate_over_sum

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -35,10 +35,10 @@
<meta property="og:title" content="UI for VictoriaLogs">
<meta property="og:url" content="https://victoriametrics.com/products/victorialogs/">
<meta property="og:description" content="Explore your log data with VictoriaLogs UI">
<script type="module" crossorigin src="./assets/index-721xTF8u.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-V4vnRsM-.js">
<script type="module" crossorigin src="./assets/index-BaRvaPfA.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-D8IJGiEn.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
<link rel="stylesheet" crossorigin href="./assets/index-C36SC0pJ.css">
<link rel="stylesheet" crossorigin href="./assets/index-C85_NB5q.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -253,11 +253,8 @@ func processForceFlush(w http.ResponseWriter, r *http.Request) bool {
return true
}
// Storage implements insertutil.LogRowsStorage interface
type Storage struct{}
// CanWriteData returns non-nil error if it cannot write data to vlstorage
func (*Storage) CanWriteData() error {
func CanWriteData() error {
if localStorage == nil {
// The data can be always written in non-local mode.
return nil
@@ -276,7 +273,7 @@ func (*Storage) CanWriteData() error {
// MustAddRows adds lr to vlstorage
//
// It is advised to call CanWriteData() before calling MustAddRows()
func (*Storage) MustAddRows(lr *logstorage.LogRows) {
func MustAddRows(lr *logstorage.LogRows) {
if localStorage != nil {
// Store lr in the local storage.
localStorage.MustAddRows(lr)

View File

@@ -20,8 +20,9 @@ var (
"The time filter in RFC3339 format to finish the replay by. E.g. '2020-01-01T20:07:00Z'. "+
"By default, is set to the current time.")
replayRulesDelay = flag.Duration("replay.rulesDelay", time.Second,
"Delay before evaluating the next rule within the group. Is important for chained rules. "+
"Keep it equal or bigger than -remoteWrite.flushInterval. When set to >0, replay ignores group's concurrency setting.")
"Delay between rules evaluation within the group. Could be important if there are chained rules inside the group "+
"and processing need to wait for previous rule results to be persisted by remote storage before evaluating the next rule."+
"Keep it equal or bigger than -remoteWrite.flushInterval.")
replayMaxDatapoints = flag.Int("replay.maxDatapointsPerQuery", 1e3,
"Max number of data points expected in one request. It affects the max time range for every '/query_range' request during the replay. The higher the value, the less requests will be made during replay.")
replayRuleRetryAttempts = flag.Int("replay.ruleRetryAttempts", 5,

View File

@@ -39,7 +39,7 @@ func (fr *fakeReplayQuerier) QueryRange(_ context.Context, q string, from, to ti
}
func TestReplay(t *testing.T) {
f := func(from, to string, maxDP int, ruleDelay time.Duration, cfg []config.Group, qb *fakeReplayQuerier) {
f := func(from, to string, maxDP int, cfg []config.Group, qb *fakeReplayQuerier) {
t.Helper()
fromOrig, toOrig, maxDatapointsOrig := *replayFrom, *replayTo, *replayMaxDatapoints
@@ -51,7 +51,7 @@ func TestReplay(t *testing.T) {
}()
*replayRuleRetryAttempts = 1
*replayRulesDelay = ruleDelay
*replayRulesDelay = time.Millisecond
rwb := &remotewrite.DebugClient{}
*replayFrom = from
*replayTo = to
@@ -65,7 +65,7 @@ func TestReplay(t *testing.T) {
}
// one rule + one response
f("2021-01-01T12:00:00.000Z", "2021-01-01T12:02:00.000Z", 10, time.Millisecond, []config.Group{
f("2021-01-01T12:00:00.000Z", "2021-01-01T12:02:00.000Z", 10, []config.Group{
{Rules: []config.Rule{{Record: "foo", Expr: "sum(up)"}}},
}, &fakeReplayQuerier{
registry: map[string]map[string]struct{}{
@@ -74,7 +74,7 @@ func TestReplay(t *testing.T) {
})
// one rule + multiple responses
f("2021-01-01T12:00:00.000Z", "2021-01-01T12:02:30.000Z", 1, time.Millisecond, []config.Group{
f("2021-01-01T12:00:00.000Z", "2021-01-01T12:02:30.000Z", 1, []config.Group{
{Rules: []config.Rule{{Record: "foo", Expr: "sum(up)"}}},
}, &fakeReplayQuerier{
registry: map[string]map[string]struct{}{
@@ -87,7 +87,7 @@ func TestReplay(t *testing.T) {
})
// datapoints per step
f("2021-01-01T12:00:00.000Z", "2021-01-01T15:02:30.000Z", 60, time.Millisecond, []config.Group{
f("2021-01-01T12:00:00.000Z", "2021-01-01T15:02:30.000Z", 60, []config.Group{
{Interval: promutil.NewDuration(time.Minute), Rules: []config.Rule{{Record: "foo", Expr: "sum(up)"}}},
}, &fakeReplayQuerier{
registry: map[string]map[string]struct{}{
@@ -101,7 +101,7 @@ func TestReplay(t *testing.T) {
})
// multiple recording rules + multiple responses
f("2021-01-01T12:00:00.000Z", "2021-01-01T12:02:30.000Z", 1, time.Millisecond, []config.Group{
f("2021-01-01T12:00:00.000Z", "2021-01-01T12:02:30.000Z", 1, []config.Group{
{Rules: []config.Rule{{Record: "foo", Expr: "sum(up)"}}},
{Rules: []config.Rule{{Record: "bar", Expr: "max(up)"}}},
}, &fakeReplayQuerier{
@@ -120,7 +120,7 @@ func TestReplay(t *testing.T) {
})
// multiple alerting rules + multiple responses
f("2021-01-01T12:00:00.000Z", "2021-01-01T12:02:30.000Z", 1, time.Millisecond, []config.Group{
f("2021-01-01T12:00:00.000Z", "2021-01-01T12:02:30.000Z", 1, []config.Group{
{Rules: []config.Rule{{Alert: "foo", Expr: "sum(up) > 1"}}},
{Rules: []config.Rule{{Alert: "bar", Expr: "max(up) < 1"}}},
}, &fakeReplayQuerier{
@@ -137,21 +137,4 @@ func TestReplay(t *testing.T) {
},
},
})
// multiple alerting rules in one group+ multiple responses + concurrency
f("2021-01-01T12:00:00.000Z", "2021-01-01T12:02:30.000Z", 1, 0, []config.Group{
{Rules: []config.Rule{{Alert: "foo", Expr: "sum(up) > 1"}, {Alert: "bar", Expr: "max(up) < 1"}}, Concurrency: 2}}, &fakeReplayQuerier{
registry: map[string]map[string]struct{}{
"sum(up) > 1": {
"12:00:00+12:01:00": {},
"12:01:00+12:02:00": {},
"12:02:00+12:02:30": {},
},
"max(up) < 1": {
"12:00:00+12:01:00": {},
"12:01:00+12:02:00": {},
"12:02:00+12:02:30": {},
},
},
})
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"hash/fnv"
"math"
"sort"
"strings"
"sync"
@@ -336,9 +335,7 @@ func (ar *AlertingRule) execRange(ctx context.Context, start, end time.Time) ([]
var result []prompbmarshal.TimeSeries
holdAlertState := make(map[uint64]*notifier.Alert)
qFn := func(_ string) ([]datasource.Metric, error) {
logger.Warnf("`query` template isn't supported in replay mode, mocked data is used")
// mock query results to allow common used template {{ query <$expr> | first | value }}
return []datasource.Metric{{Timestamps: []int64{0}, Values: []float64{math.NaN()}}}, nil
return nil, fmt.Errorf("`query` template isn't supported in replay mode")
}
for _, s := range res.Data {
ls, as, err := ar.expandTemplates(s, qFn, time.Time{})
@@ -416,7 +413,7 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
return nil, fmt.Errorf("failed to execute query %q: %w", ar.Expr, err)
}
ar.logDebugf(ts, nil, "query returned %d series (elapsed: %s, isPartial: %t)", curState.Samples, curState.Duration, isPartialResponse(res))
ar.logDebugf(ts, nil, "query returned %d samples (elapsed: %s, isPartial: %t)", curState.Samples, curState.Duration, isPartialResponse(res))
qFn := func(query string) ([]datasource.Metric, error) {
res, _, err := ar.q.Query(ctx, query, ts)
return res.Data, err

View File

@@ -445,17 +445,11 @@ func (g *Group) Start(ctx context.Context, nts func() []notifier.Notifier, rw re
g.infof("re-started")
case <-t.C:
// calculate the real wall clock offset by stripping the monotonic clock first,
// then evalTS can be corrected when wall clock is adjusted.
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8790#issuecomment-2986541829
offset := time.Now().Round(0).Sub(evalTS.Round(0))
missed := (offset / g.Interval) - 1
missed := (time.Since(evalTS) / g.Interval) - 1
if missed < 0 {
// missed can become < 0 due to irregular delays during evaluation
// which can result in time.Since(evalTS) < g.Interval;
// or the system wall clock was changed backward
// which can result in time.Since(evalTS) < g.Interval
missed = 0
evalTS = time.Now()
}
if missed > 0 {
g.metrics.iterationMissed.Inc()
@@ -520,84 +514,36 @@ func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoi
iterations := int(end.Sub(start)/step) + 1
fmt.Printf("\nGroup %q"+
"\ninterval: \t%v"+
"\nconcurrency: \t %d"+
"\nrequests to make per rule: \t%d"+
"\nrequests to make: \t%d"+
"\nmax range per request: \t%v\n",
g.Name, g.Interval, g.Concurrency, iterations, step)
g.Name, g.Interval, iterations, step)
if g.Limit > 0 {
fmt.Printf("\nWarning: `limit: %d` param has no effect during replay.\n",
fmt.Printf("\nPlease note, `limit: %d` param has no effect during replay.\n",
g.Limit)
}
concurrency := g.Concurrency
if g.Concurrency > 1 && replayDelay > 0 {
fmt.Printf("\nWarning: group concurrency %d will be ignored since `-replay.rulesDelay` is %.3f seconds."+
" Set -replay.rulesDelay=0 to enable concurrency for replay.\n", g.Concurrency, replayDelay.Seconds())
concurrency = 1
}
if concurrency == 1 {
for _, rule := range g.Rules {
var bar *pb.ProgressBar
if !disableProgressBar {
bar = pb.StartNew(iterations)
}
// pass ri as a copy, so it can be modified within the replayRuleRange
total += replayRuleRange(rule, ri, bar, rw, replayRuleRetryAttempts)
if bar != nil {
bar.Finish()
}
// sleep to let remote storage to flush data on-disk
// so chained rules could be calculated correctly
time.Sleep(replayDelay)
for _, rule := range g.Rules {
fmt.Printf("> Rule %q (ID: %d)\n", rule, rule.ID())
var bar *pb.ProgressBar
if !disableProgressBar {
bar = pb.StartNew(iterations)
}
return total
}
sem := make(chan struct{}, g.Concurrency)
res := make(chan int, len(g.Rules)*iterations)
wg := sync.WaitGroup{}
var bar *pb.ProgressBar
if !disableProgressBar {
bar = pb.StartNew(iterations * len(g.Rules))
}
for _, r := range g.Rules {
sem <- struct{}{}
wg.Add(1)
go func(r Rule, ri rangeIterator) {
// pass ri as a copy, so it can be modified within the replayRuleRange
res <- replayRuleRange(r, ri, bar, rw, replayRuleRetryAttempts)
<-sem
wg.Done()
}(r, ri)
}
wg.Wait()
close(res)
close(sem)
if bar != nil {
bar.Finish()
}
total = 0
for n := range res {
total += n
}
return total
}
func replayRuleRange(r Rule, ri rangeIterator, bar *pb.ProgressBar, rw remotewrite.RWClient, replayRuleRetryAttempts int) int {
fmt.Printf("> Rule %q (ID: %d)\n", r, r.ID())
total := 0
for ri.next() {
n, err := replayRule(r, ri.s, ri.e, rw, replayRuleRetryAttempts)
if err != nil {
logger.Fatalf("rule %q: %s", r, err)
ri.reset()
for ri.next() {
n, err := replayRule(rule, ri.s, ri.e, rw, replayRuleRetryAttempts)
if err != nil {
logger.Fatalf("rule %q: %s", rule, err)
}
total += n
if bar != nil {
bar.Increment()
}
}
if bar != nil {
bar.Increment()
bar.Finish()
}
total += n
// sleep to let remote storage to flush data on-disk
// so chained rules could be calculated correctly
time.Sleep(replayDelay)
}
return total
}
@@ -624,10 +570,11 @@ type rangeIterator struct {
s, e time.Time
}
// next iterates with given step between start and end
// by modifying iter, s and e.
// Returns true until it reaches end.
// next modifies ri and isn't thread-safe.
func (ri *rangeIterator) reset() {
ri.iter = 0
ri.s, ri.e = time.Time{}, time.Time{}
}
func (ri *rangeIterator) next() bool {
ri.s = ri.start.Add(ri.step * time.Duration(ri.iter))
if !ri.end.After(ri.s) {

View File

@@ -162,7 +162,7 @@
<thead>
<tr>
<th scope="col" style="width: 60%">Rule</th>
<th scope="col" style="width: 20%" class="text-center" title="How many series were produced by the rule">Series</th>
<th scope="col" style="width: 20%" class="text-center" title="How many samples were produced by the rule">Samples</th>
<th scope="col" style="width: 20%" class="text-center" title="How many seconds ago rule was executed">Updated</th>
</tr>
</thead>
@@ -594,7 +594,7 @@
<thead>
<tr>
<th scope="col" title="The time when event was created">Updated at</th>
<th scope="col" style="width: 10%" class="text-center" title="How many series expression returns. Each series will represent an alert.">Series returned</th>
<th scope="col" style="width: 10%" class="text-center" title="How many samples were returned">Samples</th>
{% if seriesFetchedEnabled %}<th scope="col" style="width: 10%" class="text-center" title="How many series were scanned by datasource during the evaluation">Series fetched</th>{% endif %}
<th scope="col" style="width: 10%" class="text-center" title="How many seconds request took">Duration</th>
<th scope="col" class="text-center" title="Time used for rule execution">Executed at</th>

View File

@@ -524,7 +524,7 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []apiGr
<thead>
<tr>
<th scope="col" style="width: 60%">Rule</th>
<th scope="col" style="width: 20%" class="text-center" title="How many series were produced by the rule">Series</th>
<th scope="col" style="width: 20%" class="text-center" title="How many samples were produced by the rule">Samples</th>
<th scope="col" style="width: 20%" class="text-center" title="How many seconds ago rule was executed">Updated</th>
</tr>
</thead>
@@ -1697,7 +1697,7 @@ func StreamRuleDetails(qw422016 *qt422016.Writer, r *http.Request, rule apiRule)
<thead>
<tr>
<th scope="col" title="The time when event was created">Updated at</th>
<th scope="col" style="width: 10%" class="text-center" title="How many series expression returns. Each series will represent an alert.">Series returned</th>
<th scope="col" style="width: 10%" class="text-center" title="How many samples were returned">Samples</th>
`)
//line app/vmalert/web.qtpl:598
if seriesFetchedEnabled {

View File

@@ -0,0 +1,351 @@
package main
import (
"context"
"testing"
"time"
"github.com/prometheus/prometheus/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/remoteread"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/stepper"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/testdata/servers_integration_test"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
)
func TestRemoteRead(t *testing.T) {
barpool.Disable(true)
defer func() {
barpool.Disable(false)
}()
defer func() { isSilent = false }()
var testCases = []struct {
name string
remoteReadConfig remoteread.Config
vmCfg vm.Config
start string
end string
numOfSamples int64
numOfSeries int64
rrp remoteReadProcessor
chunk string
remoteReadSeries func(start, end, numOfSeries, numOfSamples int64) []*prompb.TimeSeries
expectedSeries []vm.TimeSeries
}{
{
name: "step minute on minute time range",
remoteReadConfig: remoteread.Config{Addr: "", LabelName: "__name__", LabelValue: ".*"},
vmCfg: vm.Config{Addr: "", Concurrency: 1},
start: "2022-11-26T11:23:05+02:00",
end: "2022-11-26T11:24:05+02:00",
numOfSamples: 2,
numOfSeries: 3,
chunk: stepper.StepMinute,
remoteReadSeries: remote_read_integration.GenerateRemoteReadSeries,
expectedSeries: []vm.TimeSeries{
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1669454585000, 1669454615000},
Values: []float64{0, 0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1669454585000, 1669454615000},
Values: []float64{100, 100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1669454585000, 1669454615000},
Values: []float64{200, 200},
},
},
},
{
name: "step month on month time range",
remoteReadConfig: remoteread.Config{Addr: "", LabelName: "__name__", LabelValue: ".*"},
vmCfg: vm.Config{
Addr: "",
Concurrency: 1,
Transport: httputil.NewTransport(false, "vmctl_test_read"),
},
start: "2022-09-26T11:23:05+02:00",
end: "2022-11-26T11:24:05+02:00",
numOfSamples: 2,
numOfSeries: 3,
chunk: stepper.StepMonth,
remoteReadSeries: remote_read_integration.GenerateRemoteReadSeries,
expectedSeries: []vm.TimeSeries{
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1664184185000},
Values: []float64{0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1664184185000},
Values: []float64{100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1664184185000},
Values: []float64{200},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1666819415000},
Values: []float64{0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1666819415000},
Values: []float64{100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1666819415000},
Values: []float64{200}},
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
remoteReadServer := remote_read_integration.NewRemoteReadServer(t)
defer remoteReadServer.Close()
remoteWriteServer := remote_read_integration.NewRemoteWriteServer(t)
defer remoteWriteServer.Close()
tt.remoteReadConfig.Addr = remoteReadServer.URL()
rr, err := remoteread.NewClient(tt.remoteReadConfig)
if err != nil {
t.Fatalf("error create remote read client: %s", err)
}
start, err := time.Parse(time.RFC3339, tt.start)
if err != nil {
t.Fatalf("Error parse start time: %s", err)
}
end, err := time.Parse(time.RFC3339, tt.end)
if err != nil {
t.Fatalf("Error parse end time: %s", err)
}
rrs := tt.remoteReadSeries(start.Unix(), end.Unix(), tt.numOfSeries, tt.numOfSamples)
remoteReadServer.SetRemoteReadSeries(rrs)
remoteWriteServer.ExpectedSeries(tt.expectedSeries)
tt.vmCfg.Addr = remoteWriteServer.URL()
b, err := backoff.New(10, 1.8, time.Second*2)
if err != nil {
t.Fatalf("failed to create backoff: %s", err)
}
tt.vmCfg.Backoff = b
importer, err := vm.NewImporter(ctx, tt.vmCfg)
if err != nil {
t.Fatalf("failed to create VM importer: %s", err)
}
defer importer.Close()
rmp := remoteReadProcessor{
src: rr,
dst: importer,
filter: remoteReadFilter{
timeStart: &start,
timeEnd: &end,
chunk: tt.chunk,
},
cc: 1,
isVerbose: false,
}
err = rmp.run(ctx)
if err != nil {
t.Fatalf("failed to run remote read processor: %s", err)
}
})
}
}
func TestSteamRemoteRead(t *testing.T) {
barpool.Disable(true)
defer func() {
barpool.Disable(false)
}()
defer func() { isSilent = false }()
var testCases = []struct {
name string
remoteReadConfig remoteread.Config
vmCfg vm.Config
start string
end string
numOfSamples int64
numOfSeries int64
rrp remoteReadProcessor
chunk string
remoteReadSeries func(start, end, numOfSeries, numOfSamples int64) []*prompb.TimeSeries
expectedSeries []vm.TimeSeries
}{
{
name: "step minute on minute time range",
remoteReadConfig: remoteread.Config{Addr: "", LabelName: "__name__", LabelValue: ".*", UseStream: true},
vmCfg: vm.Config{Addr: "", Concurrency: 1},
start: "2022-11-26T11:23:05+02:00",
end: "2022-11-26T11:24:05+02:00",
numOfSamples: 2,
numOfSeries: 3,
chunk: stepper.StepMinute,
remoteReadSeries: remote_read_integration.GenerateRemoteReadSeries,
expectedSeries: []vm.TimeSeries{
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1669454585000, 1669454615000},
Values: []float64{0, 0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1669454585000, 1669454615000},
Values: []float64{100, 100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1669454585000, 1669454615000},
Values: []float64{200, 200},
},
},
},
{
name: "step month on month time range",
remoteReadConfig: remoteread.Config{Addr: "", LabelName: "__name__", LabelValue: ".*", UseStream: true},
vmCfg: vm.Config{Addr: "", Concurrency: 1},
start: "2022-09-26T11:23:05+02:00",
end: "2022-11-26T11:24:05+02:00",
numOfSamples: 2,
numOfSeries: 3,
chunk: stepper.StepMonth,
remoteReadSeries: remote_read_integration.GenerateRemoteReadSeries,
expectedSeries: []vm.TimeSeries{
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1664184185000},
Values: []float64{0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1664184185000},
Values: []float64{100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1664184185000},
Values: []float64{200},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1666819415000},
Values: []float64{0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1666819415000},
Values: []float64{100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1666819415000},
Values: []float64{200}},
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
remoteReadServer := remote_read_integration.NewRemoteReadStreamServer(t)
defer remoteReadServer.Close()
remoteWriteServer := remote_read_integration.NewRemoteWriteServer(t)
defer remoteWriteServer.Close()
tt.remoteReadConfig.Addr = remoteReadServer.URL()
rr, err := remoteread.NewClient(tt.remoteReadConfig)
if err != nil {
t.Fatalf("error create remote read client: %s", err)
}
start, err := time.Parse(time.RFC3339, tt.start)
if err != nil {
t.Fatalf("Error parse start time: %s", err)
}
end, err := time.Parse(time.RFC3339, tt.end)
if err != nil {
t.Fatalf("Error parse end time: %s", err)
}
rrs := tt.remoteReadSeries(start.Unix(), end.Unix(), tt.numOfSeries, tt.numOfSamples)
remoteReadServer.InitMockStorage(rrs)
remoteWriteServer.ExpectedSeries(tt.expectedSeries)
tt.vmCfg.Addr = remoteWriteServer.URL()
b, err := backoff.New(10, 1.8, time.Second*2)
if err != nil {
t.Fatalf("failed to create backoff: %s", err)
}
tt.vmCfg.Backoff = b
importer, err := vm.NewImporter(ctx, tt.vmCfg)
if err != nil {
t.Fatalf("failed to create VM importer: %s", err)
}
defer importer.Close()
rmp := remoteReadProcessor{
src: rr,
dst: importer,
filter: remoteReadFilter{
timeStart: &start,
timeEnd: &end,
chunk: tt.chunk,
},
cc: 1,
isVerbose: false,
}
err = rmp.run(ctx)
if err != nil {
t.Fatalf("failed to run remote read processor: %s", err)
}
})
}
}

View File

@@ -1,4 +1,4 @@
package tests
package remote_read_integration
import (
"context"
@@ -12,29 +12,32 @@ import (
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/storage/remote"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/prometheus/prometheus/tsdb/chunks"
"github.com/prometheus/prometheus/util/annotations"
)
const (
maxBytesInFrame = 1024 * 1024
)
// RemoteReadServer is a mock server that implements the Prometheus remote read protocol.
type RemoteReadServer struct {
server *httptest.Server
storage *PrometheusMockStorage
series []*prompb.TimeSeries
storage *MockStorage
}
// NewRemoteReadServer creates a remote read server. It exposes a single endpoint and responds with the
// passed series based on the request to the read endpoint. It returns a server which should be closed after
// being used.
func NewRemoteReadServer(t *testing.T, series []*prompb.TimeSeries) *RemoteReadServer {
mockStorage := NewPrometheusMockStorage(series)
func NewRemoteReadServer(t *testing.T) *RemoteReadServer {
rrs := &RemoteReadServer{
storage: mockStorage,
series: make([]*prompb.TimeSeries, 0),
}
rrs.server = httptest.NewServer(rrs.getReadHandler(t))
return rrs
@@ -45,11 +48,14 @@ func (rrs *RemoteReadServer) Close() {
rrs.server.Close()
}
// HTTPAddr returns the HTTP address of the server.
func (rrs *RemoteReadServer) HTTPAddr() string {
func (rrs *RemoteReadServer) URL() string {
return rrs.server.URL
}
func (rrs *RemoteReadServer) SetRemoteReadSeries(series []*prompb.TimeSeries) {
rrs.series = append(rrs.series, series...)
}
func (rrs *RemoteReadServer) getReadHandler(t *testing.T) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !validateReadHeaders(t, r) {
@@ -78,8 +84,8 @@ func (rrs *RemoteReadServer) getReadHandler(t *testing.T) http.Handler {
for i, r := range req.Queries {
startTs := r.StartTimestampMs
endTs := r.EndTimestampMs
ts := make([]*prompb.TimeSeries, len(rrs.storage.store))
for i, s := range rrs.storage.store {
ts := make([]*prompb.TimeSeries, len(rrs.series))
for i, s := range rrs.series {
var samples []prompb.Sample
for _, sample := range s.Samples {
if sample.Timestamp >= startTs && sample.Timestamp < endTs {
@@ -113,18 +119,18 @@ func (rrs *RemoteReadServer) getReadHandler(t *testing.T) http.Handler {
})
}
// NewRemoteReadStreamServer creates a remote read server that supports streaming responses.
// passed series based on the request to the read endpoint. It returns a server which should be closed after
// being used.
func NewRemoteReadStreamServer(t *testing.T, series []*prompb.TimeSeries) *RemoteReadServer {
mockStorage := NewPrometheusMockStorage(series)
func NewRemoteReadStreamServer(t *testing.T) *RemoteReadServer {
rrs := &RemoteReadServer{
storage: mockStorage,
series: make([]*prompb.TimeSeries, 0),
}
rrs.server = httptest.NewServer(rrs.getStreamReadHandler(t))
return rrs
}
func (rrs *RemoteReadServer) InitMockStorage(series []*prompb.TimeSeries) {
rrs.storage = NewMockStorage(series)
}
func (rrs *RemoteReadServer) getStreamReadHandler(t *testing.T) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !validateStreamReadHeaders(t, r) {
@@ -174,10 +180,10 @@ func (rrs *RemoteReadServer) getStreamReadHandler(t *testing.T) http.Handler {
for ss.Next() {
series := ss.At()
iter = series.Iterator(iter)
lbls := remote.MergeLabels(labelsToLabelsProto(series.Labels()), nil)
labels := remote.MergeLabels(labelsToLabelsProto(series.Labels()), nil)
frameBytesLeft := maxBytesInFrame
for _, lb := range lbls {
for _, lb := range labels {
frameBytesLeft -= lb.Size()
}
@@ -207,7 +213,7 @@ func (rrs *RemoteReadServer) getStreamReadHandler(t *testing.T) http.Handler {
resp := &prompb.ChunkedReadResponse{
ChunkedSeries: []*prompb.ChunkedSeries{
{Labels: lbls, Chunks: chks},
{Labels: labels, Chunks: chks},
},
QueryIndex: int64(idx),
}
@@ -274,7 +280,6 @@ func validateStreamReadHeaders(t *testing.T, r *http.Request) bool {
return true
}
// GenerateRemoteReadSeries generates a set of remote read series with the given parameters.
func GenerateRemoteReadSeries(start, end, numOfSeries, numOfSamples int64) []*prompb.TimeSeries {
var ts []*prompb.TimeSeries
j := 0
@@ -317,6 +322,141 @@ func generateRemoteReadSamples(idx int, startTime, endTime, numOfSamples int64)
return samples
}
type MockStorage struct {
query *prompb.Query
store []*prompb.TimeSeries
}
func NewMockStorage(series []*prompb.TimeSeries) *MockStorage {
return &MockStorage{store: series}
}
func (ms *MockStorage) Read(_ context.Context, query *prompb.Query, sortSeries bool) (storage.SeriesSet, error) {
if sortSeries {
return nil, fmt.Errorf("unexpected sortSeries=true")
}
if ms.query != nil {
return nil, fmt.Errorf("expected only one call to remote client got: %v", query)
}
ms.query = query
tss := make([]*prompb.TimeSeries, 0, len(ms.store))
for _, s := range ms.store {
var samples []prompb.Sample
for _, sample := range s.Samples {
if sample.Timestamp >= query.StartTimestampMs && sample.Timestamp < query.EndTimestampMs {
samples = append(samples, sample)
}
}
var series prompb.TimeSeries
if len(samples) > 0 {
series.Labels = s.Labels
series.Samples = samples
}
tss = append(tss, &series)
}
return &mockSeriesSet{
tss: tss,
}, nil
}
func (ms *MockStorage) Reset() {
ms.query = nil
}
type mockSeriesSet struct {
tss []*prompb.TimeSeries
next int
}
func (ss *mockSeriesSet) Next() bool {
if ss.next >= len(ss.tss) {
return false
}
ss.next++
return true
}
func (ss *mockSeriesSet) At() storage.Series {
return &mockSeries{
s: ss.tss[ss.next-1],
}
}
func (ss *mockSeriesSet) Err() error {
return nil
}
func (ss *mockSeriesSet) Warnings() annotations.Annotations {
return nil
}
type mockSeries struct {
s *prompb.TimeSeries
}
func (s *mockSeries) Labels() labels.Labels {
a := make(labels.Labels, len(s.s.Labels))
for i, label := range s.s.Labels {
a[i] = labels.Label{
Name: label.Name,
Value: label.Value,
}
}
return a
}
func (s *mockSeries) Iterator(chunkenc.Iterator) chunkenc.Iterator {
return &mockSamplesIterator{
samples: s.s.Samples,
}
}
type mockSamplesIterator struct {
samples []prompb.Sample
next int
}
func (si *mockSamplesIterator) Next() chunkenc.ValueType {
if si.next >= len(si.samples) {
return chunkenc.ValNone
}
si.next++
return chunkenc.ValFloat
}
func (si *mockSamplesIterator) Seek(t int64) chunkenc.ValueType {
for i := range si.samples {
if si.samples[i].Timestamp >= t {
si.next = i + 1
return chunkenc.ValFloat
}
}
return chunkenc.ValNone
}
func (si *mockSamplesIterator) At() (int64, float64) {
s := si.samples[si.next-1]
return s.Timestamp, s.Value
}
func (si *mockSamplesIterator) AtHistogram(*histogram.Histogram) (int64, *histogram.Histogram) {
panic("BUG: mustn't be called")
}
func (si *mockSamplesIterator) AtFloatHistogram(*histogram.FloatHistogram) (int64, *histogram.FloatHistogram) {
panic("BUG: mustn't be called")
}
func (si *mockSamplesIterator) AtT() int64 {
return si.samples[si.next-1].Timestamp
}
func (si *mockSamplesIterator) Err() error {
return nil
}
func labelsToLabelsProto(labels labels.Labels) []prompb.Label {
result := make([]prompb.Label, 0, len(labels))
for _, l := range labels {

View File

@@ -0,0 +1,306 @@
package remote_read_integration
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
"reflect"
"sort"
"strconv"
"sync"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/prometheus"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/native/stream"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
parser "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/vmimport"
)
// LabelValues represents series from api/v1/series response
type LabelValues map[string]string
// Response represents response from api/v1/series
type Response struct {
Status string `json:"status"`
Series []LabelValues `json:"data"`
}
type MetricNamesResponse struct {
Status string `json:"status"`
Data []string `json:"data"`
}
// RemoteWriteServer represents fake remote write server with database
type RemoteWriteServer struct {
server *httptest.Server
series []vm.TimeSeries
expectedSeries []vm.TimeSeries
tss []vm.TimeSeries
}
// NewRemoteWriteServer prepares test remote write server
func NewRemoteWriteServer(t *testing.T) *RemoteWriteServer {
rws := &RemoteWriteServer{series: make([]vm.TimeSeries, 0)}
mux := http.NewServeMux()
mux.Handle("/api/v1/import", rws.getWriteHandler(t))
mux.Handle("/health", rws.handlePing())
mux.Handle("/api/v1/series", rws.seriesHandler())
mux.Handle("/api/v1/label/__name__/values", rws.valuesHandler())
mux.Handle("/api/v1/export/native", rws.exportNativeHandler())
mux.Handle("/api/v1/import/native", rws.importNativeHandler(t))
rws.server = httptest.NewServer(mux)
return rws
}
// Close closes the server
func (rws *RemoteWriteServer) Close() {
rws.server.Close()
}
// Series saves generated series for fake database
func (rws *RemoteWriteServer) Series(series []vm.TimeSeries) {
rws.series = append(rws.series, series...)
}
// ExpectedSeries saves expected results to check in the handler
func (rws *RemoteWriteServer) ExpectedSeries(series []vm.TimeSeries) {
rws.expectedSeries = append(rws.expectedSeries, series...)
}
func (rws *RemoteWriteServer) GetCollectedTimeSeries() []vm.TimeSeries {
return rws.tss
}
// URL returns server url
func (rws *RemoteWriteServer) URL() string {
return rws.server.URL
}
func (rws *RemoteWriteServer) getWriteHandler(t *testing.T) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
scanner := bufio.NewScanner(r.Body)
var rows parser.Rows
for scanner.Scan() {
rows.Unmarshal(scanner.Text())
for _, row := range rows.Rows {
var labelPairs []vm.LabelPair
var ts vm.TimeSeries
nameValue := ""
for _, tag := range row.Tags {
if string(tag.Key) == "__name__" {
nameValue = string(tag.Value)
continue
}
labelPairs = append(labelPairs, vm.LabelPair{Name: string(tag.Key), Value: string(tag.Value)})
}
ts.Values = append(ts.Values, row.Values...)
ts.Timestamps = append(ts.Timestamps, row.Timestamps...)
ts.Name = nameValue
ts.LabelPairs = labelPairs
rws.tss = append(rws.tss, ts)
}
rows.Reset()
}
w.WriteHeader(http.StatusNoContent)
return
})
}
func (rws *RemoteWriteServer) handlePing() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})
}
func (rws *RemoteWriteServer) seriesHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var labelValues []LabelValues
for _, ser := range rws.series {
metricNames := make(LabelValues)
if ser.Name != "" {
metricNames["__name__"] = ser.Name
}
for _, p := range ser.LabelPairs {
metricNames[p.Name] = p.Value
}
labelValues = append(labelValues, metricNames)
}
resp := Response{
Status: "success",
Series: labelValues,
}
err := json.NewEncoder(w).Encode(resp)
if err != nil {
log.Printf("error send series: %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
})
}
func (rws *RemoteWriteServer) valuesHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
labelNames := make(map[string]struct{})
for _, ser := range rws.series {
if ser.Name != "" {
labelNames[ser.Name] = struct{}{}
}
}
metricNames := make([]string, 0, len(labelNames))
for k := range labelNames {
metricNames = append(metricNames, k)
}
resp := MetricNamesResponse{
Status: "success",
Data: metricNames,
}
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(resp)
if err != nil {
log.Printf("error send series: %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
_, err = w.Write(buf.Bytes())
if err != nil {
log.Printf("error send series: %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
return
})
}
func (rws *RemoteWriteServer) exportNativeHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
err := prometheus.ExportNativeHandler(now, w, r)
if err != nil {
log.Printf("error export series via native protocol: %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
return
})
}
func (rws *RemoteWriteServer) importNativeHandler(t *testing.T) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
protoparserutil.StartUnmarshalWorkers()
defer protoparserutil.StopUnmarshalWorkers()
var gotTimeSeries []vm.TimeSeries
var mx sync.RWMutex
err := stream.Parse(r.Body, "", func(block *stream.Block) error {
mn := &block.MetricName
var timeseries vm.TimeSeries
timeseries.Name = string(mn.MetricGroup)
timeseries.Timestamps = append(timeseries.Timestamps, block.Timestamps...)
timeseries.Values = append(timeseries.Values, block.Values...)
for i := range mn.Tags {
tag := &mn.Tags[i]
timeseries.LabelPairs = append(timeseries.LabelPairs, vm.LabelPair{
Name: string(tag.Key),
Value: string(tag.Value),
})
}
mx.Lock()
gotTimeSeries = append(gotTimeSeries, timeseries)
mx.Unlock()
return nil
})
if err != nil {
log.Printf("error parse stream blocks: %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// got timeseries should be sorted
// because they are processed independently
sort.SliceStable(gotTimeSeries, func(i, j int) bool {
iv, jv := gotTimeSeries[i], gotTimeSeries[j]
switch {
case iv.Values[0] != jv.Values[0]:
return iv.Values[0] < jv.Values[0]
case iv.Timestamps[0] != jv.Timestamps[0]:
return iv.Timestamps[0] < jv.Timestamps[0]
default:
return iv.Name < jv.Name
}
})
if !reflect.DeepEqual(gotTimeSeries, rws.expectedSeries) {
w.WriteHeader(http.StatusInternalServerError)
t.Fatalf("datasets not equal, expected: %#v;\n got: %#v", rws.expectedSeries, gotTimeSeries)
}
w.WriteHeader(http.StatusNoContent)
return
})
}
// GenerateVNSeries generates test timeseries
func GenerateVNSeries(start, end, numOfSeries, numOfSamples int64) []vm.TimeSeries {
var ts []vm.TimeSeries
j := 0
for i := 0; i < int(numOfSeries); i++ {
if i%3 == 0 {
j++
}
timeSeries := vm.TimeSeries{
Name: fmt.Sprintf("vm_metric_%d", j),
LabelPairs: []vm.LabelPair{
{Name: "job", Value: strconv.Itoa(i)},
},
}
ts = append(ts, timeSeries)
}
for i := range ts {
t, v := generateTimeStampsAndValues(i, start, end, numOfSamples)
ts[i].Timestamps = t
ts[i].Values = v
}
return ts
}
func generateTimeStampsAndValues(idx int, startTime, endTime, numOfSamples int64) ([]int64, []float64) {
delta := (endTime - startTime) / numOfSamples
var timestamps []int64
var values []float64
t := startTime
for t != endTime {
v := 100 * int64(idx)
timestamps = append(timestamps, t*1000)
values = append(values, float64(v))
t = t + delta
}
return timestamps, values
}

View File

@@ -1,9 +1,268 @@
package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/native"
remote_read_integration "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/testdata/servers_integration_test"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
const (
storagePath = "TestStorage"
retentionPeriod = "100y"
deleteSeriesLimit = 3e3
)
func TestVMNativeProcessorRun(t *testing.T) {
f := func(startStr, endStr string, numOfSeries, numOfSamples int, resultExpected []vm.TimeSeries) {
t.Helper()
src := remote_read_integration.NewRemoteWriteServer(t)
dst := remote_read_integration.NewRemoteWriteServer(t)
defer func() {
src.Close()
dst.Close()
}()
start, err := time.Parse(time.RFC3339, startStr)
if err != nil {
t.Fatalf("cannot parse start time: %s", err)
}
end, err := time.Parse(time.RFC3339, endStr)
if err != nil {
t.Fatalf("cannot parse end time: %s", err)
}
matchName := "__name__"
matchValue := ".*"
filter := native.Filter{
Match: fmt.Sprintf("{%s=~%q}", matchName, matchValue),
TimeStart: startStr,
TimeEnd: endStr,
}
rws := remote_read_integration.GenerateVNSeries(start.Unix(), end.Unix(), int64(numOfSeries), int64(numOfSamples))
src.Series(rws)
dst.ExpectedSeries(resultExpected)
if err := fillStorage(rws); err != nil {
t.Fatalf("cannot add series to storage: %s", err)
}
tr := httputil.NewTransport(false, "test_client")
tr.DisableKeepAlives = false
srcClient := &native.Client{
AuthCfg: nil,
Addr: src.URL(),
ExtraLabels: []string{},
HTTPClient: &http.Client{
Transport: tr,
},
}
dstClient := &native.Client{
AuthCfg: nil,
Addr: dst.URL(),
ExtraLabels: []string{},
HTTPClient: &http.Client{
Transport: tr,
},
}
isSilent = true
defer func() { isSilent = false }()
bf, err := backoff.New(10, 1.8, time.Second*2)
if err != nil {
t.Fatalf("cannot create backoff: %s", err)
}
p := &vmNativeProcessor{
filter: filter,
dst: dstClient,
src: srcClient,
backoff: bf,
cc: 1,
isNative: true,
}
ctx := context.Background()
if err := p.run(ctx); err != nil {
t.Fatalf("run() error: %s", err)
}
deleted, err := deleteSeries(matchName, matchValue)
if err != nil {
t.Fatalf("cannot delete series: %s", err)
}
if deleted != numOfSeries {
t.Fatalf("unexpected number of deleted series; got %d; want %d", deleted, numOfSeries)
}
}
processFlags()
vmstorage.Init(promql.ResetRollupResultCacheIfNeeded)
defer func() {
vmstorage.Stop()
if err := os.RemoveAll(storagePath); err != nil {
log.Fatalf("cannot remove %q: %s", storagePath, err)
}
}()
barpool.Disable(true)
defer func() {
barpool.Disable(false)
}()
// step minute on minute time range
start := "2022-11-25T11:23:05+02:00"
end := "2022-11-27T11:24:05+02:00"
numOfSeries := 3
numOfSamples := 2
resultExpected := []vm.TimeSeries{
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1669368185000, 1669454615000},
Values: []float64{0, 0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1669368185000, 1669454615000},
Values: []float64{100, 100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1669368185000, 1669454615000},
Values: []float64{200, 200},
},
}
f(start, end, numOfSeries, numOfSamples, resultExpected)
// step month on month time range
start = "2022-09-26T11:23:05+02:00"
end = "2022-11-26T11:24:05+02:00"
numOfSeries = 3
numOfSamples = 2
resultExpected = []vm.TimeSeries{
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1664184185000},
Values: []float64{0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1666819415000},
Values: []float64{0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1664184185000},
Values: []float64{100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1666819415000},
Values: []float64{100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1664184185000},
Values: []float64{200},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1666819415000},
Values: []float64{200},
},
}
f(start, end, numOfSeries, numOfSamples, resultExpected)
}
func processFlags() {
flag.Parse()
for _, fv := range []struct {
flag string
value string
}{
{flag: "storageDataPath", value: storagePath},
{flag: "retentionPeriod", value: retentionPeriod},
} {
// panics if flag doesn't exist
if err := flag.Lookup(fv.flag).Value.Set(fv.value); err != nil {
log.Fatalf("unable to set %q with value %q, err: %v", fv.flag, fv.value, err)
}
}
}
func fillStorage(series []vm.TimeSeries) error {
var mrs []storage.MetricRow
for _, series := range series {
var labels []prompbmarshal.Label
for _, lp := range series.LabelPairs {
labels = append(labels, prompbmarshal.Label{
Name: lp.Name,
Value: lp.Value,
})
}
if series.Name != "" {
labels = append(labels, prompbmarshal.Label{
Name: "__name__",
Value: series.Name,
})
}
mr := storage.MetricRow{}
mr.MetricNameRaw = storage.MarshalMetricNameRaw(mr.MetricNameRaw[:0], labels)
timestamps := series.Timestamps
values := series.Values
for i, value := range values {
mr.Timestamp = timestamps[i]
mr.Value = value
mrs = append(mrs, mr)
}
}
if err := vmstorage.AddRows(mrs); err != nil {
return fmt.Errorf("unexpected error in AddRows: %s", err)
}
vmstorage.Storage.DebugFlush()
return nil
}
func deleteSeries(name, value string) (int, error) {
tfs := storage.NewTagFilters()
if err := tfs.Add([]byte(name), []byte(value), false, true); err != nil {
return 0, fmt.Errorf("unexpected error in TagFilters.Add: %w", err)
}
return vmstorage.DeleteSeries(nil, []*storage.TagFilters{tfs}, deleteSeriesLimit)
}
func TestBuildMatchWithFilter_Failure(t *testing.T) {
f := func(filter, metricName string) {
t.Helper()

View File

@@ -818,7 +818,6 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
LookbackDelta: lookbackDelta,
RoundDigits: getRoundDigits(r),
EnforcedTagFilterss: etfs,
CacheTagFilters: etfs,
GetRequestURI: func() string {
return httpserver.GetRequestURI(r)
},
@@ -928,7 +927,6 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
LookbackDelta: lookbackDelta,
RoundDigits: getRoundDigits(r),
EnforcedTagFilterss: etfs,
CacheTagFilters: etfs,
GetRequestURI: func() string {
return httpserver.GetRequestURI(r)
},

View File

@@ -140,13 +140,6 @@ type EvalConfig struct {
// EnforcedTagFilterss may contain additional label filters to use in the query.
EnforcedTagFilterss [][]storage.TagFilter
// CacheTagFilters stores the original tag-filter sets and extra_label from the request.
// The slice is never modified after creation and is used only to build
// the query-cache key.
//
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9001
CacheTagFilters [][]storage.TagFilter
// The callback, which returns the request URI during logging.
// The request URI isn't stored here because its' construction may take non-trivial amounts of CPU.
GetRequestURI func() string
@@ -173,7 +166,6 @@ func copyEvalConfig(src *EvalConfig) *EvalConfig {
ec.LookbackDelta = src.LookbackDelta
ec.RoundDigits = src.RoundDigits
ec.EnforcedTagFilterss = src.EnforcedTagFilterss
ec.CacheTagFilters = src.CacheTagFilters
ec.GetRequestURI = src.GetRequestURI
ec.QueryStats = src.QueryStats
@@ -1974,14 +1966,11 @@ func sumNoOverflow(a, b int64) int64 {
}
func dropStaleNaNs(funcName string, values []float64, timestamps []int64) ([]float64, []int64) {
if *noStaleMarkers || funcName == "stale_samples_over_time" ||
funcName == "default_rollup" || funcName == "increase" || funcName == "rate" {
if *noStaleMarkers || funcName == "default_rollup" || funcName == "stale_samples_over_time" {
// Do not drop Prometheus staleness marks (aka stale NaNs) for default_rollup() function,
// since it uses them for Prometheus-style staleness detection.
// Do not drop staleness marks for stale_samples_over_time() function, since it needs
// to calculate the number of staleness markers.
// Do not drop staleness marks for increase() and rate() function, so they could stop
// returning results for stale series. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8891
return values, timestamps
}
// Remove Prometheus staleness marks, so non-default rollup functions don't hit NaN values.

View File

@@ -71,8 +71,7 @@ var rollupFuncs = map[string]newRollupFunc{
"quantile_over_time": newRollupQuantile,
"quantiles_over_time": newRollupQuantiles,
"range_over_time": newRollupFuncOneArg(rollupRange),
"rate": newRollupFuncOneArg(rollupDerivFast), // + rollupFuncsRemoveCounterResets
"rate_prometheus": newRollupFuncOneArg(rollupDerivFastPrometheus), // + rollupFuncsRemoveCounterResets
"rate": newRollupFuncOneArg(rollupDerivFast), // + rollupFuncsRemoveCounterResets
"rate_over_sum": newRollupFuncOneArg(rollupRateOverSum),
"resets": newRollupFuncOneArg(rollupResets),
"rollup": newRollupFuncOneOrTwoArgs(rollupFake),
@@ -196,7 +195,7 @@ var rollupAggrFuncs = map[string]rollupFunc{
"zscore_over_time": rollupZScoreOverTime,
}
// VictoriaMetrics can extend lookbehind window for these functions
// VictoriaMetrics can extends lookbehind window for these functions
// in order to make sure it contains enough points for returning non-empty results.
//
// This is needed for returning the expected non-empty graphs when zooming in the graph in Grafana,
@@ -226,7 +225,6 @@ var rollupFuncsRemoveCounterResets = map[string]bool{
"increase_pure": true,
"irate": true,
"rate": true,
"rate_prometheus": true,
"rollup_increase": true,
"rollup_rate": true,
}
@@ -254,7 +252,6 @@ var rollupFuncsSamplesScannedPerCall = map[string]int{
"lifetime": 2,
"present_over_time": 1,
"rate": 2,
"rate_prometheus": 2,
"scrape_interval": 2,
"tfirst_over_time": 1,
"timestamp": 1,
@@ -916,18 +913,15 @@ func getMaxPrevInterval(scrapeInterval int64) int64 {
return scrapeInterval + scrapeInterval/8
}
// removeCounterResets removes resets for rollup functions over counters - see rollupFuncsRemoveCounterResets
// it doesn't remove resets between samples with staleNaNs, or samples that exceed maxStalenessInterval
func removeCounterResets(values []float64, timestamps []int64, maxStalenessInterval int64) {
// There is no need in handling NaNs here, since they are impossible
// on values from vmstorage.
if len(values) == 0 {
return
}
var correction float64
prevValue := values[0]
for i, v := range values {
if decimal.IsStaleNaN(v) {
continue
}
d := v - prevValue
if d < 0 {
if (-d * 8) < prevValue {
@@ -1859,13 +1853,8 @@ func rollupIncreasePure(rfa *rollupFuncArg) float64 {
func rollupDelta(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs. Only StaleNaNs could remain in values - see dropStaleNaNs().
// before calling rollup funcs.
values := rfa.values
if len(values) > 0 && decimal.IsStaleNaN(values[len(values)-1]) {
// if last sample on interval is staleness marker then the selected series is expected
// to stop rendering immediately. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8891
return nan
}
prevValue := rfa.prevValue
if math.IsNaN(prevValue) {
if len(values) == 0 {
@@ -1949,23 +1938,10 @@ func rollupDerivSlow(rfa *rollupFuncArg) float64 {
return k
}
func rollupDerivFastPrometheus(rfa *rollupFuncArg) float64 {
delta := rollupDeltaPrometheus(rfa)
if math.IsNaN(delta) || rfa.window == 0 {
return nan
}
return delta / (float64(rfa.window) / 1e3)
}
func rollupDerivFast(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs. Only StaleNaNs could remain in values - see - see dropStaleNaNs().
// before calling rollup funcs.
values := rfa.values
if len(values) > 0 && decimal.IsStaleNaN(values[len(values)-1]) {
// if last sample on interval is staleness marker then the selected series is expected
// to stop rendering immediately. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8891
return nan
}
timestamps := rfa.timestamps
prevValue := rfa.prevValue
prevTimestamp := rfa.prevTimestamp

View File

@@ -291,7 +291,7 @@ func (rrc *rollupResultCache) GetSeries(qt *querytracer.Tracer, ec *EvalConfig,
bb := bbPool.Get()
defer bbPool.Put(bb)
bb.B = marshalRollupResultCacheKeyForSeries(bb.B[:0], expr, window, ec.Step, ec.CacheTagFilters)
bb.B = marshalRollupResultCacheKeyForSeries(bb.B[:0], expr, window, ec.Step, ec.EnforcedTagFilterss)
metainfoBuf := rrc.c.Get(nil, bb.B)
if len(metainfoBuf) == 0 {
qt.Printf("nothing found")
@@ -313,7 +313,7 @@ func (rrc *rollupResultCache) GetSeries(qt *querytracer.Tracer, ec *EvalConfig,
if !ok {
mi.RemoveKey(key)
metainfoBuf = mi.Marshal(metainfoBuf[:0])
bb.B = marshalRollupResultCacheKeyForSeries(bb.B[:0], expr, window, ec.Step, ec.CacheTagFilters)
bb.B = marshalRollupResultCacheKeyForSeries(bb.B[:0], expr, window, ec.Step, ec.EnforcedTagFilterss)
rrc.c.Set(bb.B, metainfoBuf)
return nil, ec.Start
}
@@ -419,7 +419,7 @@ func (rrc *rollupResultCache) PutSeries(qt *querytracer.Tracer, ec *EvalConfig,
metainfoBuf := bbPool.Get()
defer bbPool.Put(metainfoBuf)
metainfoKey.B = marshalRollupResultCacheKeyForSeries(metainfoKey.B[:0], expr, window, ec.Step, ec.CacheTagFilters)
metainfoKey.B = marshalRollupResultCacheKeyForSeries(metainfoKey.B[:0], expr, window, ec.Step, ec.EnforcedTagFilterss)
metainfoBuf.B = rrc.c.Get(metainfoBuf.B[:0], metainfoKey.B)
var mi rollupResultCacheMetainfo
if len(metainfoBuf.B) > 0 {

View File

@@ -156,14 +156,6 @@ func TestRemoveCounterResets(t *testing.T) {
removeCounterResets(values, timestamps, 10)
testRowsEqual(t, values, timestamps, valuesExpected, timestamps)
// verify that staleNaNs are respected
// it is important to have counter reset in values below to trigger correction logic
values = []float64{2, 4, 2, decimal.StaleNaN}
timestamps = []int64{10, 20, 30, 40}
valuesExpected = []float64{2, 4, 6, decimal.StaleNaN}
removeCounterResets(values, timestamps, 10)
testRowsEqual(t, values, timestamps, valuesExpected, timestamps)
// verify results always increase monotonically with possible float operations precision error
values = []float64{34.094223, 2.7518, 2.140669, 0.044878, 1.887095, 2.546569, 2.490149, 0.045, 0.035684, 0.062454, 0.058296}
timestampsExpected = []int64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
@@ -656,7 +648,6 @@ func TestRollupNewRollupFuncSuccess(t *testing.T) {
f("irate", 0)
f("outlier_iqr_over_time", nan)
f("rate", 2200)
f("rate_prometheus", 2200)
f("resets", 5)
f("range_over_time", 111)
f("avg_over_time", 47.083333333333336)
@@ -1534,31 +1525,16 @@ func testRowsEqual(t *testing.T, values []float64, timestamps []int64, valuesExp
i, ts, tsExpected, timestamps, timestampsExpected)
}
vExpected := valuesExpected[i]
if decimal.IsStaleNaN(v) {
if !decimal.IsStaleNaN(vExpected) {
t.Fatalf("unexpected stale NaN value at values[%d]; want %f\nvalues=\n%v\nvaluesExpected=\n%v",
i, vExpected, values, valuesExpected)
}
continue
}
// staleNaNBits == math.NaN(), but decimal.IsStaleNaN(math.NaN()) == false
// so we check for decimal.IsStaleNaN first.
if decimal.IsStaleNaN(vExpected) {
if !decimal.IsStaleNaN(v) {
t.Fatalf("unexpected value at values[%d]; got %f; want stale NaN\nvalues=\n%v\nvaluesExpected=\n%v",
i, v, values, valuesExpected)
}
}
if math.IsNaN(v) {
if !math.IsNaN(vExpected) {
t.Fatalf("unexpected NaN value at values[%d]; want %f\nvalues=\n%v\nvaluesExpected=\n%v",
t.Fatalf("unexpected nan value at values[%d]; want %f\nvalues=\n%v\nvaluesExpected=\n%v",
i, vExpected, values, valuesExpected)
}
continue
}
if math.IsNaN(vExpected) {
if !math.IsNaN(v) {
t.Fatalf("unexpected value at values[%d]; got %f; want NaN\nvalues=\n%v\nvaluesExpected=\n%v",
t.Fatalf("unexpected value at values[%d]; got %f; want nan\nvalues=\n%v\nvaluesExpected=\n%v",
i, v, values, valuesExpected)
}
continue
@@ -1632,33 +1608,6 @@ func TestRollupDelta(t *testing.T) {
f(100, nan, nan, nil, 0)
}
func TestRollupDerivFastPrometheus(t *testing.T) {
f := func(values []float64, window int64, resultExpected float64) {
t.Helper()
rfa := &rollupFuncArg{
values: values,
window: window,
}
result := rollupDerivFastPrometheus(rfa)
if math.IsNaN(result) {
if !math.IsNaN(resultExpected) {
t.Fatalf("unexpected result; got %v; want %v", result, resultExpected)
}
return
}
if result != resultExpected {
t.Fatalf("unexpected result; got %v; want %v", result, resultExpected)
}
}
f(nil, 0, nan)
f(nil, 10, nan)
f([]float64{0, 10}, 0, nan)
f([]float64{10}, 10, nan)
f([]float64{0, 20}, 10e3, 2)
f([]float64{0, 10, 20}, 10e3, 2)
}
func TestRollupDeltaWithStaleness(t *testing.T) {
// there is a gap between samples in the dataset below
timestamps := []int64{0, 15000, 30000, 70000}
@@ -1797,28 +1746,6 @@ func TestRollupDeltaWithStaleness(t *testing.T) {
timestampsExpected := []int64{0, 30e3, 60e3, 90e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// the last sample is stale NaN
timestamps = []int64{0, 10000, 20000, 30000, 40000}
values = []float64{0, 0, 0, 10, decimal.StaleNaN}
t.Run("last point is stale nan", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDelta,
Start: 40001,
End: 40001,
Step: 50000,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 10 {
t.Fatalf("expecting 10 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{nan}
timestampsExpected := []int64{40001}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
}
func TestRollupIncreasePureWithStaleness(t *testing.T) {
@@ -1933,48 +1860,3 @@ func TestRollupIncreasePureWithStaleness(t *testing.T) {
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
}
func TestRollupDerivFastWithStaleness(t *testing.T) {
timestamps := []int64{0, 10000, 20000, 30000, 40000}
values := []float64{0, 0, 0, 0, 10}
t.Run("no stale marker", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDerivFast,
Start: 40001,
End: 40001,
Step: 50000,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 10 {
t.Fatalf("expecting 10 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{0.25}
timestampsExpected := []int64{40001}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// the last sample is stale NaN
timestamps = []int64{0, 10000, 20000, 30000, 40000}
values = []float64{0, 0, 0, 10, decimal.StaleNaN}
t.Run("last point is stale nan", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDerivFast,
Start: 40001,
End: 40001,
Step: 50000,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 10 {
t.Fatalf("expecting 10 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{nan}
timestampsExpected := []int64{40001}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
}

View File

@@ -742,23 +742,7 @@ Metric names are stripped from the resulting rollups. Add [keep_metric_names](#k
This function is supported by PromQL.
See also [irate](#irate), [rollup_rate](#rollup_rate) and [rate_prometheus](#rate_prometheus).
#### rate_prometheus
`rate_prometheus(series_selector[d])` {{% available_from "#" %}} is a [rollup function](#rollup-functions), which calculates the average per-second
increase rate over the given lookbehind window `d` per each time series returned from the given [series_selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering).
The resulting calculation is equivalent to `increase_prometheus(series_selector[d]) / d`.
It doesn't take into account the last sample before the given lookbehind window `d` when calculating the result in the same way as Prometheus does.
See [this article](https://medium.com/@romanhavronenko/victoriametrics-promql-compliance-d4318203f51e) for details.
Metric names are stripped from the resulting rollups. Add [keep_metric_names](#keep_metric_names) modifier in order to keep metric names.
This function is usually applied to [counters](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#counter).
See also [increase_prometheus](#increase_prometheus) and [rate](#rate).
See also [irate](#irate) and [rollup_rate](#rollup_rate).
#### rate_over_sum

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -36,10 +36,10 @@
<meta property="og:title" content="UI for VictoriaMetrics">
<meta property="og:url" content="https://victoriametrics.com/">
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
<script type="module" crossorigin src="./assets/index-BiQY-19a.js"></script>
<script type="module" crossorigin src="./assets/index-xmjGcv4-.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-D8IJGiEn.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
<link rel="stylesheet" crossorigin href="./assets/index-ojCMu5lE.css">
<link rel="stylesheet" crossorigin href="./assets/index-C85_NB5q.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -63,8 +63,6 @@ var (
cacheSizeStorageTSID = flagutil.NewBytes("storage.cacheSizeStorageTSID", 0, "Overrides max size for storage/tsid cache. "+
"See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cache-tuning")
cacheSizeStorageMetricName = flagutil.NewBytes("storage.cacheSizeStorageMetricName", 0, "Overrides max size for storage/metricName cache. "+
"See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cache-tuning")
cacheSizeIndexDBIndexBlocks = flagutil.NewBytes("storage.cacheSizeIndexDBIndexBlocks", 0, "Overrides max size for indexdb/indexBlocks cache. "+
"See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cache-tuning")
cacheSizeIndexDBDataBlocks = flagutil.NewBytes("storage.cacheSizeIndexDBDataBlocks", 0, "Overrides max size for indexdb/dataBlocks cache. "+
@@ -113,7 +111,6 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
storage.SetTSIDCacheSize(cacheSizeStorageTSID.IntN())
storage.SetTagFiltersCacheSize(cacheSizeIndexDBTagFilters.IntN())
storage.SetMetricNamesStatsCacheSize(cacheSizeMetricNamesStats.IntN())
storage.SetMetricNameCacheSize(cacheSizeStorageMetricName.IntN())
mergeset.SetIndexBlocksCacheSize(cacheSizeIndexDBIndexBlocks.IntN())
mergeset.SetDataBlocksCacheSize(cacheSizeIndexDBDataBlocks.IntN())
mergeset.SetDataBlocksSparseCacheSize(cacheSizeIndexDBDataBlocksSparse.IntN())

View File

@@ -1,4 +1,4 @@
FROM golang:1.24.4 AS build-web-stage
FROM golang:1.24.3 AS build-web-stage
COPY build /build
WORKDIR /build
@@ -6,7 +6,7 @@ COPY web/ /build/
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o web-amd64 github.com/VictoriMetrics/vmui/ && \
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o web-windows github.com/VictoriMetrics/vmui/
FROM alpine:3.22.0
FROM alpine:3.21.3
USER root
COPY --from=build-web-stage /build/web-amd64 /app/web

View File

@@ -10,8 +10,6 @@
"dependencies": {
"@types/lodash.debounce": "^4.0.9",
"@types/lodash.get": "^4.4.9",
"@types/lodash.orderBy": "^4.6.9",
"@types/lodash.throttle": "^4.1.9",
"@types/qs": "^6.9.18",
"@types/react": "^19.1.2",
"@types/react-input-mask": "^3.0.6",
@@ -20,8 +18,6 @@
"dayjs": "^1.11.13",
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.orderBy": "^4.6.0",
"lodash.throttle": "^4.1.1",
"marked": "^15.0.8",
"marked-emoji": "^2.0.0",
"preact": "^10.26.5",
@@ -2195,24 +2191,6 @@
"@types/lodash": "*"
}
},
"node_modules/@types/lodash.orderBy": {
"version": "4.6.9",
"resolved": "https://registry.npmjs.org/@types/lodash.orderby/-/lodash.orderby-4.6.9.tgz",
"integrity": "sha512-T9o2wkIJOmxXwVTPTmwJ59W6eTi2FseiLR369fxszG649Po/xe9vqFNhf/MtnvT5jrbDiyWKxPFPZbpSVK0SVQ==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/lodash.throttle": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz",
"integrity": "sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/node": {
"version": "22.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
@@ -5764,18 +5742,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.orderBy": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.orderby/-/lodash.orderby-4.6.0.tgz",
"integrity": "sha512-T0rZxKmghOOf5YPnn8EY5iLYeWCpZq8G41FfqoVHH5QDTAFaghJRmAdLiadEDq+ztgM2q5PjA+Z1fOwGrLgmtg==",
"license": "MIT"
},
"node_modules/lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",

View File

@@ -7,8 +7,6 @@
"dependencies": {
"@types/lodash.debounce": "^4.0.9",
"@types/lodash.get": "^4.4.9",
"@types/lodash.orderBy": "^4.6.9",
"@types/lodash.throttle": "^4.1.9",
"@types/qs": "^6.9.18",
"@types/react": "^19.1.2",
"@types/react-input-mask": "^3.0.6",
@@ -17,8 +15,6 @@
"dayjs": "^1.11.13",
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.orderBy": "^4.6.0",
"lodash.throttle": "^4.1.1",
"marked": "^15.0.8",
"marked-emoji": "^2.0.0",
"preact": "^10.26.5",

View File

@@ -742,23 +742,7 @@ Metric names are stripped from the resulting rollups. Add [keep_metric_names](#k
This function is supported by PromQL.
See also [irate](#irate), [rollup_rate](#rollup_rate) and [rate_prometheus](#rate_prometheus).
#### rate_prometheus
`rate_prometheus(series_selector[d])` {{% available_from "#" %}} is a [rollup function](#rollup-functions), which calculates the average per-second
increase rate over the given lookbehind window `d` per each time series returned from the given [series_selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering).
The resulting calculation is equivalent to `increase_prometheus(series_selector[d]) / d`.
It doesn't take into account the last sample before the given lookbehind window `d` when calculating the result in the same way as Prometheus does.
See [this article](https://medium.com/@romanhavronenko/victoriametrics-promql-compliance-d4318203f51e) for details.
Metric names are stripped from the resulting rollups. Add [keep_metric_names](#keep_metric_names) modifier in order to keep metric names.
This function is usually applied to [counters](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#counter).
See also [increase_prometheus](#increase_prometheus) and [rate](#rate).
See also [irate](#irate) and [rollup_rate](#rollup_rate).
#### rate_over_sum

View File

@@ -33,12 +33,8 @@ const LogsQueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
const part = logicalParts.find(p => caretPosition[0] >= p.position[0] && caretPosition[0] <= p.position[1]);
if (!part) return;
const cursorStartPosition = caretPosition[0] - part.position[0];
const prevPart = logicalParts.find(p => p.id === part.id - 1);
const queryBeforeIncompleteFilter = prevPart ? value.substring(0, prevPart.position[1] + 1) : undefined;
return {
...part,
queryBeforeIncompleteFilter,
query: value,
...getContextData(part, cursorStartPosition)
};
}, [logicalParts, caretPosition]);
@@ -54,8 +50,6 @@ const LogsQueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
return fieldValues;
case ContextType.PipeName:
return pipeList;
case ContextType.FilterOrPipeName:
return [...fieldNames, ...pipeList];
default:
return [];
}
@@ -64,7 +58,7 @@ const LogsQueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
const getUpdatedValue = (insertValue: string, logicalParts: LogicalPart[], id?: number) => {
return logicalParts.reduce((acc, part) => {
const value = part.id === id ? insertValue : part.value;
const separator = part.separator === "|" ? " | " : " ";
const separator = part.type === LogicalPartType.Pipe ? " | " : " ";
return `${acc}${separator}${value}`;
}, "").trim();
};
@@ -76,7 +70,7 @@ const LogsQueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
modifiedInsert += ":";
} else if (contextType === ContextType.FilterValue) {
const insertWithQuotes = value.startsWith("_stream:") ? modifiedInsert : `${JSON.stringify(modifiedInsert)}`;
modifiedInsert = `${contextData?.filterName || ""}${contextData?.operator || ":"}${insertWithQuotes}`;
modifiedInsert = `${contextData?.filterName || ""}:${insertWithQuotes}`;
}
return modifiedInsert;
@@ -92,13 +86,7 @@ const LogsQueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
const insertValue = getModifyInsert(insert, contextType, value, item.type);
const newValue = getUpdatedValue(insertValue, logicalParts, id);
const logicalPart = logicalParts.find(p => p.id === id);
const getPositionCorrection = () => {
if (logicalPart?.type === LogicalPartType.FilterOrPipe) return 1;
if (item.type === ContextType.PipeName) return 1;
return 0;
};
const updatedPosition = (position[0] || 1) + insertValue.length + getPositionCorrection();
const updatedPosition = (position[0] || 1) + insertValue.length + (item.type === ContextType.PipeName ? 1 : 0);
onSelect(newValue, updatedPosition);
}, [contextData, logicalParts]);

View File

@@ -9,7 +9,6 @@ export const splitLogicalParts = (expr: string) => {
const input = expr; //.replace(/\s*:\s*/g, ":");
const parts: LogicalPart[] = [];
let currentPart = "";
let separator: undefined | " " | "|" = undefined;
let isPipePart = false;
const quotes = ["'", "\"", "`"];
@@ -44,9 +43,8 @@ export const splitLogicalParts = (expr: string) => {
isPipePart = true;
const countStartSpaces = currentPart.match(/^ */)?.[0].length || 0;
const countEndSpaces = currentPart.match(/ *$/)?.[0].length || 0;
pushPart(currentPart, true, [startIndex + countStartSpaces, i - countEndSpaces - 1], parts, separator);
pushPart(currentPart, true, [startIndex + countStartSpaces, i - countEndSpaces - 1], parts);
currentPart = "";
separator = "|";
startIndex = i + 1;
continue;
}
@@ -56,8 +54,7 @@ export const splitLogicalParts = (expr: string) => {
const nextStr = input.slice(i).replace(/^\s*/, "");
const prevStr = input.slice(0, i).replace(/\s*$/, "");
if (!nextStr.startsWith(":") && !prevStr.endsWith(":")) {
pushPart(currentPart, false, [startIndex, i - 1], parts, separator);
separator = " ";
pushPart(currentPart, false, [startIndex, i - 1], parts);
currentPart = "";
startIndex = i + 1;
continue;
@@ -68,35 +65,26 @@ export const splitLogicalParts = (expr: string) => {
}
// push the last part
pushPart(currentPart, isPipePart, [startIndex, input.length], parts, separator);
pushPart(currentPart, isPipePart, [startIndex, input.length], parts);
return parts;
};
const pushPart = (currentPart: string, isPipePart: boolean, position: LogicalPartPosition, parts: LogicalPart[], separator: LogicalPart["separator"]) => {
const pushPart = (currentPart: string, isPipePart: boolean, position: LogicalPartPosition, parts: LogicalPart[]) => {
const trimmedPart = currentPart.trim();
if (!trimmedPart) return;
const isOperator = BUILDER_OPERATORS.includes(trimmedPart.toUpperCase());
const pipesTypes = [LogicalPartType.Pipe, LogicalPartType.FilterOrPipe];
const isPreviousPartPipe = parts.length > 0 && pipesTypes.includes(parts[parts.length - 1].type);
const getType = () => {
if (isPreviousPartPipe) return LogicalPartType.FilterOrPipe;
if (isPipePart) return LogicalPartType.Pipe;
if (isOperator) return LogicalPartType.Operator;
return LogicalPartType.Filter;
};
parts.push({
id: parts.length,
value: trimmedPart,
position,
type: getType(),
separator,
type: isPipePart
? LogicalPartType.Pipe
: isOperator ? LogicalPartType.Operator : LogicalPartType.Filter,
});
};
export const getContextData = (part: LogicalPart, cursorPos: number): ContextData => {
export const getContextData = (part: LogicalPart, cursorPos: number) => {
const valueBeforeCursor = part.value.substring(0, cursorPos);
const valueAfterCursor = part.value.substring(cursorPos);
@@ -107,91 +95,23 @@ export const getContextData = (part: LogicalPart, cursorPos: number): ContextDat
contextType: ContextType.Unknown,
};
// Determine context type based on logical part type
determineContextType(part, valueBeforeCursor, valueAfterCursor, metaData);
if (part.type === LogicalPartType.Filter) {
const noColon = !valueBeforeCursor.includes(":") && !valueAfterCursor.includes(":");
if (noColon) {
metaData.contextType = ContextType.FilterUnknown;
} else if (valueBeforeCursor.includes(":")) {
const [filterName, filterValue] = valueBeforeCursor.split(":");
metaData.contextType = ContextType.FilterValue;
metaData.filterName = filterName;
metaData.valueContext = filterValue;
} else {
metaData.contextType = ContextType.FilterName;
}
} else if (part.type === LogicalPartType.Pipe) {
const valueStartWithPipe = PIPE_NAMES.some(p => part.value.startsWith(p));
metaData.contextType = valueStartWithPipe ? ContextType.PipeValue : ContextType.PipeName;
}
// Clean up quotes in valueContext
metaData.valueContext = metaData.valueContext.replace(/^["']|["']$/g, "");
return metaData;
};
/** Helper function to determine if a string starts with any of the pipe names */
const startsWithPipe = (value: string): boolean => {
return PIPE_NAMES.some(p => value.startsWith(p));
};
/** Helper function to check for colon presence */
const hasNoColon = (before: string, after: string): boolean => {
return !before.includes(":") && !after.includes(":");
};
/** Helper function to extract filter name and update metadata for filter values */
const handleFilterValue = (valueBeforeCursor: string, metaData: ContextData): void => {
const [filterName, ...filterValue] = valueBeforeCursor.split(":");
metaData.contextType = ContextType.FilterValue;
metaData.filterName = filterName;
const enhanceOperators = ["=", "-", "!", "~", "<", ">", "<=", ">="] as const;
const enhanceOperator = enhanceOperators.find(op => op === filterValue[0]);
if (enhanceOperator) {
metaData.valueContext = filterValue.slice(1).join(":");
metaData.operator = `:${enhanceOperator}`;
} else {
metaData.valueContext = filterValue.join(":");
metaData.operator = ":";
}
};
/** Function to determine context type based on part type and value */
const determineContextType = (
part: LogicalPart,
valueBeforeCursor: string,
valueAfterCursor: string,
metaData: ContextData
): void => {
switch (part.type) {
case LogicalPartType.Filter:
handleFilterType(valueBeforeCursor, valueAfterCursor, metaData);
break;
case LogicalPartType.Pipe:
metaData.contextType = startsWithPipe(part.value)
? ContextType.PipeValue
: ContextType.PipeName;
break;
case LogicalPartType.FilterOrPipe:
handleFilterOrPipeType(part.value, valueBeforeCursor, metaData);
break;
}
};
/** Handle filter type context determination */
const handleFilterType = (
valueBeforeCursor: string,
valueAfterCursor: string,
metaData: ContextData
): void => {
if (hasNoColon(valueBeforeCursor, valueAfterCursor)) {
metaData.contextType = ContextType.FilterUnknown;
} else if (valueBeforeCursor.includes(":")) {
handleFilterValue(valueBeforeCursor, metaData);
} else {
metaData.contextType = ContextType.FilterName;
}
};
/** Handle FilterOrPipeType context determination */
const handleFilterOrPipeType = (
value: string,
valueBeforeCursor: string,
metaData: ContextData
): void => {
if (startsWithPipe(value)) {
metaData.contextType = ContextType.PipeValue;
} else if (valueBeforeCursor.includes(":")) {
handleFilterValue(valueBeforeCursor, metaData);
} else {
metaData.contextType = ContextType.FilterOrPipeName;
}
};

View File

@@ -2,19 +2,15 @@ export enum LogicalPartType {
Filter = "Filter",
Pipe = "Pipe",
Operator = "Operator",
FilterOrPipe = "FilterOrPipe",
}
export type LogicalPartPosition = [start: number, end: number];
export type LogicalPartSeparator = " " | "|";
export interface LogicalPart {
id: number;
value: string;
type: LogicalPartType;
position: LogicalPartPosition;
separator?: LogicalPartSeparator;
}
export interface ContextData {
@@ -23,10 +19,6 @@ export interface ContextData {
contextType: ContextType;
valueContext: string;
filterName?: string;
query?: string;
queryBeforeIncompleteFilter?: string;
separator?: LogicalPartSeparator;
operator?: ":" | ":!" | ":-" | ":=" | ":~" | ":<" | ":>" | ":<=" | ":>=";
}
export enum ContextType {
@@ -36,5 +28,4 @@ export enum ContextType {
PipeName = "Pipes",
PipeValue = "PipeValue",
Unknown = "Unknown",
FilterOrPipeName = "FilterOrPipeName",
}

View File

@@ -10,11 +10,11 @@ import { AUTOCOMPLETE_LIMITS } from "../../../../constants/queryAutocomplete";
import { LogsFiledValues } from "../../../../api/types";
import { useLogsDispatch, useLogsState } from "../../../../state/logsPanel/LogsStateContext";
import { useTenant } from "../../../../hooks/useTenant";
import { generateQuery } from "./utils";
type FetchDataArgs = {
urlSuffix: string;
setter: (value: LogsFiledValues[]) => void;
setter: Dispatch<SetStateAction<AutocompleteOptions[]>>
type: ContextType;
params?: URLSearchParams;
}
@@ -24,8 +24,7 @@ const icons = {
[ContextType.FilterValue]: <ValueIcon/>,
[ContextType.PipeName]: <FunctionIcon/>,
[ContextType.PipeValue]: <LabelIcon/>,
[ContextType.Unknown]: <ValueIcon/>,
[ContextType.FilterOrPipeName]: <FunctionIcon/>
[ContextType.Unknown]: <ValueIcon/>
};
export const useFetchLogsQLOptions = (contextData?: ContextData) => {
@@ -62,7 +61,7 @@ export const useFetchLogsQLOptions = (contextData?: ContextData) => {
}));
};
const fetchData = async ({ urlSuffix, setter, params }: FetchDataArgs) => {
const fetchData = async ({ urlSuffix, setter, type, params }: FetchDataArgs) => {
abortControllerRef.current.abort();
abortControllerRef.current = new AbortController();
const { signal } = abortControllerRef.current;
@@ -74,7 +73,7 @@ export const useFetchLogsQLOptions = (contextData?: ContextData) => {
try {
const cachedData = autocompleteCache.get(key);
if (cachedData) {
setter(cachedData);
setter(processData(cachedData, type));
setLoading(false);
return;
}
@@ -87,7 +86,7 @@ export const useFetchLogsQLOptions = (contextData?: ContextData) => {
if (response.ok) {
const data = await response.json();
const value = (data?.values || []) as LogsFiledValues[];
setter(value || []);
setter(value ? processData(value, type) : []);
dispatch({ type: "SET_AUTOCOMPLETE_CACHE", payload: { key, value } });
}
setLoading(false);
@@ -102,7 +101,7 @@ export const useFetchLogsQLOptions = (contextData?: ContextData) => {
// fetch field names
useEffect(() => {
const validContexts = [ContextType.FilterName, ContextType.FilterUnknown, ContextType.FilterOrPipeName];
const validContexts = [ContextType.FilterName, ContextType.FilterUnknown];
const isInvalidContext = !validContexts.includes(contextData?.contextType || ContextType.Unknown);
if (!serverUrl || isInvalidContext) {
return;
@@ -110,14 +109,11 @@ export const useFetchLogsQLOptions = (contextData?: ContextData) => {
setFieldNames([]);
const setter = (filterNames: LogsFiledValues[]) => {
setFieldNames(processData(filterNames, ContextType.FilterName));
};
fetchData({
urlSuffix: "field_names",
setter: setter,
params: getQueryParams({ query: contextData?.queryBeforeIncompleteFilter || "*" })
setter: setFieldNames,
type: ContextType.FilterName,
params: getQueryParams({ query: "*" })
});
return () => abortControllerRef.current?.abort();
@@ -132,14 +128,11 @@ export const useFetchLogsQLOptions = (contextData?: ContextData) => {
setFieldValues([]);
const setter = (filterValues: LogsFiledValues[]) => {
setFieldValues(processData(filterValues, ContextType.FilterValue));
};
fetchData({
urlSuffix: "field_values",
setter: setter,
params: getQueryParams({ query: generateQuery(contextData), field: contextData.filterName })
setter: setFieldValues,
type: ContextType.FilterValue,
params: getQueryParams({ query: "*", field: contextData.filterName })
});
return () => abortControllerRef.current?.abort();

View File

@@ -1,131 +0,0 @@
import { expect } from "vitest";
import { generateQuery } from "./utils";
import { ContextType } from "./types";
describe("utils", () => {
describe("_time", () => {
it("should return the trimmed value by `-`", () => {
expect(generateQuery({
queryBeforeIncompleteFilter: "_stream:{type=\"WatchEvent\"}",
contextType: ContextType.FilterValue,
filterName: "_time",
query: "_stream:{type=\"WatchEvent\"} _time:2025-04-1",
valueAfterCursor: "",
valueBeforeCursor: "_time=2025-04-1",
valueContext: "2025-04-1"
})).toStrictEqual("_stream:{type=\"WatchEvent\"} _time:2025-04");
});
it("should return the trimmed value by `:` if char `-` also exist in the query", () => {
expect(generateQuery({
queryBeforeIncompleteFilter: "_stream:{type=\"WatchEvent\"}",
contextType: ContextType.FilterValue,
filterName: "_time",
query: "_stream:{type=\"WatchEvent\"} _time:2025-04-10T23:45:5",
valueAfterCursor: "",
valueBeforeCursor: "_time=2025-04-10T23:45:5",
valueContext: "2025-04-10T23:45:5"
})).toStrictEqual("_stream:{type=\"WatchEvent\"} _time:2025-04-10T23:45");
});
it("should return default `*` instead of -time filter", () => {
expect(generateQuery({
queryBeforeIncompleteFilter: "_stream:{type=\"WatchEvent\"}",
contextType: ContextType.FilterValue,
filterName: "_time",
query: "_stream:{type=\"WatchEvent\"} _time:202",
valueAfterCursor: "",
valueBeforeCursor: "_time=202",
valueContext: "202"
})).toStrictEqual("_stream:{type=\"WatchEvent\"} *");
});
});
describe("_stream", () => {
it("should add regexp to filter value", () => {
expect(generateQuery({
queryBeforeIncompleteFilter: "",
contextType: ContextType.FilterValue,
filterName: "_stream",
query: "_stream:{type=\"WatchEve",
valueAfterCursor: "",
valueBeforeCursor: "_stream:{type=\"WatchEve",
valueContext: "{type=\"WatchEve"
})).toStrictEqual("_stream:{type=~\"WatchEve.*\"}");
});
it("should add regexp to filter value if cursor in the middle of value", () => {
expect(generateQuery({
queryBeforeIncompleteFilter: "",
contextType: ContextType.FilterValue,
filterName: "_stream",
query: "_stream:{type=\"WatchEve\"}",
valueAfterCursor: "",
valueBeforeCursor: "_stream:{type=\"WatchEve",
valueContext: "{type=\"WatchEve"
})).toStrictEqual("_stream:{type=~\"WatchEve.*\"}");
});
it("should return * if do not have value after =", () => {
expect(generateQuery({
queryBeforeIncompleteFilter: "",
contextType: ContextType.FilterValue,
filterName: "_stream",
query: "_stream:{type=",
valueAfterCursor: "",
valueBeforeCursor: "_stream:{type=",
valueContext: "{type="
})).toStrictEqual("*");
});
});
it("_msg", () => {
expect(generateQuery({
queryBeforeIncompleteFilter: "_stream:{type=\"WatchEvent\"}",
contextType: ContextType.FilterValue,
filterName: "_msg",
query: "_stream:{type=\"WatchEvent\"} _msg:453",
valueAfterCursor: "",
valueBeforeCursor: "_msg:453",
valueContext: "453"
})).toStrictEqual("_stream:{type=\"WatchEvent\"} *");
});
it("_stream_id", () => {
expect(generateQuery({
queryBeforeIncompleteFilter: "_stream:{type=\"WatchEvent\"}",
contextType: ContextType.FilterValue,
filterName: "_stream_id",
query: "_stream:{type=\"WatchEvent\"} _stream_id:453",
valueAfterCursor: "",
valueBeforeCursor: "_stream_id:453",
valueContext: "453"
})).toStrictEqual("_stream:{type=\"WatchEvent\"} *");
});
describe("other fields", () => {
it("should add prefix filter to other type of field names", () => {
expect(generateQuery({
queryBeforeIncompleteFilter: "",
contextType: ContextType.FilterValue,
filterName: "repo.name",
query: "repo.name:Victori",
valueAfterCursor: "",
valueBeforeCursor: "repo.name:Victori",
valueContext: "Victori"
})).toStrictEqual("repo.name:Victori*");
});
it("should add prefix filter to other type of field names with escaped via double quote", () => {
expect(generateQuery({
queryBeforeIncompleteFilter: "",
contextType: ContextType.FilterValue,
filterName: "repo.name",
query: "repo.name:\"Victori",
valueAfterCursor: "",
valueBeforeCursor: "repo.name:\"Victori",
valueContext: "Victori"
})).toStrictEqual("repo.name:Victori*");
});
});
});

View File

@@ -1,61 +0,0 @@
import { ContextData } from "./types";
const getStreamFieldQuery = (valueContext: string) => {
if (valueContext.includes("=")) {
const [fieldName, fieldValue] = valueContext.split("=");
if (fieldValue) {
return `_stream:${fieldName}=~${fieldValue}.*"}`;
}
}
return "*";
};
const getLastPartUntilDelimiter = (value: string, delimiter: string) => {
const lastIndexOfDelimiter = value.lastIndexOf(delimiter);
return lastIndexOfDelimiter !== -1 ? value.slice(0, lastIndexOfDelimiter) : "";
};
const getDateQuery = (contextData: ContextData) => {
let fieldValue = "";
if (contextData.valueContext.includes(":")) {
fieldValue = getLastPartUntilDelimiter(contextData.valueContext, ":");
} else if (contextData.valueContext.includes("-")) {
fieldValue = getLastPartUntilDelimiter(contextData.valueContext, "-");
}
return fieldValue ? `${contextData.filterName}:${fieldValue}` : "*";
};
/**
* Generates a query string based on the provided context data.
*
* The function processes the input based on the `filterName` property:
*
* - If `filterName` is `_msg` or `_stream_id`, the query cannot be generated specifically,
* so a wildcard query (`"*"`) is returned.
*
* - If `filterName` is `_stream`, the query is generated using regexp (`{type=~"value.*"}`).
*
* - If `filterName` is `_time`, a simplified query is created by trimming the value up
* to the first occurrence of a delimiter such as `-` or `:`.
*
* - For all other values of `filterName`, a prefix query is returned using
* the `query` value with a `*` appended (e.g., `"value*"`).
*
* @param {ContextData} contextData - The context object containing query parameters and metadata.
* @returns {string} The generated query string.
*/
export const generateQuery = (contextData: ContextData): string => {
let fieldQuery = "";
if (!contextData.filterName || !contextData.query || ["_msg", "_stream_id"].includes(contextData.filterName)) {
fieldQuery = "*";
} else if ("_stream" === contextData.filterName) {
fieldQuery = getStreamFieldQuery(contextData.valueContext);
} else if ("_time" === contextData.filterName) {
fieldQuery = getDateQuery(contextData);
} else {
fieldQuery = `${contextData.filterName}:${contextData.valueContext}*`;
}
return contextData.queryBeforeIncompleteFilter ? `${contextData.queryBeforeIncompleteFilter}${contextData.separator ?? " "}${fieldQuery}` : fieldQuery;
};

View File

@@ -19,8 +19,8 @@ import {
LOGS_URL_PARAMS,
WITHOUT_GROUPING
} from "../../../constants/logs";
import { getFromStorage, saveToStorage } from "../../../utils/storage";
import LogParsingSwitches from "../../Configurators/LogsSettings/LogParsingSwitches";
import { useLocalStorageBoolean } from "../../../hooks/useLocalStorageBoolean";
const {
GROUP_BY,
@@ -48,7 +48,7 @@ const GroupLogsConfigurators: FC<Props> = ({ logs }) => {
const [dateFormat, setDateFormat] = useState(searchParams.get(DATE_FORMAT) || LOGS_DATE_FORMAT);
const [errorFormat, setErrorFormat] = useState("");
const [disabledHovers, handleSetDisabledHovers] = useLocalStorageBoolean("LOGS_DISABLED_HOVERS");
const [disabledHovers, setDisabledHovers] = useState(!!getFromStorage("LOGS_DISABLED_HOVERS"));
const isGroupChanged = groupBy !== LOGS_GROUP_BY;
const isDisplayFieldsChanged = displayFields.length !== 1 || displayFields[0] !== LOGS_DISPLAY_FIELDS;
@@ -117,6 +117,11 @@ const GroupLogsConfigurators: FC<Props> = ({ logs }) => {
handleClose();
};
const handleSetDisabledHovers = (value: boolean) => {
setDisabledHovers(value);
saveToStorage("LOGS_DISABLED_HOVERS", value);
};
const tooltipContent = () => {
if (!hasChanges) return title;
return (

View File

@@ -1,4 +1,4 @@
import { FC } from "preact/compat";
import React, { FC } from "preact/compat";
import classNames from "classnames";
import { MouseEvent as ReactMouseEvent, ReactNode } from "react";
import "./style.scss";
@@ -14,7 +14,6 @@ interface ButtonProps {
disabled?: boolean
children?: ReactNode
className?: string
"data-id"?: string
onClick?: (e: ReactMouseEvent<HTMLButtonElement>) => void
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void
}
@@ -32,7 +31,6 @@ const Button: FC<ButtonProps> = ({
disabled,
onClick,
onMouseDown,
"data-id": dataId
}) => {
const classesButton = classNames({
@@ -52,7 +50,6 @@ const Button: FC<ButtonProps> = ({
aria-label={ariaLabel}
onClick={onClick}
onMouseDown={onMouseDown}
data-id={dataId}
>
{startIcon}{children}{endIcon}
</button>

View File

@@ -1,3 +1,4 @@
import React from "react";
import { getCssVariable } from "../../../utils/theme";
export const LogoIcon = () => (
@@ -642,47 +643,3 @@ export const PauseIcon = () => (
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
</svg>
);
export const ScrollToTopIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M8 12l4-4 4 4m-4-4v12"
strokeWidth="2"
stroke="currentColor"
fill="none"
/>
</svg>
);
export const SortIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M4 3 L4 15 L1.5 15 L5.5 21 L9.5 15 L7 15 L7 3 Z"/>
<path d="M13 21 L13 9 L10.5 9 L14.5 3 L18.5 9 L16 9 L16 21 Z"/>
</svg>
);
export const SortArrowDownIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M10.5 3 L10.5 15 L8 15 L12 21 L16 15 L13.5 15 L13.5 3 Z"/>
</svg>
);
export const SortArrowUpIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M10.5 21 L10.5 9 L8 9 L12 3 L16 9 L13.5 9 L13.5 21 Z"/>
</svg>
);

View File

@@ -1,59 +0,0 @@
import { FC, useEffect, useState } from "preact/compat";
import Button from "../Main/Button/Button";
import Tooltip from "../Main/Tooltip/Tooltip";
import { ScrollToTopIcon } from "../Main/Icons";
import classNames from "classnames";
import "./style.scss";
import { useCallback } from "react";
interface ScrollToTopButtonProps {
className?: string;
}
const ScrollToTopButton: FC<ScrollToTopButtonProps> = ({ className }) => {
const [isVisible, setIsVisible] = useState(false);
const checkScrollPosition = () => {
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
const visibleHeightThreshold = window.innerHeight;
setIsVisible(scrollPosition > visibleHeightThreshold);
};
const scrollToTop = useCallback(() => {
window.scrollTo({
top: 0,
behavior: "smooth"
});
}, []);
useEffect(() => {
window.addEventListener("scroll", checkScrollPosition);
checkScrollPosition();
return () => {
window.removeEventListener("scroll", checkScrollPosition);
};
}, []);
return (
<div
className={classNames({
"vm-scroll-to-top-button": true,
"vm-scroll-to-top-button_visible": isVisible
}, className)}
>
<Tooltip title="Scroll to top">
<Button
variant="contained"
color="primary"
onClick={scrollToTop}
ariaLabel="Scroll to top"
startIcon={<ScrollToTopIcon />}
/>
</Tooltip>
</div>
);
};
export default ScrollToTopButton;

View File

@@ -1,26 +0,0 @@
@use "src/styles/variables" as *;
.vm-scroll-to-top-button {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 4;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
&_visible {
opacity: 1;
visibility: visible;
}
.vm-button {
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
}

View File

@@ -18,8 +18,8 @@ const title = "Table settings";
interface TableSettingsProps {
columns: string[];
selectedColumns?: string[];
tableCompact?: boolean;
toggleTableCompact?: () => void;
tableCompact: boolean;
toggleTableCompact: () => void;
onChangeColumns: (arr: string[]) => void
}
@@ -195,20 +195,18 @@ const TableSettings: FC<TableSettingsProps> = ({
</div>
</div>
</div>
{toggleTableCompact && tableCompact !== undefined && (
<div className="vm-table-settings-modal-section">
<div className="vm-table-settings-modal-section__title">
<div className="vm-table-settings-modal-section">
<div className="vm-table-settings-modal-section__title">
Table view
</div>
<div className="vm-table-settings-modal-columns-list__item">
<Switch
label={"Compact view"}
value={tableCompact}
onChange={toggleTableCompact}
/>
</div>
</div>
)}
<div className="vm-table-settings-modal-columns-list__item">
<Switch
label={"Compact view"}
value={tableCompact}
onChange={toggleTableCompact}
/>
</div>
</div>
</Modal>)}
</div>
);

View File

@@ -1,70 +0,0 @@
import { act, renderHook } from "@testing-library/preact";
import { useLocalStorageBoolean } from "./useLocalStorageBoolean";
import * as storageUtils from "../utils/storage";
import { Mock } from "vitest";
import { StorageKeys } from "../utils/storage";
vi.mock("../utils/storage");
const testStorageKey = "TEST_STORAGE_KEY" as StorageKeys;
describe("useLocalStorageBoolean", () => {
const { getFromStorage, saveToStorage } = storageUtils;
beforeEach(() => {
vi.clearAllMocks();
});
it("initializes with the value from localStorage", () => {
const mockGetFromStorage = getFromStorage as Mock;
mockGetFromStorage.mockReturnValueOnce(true);
const { result } = renderHook(() => useLocalStorageBoolean(testStorageKey));
expect(result.current[0]).toBe(true);
expect(getFromStorage).toHaveBeenCalledWith(testStorageKey);
});
it("updates localStorage and state when setter is called", () => {
const mockGetFromStorage = getFromStorage as Mock;
mockGetFromStorage.mockReturnValueOnce(false);
const { result } = renderHook(() => useLocalStorageBoolean(testStorageKey));
act(() => {
result.current[1](true);
});
expect(saveToStorage).toHaveBeenCalledWith(testStorageKey, true);
expect(result.current[0]).toBe(false);
});
it("reacts to changes in localStorage by storage events", () => {
const mockGetFromStorage = getFromStorage as Mock;
mockGetFromStorage.mockReturnValueOnce(false);
const { result } = renderHook(() => useLocalStorageBoolean(testStorageKey));
// Simulate a storage event
act(() => {
mockGetFromStorage.mockReturnValueOnce(true);
window.dispatchEvent(new StorageEvent("storage", { key: testStorageKey, newValue: "true" }));
});
expect(result.current[0]).toBe(true);
});
it("does not update state if the localStorage value remains the same", () => {
const mockGetFromStorage = getFromStorage as Mock;
mockGetFromStorage.mockReturnValueOnce(false);
const { result } = renderHook(() => useLocalStorageBoolean(testStorageKey));
act(() => {
mockGetFromStorage.mockReturnValueOnce(false);
window.dispatchEvent(new StorageEvent("storage", { key: testStorageKey, newValue: "false" }));
});
expect(result.current[0]).toBe(false);
});
});

View File

@@ -1,31 +0,0 @@
import { useMemo, useState } from "preact/compat";
import { getFromStorage, saveToStorage, StorageKeys } from "../utils/storage";
import useEventListener from "./useEventListener";
import { useCallback } from "react";
/**
* A custom hook that synchronizes a boolean state with a value stored in localStorage.
*
* @param {StorageKeys} key - The key used to access the corresponding value in localStorage.
* @returns {[boolean, function]} A tuple containing the current boolean value from localStorage and a setter function to update the value in localStorage.
*
* The hook listens to the "storage" event to automatically update the state when the localStorage value changes.
*/
export const useLocalStorageBoolean = (key: StorageKeys): [boolean, (value: boolean) => void] => {
const [value, setValue] = useState(!!getFromStorage(key));
const handleUpdateStorage = useCallback(() => {
const newValue = !!getFromStorage(key);
if (newValue !== value) {
setValue(newValue);
}
}, [key, value]);
const setNewValue = useCallback((newValue: boolean) => {
saveToStorage(key, newValue);
}, [key]);
useEventListener("storage", handleUpdateStorage);
return useMemo(() => [value, setNewValue], [value, setNewValue]);
};

View File

@@ -5,7 +5,7 @@
&-header {
background-color: $color-background-block;
z-index: 3;
z-index: 1;
margin: -$padding-medium 0-$padding-medium 0;
position: sticky;
top: 0;

View File

@@ -1,67 +1,21 @@
import React, { FC, useMemo, useCallback, createPortal } from "preact/compat";
import React, { FC } from "preact/compat";
import DownloadLogsButton from "../../../DownloadLogsButton/DownloadLogsButton";
import { createPortal } from "preact/compat";
import JsonViewComponent from "../../../../../components/Views/JsonView/JsonView";
import { ViewProps } from "../../types";
import EmptyLogs from "../components/EmptyLogs/EmptyLogs";
import JsonViewSettings from "./JsonViewSettings/JsonViewSettings";
import { useSearchParams } from "react-router-dom";
import orderBy from "lodash.orderBy";
import "./style.scss";
import { Logs } from "../../../../../api/types";
import { SortDirection } from "./types";
import { useCallback } from "react";
const MemoizedJsonView = React.memo(JsonViewComponent);
const jsonQuerySortParam = "json_sort";
const fieldSortQueryParamName = "json_field_sort";
const JsonView: FC<ViewProps> = ({ data, settingsRef }) => {
const getLogs = useCallback(() => data, [data]);
const [searchParams] = useSearchParams();
const sortParam = searchParams.get(jsonQuerySortParam);
const fieldSortParam = searchParams.get(fieldSortQueryParamName) as SortDirection;
const [sortField, sortDirection] = useMemo(() => {
const [sortField, sortDirection] = sortParam?.split(":").map(decodeURIComponent) || [];
return [sortField, sortDirection as "asc" | "desc" | undefined];
}, [sortParam]);
const fields = useMemo(() => {
const keys = new Set(data.flatMap(Object.keys));
return Array.from(keys);
}, [data]);
const orderedFieldsData = useMemo(() => {
if (!fieldSortParam) return data;
const orderedFields = fields.toSorted((a, b) => fieldSortParam === "asc" ? a.localeCompare(b): b.localeCompare(a));
return data.map((item) => {
return orderedFields.reduce((acc, field) => {
if (item[field]) acc[field] = item[field];
return acc;
}, {} as Logs);
});
}, [fields, fieldSortParam, data]);
const sortedData = useMemo(() => {
if (!sortField || !sortDirection) return orderedFieldsData;
return orderBy(orderedFieldsData, [sortField], [sortDirection]);
}, [orderedFieldsData, sortField, sortDirection]);
const renderSettings = () => {
if (!settingsRef.current) return null;
return createPortal(
data.length > 0 && (
<div className="vm-json-view__settings-container">
<DownloadLogsButton getLogs={getLogs} />
<JsonViewSettings
fields={fields}
sortQueryParamName={jsonQuerySortParam}
fieldSortQueryParamName={fieldSortQueryParamName}
/>
</div>
),
data.length > 0 && <DownloadLogsButton getLogs={getLogs} />,
settingsRef.current
);
};
@@ -71,11 +25,9 @@ const JsonView: FC<ViewProps> = ({ data, settingsRef }) => {
return (
<>
{renderSettings()}
<MemoizedJsonView
data={sortedData}
/>
<MemoizedJsonView data={data} />
</>
);
};
export default JsonView;
export default JsonView;

View File

@@ -1,185 +0,0 @@
import { FC, useMemo, useRef } from "preact/compat";
import Button from "../../../../../../components/Main/Button/Button";
import { SettingsIcon, SortArrowDownIcon, SortArrowUpIcon, SortIcon } from "../../../../../../components/Main/Icons";
import Tooltip from "../../../../../../components/Main/Tooltip/Tooltip";
import Select from "../../../../../../components/Main/Select/Select";
import useBoolean from "../../../../../../hooks/useBoolean";
import { useState, useEffect, useCallback } from "react";
import Modal from "../../../../../../components/Main/Modal/Modal";
import { useSearchParams } from "react-router-dom";
import "./style.scss";
import { SortDirection } from "../types";
const title = "JSON settings";
const directionList = ["asc", "desc"];
interface JsonSettingsProps {
fields: string[];
sortQueryParamName: string;
fieldSortQueryParamName: string;
}
const JsonViewSettings: FC<JsonSettingsProps> = ({
fields,
sortQueryParamName,
fieldSortQueryParamName
}) => {
const [searchParams, setSearchParams] = useSearchParams();
const buttonRef = useRef<HTMLDivElement>(null);
const [fieldSortDirection, setFieldSortDirection] = useState<SortDirection>(null);
const {
value: openSettings,
toggle: toggleOpenSettings,
setFalse: handleClose,
} = useBoolean(false);
const [sortField, setSortField] = useState<string | null>(null);
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
useEffect(() => {
const sortParam = searchParams.get(sortQueryParamName);
const isSortDirection = (value: string) : value is Exclude<SortDirection, null> => directionList.includes(value);
if (sortParam) {
const [field, direction] = sortParam.split(":").map(decodeURIComponent);
if (field && (isSortDirection(direction))) {
setSortField(field);
setSortDirection(direction);
}
}
const fieldSortParam = searchParams.get(fieldSortQueryParamName);
if (fieldSortParam === "asc" || fieldSortParam === "desc") {
setFieldSortDirection(fieldSortParam);
}
}, [searchParams, sortQueryParamName, fieldSortQueryParamName, setSortField, setSortDirection, setFieldSortDirection]);
const updateSortParams = useCallback((field: string | null, direction: SortDirection) => {
const updatedParams = new URLSearchParams(searchParams.toString());
if (!field || !direction) {
updatedParams.delete(sortQueryParamName);
} else {
updatedParams.set(sortQueryParamName, `${field}:${direction || ""}`);
}
setSearchParams(updatedParams);
}, [searchParams, sortQueryParamName]);
const handleSort = (field: string) => {
const newDirection: SortDirection = sortDirection || "asc";
setSortField(field);
setSortDirection(newDirection);
updateSortParams(field, newDirection);
};
const resetSort = () => {
setSortField(null);
setSortDirection(null);
updateSortParams(null, null);
};
const changeFieldSortDirection = useCallback(() => {
let newFieldSortDirection: SortDirection = null;
if (fieldSortDirection === null) {
newFieldSortDirection = "asc";
}else if (fieldSortDirection === "asc") {
newFieldSortDirection = "desc";
}
setFieldSortDirection(newFieldSortDirection);
const updatedParams = new URLSearchParams(searchParams.toString());
if (!newFieldSortDirection) {
updatedParams.delete(fieldSortQueryParamName);
} else {
updatedParams.set(fieldSortQueryParamName, encodeURIComponent(newFieldSortDirection));
}
setSearchParams(updatedParams);
},[fieldSortDirection, searchParams, fieldSortQueryParamName]);
const handleChangeSortDirection = (direction: string) => {
const field = sortField || fields[0];
setSortField(field);
setSortDirection(direction as SortDirection);
updateSortParams(field, direction as SortDirection);
};
const fieldSortMeta = useMemo(() => ({
default: {
title: "Set field sort order. Click to sort in ascending order",
icon: <SortIcon />
},
asc: {
title: "Fields sorted ascending. Click to sort in descending order",
icon: <SortArrowDownIcon />
},
desc: {
title: "Fields sorted descending. Click to reset sort",
icon: <SortArrowUpIcon />
},
}), []);
const fieldSortButton = useMemo(() => {
const { title, icon } = fieldSortMeta[fieldSortDirection ?? "default"];
return <Tooltip title={title}>
<Button
variant="text"
startIcon={icon}
onClick={changeFieldSortDirection}
ariaLabel={title}
/>
</Tooltip>;
}, [fieldSortDirection, toggleOpenSettings, changeFieldSortDirection, fieldSortMeta]);
return (
<div className="vm-json-settings">
{fieldSortButton}
<Tooltip title={title}>
<div ref={buttonRef}>
<Button
variant="text"
startIcon={<SettingsIcon/>}
onClick={toggleOpenSettings}
ariaLabel={title}
/>
</div>
</Tooltip>
{openSettings && (
<Modal
title={title}
className="vm-json-settings-modal"
onClose={handleClose}
>
<div className="vm-json-settings-modal-section">
<div className="vm-json-settings-modal-section__sort-settings-container">
<Select
value={sortField || ""}
onChange={handleSort}
list={fields}
label="Select field"
/>
<Select
value={sortDirection || ""}
onChange={handleChangeSortDirection}
list={directionList}
label="Sort direction"
/>
{(sortField || sortDirection) && (
<Button
variant="outlined"
color="error"
onClick={resetSort}
>
Reset sort
</Button>
)}
</div>
</div>
</Modal>)}
</div>
);
};
export default JsonViewSettings;

View File

@@ -1,34 +0,0 @@
@use "src/styles/variables" as *;
.vm-json-settings {
display: flex;
flex-direction: row;
&-modal {
.vm-modal-content-body {
min-width: clamp(300px, 600px, 90vw);
padding: 0;
}
&-section {
padding-block: $padding-global;
border-top: $border-divider;
&:first-child {
padding-top: 0;
border-top: none;
}
&__sort-settings-container {
display: grid;
padding: $padding-medium;
grid-template-columns: 1fr 1fr 80px;
gap: $padding-medium;
@media (max-width: 500px) {
grid-template-columns: 1fr;
}
}
}
}
}

View File

@@ -1,9 +0,0 @@
@use "src/styles/variables" as *;
.vm-json-view {
&__settings-container {
display: flex;
flex-direction: row;
align-items: center;
}
}

View File

@@ -1 +0,0 @@
export type SortDirection = "asc" | "desc" | null;

View File

@@ -19,8 +19,8 @@ interface LiveTailingSettingsProps {
handleResumeLiveTailing: () => void;
pauseLiveTailing: () => void;
clearLogs: () => void;
isRawJsonView: boolean;
onRawJsonViewChange: (value: boolean) => void;
isCompactTailingNumber: boolean;
handleSetCompactTailing: (value: boolean) => void;
}
const LiveTailingSettings: FC<LiveTailingSettingsProps> = ({
@@ -32,8 +32,8 @@ const LiveTailingSettings: FC<LiveTailingSettingsProps> = ({
handleResumeLiveTailing,
pauseLiveTailing,
clearLogs,
isRawJsonView,
onRawJsonViewChange
isCompactTailingNumber,
handleSetCompactTailing
}) => {
const settingButtonRef = useRef<HTMLDivElement>(null);
const { value: isSettingsOpen, setFalse: closeSettings, setTrue: openSettings } = useBoolean(false);
@@ -106,12 +106,12 @@ const LiveTailingSettings: FC<LiveTailingSettingsProps> = ({
<div className="vm-live-tailing-view__settings-modal">
<div className={"vm-live-tailing-view__settings-modal-item"}>
<Switch
label={"Raw JSON View"}
value={isRawJsonView}
onChange={onRawJsonViewChange}
label={"Expandable Properties View"}
value={isCompactTailingNumber}
onChange={handleSetCompactTailing}
/>
<span className="vm-group-logs-configurator-item__info">
When this option is enabled, logs will be displayed in raw JSON format. This improves performance and uses less CPU and memory.
Switches log display to expandable properties view with additional visualization settings. Please note: when processing large volumes of data, it may increase system response time.
</span>
</div>
</div>

View File

@@ -7,18 +7,16 @@ import { useLiveTailingLogs } from "./useLiveTailingLogs";
import { LOGS_DISPLAY_FIELDS, LOGS_URL_PARAMS } from "../../../../../constants/logs";
import { useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import throttle from "lodash.throttle";
import throttle from "lodash/throttle";
import GroupLogsItem from "../../../GroupLogs/GroupLogsItem";
import LiveTailingSettings from "./LiveTailingSettings";
import Alert from "../../../../../components/Main/Alert/Alert";
import { isDecreasing } from "../../../../../utils/array";
import { useLocalStorageBoolean } from "../../../../../hooks/useLocalStorageBoolean";
import ScrollToTopButton from "../../../../../components/ScrollToTopButton/ScrollToTopButton";
const SCROLL_THRESHOLD = 100;
const scrollToBottom = () => window.scrollTo({
top: document.documentElement.scrollHeight,
behavior: "smooth"
behavior: "instant"
});
const throttledScrollToBottom = throttle(scrollToBottom, 200);
@@ -30,7 +28,8 @@ const LiveTailingView: FC<ViewProps> = ({ settingsRef }) => {
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [rowsPerPage, setRowsPerPage] = useStateSearchParams(100, "rows_per_page");
const [query, _setQuery] = useStateSearchParams("*", "query");
const [isRawJsonView, setIsRawJsonView] = useLocalStorageBoolean("RAW_JSON_LIVE_VIEW");
const [isCompactTailingStr] = useStateSearchParams(0, "compact_tailing");
const isCompactTailingNumber = Boolean(Number(isCompactTailingStr));
const {
logs,
isPaused,
@@ -55,6 +54,10 @@ const LiveTailingView: FC<ViewProps> = ({ settingsRef }) => {
setSearchParamsFromKeys({ rows_per_page: limit });
}, [setRowsPerPage, setSearchParamsFromKeys]);
const handleSetCompactTailing = useCallback((value: boolean) => {
setSearchParamsFromKeys({ compact_tailing: Number(value) });
}, [setSearchParamsFromKeys]);
useEffect(() => {
startLiveTailing();
return () => stopLiveTailing();
@@ -108,10 +111,9 @@ const LiveTailingView: FC<ViewProps> = ({ settingsRef }) => {
handleResumeLiveTailing={handleResumeLiveTailing}
pauseLiveTailing={pauseLiveTailing}
clearLogs={clearLogs}
isRawJsonView={isRawJsonView}
onRawJsonViewChange={setIsRawJsonView}
isCompactTailingNumber={isCompactTailingNumber}
handleSetCompactTailing={handleSetCompactTailing}
/>
<ScrollToTopButton />
<div
ref={containerRef}
className="vm-live-tailing-view__container"
@@ -120,31 +122,28 @@ const LiveTailingView: FC<ViewProps> = ({ settingsRef }) => {
? (<div className="vm-live-tailing-view__empty">Waiting for logs...</div>)
: (<div className="vm-live-tailing-view__logs">
{logs.map(({ _log_id, ...log }, idx) =>
isRawJsonView ? (
<pre
key={idx}
className="vm-live-tailing-view__log-row"
onMouseDown={pauseLiveTailing}
>
{JSON.stringify(log)}
</pre>
) : (
<GroupLogsItem
key={_log_id}
log={log}
onItemClick={pauseLiveTailing}
hideGroupButton={true}
displayFields={displayFields}
/>
)
isCompactTailingNumber
? (
<GroupLogsItem
key={_log_id}
log={log}
onItemClick={pauseLiveTailing}
hideGroupButton={true}
displayFields={displayFields}
/>
) : (
<pre
key={idx}
className="vm-live-tailing-view__log-row"
>
{JSON.stringify(log)}
</pre>
)
)}
</div>
)}
</div>
{isLimitedLogsPerUpdate && (
<Alert variant="warning">Too many logs per second detected. Large volumes of log data are difficult to process
and may impact performance. We recommend adding filters to your query for better analysis and system
performance.</Alert>)}
{isLimitedLogsPerUpdate && (<Alert variant="warning">Too many logs per second detected. Large volumes of log data are difficult to process and may impact performance. We recommend adding filters to your query for better analysis and system performance.</Alert>)}
</>
);
};

View File

@@ -34,10 +34,9 @@
width: 100%;
height: 100%;
overflow: auto;
min-height: calc(100vh - 120px);
min-height: 200px;
font-family: $font-family-monospace;
padding-bottom: $padding-medium;
transition: min-height 0.3s ease;
}
&__empty {

View File

@@ -4,8 +4,19 @@ import { Logs } from "../../../../../api/types";
import { useAppState } from "../../../../../state/common/StateContext";
import useBoolean from "../../../../../hooks/useBoolean";
import { useTenant } from "../../../../../hooks/useTenant";
import { LogFlowAnalyzer } from "./utils";
/**
* Defines the maximum number of consecutive times logs can be fetched above the threshold
* before showing a warning notification, and vice versa:
* - If logs are fetched above a threshold this many times in a row -> show warning
* - If warning is shown, it won't disappear until logs are fetched below a threshold
* this many times in a row
*
* This threshold helps optimize log display performance when dealing with large volumes of logs.
* If the threshold is consistently exceeded, users will be prompted to add filters to their query
* for better system performance and more focused log analysis.
*/
const MAX_ATTEMPTS_FETCH_LOGS_PER_SECOND = 5;
/**
* Defines the log's threshold, after which will be shown a warning notification
*/
@@ -45,7 +56,7 @@ const createStreamProcessor = (
} catch (e) {
if (e instanceof Error && e.name !== "AbortError") {
console.error("Stream processing error:", e);
restartTailing();
setError(String(e));
}
} finally {
clearInterval(connectionCheckInterval);
@@ -53,6 +64,31 @@ const createStreamProcessor = (
};
};
const updateLimitModeTracking = (
linesCount: number,
attemptsFetchLimitRef: React.MutableRefObject<number>,
attemptsFetchLowRef: React.MutableRefObject<number>,
isLimitedLogsPerUpdate: boolean,
) => {
if (linesCount > LOGS_THRESHOLD) {
attemptsFetchLimitRef.current++;
attemptsFetchLowRef.current = 0;
} else {
attemptsFetchLowRef.current++;
attemptsFetchLimitRef.current = 0;
}
if (attemptsFetchLimitRef.current > MAX_ATTEMPTS_FETCH_LOGS_PER_SECOND) {
return true;
}
if (attemptsFetchLowRef.current > MAX_ATTEMPTS_FETCH_LOGS_PER_SECOND) {
return false;
}
return isLimitedLogsPerUpdate;
};
const parseLogLines = (lines: string[], counterRef: React.MutableRefObject<bigint>): Logs[] => {
return lines
.map(line => {
@@ -72,22 +108,27 @@ interface ProcessBufferedLogsParams {
lines: string[];
limit: number;
counterRef: React.MutableRefObject<bigint>;
attemptsFetchLimitRef: React.MutableRefObject<number>;
attemptsFetchLowRef: React.MutableRefObject<number>;
setIsLimitedLogsPerUpdate: (isLimited: boolean) => void;
setLogs: React.Dispatch<React.SetStateAction<Logs[]>>;
bufferLinesRef: React.MutableRefObject<string[]>;
logFlowAnalyzerRef?: React.MutableRefObject<LogFlowAnalyzer>;
isLimitedLogsPerUpdate: boolean;
}
const processBufferedLogs = ({
lines,
limit,
counterRef,
attemptsFetchLimitRef,
attemptsFetchLowRef,
setIsLimitedLogsPerUpdate,
setLogs,
bufferLinesRef,
logFlowAnalyzerRef
isLimitedLogsPerUpdate
}: ProcessBufferedLogsParams) => {
const isLimitLogsMode = logFlowAnalyzerRef?.current?.update(lines.length) === "high";
const isLimitLogsMode = updateLimitModeTracking(lines.length, attemptsFetchLimitRef, attemptsFetchLowRef, isLimitedLogsPerUpdate);
const limitedLines = isLimitLogsMode && lines.length > LOGS_THRESHOLD ? lines.slice(-LOGS_THRESHOLD) : lines;
const newLogs = parseLogLines(limitedLines, counterRef);
@@ -114,7 +155,8 @@ export const useLiveTailingLogs = (query: string, limit: number) => {
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const bufferRef = useRef<string>("");
const bufferLinesRef = useRef<string[]>([]);
const logFlowAnalyzerRef = useRef(new LogFlowAnalyzer());
const attemptsFetchLimitLogsPerSecondCountRef = useRef<number>(0);
const attemptsFetchLowLogsPerSecondCountRef = useRef<number>(0);
const stopLiveTailing = useCallback(() => {
if (readerRef.current) {
@@ -197,10 +239,12 @@ export const useLiveTailingLogs = (query: string, limit: number) => {
lines,
limit,
counterRef,
attemptsFetchLimitRef: attemptsFetchLimitLogsPerSecondCountRef,
attemptsFetchLowRef: attemptsFetchLowLogsPerSecondCountRef,
setIsLimitedLogsPerUpdate,
isLimitedLogsPerUpdate,
setLogs,
bufferLinesRef,
logFlowAnalyzerRef
bufferLinesRef
});
}, PROCESSING_INTERVAL_MS);

View File

@@ -1,45 +0,0 @@
export class LogFlowAnalyzer {
private threshold: number;
private windowSize: number;
private minHighCount: number;
private minNormalCount: number;
private window: number[];
private state: "normal" | "high";
/**
* @param {number} threshold - The threshold value used for state evaluation. Defaults to 200.
* @param {number} windowSize - The size of the window used for tracking data. Defaults to 10.
* @param {number} minHighCount - The minimum number of high occurrences needed for state transition. Defaults to 6.
* @param {number} minNormalCount - The minimum number of normal occurrences needed for state reset. Defaults to 2.
* @return {void}
*/
constructor(threshold: number = 200, windowSize: number = 10, minHighCount: number = 6, minNormalCount: number = 2) {
this.threshold = threshold;
this.windowSize = windowSize;
this.minHighCount = minHighCount;
this.minNormalCount = minNormalCount;
this.window = [];
this.state = "normal";
}
update(logCount: number): "normal" | "high" {
this.window.push(logCount);
if (this.window.length > this.windowSize) {
this.window.shift();
}
const highCount = this.window.filter((x) => x > this.threshold).length;
if (this.state === "normal") {
if (highCount >= this.minHighCount) {
this.state = "high";
}
} else if (this.state === "high") {
if (highCount < this.minNormalCount) {
this.state = "normal";
}
}
return this.state;
}
}

View File

@@ -3,6 +3,7 @@ import DownloadLogsButton from "../../../DownloadLogsButton/DownloadLogsButton";
import { createPortal } from "preact/compat";
import "./style.scss";
import { ViewProps } from "../../types";
import useBoolean from "../../../../../hooks/useBoolean";
import useStateSearchParams from "../../../../../hooks/useStateSearchParams";
import TableLogs from "../../TableLogs";
import SelectLimit from "../../../../../components/Main/Pagination/SelectLimit/SelectLimit";
@@ -17,6 +18,7 @@ const TableView: FC<ViewProps> = ({ data, settingsRef }) => {
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [displayColumns, setDisplayColumns] = useState<string[]>([]);
const [rowsPerPage, setRowsPerPage] = useStateSearchParams(100, "rows_per_page");
const { value: tableCompact, toggle: toggleTableCompact } = useBoolean(false);
const columns = useMemo(() => {
const keys = new Set<string>();
@@ -50,6 +52,8 @@ const TableView: FC<ViewProps> = ({ data, settingsRef }) => {
columns={columns}
selectedColumns={displayColumns}
onChangeColumns={setDisplayColumns}
tableCompact={tableCompact}
toggleTableCompact={toggleTableCompact}
/>
</div>
</div>,
@@ -65,7 +69,7 @@ const TableView: FC<ViewProps> = ({ data, settingsRef }) => {
<MemoizedTableView
logs={data}
displayColumns={displayColumns}
tableCompact={false}
tableCompact={tableCompact}
columns={columns}
rowsPerPage={Number(rowsPerPage)}
/>

View File

@@ -1,9 +1,10 @@
import { FC, useMemo } from "preact/compat";
import React, { FC, useMemo, useState } from "preact/compat";
import { Logs } from "../../../api/types";
import "./style.scss";
import classNames from "classnames";
import GroupLogsFieldRow from "./GroupLogsFieldRow";
import { useLocalStorageBoolean } from "../../../hooks/useLocalStorageBoolean";
import useEventListener from "../../../hooks/useEventListener";
import { getFromStorage } from "../../../utils/storage";
interface Props {
log: Logs;
@@ -16,7 +17,16 @@ const GroupLogsFields: FC<Props> = ({ log, hideGroupButton }) => {
.sort(([aKey], [bKey]) => aKey.localeCompare(bKey));
}, [log]);
const [disabledHovers] = useLocalStorageBoolean("LOGS_DISABLED_HOVERS");
const [disabledHovers, setDisabledHovers] = useState(!!getFromStorage("LOGS_DISABLED_HOVERS"));
const handleUpdateStage = () => {
const newValDisabledHovers = !!getFromStorage("LOGS_DISABLED_HOVERS");
if (newValDisabledHovers !== disabledHovers) {
setDisabledHovers(newValDisabledHovers);
}
};
useEventListener("storage", handleUpdateStage);
return (
<div

View File

@@ -1,8 +1,8 @@
import React, { FC, memo, useMemo } from "preact/compat";
import React, { FC, memo, useMemo, useState } from "preact/compat";
import { Logs } from "../../../api/types";
import "./style.scss";
import useBoolean from "../../../hooks/useBoolean";
import { ArrowDownIcon, CopyIcon } from "../../../components/Main/Icons";
import { ArrowDownIcon } from "../../../components/Main/Icons";
import classNames from "classnames";
import { useLogsState } from "../../../state/logsPanel/LogsStateContext";
import dayjs from "dayjs";
@@ -10,13 +10,10 @@ import { useTimeState } from "../../../state/time/TimeStateContext";
import { marked } from "marked";
import { useSearchParams } from "react-router-dom";
import { LOGS_DATE_FORMAT, LOGS_URL_PARAMS } from "../../../constants/logs";
import useEventListener from "../../../hooks/useEventListener";
import { getFromStorage } from "../../../utils/storage";
import { parseAnsiToHtml } from "../../../utils/ansiParser";
import GroupLogsFields from "./GroupLogsFields";
import { useLocalStorageBoolean } from "../../../hooks/useLocalStorageBoolean";
import Button from "../../../components/Main/Button/Button";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import { useCallback, useEffect, useState } from "react";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
interface Props {
log: Logs;
@@ -30,8 +27,6 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"], onItemClick,
value: isOpenFields,
toggle: toggleOpenFields,
} = useBoolean(false);
const [copied, setCopied] = useState<boolean>(false);
const copyToClipboard = useCopyToClipboard();
const [searchParams] = useSearchParams();
const { markdownParsing, ansiParsing } = useLogsState();
@@ -73,29 +68,21 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"], onItemClick,
return values;
}, [log, hasFields, displayFields, ansiParsing]);
const [disabledHovers] = useLocalStorageBoolean("LOGS_DISABLED_HOVERS");
const [disabledHovers, setDisabledHovers] = useState(!!getFromStorage("LOGS_DISABLED_HOVERS"));
const handleUpdateStage = () => {
const newValDisabledHovers = !!getFromStorage("LOGS_DISABLED_HOVERS");
if (newValDisabledHovers !== disabledHovers) {
setDisabledHovers(newValDisabledHovers);
}
};
const handleClick = () => {
toggleOpenFields();
onItemClick?.(log);
};
const handleCopy = useCallback(async (e: Event) => {
e.stopPropagation();
if (copied) return;
try {
await copyToClipboard(JSON.stringify(log, null, 2));
setCopied(true);
} catch (e) {
console.error(e);
}
}, [copied, copyToClipboard]);
useEffect(() => {
if (copied === null) return;
const timeout = setTimeout(() => setCopied(false), 2000);
return () => clearTimeout(timeout);
}, [copied]);
useEventListener("storage", handleUpdateStage);
return (
<div className="vm-group-logs-row">
@@ -106,17 +93,6 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"], onItemClick,
})}
onClick={handleClick}
>
<Tooltip title={copied ? "Copied" : "Copy to clipboard"}>
<Button
className="vm-group-logs-row-content__copy-row"
variant="text"
color="gray"
size="small"
startIcon={<CopyIcon/>}
onClick={handleCopy}
ariaLabel="copy to clipboard"
/>
</Tooltip>
{hasFields && (
<div
className={classNames({

View File

@@ -132,7 +132,7 @@ $font-size-logs: var(--font-size-logs, $font-size-small);
&-content {
display: flex;
padding: 2px 24px 2px 0;
padding: 2px 0;
cursor: pointer;
&_interactive {
@@ -140,23 +140,8 @@ $font-size-logs: var(--font-size-logs, $font-size-small);
will-change: background-color;
}
&__copy-row {
position: absolute;
top: 0;
right: 0;
z-index: 1;
visibility: hidden;
&.vm-button {
padding: 2px;
}
}
&_interactive:hover {
background-color: $color-hover-black;
.vm-group-logs-row-content__copy-row {
visibility: visible;
}
}
&__arrow {

View File

@@ -74,7 +74,7 @@ export const useFetchLogHits = (server: string, query: string) => {
}
}
setIsLoading(prev => ({ ...prev, [id]: false }));
}, [url, query, tenant]);
}, [url, query]);
useEffect(() => {
return () => {

View File

@@ -14,13 +14,11 @@ export type StorageKeys = "AUTOCOMPLETE"
| "THEME"
| "LOGS_LIMIT"
| "LOGS_MARKDOWN"
| "LOGS_ANSI"
| "LOGS_DISABLED_HOVERS"
| "EXPLORE_METRICS_TIPS"
| "LOGS_QUERY_HISTORY"
| "METRICS_QUERY_HISTORY"
| "SERVER_URL"
| "RAW_JSON_LIVE_VIEW"
| DeprecatedStorageKeys;

View File

@@ -22,8 +22,6 @@ var (
vminsertAddrRE = regexp.MustCompile(`accepting vminsert conns at (.*:\d{1,5})$`)
vminsertClusterNativeAddrRE = regexp.MustCompile(`started TCP clusternative server at "(.*:\d{1,5})"`)
vmselectAddrRE = regexp.MustCompile(`accepting vmselect conns at (.*:\d{1,5})$`)
logsStorageDataPathRE = regexp.MustCompile(`opening storage at -storageDataPath=(.*)`)
)
// app represents an instance of some VictoriaMetrics server (such as vmstorage,

View File

@@ -1,11 +1,8 @@
package apptest
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net/url"
"slices"
@@ -114,30 +111,6 @@ func (qos *QueryOpts) getTenant() string {
return qos.Tenant
}
// QueryOptsLogs contains various params used for VictoriaLogs querying or ingesting data
type QueryOptsLogs struct {
MessageField string
StreamFields string
TimeField string
}
func (qos *QueryOptsLogs) asURLValues() url.Values {
uv := make(url.Values)
addNonEmpty := func(name string, values ...string) {
for _, value := range values {
if len(value) == 0 {
continue
}
uv.Add(name, value)
}
}
addNonEmpty("_time_field", qos.TimeField)
addNonEmpty("_stream_fields", qos.StreamFields)
addNonEmpty("_msg_field", qos.MessageField)
return uv
}
// PrometheusAPIV1QueryResponse is an inmemory representation of the
// /prometheus/api/v1/query or /prometheus/api/v1/query_range response.
type PrometheusAPIV1QueryResponse struct {
@@ -395,13 +368,6 @@ type TSDBStatusResponse struct {
Data TSDBStatusResponseData
}
// AdminTenantsResponse is an in-memory representation of the json response
// returned by the /api/v1/admin/tenants endpoint.
type AdminTenantsResponse struct {
Status string
Data []string
}
// Sort performs sorting of stats entries
func (tsr *TSDBStatusResponse) Sort() {
sortTSDBStatusResponseEntries(tsr.Data.SeriesCountByLabelName)
@@ -444,44 +410,3 @@ func sortTSDBStatusResponseEntries(entries []TSDBStatusResponseEntry) {
return left.Count < right.Count
})
}
// LogsQLQueryResponse is an in-memory representation of the
// /select/logsql/query response.
type LogsQLQueryResponse struct {
LogLines []string
}
// NewLogsQLQueryResponse is a test helper function that creates a new
// instance of LogsQLQueryResponse by unmarshalling a json string.
func NewLogsQLQueryResponse(t *testing.T, s string) *LogsQLQueryResponse {
t.Helper()
res := &LogsQLQueryResponse{}
if len(s) == 0 {
return res
}
bs := bytes.NewBufferString(s)
for {
logLine, err := bs.ReadString('\n')
if err != nil {
if errors.Is(err, io.EOF) {
if len(logLine) > 0 {
t.Fatalf("BUG: unexpected non-empty line=%q with io.EOF", logLine)
}
break
}
t.Fatalf("BUG: cannot read logline from buffer: %s", err)
}
var lv map[string]any
if err := json.Unmarshal([]byte(logLine), &lv); err != nil {
t.Fatalf("cannot parse log line=%q: %s", logLine, err)
}
delete(lv, "_stream_id")
normalizedLine, err := json.Marshal(lv)
if err != nil {
t.Fatalf("cannot marshal parsed logline=%q: %s", logLine, err)
}
res.LogLines = append(res.LogLines, string(normalizedLine))
}
return res
}

View File

@@ -189,36 +189,6 @@ func (tc *TestCase) MustStartVmauth(instance string, flags []string, configFileY
return app
}
// MustStartVmbackup is a test helper that starts an instance of vmbackup
// and waits until the app exits. It fails the test if the app fails to start or
// exits with non zero code.
func (tc *TestCase) MustStartVmbackup(instance, storageDataPath, snapshotCreateURL, dst string) {
tc.t.Helper()
if err := StartVmbackup(instance, storageDataPath, snapshotCreateURL, dst); err != nil {
tc.t.Fatalf("vmbackup %q failed to start or exited with non-zero code: %v", instance, err)
}
// Do not add the process to the list of running apps using
// tc.addApp(instance, app), because the method blocks until the process
// exits.
}
// MustStartVmrestore is a test helper that starts an instance of vmrestore
// and waits until the app exits. It fails the test if the app fails to start or
// exits with non zero code.
func (tc *TestCase) MustStartVmrestore(instance, src, storageDataPath string) {
tc.t.Helper()
if err := StartVmrestore(instance, src, storageDataPath); err != nil {
tc.t.Fatalf("vmrestore %q failed to start or exited with non-zero code: %v", instance, err)
}
// Do not add the process to the list of running apps using
// tc.addApp(instance, app), because the method blocks until the process
// exits.
}
// MustStartDefaultCluster starts a typical cluster configuration with default
// flags.
func (tc *TestCase) MustStartDefaultCluster() *Vmcluster {
@@ -284,13 +254,15 @@ func (tc *TestCase) MustStartCluster(opts *ClusterOptions) *Vmcluster {
}
// MustStartVmctl is a test helper function that starts an instance of vmctl
func (tc *TestCase) MustStartVmctl(instance string, flags []string) {
func (tc *TestCase) MustStartVmctl(instance string, flags []string) *Vmctl {
tc.t.Helper()
err := StartVmctl(instance, flags)
app, err := StartVmctl(instance, flags)
if err != nil {
tc.t.Fatalf("Could not start %s: %v", instance, err)
}
tc.addApp(instance, app)
return app
}
func (tc *TestCase) addApp(instance string, app Stopper) {
@@ -409,27 +381,3 @@ func (tc *TestCase) Assert(opts *AssertOptions) {
tc.t.Error(msg)
}
}
// MustStartDefaultVlsingle is a test helper function that starts an instance of
// vlsingle with defaults suitable for most tests.
func (tc *TestCase) MustStartDefaultVlsingle() *Vlsingle {
tc.t.Helper()
return tc.MustStartVlsingle("vlsingle", []string{
"-storageDataPath=" + tc.Dir() + "/vlsingle",
"-retentionPeriod=100y",
})
}
// MustStartVlsingle is a test helper function that starts an instance of
// vlsingle and fails the test if the app fails to start.
func (tc *TestCase) MustStartVlsingle(instance string, flags []string) *Vlsingle {
tc.t.Helper()
app, err := StartVlsingle(instance, flags, tc.cli)
if err != nil {
tc.t.Fatalf("Could not start %s: %v", instance, err)
}
tc.addApp(instance, app)
return app
}

View File

@@ -1,239 +0,0 @@
package tests
import (
"fmt"
"path/filepath"
"slices"
"testing"
"time"
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
)
type testBackupRestoreOpts struct {
startSUT func() at.PrometheusWriteQuerier
stopSUT func()
storageDataPaths []string
snapshotCreateURLs func(at.PrometheusWriteQuerier) []string
}
func TestSingleBackupRestore(t *testing.T) {
tc := at.NewTestCase(t)
defer tc.Stop()
storageDataPath := filepath.Join(tc.Dir(), "vmsingle")
opts := testBackupRestoreOpts{
startSUT: func() at.PrometheusWriteQuerier {
return tc.MustStartVmsingle("vmsingle", []string{
"-storageDataPath=" + storageDataPath,
"-retentionPeriod=100y",
"-search.maxStalenessInterval=1m",
})
},
stopSUT: func() {
tc.StopApp("vmsingle")
},
storageDataPaths: []string{
storageDataPath,
},
snapshotCreateURLs: func(sut at.PrometheusWriteQuerier) []string {
return []string{
sut.(*at.Vmsingle).SnapshotCreateURL(),
}
},
}
testBackupRestore(tc, opts)
}
func TestClusterBackupRestore(t *testing.T) {
tc := at.NewTestCase(t)
defer tc.Stop()
storage1DataPath := filepath.Join(tc.Dir(), "vmstorage1")
storage2DataPath := filepath.Join(tc.Dir(), "vmstorage2")
opts := testBackupRestoreOpts{
startSUT: func() at.PrometheusWriteQuerier {
return tc.MustStartCluster(&at.ClusterOptions{
Vmstorage1Instance: "vmstorage1",
Vmstorage1Flags: []string{
"-storageDataPath=" + storage1DataPath,
"-retentionPeriod=100y",
},
Vmstorage2Instance: "vmstorage2",
Vmstorage2Flags: []string{
"-storageDataPath=" + storage2DataPath,
"-retentionPeriod=100y",
},
VminsertInstance: "vminsert",
VminsertFlags: []string{},
VmselectInstance: "vmselect",
VmselectFlags: []string{
"-search.maxStalenessInterval=1m",
},
})
},
stopSUT: func() {
tc.StopApp("vminsert")
tc.StopApp("vmselect")
tc.StopApp("vmstorage1")
tc.StopApp("vmstorage2")
},
storageDataPaths: []string{
storage1DataPath,
storage2DataPath,
},
snapshotCreateURLs: func(sut at.PrometheusWriteQuerier) []string {
c := sut.(*at.Vmcluster)
return []string{
c.Vmstorages[0].SnapshotCreateURL(),
c.Vmstorages[1].SnapshotCreateURL(),
}
},
}
testBackupRestore(tc, opts)
}
func testBackupRestore(tc *at.TestCase, opts testBackupRestoreOpts) {
t := tc.T()
const msecPerMinute = 60 * 1000
genData := func(count int, prefix string, start int64) (recs []string, wantSeries []map[string]string, wantQueryResults []*at.QueryResult) {
recs = make([]string, count)
wantSeries = make([]map[string]string, count)
wantQueryResults = make([]*at.QueryResult, count)
for i := range count {
name := fmt.Sprintf("%s_%03d", prefix, i)
value := float64(i)
timestamp := start + int64(i)*msecPerMinute
recs[i] = fmt.Sprintf("%s %f %d", name, value, timestamp)
wantSeries[i] = map[string]string{"__name__": name}
wantQueryResults[i] = &at.QueryResult{
Metric: map[string]string{"__name__": name},
Samples: []*at.Sample{{Timestamp: timestamp, Value: value}},
}
}
return recs, wantSeries, wantQueryResults
}
backupBaseDir, err := filepath.Abs(filepath.Join(tc.Dir(), "backups"))
if err != nil {
t.Fatalf("could not get absolute path for the backup base dir")
}
// assertSeries retrieves set of all metric names from the storage and
// compares it with the expected set.
assertSeries := func(app at.PrometheusQuerier, query string, start, end int64, want []map[string]string) {
t.Helper()
tc.Assert(&at.AssertOptions{
Msg: "unexpected /api/v1/series response",
Got: func() any {
return app.PrometheusAPIV1Series(t, query, at.QueryOpts{
Start: fmt.Sprintf("%d", start),
End: fmt.Sprintf("%d", end),
}).Sort()
},
Want: &at.PrometheusAPIV1SeriesResponse{
Status: "success",
Data: want,
},
FailNow: true,
})
}
// assertSeries retrieves all data from the storage and compares it with the
// expected result.
assertQueryResults := func(app at.PrometheusQuerier, query string, start, end int64, want []*at.QueryResult) {
t.Helper()
tc.Assert(&at.AssertOptions{
Msg: "unexpected /api/v1/query_range response",
Got: func() any {
return app.PrometheusAPIV1QueryRange(t, query, at.QueryOpts{
Start: fmt.Sprintf("%d", start),
End: fmt.Sprintf("%d", end),
Step: "60s",
})
},
Want: &at.PrometheusAPIV1QueryResponse{
Status: "success",
Data: &at.QueryData{
ResultType: "matrix",
Result: want,
},
},
FailNow: true,
Retries: 300,
})
}
createBackup := func(sut at.PrometheusWriteQuerier, name string) {
for i, storageDataPath := range opts.storageDataPaths {
replica := fmt.Sprintf("replica-%d", i)
instance := fmt.Sprintf("vmbackup-%s-%s", name, replica)
snapshotCreateURL := opts.snapshotCreateURLs(sut)[i]
backupPath := "fs://" + filepath.Join(backupBaseDir, name, replica)
tc.MustStartVmbackup(instance, storageDataPath, snapshotCreateURL, backupPath)
}
}
restoreFromBackup := func(name string) {
for i, storageDataPath := range opts.storageDataPaths {
replica := fmt.Sprintf("replica-%d", i)
instance := fmt.Sprintf("vmrestore-%s-%s", name, replica)
backupPath := "fs://" + filepath.Join(backupBaseDir, name, replica)
tc.MustStartVmrestore(instance, backupPath, storageDataPath)
}
}
// Use the same number of metrics and time range for all the data ingestions
// below.
const numMetrics = 1000
// With 1000 metrics (one per minute), the time range spans 2 months.
end := time.Date(2025, 3, 1, 10, 0, 0, 0, time.UTC).UnixMilli()
start := end - numMetrics*msecPerMinute
// Verify backup/restore:
//
// - Start vmsingle with empty storage data dir.
// - Ingest first batch or records (batch1) and ensure they can be queried.
// - Create batch1 backup
// - Ingest second batch of records (batch2) and ensure the queries return
// (batch1 + batch2) data.
// - Stop vmsingle
// - Restore batch1 from backup
// - Start vmsingle
// - Ensure that the queries return batch1 data only.
batch1Data, wantBatch1Series, wantBatch1QueryResults := genData(numMetrics, "batch1", start)
batch2Data, wantBatch2Series, wantBatch2QueryResults := genData(numMetrics, "batch2", start)
wantBatch12Series := slices.Concat(wantBatch1Series, wantBatch2Series)
wantBatch12QueryResults := slices.Concat(wantBatch1QueryResults, wantBatch2QueryResults)
sut := opts.startSUT()
sut.PrometheusAPIV1ImportPrometheus(t, batch1Data, at.QueryOpts{})
sut.ForceFlush(t)
assertSeries(sut, `{__name__=~"batch1.*"}`, start, end, wantBatch1Series)
assertQueryResults(sut, `{__name__=~"batch1.*"}`, start, end, wantBatch1QueryResults)
createBackup(sut, "batch1")
sut.PrometheusAPIV1ImportPrometheus(t, batch2Data, at.QueryOpts{})
sut.ForceFlush(t)
assertSeries(sut, `{__name__=~"batch(1|2).*"}`, start, end, wantBatch12Series)
assertQueryResults(sut, `{__name__=~"batch(1|2).*"}`, start, end, wantBatch12QueryResults)
createBackup(sut, "batch12")
opts.stopSUT()
restoreFromBackup("batch1")
sut = opts.startSUT()
assertSeries(sut, `{__name__=~"batch1.*"}`, start, end, wantBatch1Series)
assertQueryResults(sut, `{__name__=~"batch1.*"}`, start, end, wantBatch1QueryResults)
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/google/go-cmp/cmp/cmpopts"
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
pb "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
@@ -494,65 +493,3 @@ func TestClusterIngestionProtocols(t *testing.T) {
},
})
}
func TestVlsingleIngestionProtocols(t *testing.T) {
os.RemoveAll(t.Name())
tc := at.NewTestCase(t)
defer tc.Stop()
sut := tc.MustStartDefaultVlsingle()
type opts struct {
query string
wantLogLines []string
}
f := func(opts *opts) {
t.Helper()
sut.ForceFlush(t)
got := sut.LogsQLQuery(t, opts.query, at.QueryOptsLogs{})
assertLogsQLResponseEqual(t, got, &at.LogsQLQueryResponse{LogLines: opts.wantLogLines})
}
// json line ingest
sut.JSONLineWrite(t, []string{
`{"_msg":"ingest jsonline","_time": "2025-06-05T14:30:19.088007Z", "foo":"bar"}`,
`{"_msg":"ingest jsonline","_time": "2025-06-05T14:30:19.088007Z", "bar":"foo"}`,
}, at.QueryOptsLogs{})
f(&opts{
query: "ingest jsonline",
wantLogLines: []string{
`{"_msg":"ingest jsonline","_stream":"{}","_time":"2025-06-05T14:30:19.088007Z","bar":"foo"}`,
`{"_msg":"ingest jsonline","_stream":"{}","_time":"2025-06-05T14:30:19.088007Z","foo":"bar"}`,
},
})
// native format ingest
sut.NativeWrite(t, []logstorage.InsertRow{
{
StreamTagsCanonical: canonicalStreamTagsFromSet(map[string]string{"foo": "bar"}),
Timestamp: 1749141697409000000, // 2025-06-05T:18:41:37.000000Z
Fields: []logstorage.Field{
{
Name: "_msg",
Value: "ingest native",
},
{
Name: "qwe",
Value: "rty",
},
},
},
}, at.QueryOpts{})
f(&opts{
query: "ingest native",
wantLogLines: []string{
`{"_msg":"ingest native","_time":"2025-06-05T16:41:37.409Z", "_stream":"{foo=\"bar\"}", "qwe": "rty"}`,
},
})
}
func canonicalStreamTagsFromSet(set map[string]string) string {
var st logstorage.StreamTags
for key, value := range set {
st.Add(key, value)
}
return string(st.MarshalCanonical(nil))
}

View File

@@ -1,10 +1,6 @@
package tests
import (
"encoding/json"
"os"
"sort"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
@@ -371,131 +367,3 @@ func testMillisecondPrecisionInInstantQueries(tc *at.TestCase, sut at.Prometheus
wantSample: &at.Sample{Timestamp: 1642801537346, Value: 2},
})
}
// TestVlsingleKeyConcepts verifies cases from https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model
// for vl-single.
func TestVlsingleKeyConcepts(t *testing.T) {
os.RemoveAll(t.Name())
tc := at.NewTestCase(t)
defer tc.Stop()
sut := tc.MustStartDefaultVlsingle()
type opts struct {
ingestRecords []string
ingestQueryArgs at.QueryOptsLogs
wantResponse *at.LogsQLQueryResponse
query string
selectQueryArgs at.QueryOptsLogs
}
f := func(opts *opts) {
t.Helper()
sut.JSONLineWrite(t, opts.ingestRecords, opts.ingestQueryArgs)
sut.ForceFlush(t)
got := sut.LogsQLQuery(t, opts.query, opts.selectQueryArgs)
assertLogsQLResponseEqual(t, got, opts.wantResponse)
}
// nested objects flatten
f(&opts{
ingestRecords: []string{
`{"_msg":"case 1","_time": "2025-06-05T14:30:19.088007Z", "host": {"name": "foobar","os": {"version": "1.2.3"}}}`,
`{"_msg":"case 1","_time": "2025-06-05T14:30:19.088007Z", "tags": ["foo", "bar"], "offset": 12345, "is_error": false}`,
},
wantResponse: &at.LogsQLQueryResponse{
LogLines: []string{
`{"_msg":"case 1","_stream":"{}","_time":"2025-06-05T14:30:19.088007Z","host.name":"foobar","host.os.version":"1.2.3"}`,
`{"_msg":"case 1","_stream":"{}","_time":"2025-06-05T14:30:19.088007Z","is_error":"false","offset":"12345","tags":"[\"foo\",\"bar\"]"}`,
},
},
query: "case 1",
})
// obtain _msg value from non-default field
f(&opts{
ingestRecords: []string{
`{"my_msg":"case 2","_time": "2025-06-05T14:30:19.088007Z", "foo":"bar"}`,
`{"my_msg_other":"case 2","_time": "2025-06-05T14:30:19.088007Z", "bar":"foo"}`,
},
ingestQueryArgs: at.QueryOptsLogs{
MessageField: "my_msg,my_msg_other",
},
query: "case 2",
wantResponse: &at.LogsQLQueryResponse{
LogLines: []string{
`{"_msg":"case 2","_stream":"{}","_time":"2025-06-05T14:30:19.088007Z","foo":"bar"}`,
`{"_msg":"case 2","_stream":"{}","_time":"2025-06-05T14:30:19.088007Z","bar":"foo"}`,
},
},
})
// populate stream fields
f(&opts{
ingestRecords: []string{
`{"my_msg":"case 3","_time": "2025-06-05T14:30:19.088007Z", "foo":"bar"}`,
`{"my_msg":"case 3","_time": "2025-06-05T14:30:19.088007Z", "bar":"foo"}`,
`{"my_msg":"case 3","_time": "2025-06-05T14:30:19.088007Z", "bar":"foo","foo":"bar","baz":"bar"}`,
},
ingestQueryArgs: at.QueryOptsLogs{
MessageField: "my_msg",
StreamFields: "foo,bar,baz",
},
wantResponse: &at.LogsQLQueryResponse{
LogLines: []string{
`{"_msg":"case 3","_stream":"{foo=\"bar\"}","_time":"2025-06-05T14:30:19.088007Z","foo":"bar"}`,
`{"_msg":"case 3","_stream":"{bar=\"foo\"}","_time":"2025-06-05T14:30:19.088007Z","bar":"foo"}`,
`{"_msg":"case 3","_stream":"{bar=\"foo\",baz=\"bar\",foo=\"bar\"}","_time":"2025-06-05T14:30:19.088007Z","bar":"foo","foo":"bar","baz":"bar"}`,
},
},
query: "case 3",
})
// obtain _time value from non-default field
f(&opts{
ingestRecords: []string{
`{"_msg":"case 4","my_time_field": "2025-06-05T14:30:19.088007Z", "foo":"bar"}`,
`{"_msg":"case 4","my_other_time_field": "2025-06-05T14:30:19.088007Z", "bar":"foo"}`,
},
ingestQueryArgs: at.QueryOptsLogs{
TimeField: "my_time_field,my_other_time_field",
},
wantResponse: &at.LogsQLQueryResponse{
LogLines: []string{
`{"_msg":"case 4","_stream":"{}","_time":"2025-06-05T14:30:19.088007Z","foo":"bar"}`,
`{"_msg":"case 4","_stream":"{}","_time":"2025-06-05T14:30:19.088007Z","bar":"foo"}`,
},
},
query: "case 4",
})
}
func assertLogsQLResponseEqual(t *testing.T, got, want *at.LogsQLQueryResponse) {
t.Helper()
sort.Strings(got.LogLines)
sort.Strings(want.LogLines)
if len(got.LogLines) != len(want.LogLines) {
t.Errorf("unexpected response len: -%d: +%d\n%s", len(want.LogLines), len(got.LogLines), strings.Join(got.LogLines, "\n"))
return
}
for i := range len(want.LogLines) {
gotLine, wantLine := got.LogLines[i], want.LogLines[i]
var gotLineJSON map[string]any
var wantLineJSON map[string]any
if err := json.Unmarshal([]byte(gotLine), &gotLineJSON); err != nil {
t.Errorf("cannot parse got line=%q: %s", gotLine, err)
return
}
if err := json.Unmarshal([]byte(wantLine), &wantLineJSON); err != nil {
t.Errorf("cannot parse want line=%q: %s", wantLine, err)
return
}
// stream_id is always unique, remove it from comparison
delete(gotLineJSON, "_stream_id")
delete(wantLineJSON, "_stream_id")
if diff := cmp.Diff(gotLineJSON, wantLineJSON); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s\n%s\n%s", diff, wantLine, gotLine)
return
}
}
}

View File

@@ -1,62 +0,0 @@
package tests
import (
"context"
"fmt"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/storage/remote"
)
// PrometheusMockStorage is a mock implementation of the Prometheus remote read storage interface.
type PrometheusMockStorage struct {
query *prompb.Query
store []*prompb.TimeSeries
b labels.ScratchBuilder
}
// NewPrometheusMockStorage creates a new PrometheusMockStorage with the provided series.
func NewPrometheusMockStorage(series []*prompb.TimeSeries) *PrometheusMockStorage {
return &PrometheusMockStorage{store: series}
}
// Read implements the storage.Storage interface for reading time series data.
func (ms *PrometheusMockStorage) Read(_ context.Context, query *prompb.Query, sortSeries bool) (storage.SeriesSet, error) {
if ms.query != nil {
return nil, fmt.Errorf("expected only one call to remote client got: %v", query)
}
ms.query = query
matchers, err := remote.FromLabelMatchers(query.Matchers)
if err != nil {
return nil, err
}
q := &prompb.QueryResult{}
for _, s := range ms.store {
l := s.ToLabels(&ms.b, nil)
var notMatch bool
for _, m := range matchers {
if v := l.Get(m.Name); v != "" {
if !m.Matches(v) {
notMatch = true
break
}
}
}
if !notMatch {
q.Timeseries = append(q.Timeseries, &prompb.TimeSeries{Labels: s.Labels, Samples: s.Samples})
}
}
return remote.FromQueryResult(sortSeries, q), nil
}
// Reset resets the PrometheusMockStorage, clearing any stored query and series.
func (ms *PrometheusMockStorage) Reset() {
ms.query = nil
}

View File

@@ -1,78 +0,0 @@
package tests
import (
"os"
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestClusterRollupResultCache(t *testing.T) {
os.RemoveAll(t.Name())
cmpOpt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
tc := apptest.NewTestCase(t)
defer tc.Stop()
vmstorage := tc.MustStartVmstorage("vmstorage", []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage",
"-retentionPeriod=100y",
})
vminsert := tc.MustStartVminsert("vminsert", []string{
"-storageNode=" + vmstorage.VminsertAddr(),
})
vmselect := tc.MustStartVmselect("vmselect", []string{
"-storageNode=" + vmstorage.VmselectAddr(),
"-search.tenantCacheExpireDuration=0",
})
var tenantLabelsSamples = []string{
`foo_bar{vm_account_id="5"} 1.00 1652169720000`, // 2022-05-10T08:00:00Z'
`foo_bar{vm_account_id="5",vm_project_id="15"} 3.00 1652169720000`, // 2022-05-10T08:02:00Z
}
vminsert.PrometheusAPIV1ImportPrometheus(t, tenantLabelsSamples, apptest.QueryOpts{Tenant: "multitenant"})
vmstorage.ForceFlush(t)
want := apptest.NewPrometheusAPIV1QueryResponse(t,
`{"data":
{"result":[
{"metric":{"__name__":"foo_bar","vm_account_id":"5","vm_project_id": "0"},"values":[[1652169720,"1"],[1652169780,"1"]]},
{"metric":{"__name__":"foo_bar","vm_account_id":"5","vm_project_id":"15"},"values":[[1652169720,"3"],[1652169780,"3"]]}
]
}
}`,
)
got := vmselect.PrometheusAPIV1QueryRange(t, `foo_bar{}`, apptest.QueryOpts{
Tenant: "multitenant",
Start: "2022-05-10T07:59:00.000Z",
End: "2022-05-10T08:05:00.000Z",
Step: "1m",
ExtraFilters: []string{`{vm_account_id="5",vm_project_id="15"}`, `{vm_account_id="5",vm_project_id="0"}`},
})
if diff := cmp.Diff(want, got, cmpOpt); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
want = apptest.NewPrometheusAPIV1QueryResponse(t,
`{"data":
{"result":[]}
}`,
)
got = vmselect.PrometheusAPIV1QueryRange(t, `foo_bar{}`, apptest.QueryOpts{
Tenant: "multitenant",
Start: "2022-05-10T07:59:00.000Z",
End: "2022-05-10T08:05:00.000Z",
Step: "1m",
ExtraFilters: []string{`{vm_account_id="99",vm_project_id="99"}`},
})
if diff := cmp.Diff(want, got, cmpOpt); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
}

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"sync"
"testing"
@@ -12,82 +11,6 @@ import (
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
)
// TestSingleVMAgentReloadConfigs verifies that vmagent reload new configurations on SIGHUP signal
func TestSingleVMAgentReloadConfigs(t *testing.T) {
tc := apptest.NewTestCase(t)
defer tc.Stop()
vmsingle := tc.MustStartDefaultVmsingle()
relabelingRules := `
- replacement: value1
target_label: label1
`
relabelFilePath := fmt.Sprintf("%s/%s", t.TempDir(), "relabel_config.yaml")
if err := os.WriteFile(relabelFilePath, []byte(relabelingRules), os.ModePerm); err != nil {
t.Fatalf("cannot create file=%q: %s", relabelFilePath, err)
}
vmagent := tc.MustStartVmagent("vmagent", []string{
`-remoteWrite.flushInterval=50ms`,
`-remoteWrite.forcePromProto=true`,
"-remoteWrite.tmpDataPath=" + tc.Dir() + "/vmagent",
fmt.Sprintf(`-remoteWrite.url=http://%s/api/v1/write`, vmsingle.HTTPAddr()),
fmt.Sprintf(`-remoteWrite.urlRelabelConfig=%s`, relabelFilePath),
}, ``)
vmagent.APIV1ImportPrometheus(t, []string{
"foo_bar 1 1652169600000", // 2022-05-10T08:00:00Z
}, apptest.QueryOpts{})
vmsingle.ForceFlush(t)
tc.Assert(&at.AssertOptions{
Msg: `unexpected metrics stored on vmagent remote write`,
Got: func() any {
return vmsingle.PrometheusAPIV1Series(t, `{__name__="foo_bar"}`, at.QueryOpts{
Start: "2022-05-10T00:00:00Z",
End: "2022-05-10T23:59:59Z",
}).Sort()
},
Want: &at.PrometheusAPIV1SeriesResponse{
Status: "success",
Data: []map[string]string{{"__name__": "foo_bar", "label1": "value1"}},
},
})
relabelingRules = `
- replacement: value2
target_label: label1
`
if err := os.WriteFile(relabelFilePath, []byte(relabelingRules), os.ModePerm); err != nil {
t.Fatalf("cannot create file=%q: %s", relabelFilePath, err)
}
vmagent.ReloadRelabelConfigs(t)
vmagent.APIV1ImportPrometheus(t, []string{
"bar_foo 1 1652169600001", // 2022-05-10T08:00:00Z
}, apptest.QueryOpts{})
vmsingle.ForceFlush(t)
tc.Assert(&at.AssertOptions{
Msg: `unexpected metrics stored on vmagent remote write`,
Got: func() any {
return vmsingle.PrometheusAPIV1Series(t, `{__name__="bar_foo"}`, at.QueryOpts{
Start: "2022-05-10T00:00:00Z",
End: "2022-05-10T23:59:59Z",
}).Sort()
},
Want: &at.PrometheusAPIV1SeriesResponse{
Status: "success",
Data: []map[string]string{{"__name__": "bar_foo", "label1": "value2"}},
},
})
}
// TestSingleVMAgentZstdRemoteWrite verifies that vmagent can successfully perform
// a remote write to vmsingle using VM protocol (zstd).
func TestSingleVMAgentZstdRemoteWrite(t *testing.T) {

View File

@@ -1,149 +0,0 @@
package tests
import (
"fmt"
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
)
func TestSingleToSingleVmctlNativeProtocol(t *testing.T) {
os.RemoveAll(t.Name())
tc := apptest.NewTestCase(t)
defer tc.Stop()
vmsingleSrc := tc.MustStartVmsingle("vmsingle_src", []string{
"-storageDataPath=" + tc.Dir() + "/vmsingle_src",
"-retentionPeriod=100y",
})
// we need a separate vmsingle for the destination to avoid conflicts
vmsingleDst := tc.MustStartVmsingle("vmsingle_dst", []string{
"-storageDataPath=" + tc.Dir() + "/vmsingle_dst",
"-retentionPeriod=100y",
})
vmSrcAddr := fmt.Sprintf("http://%s/", vmsingleSrc.HTTPAddr())
vmDstAddr := fmt.Sprintf("http://%s/", vmsingleDst.HTTPAddr())
flags := []string{
`vm-native`,
`--vm-native-src-addr=` + vmSrcAddr,
`--vm-native-dst-addr=` + vmDstAddr,
`--vm-native-filter-match={__name__=~".*"}`,
`--vm-native-filter-time-start=2025-05-30T16:39:00Z`,
`--disable-progress-bar=true`,
}
testVmctlNativeProtocol(tc, vmsingleSrc, vmsingleDst, flags)
}
func TestClusterTenantsToTenantsVmctlNativeProtocol(t *testing.T) {
os.RemoveAll(t.Name())
tc := apptest.NewTestCase(t)
defer tc.Stop()
clusterSrc := tc.MustStartCluster(&apptest.ClusterOptions{
Vmstorage1Instance: "vmstorageSrc1",
Vmstorage2Instance: "vmstorageSrc2",
VminsertInstance: "vminsertSrc",
VmselectInstance: "vmselectSrc",
})
clusterDst := tc.MustStartCluster(&apptest.ClusterOptions{
Vmstorage1Instance: "vmstorageDst1",
Vmstorage2Instance: "vmstorageDst2",
VminsertInstance: "vminsertDst",
VmselectInstance: "vmselectDst",
})
vmSrcAddr := fmt.Sprintf("http://%s/", clusterSrc.Vmselect.HTTPAddr())
vmDstAddr := fmt.Sprintf("http://%s/", clusterDst.Vminsert.HTTPAddr())
flags := []string{
`vm-native`,
`--vm-native-src-addr=` + vmSrcAddr,
`--vm-native-dst-addr=` + vmDstAddr,
`--vm-native-filter-match={__name__=~".*"}`,
`--vm-native-filter-time-start=2025-05-30T16:39:00Z`,
`--disable-progress-bar=true`,
`--vm-intercluster`,
}
testVmctlNativeProtocol(tc, clusterSrc, clusterDst, flags)
}
func testVmctlNativeProtocol(tc *apptest.TestCase, srcSut apptest.PrometheusWriteQuerier, dstSut apptest.PrometheusWriteQuerier, vmctlFlags []string) {
t := tc.T()
t.Helper()
cmpOpt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
// test for empty data request in the source
got := srcSut.PrometheusAPIV1Query(t, `{__name__=~".*"}`, apptest.QueryOpts{
Step: "5m",
Time: "2025-05-30T12:45:00Z",
})
want := apptest.NewPrometheusAPIV1QueryResponse(t, `{"data":{"result":[]}}`)
if diff := cmp.Diff(want, got, cmpOpt); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
// Prepare the source vmsingle with some data
// Insert some data.
const numSamples = 1000
const ingestTimestamp = " 1748623176000" // 2025-05-30T16:39:36Z
expectedQueryData := apptest.QueryData{
ResultType: "matrix",
Result: make([]*apptest.QueryResult, 0, numSamples),
}
dataSet := make([]string, numSamples)
for i := range numSamples {
metricsName := fmt.Sprintf("metric_%03d", i)
metrics := map[string]string{"__name__": metricsName}
sample := &apptest.Sample{Value: float64(i), Timestamp: 1748623176000}
expectedQueryData.Result = append(expectedQueryData.Result, &apptest.QueryResult{
Metric: metrics,
Samples: []*apptest.Sample{sample},
})
dataSet[i] = fmt.Sprintf("%s %d %s", metricsName, i, ingestTimestamp)
}
wantResponse := apptest.PrometheusAPIV1QueryResponse{
Status: "success",
Data: &expectedQueryData,
}
wantResponse.Sort()
srcSut.PrometheusAPIV1ImportPrometheus(t, dataSet, apptest.QueryOpts{})
srcSut.ForceFlush(t)
tc.MustStartVmctl("vmctl", vmctlFlags)
dstSut.ForceFlush(t)
tc.Assert(&apptest.AssertOptions{
Retries: 300,
Msg: `unexpected metrics stored on vmsingle via the native protocol`,
Got: func() any {
exported := dstSut.PrometheusAPIV1Export(t, `{__name__=~".*"}`, apptest.QueryOpts{
Start: "2025-05-30T16:39:36Z",
End: "2025-05-30T16:39:37Z",
})
exported.Sort()
return exported.Data.Result
},
Want: wantResponse.Data.Result,
CmpOpts: []cmp.Option{
cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType"),
},
})
}

View File

@@ -72,7 +72,7 @@ func testPrometheusProtocol(tc *apptest.TestCase, sut apptest.PrometheusWriteQue
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
tc.MustStartVmctl("vmctl", vmctlFlags)
_ = tc.MustStartVmctl("vmctl", vmctlFlags)
sut.ForceFlush(t)

View File

@@ -1,157 +0,0 @@
package tests
import (
"fmt"
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/prometheus/prometheus/prompb"
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
)
func TestSingleVmctlRemoteReadProtocol(t *testing.T) {
os.RemoveAll(t.Name())
tc := at.NewTestCase(t)
defer tc.Stop()
vmsingleDst := tc.MustStartDefaultVmsingle()
vmAddr := fmt.Sprintf("http://%s/", vmsingleDst.HTTPAddr())
vmctlFlags := []string{
`remote-read`,
`--remote-read-filter-time-start=2025-06-11T15:31:10Z`,
`--remote-read-filter-time-end=2025-06-11T15:31:20Z`,
`--remote-read-step-interval=minute`,
`--vm-addr=` + vmAddr,
`--disable-progress-bar=true`,
}
testRemoteReadProtocol(tc, vmsingleDst, newRemoteReadServer, vmctlFlags)
}
func TestSingleVmctlRemoteReadStreamProtocol(t *testing.T) {
os.RemoveAll(t.Name())
tc := at.NewTestCase(t)
defer tc.Stop()
vmsingleDst := tc.MustStartDefaultVmsingle()
vmAddr := fmt.Sprintf("http://%s/", vmsingleDst.HTTPAddr())
vmctlFlags := []string{
`remote-read`,
`--remote-read-filter-time-start=2025-06-11T15:31:10Z`,
`--remote-read-filter-time-end=2025-06-11T15:31:20Z`,
`--remote-read-step-interval=minute`,
`--vm-addr=` + vmAddr,
`--remote-read-use-stream=true`,
`--disable-progress-bar=true`,
}
testRemoteReadProtocol(tc, vmsingleDst, newRemoteReadStreamServer, vmctlFlags)
}
func TestClusterVmctlRemoteReadProtocol(t *testing.T) {
os.RemoveAll(t.Name())
tc := at.NewTestCase(t)
defer tc.Stop()
clusterDst := tc.MustStartDefaultCluster()
vmAddr := fmt.Sprintf("http://%s/", clusterDst.Vminsert.HTTPAddr())
vmctlFlags := []string{
`remote-read`,
`--remote-read-filter-time-start=2025-06-11T15:31:10Z`,
`--remote-read-filter-time-end=2025-06-11T15:31:20Z`,
`--remote-read-step-interval=minute`,
`--vm-addr=` + vmAddr,
`--vm-account-id=0`,
`--disable-progress-bar=true`,
}
testRemoteReadProtocol(tc, clusterDst, newRemoteReadServer, vmctlFlags)
}
func testRemoteReadProtocol(tc *at.TestCase, sut at.PrometheusWriteQuerier, newRemoteReadServer func(t *testing.T) *RemoteReadServer, vmctlFlags []string) {
t := tc.T()
t.Helper()
rrs := newRemoteReadServer(t)
defer rrs.Close()
expectedResult := transformSeriesToQueryResult(rrs.storage.store)
cmpOpt := cmpopts.IgnoreFields(at.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
// test for empty data request
got := sut.PrometheusAPIV1Query(t, `{__name__=~".*"}`, at.QueryOpts{
Step: "5m",
Time: "2025-06-02T17:14:00Z",
})
want := at.NewPrometheusAPIV1QueryResponse(t, `{"data":{"result":[]}}`)
if diff := cmp.Diff(want, got, cmpOpt); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
vmctlFlags = append(vmctlFlags, `--remote-read-src-addr=`+rrs.HTTPAddr())
tc.MustStartVmctl("vmctl", vmctlFlags)
sut.ForceFlush(t)
tc.Assert(&at.AssertOptions{
// For cluster version, we need to wait longer for the metrics to be stored
Retries: 300,
Msg: `unexpected metrics stored on vmsingle via the prometheus protocol`,
Got: func() any {
expected := sut.PrometheusAPIV1Export(t, `{__name__=~".*"}`, at.QueryOpts{
Start: "2025-06-11T15:31:10Z",
End: "2025-06-11T15:32:20Z",
})
expected.Sort()
return expected.Data.Result
},
Want: expectedResult,
CmpOpts: []cmp.Option{
cmpopts.IgnoreFields(at.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType"),
},
})
}
func newRemoteReadServer(t *testing.T) *RemoteReadServer {
t.Helper()
series := GenerateRemoteReadSeries(1749655870, 1749655880, 10, 10)
rrServer := NewRemoteReadServer(t, series)
return rrServer
}
func newRemoteReadStreamServer(t *testing.T) *RemoteReadServer {
t.Helper()
series := GenerateRemoteReadSeries(1749655870, 1749655880, 10, 10)
rrServer := NewRemoteReadStreamServer(t, series)
return rrServer
}
func transformSeriesToQueryResult(series []*prompb.TimeSeries) []*at.QueryResult {
result := make([]*at.QueryResult, len(series))
for i, s := range series {
metric := make(map[string]string, len(s.Labels))
for _, label := range s.Labels {
metric[label.Name] = label.Value
}
samples := make([]*at.Sample, len(s.Samples))
for j, sample := range s.Samples {
samples[j] = &at.Sample{Timestamp: sample.Timestamp, Value: sample.Value}
}
result[i] = &at.QueryResult{Metric: metric, Samples: samples}
}
return result
}

View File

@@ -1,136 +0,0 @@
package apptest
import (
"fmt"
"net/http"
"os"
"regexp"
"strings"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
)
// Vlsingle holds the state of a vlsingle app and provides vlsingle-specific
// functions.
type Vlsingle struct {
*app
*ServesMetrics
storageDataPath string
httpListenAddr string
forceFlushURL string
}
// StartVlsingle starts an instance of vlsingle with the given flags. It also
// sets the default flags and populates the app instance state with runtime
// values extracted from the application log (such as httpListenAddr).
func StartVlsingle(instance string, flags []string, cli *Client) (*Vlsingle, error) {
app, stderrExtracts, err := startApp(instance, "../../bin/victoria-logs", flags, &appOptions{
defaultFlags: map[string]string{
"-storageDataPath": fmt.Sprintf("%s/%s-%d", os.TempDir(), instance, time.Now().UnixNano()),
"-httpListenAddr": "127.0.0.1:0",
},
extractREs: []*regexp.Regexp{
logsStorageDataPathRE,
httpListenAddrRE,
},
})
if err != nil {
return nil, err
}
return &Vlsingle{
app: app,
ServesMetrics: &ServesMetrics{
metricsURL: fmt.Sprintf("http://%s/metrics", stderrExtracts[1]),
cli: cli,
},
storageDataPath: stderrExtracts[0],
httpListenAddr: stderrExtracts[1],
forceFlushURL: fmt.Sprintf("http://%s/internal/force_flush", stderrExtracts[1]),
}, nil
}
// ForceFlush is a test helper function that forces the flushing of inserted
// data, so it becomes available for searching immediately.
func (app *Vlsingle) ForceFlush(t *testing.T) {
t.Helper()
_, statusCode := app.cli.Get(t, app.forceFlushURL)
if statusCode != http.StatusOK {
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
}
}
// JSONLineWrite is a test helper function that inserts a
// collection of records in json line format by sending a HTTP
// POST request to /insert/jsonline vlsingle endpoint.
//
// See https://docs.victoriametrics.com/victorialogs/data-ingestion/#json-stream-api
func (app *Vlsingle) JSONLineWrite(t *testing.T, records []string, opts QueryOptsLogs) {
t.Helper()
data := []byte(strings.Join(records, "\n"))
url := fmt.Sprintf("http://%s/insert/jsonline", app.httpListenAddr)
uv := opts.asURLValues()
uvs := uv.Encode()
if len(uvs) > 0 {
url += "?" + uvs
}
_, statusCode := app.cli.Post(t, url, "text/plain", data)
if statusCode != http.StatusOK {
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
}
}
// NativeWrite is a test helper function that sends a collection of records
// to /internal/insert API.
//
// See https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vlinsert/internalinsert/internalinsert.go
func (app *Vlsingle) NativeWrite(t *testing.T, records []logstorage.InsertRow, opts QueryOpts) {
t.Helper()
var data []byte
for _, record := range records {
data = record.Marshal(data)
}
dstURL := fmt.Sprintf("http://%s/internal/insert", app.httpListenAddr)
uv := opts.asURLValues()
uv.Add("version", "v1")
dstURL += "?" + uv.Encode()
app.cli.Post(t, dstURL, "application/octet-stream", data)
}
// LogsQLQuery is a test helper function that performs
// PromQL/MetricsQL range query by sending a HTTP POST request to
// /select/logsql/query endpoint.
//
// See https://docs.victoriametrics.com/victorialogs/querying/#querying-logs
func (app *Vlsingle) LogsQLQuery(t *testing.T, query string, opts QueryOptsLogs) *LogsQLQueryResponse {
t.Helper()
values := opts.asURLValues()
values.Add("query", query)
url := fmt.Sprintf("http://%s/select/logsql/query", app.httpListenAddr)
res, _ := app.cli.PostForm(t, url, values)
return NewLogsQLQueryResponse(t, res)
}
// HTTPAddr returns the address at which the vmstorage process is listening
// for http connections.
func (app *Vlsingle) HTTPAddr() string {
return app.httpListenAddr
}
// String returns the string representation of the vlsingle app state.
func (app *Vlsingle) String() string {
return fmt.Sprintf("{app: %s storageDataPath: %q httpListenAddr: %q}", []any{
app.app, app.storageDataPath, app.httpListenAddr}...)
}

View File

@@ -6,7 +6,6 @@ import (
"os"
"regexp"
"strings"
"syscall"
"testing"
"time"
)
@@ -102,30 +101,6 @@ func (app *Vmagent) RemoteWritePacketsDroppedTotal(t *testing.T) int {
return int(total)
}
// ReloadRelabelConfigs sends SIGHUP to trigger relabel config reload
// and waits until vmagent_relabel_config_reloads_total increases.
// Fails the test if no reload is detected within 3 seconds.
func (app *Vmagent) ReloadRelabelConfigs(t *testing.T) {
prevTotal := app.GetMetric(t, "vmagent_relabel_config_reloads_total")
if err := app.process.Signal(syscall.SIGHUP); err != nil {
t.Fatalf("could not send SIGHUP signal to %s process: %v", app.instance, err)
}
var currTotal float64
for i := 0; i < 30; i++ {
currTotal = app.GetMetric(t, "vmagent_relabel_config_reloads_total")
if currTotal > prevTotal {
return
}
time.Sleep(100 * time.Millisecond)
}
if currTotal <= prevTotal {
t.Fatalf("relabel configs were not reloaded after SIGHUP signal; previous total: %f, current total: %f", prevTotal, currTotal)
}
}
// sendBlocking sends the data to vmstorage by executing `send` function and
// waits until the data is actually sent.
//

View File

@@ -1,13 +0,0 @@
package apptest
// StartVmbackup starts an instance of vmbackup with the given flags and waits
// until it exits.
func StartVmbackup(instance, storageDataPath, snapshotCreateURL, dst string) error {
flags := []string{
"-storageDataPath=" + storageDataPath,
"-snapshot.createURL=" + snapshotCreateURL,
"-dst=" + dst,
}
_, _, err := startApp(instance, "../../bin/vmbackup", flags, &appOptions{wait: true})
return err
}

Some files were not shown because too many files have changed in this diff Show More