Compare commits

..

1 Commits

Author SHA1 Message Date
f41gh7
af7935ab19 CHANGELOG.md: cut v1.112.0 release 2025-02-21 14:15:38 +01:00
162 changed files with 3781 additions and 16237 deletions

View File

@@ -567,7 +567,7 @@ golangci-lint: install-golangci-lint
golangci-lint run
install-golangci-lint:
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.64.5
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.64.4
remove-golangci-lint:
rm -rf `which golangci-lint`

View File

@@ -3,7 +3,7 @@
![Latest Release](https://img.shields.io/github/v/release/VictoriaMetrics/VictoriaMetrics?sort=semver&label=&filter=!*-victorialogs&logo=github&labelColor=gray&color=gray&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Freleases%2Flatest)
![Docker Pulls](https://img.shields.io/docker/pulls/victoriametrics/victoria-metrics?label=&logo=docker&logoColor=white&labelColor=2496ED&color=2496ED&link=https%3A%2F%2Fhub.docker.com%2Fr%2Fvictoriametrics%2Fvictoria-metrics)
![Go Report](https://goreportcard.com/badge/github.com/VictoriaMetrics/VictoriaMetrics?link=https%3A%2F%2Fgoreportcard.com%2Freport%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics)
![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/actions/workflows/main.yml/badge.svg?branch=master&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Factions)
![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/workflows/main/badge.svg?link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Factions)
![codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/branch/master/graph/badge.svg?link=https%3A%2F%2Fcodecov.io%2Fgh%2FVictoriaMetrics%2FVictoriaMetrics)
![License](https://img.shields.io/github/license/VictoriaMetrics/VictoriaMetrics?labelColor=green&label=&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Fblob%2Fmaster%2FLICENSE)
![Slack](https://img.shields.io/badge/Join-4A154B?logo=slack&link=https%3A%2F%2Fslack.victoriametrics.com)
@@ -22,7 +22,7 @@ Here are some resources and information about VictoriaMetrics:
- Documentation: [docs.victoriametrics.com](https://docs.victoriametrics.com)
- Case studies: [Grammarly, Roblox, Wix,...](https://docs.victoriametrics.com/casestudies/).
- Available: [Binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest), docker images [Docker Hub](https://hub.docker.com/r/victoriametrics/victoria-metrics/) and [Quay](https://quay.io/repository/victoriametrics/victoria-metrics), [Source code](https://github.com/VictoriaMetrics/VictoriaMetrics)
- Available: [Binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest), [Docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/), [Source code](https://github.com/VictoriaMetrics/VictoriaMetrics)
- Deployment types: [Single-node version](https://docs.victoriametrics.com/), [Cluster version](https://docs.victoriametrics.com/cluster-victoriametrics/), and [Enterprise version](https://docs.victoriametrics.com/enterprise/)
- Changelog: [CHANGELOG](https://docs.victoriametrics.com/changelog/), and [How to upgrade](https://docs.victoriametrics.com/#how-to-upgrade-victoriametrics)
- Community: [Slack](https://slack.victoriametrics.com/), [X (Twitter)](https://x.com/VictoriaMetrics), [LinkedIn](https://www.linkedin.com/company/victoriametrics/), [YouTube](https://www.youtube.com/@VictoriaMetrics)

View File

@@ -60,7 +60,7 @@ func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
return true
}
switch path {
case "/", "":
case "/":
switch r.Method {
case http.MethodGet:
// Return fake response for Elasticsearch ping request.

View File

@@ -46,9 +46,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
return true
}
switch {
case strings.HasPrefix(path, "/elasticsearch"):
// some clients may omit trailing slash
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8353
case strings.HasPrefix(path, "/elasticsearch/"):
path = strings.TrimPrefix(path, "/elasticsearch")
return elasticsearch.RequestHandler(path, w, r)
case strings.HasPrefix(path, "/loki/"):

View File

@@ -18,7 +18,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)
var (
@@ -306,7 +306,7 @@ type timeFlag struct {
}
func (tf *timeFlag) Set(s string) error {
msec, err := timeutil.ParseTimeMsec(s)
msec, err := promutils.ParseTimeMsec(s)
if err != nil {
return fmt.Errorf("cannot parse time from %q: %w", s, err)
}

View File

@@ -22,7 +22,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)
// ProcessFacetsRequest handles /select/logsql/facets request.
@@ -116,7 +116,7 @@ func ProcessHitsRequest(ctx context.Context, w http.ResponseWriter, r *http.Requ
if stepStr == "" {
stepStr = "1d"
}
step, err := timeutil.ParseDuration(stepStr)
step, err := promutils.ParseDuration(stepStr)
if err != nil {
httpserver.Errorf(w, r, "cannot parse 'step' arg: %s", err)
return
@@ -131,7 +131,7 @@ func ProcessHitsRequest(ctx context.Context, w http.ResponseWriter, r *http.Requ
if offsetStr == "" {
offsetStr = "0s"
}
offset, err := timeutil.ParseDuration(offsetStr)
offset, err := promutils.ParseDuration(offsetStr)
if err != nil {
httpserver.Errorf(w, r, "cannot parse 'offset' arg: %s", err)
return
@@ -665,7 +665,7 @@ func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r
if stepStr == "" {
stepStr = "1d"
}
step, err := timeutil.ParseDuration(stepStr)
step, err := promutils.ParseDuration(stepStr)
if err != nil {
err = fmt.Errorf("cannot parse 'step' arg: %s", err)
httpserver.SendPrometheusError(w, r, err)
@@ -688,22 +688,19 @@ func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r
m := make(map[string]*statsSeries)
var mLock sync.Mutex
timestamp := q.GetTimestamp()
writeBlock := func(_ uint, timestamps []int64, columns []logstorage.BlockColumn) {
clonedColumnNames := make([]string, len(columns))
for i, c := range columns {
clonedColumnNames[i] = strings.Clone(c.Name)
}
for i := range timestamps {
// Do not move q.GetTimestamp() outside writeBlock, since ts
// must be initialized to query timestamp for every processed log row.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8312
ts := q.GetTimestamp()
labels := make([]logstorage.Field, 0, len(byFields))
for j, c := range columns {
if c.Name == "_time" {
nsec, ok := logstorage.TryParseTimestampRFC3339Nano(c.Values[i])
if ok {
ts = nsec
timestamp = nsec
continue
}
}
@@ -724,7 +721,7 @@ func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r
dst = logstorage.MarshalFieldsToJSON(dst, labels)
key := string(dst)
p := statsPoint{
Timestamp: ts,
Timestamp: timestamp,
Value: strings.Clone(c.Values[i]),
}
@@ -1122,7 +1119,7 @@ func getTimeNsec(r *http.Request, argName string) (int64, bool, error) {
return 0, false, nil
}
currentTimestamp := time.Now().UnixNano()
nsecs, err := timeutil.ParseTimeAt(s, currentTimestamp)
nsecs, err := promutils.ParseTimeAt(s, currentTimestamp)
if err != nil {
return 0, false, fmt.Errorf("cannot parse %s=%s: %w", argName, s, err)
}

View File

@@ -176,62 +176,50 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
func processSelectRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, path string) bool {
httpserver.EnableCORS(w, r)
startTime := time.Now()
switch path {
case "/select/logsql/facets":
logsqlFacetsRequests.Inc()
logsql.ProcessFacetsRequest(ctx, w, r)
logsqlFacetsDuration.UpdateDuration(startTime)
return true
case "/select/logsql/field_names":
logsqlFieldNamesRequests.Inc()
logsql.ProcessFieldNamesRequest(ctx, w, r)
logsqlFieldNamesDuration.UpdateDuration(startTime)
return true
case "/select/logsql/field_values":
logsqlFieldValuesRequests.Inc()
logsql.ProcessFieldValuesRequest(ctx, w, r)
logsqlFieldValuesDuration.UpdateDuration(startTime)
return true
case "/select/logsql/hits":
logsqlHitsRequests.Inc()
logsql.ProcessHitsRequest(ctx, w, r)
logsqlHitsDuration.UpdateDuration(startTime)
return true
case "/select/logsql/query":
logsqlQueryRequests.Inc()
logsql.ProcessQueryRequest(ctx, w, r)
logsqlQueryDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stats_query":
logsqlStatsQueryRequests.Inc()
logsql.ProcessStatsQueryRequest(ctx, w, r)
logsqlStatsQueryDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stats_query_range":
logsqlStatsQueryRangeRequests.Inc()
logsql.ProcessStatsQueryRangeRequest(ctx, w, r)
logsqlStatsQueryRangeDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stream_field_names":
logsqlStreamFieldNamesRequests.Inc()
logsql.ProcessStreamFieldNamesRequest(ctx, w, r)
logsqlStreamFieldNamesDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stream_field_values":
logsqlStreamFieldValuesRequests.Inc()
logsql.ProcessStreamFieldValuesRequest(ctx, w, r)
logsqlStreamFieldValuesDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stream_ids":
logsqlStreamIDsRequests.Inc()
logsql.ProcessStreamIDsRequest(ctx, w, r)
logsqlStreamIDsDuration.UpdateDuration(startTime)
return true
case "/select/logsql/streams":
logsqlStreamsRequests.Inc()
logsql.ProcessStreamsRequest(ctx, w, r)
logsqlStreamsDuration.UpdateDuration(startTime)
return true
default:
return false
@@ -252,39 +240,16 @@ func getMaxQueryDuration(r *http.Request) time.Duration {
}
var (
logsqlFacetsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/facets"}`)
logsqlFacetsDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/facets"}`)
logsqlFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/field_names"}`)
logsqlFieldNamesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/field_names"}`)
logsqlFieldValuesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/field_values"}`)
logsqlFieldValuesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/field_values"}`)
logsqlHitsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/hits"}`)
logsqlHitsDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/hits"}`)
logsqlQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/query"}`)
logsqlQueryDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/query"}`)
logsqlStatsQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query"}`)
logsqlStatsQueryDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stats_query"}`)
logsqlStatsQueryRangeRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query_range"}`)
logsqlStatsQueryRangeDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stats_query_range"}`)
logsqlStreamFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_names"}`)
logsqlStreamFieldNamesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stream_field_names"}`)
logsqlFacetsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/facets"}`)
logsqlFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/field_names"}`)
logsqlFieldValuesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/field_values"}`)
logsqlHitsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/hits"}`)
logsqlQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/query"}`)
logsqlStatsQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query"}`)
logsqlStatsQueryRangeRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query_range"}`)
logsqlStreamFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_names"}`)
logsqlStreamFieldValuesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_values"}`)
logsqlStreamFieldValuesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stream_field_values"}`)
logsqlStreamIDsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_ids"}`)
logsqlStreamIDsDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stream_ids"}`)
logsqlStreamsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/streams"}`)
logsqlStreamsDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/streams"}`)
// no need to track duration for tail requests, as they usually take long time
logsqlTailRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/tail"}`)
logsqlStreamIDsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_ids"}`)
logsqlStreamsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/streams"}`)
logsqlTailRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/tail"}`)
)

View File

@@ -9,7 +9,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/streamaggr"
"github.com/VictoriaMetrics/metrics"
)
@@ -95,7 +94,7 @@ func TestRemoteWriteContext_TryPush_ImmutableTimeseries(t *testing.T) {
}
offsetMsecs := time.Now().UnixMilli()
inputTss := prometheus.MustParsePromMetrics(input, offsetMsecs)
inputTss := prompbmarshal.MustParsePromMetrics(input, offsetMsecs)
expectedTss := make([]prompbmarshal.TimeSeries, len(inputTss))
// copy inputTss to make sure it is not mutated during TryPush call

View File

@@ -51,11 +51,6 @@ Examples:
Usage: `Optional external URL to template in rule's labels or annotations.`,
Required: false,
},
&cli.StringFlag{
Name: "httpListenPort",
Usage: `Optional local port for incoming HTTP requests. If not specified, a random unoccupied port will be used.`,
Required: false,
},
&cli.StringFlag{
Name: "loggerLevel",
Usage: `Minimum level of errors to log. Possible values: INFO, WARN, ERROR, FATAL, PANIC (default "ERROR").`,
@@ -63,7 +58,7 @@ Examples:
},
},
Action: func(c *cli.Context) error {
if failed := unittest.UnitTest(c.StringSlice("files"), c.Bool("disableAlertgroupLabel"), c.StringSlice("external.label"), c.String("external.url"), c.String("httpListenPort"), c.String("loggerLevel")); failed {
if failed := unittest.UnitTest(c.StringSlice("files"), c.Bool("disableAlertgroupLabel"), c.StringSlice("external.label"), c.String("external.url"), c.String("loggerLevel")); failed {
return fmt.Errorf("unittest failed")
}
return nil

View File

@@ -4,18 +4,13 @@ import (
"context"
"flag"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/signal"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
"syscall"
"time"
"gopkg.in/yaml.v2"
@@ -33,6 +28,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/prometheus"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
@@ -42,9 +38,15 @@ import (
var (
storagePath string
httpListenAddr string
httpListenAddr = ":8880"
// insert series from 1970-01-01T00:00:00
testStartTime = time.Unix(0, 0).UTC()
testStartTime = time.Unix(0, 0).UTC()
testPromWriteHTTPPath = "http://127.0.0.1" + httpListenAddr + "/api/v1/write"
testDataSourcePath = "http://127.0.0.1" + httpListenAddr + "/prometheus"
testRemoteWritePath = "http://127.0.0.1" + httpListenAddr
testHealthHTTPPath = "http://127.0.0.1" + httpListenAddr + "/health"
testLogLevel = "ERROR"
disableAlertgroupLabel bool
)
@@ -54,7 +56,7 @@ const (
)
// UnitTest runs unittest for files
func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, externalURL, httpListenPort, logLevel string) bool {
func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, externalURL, logLevel string) bool {
if logLevel != "" {
testLogLevel = logLevel
}
@@ -65,42 +67,7 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
if err := templates.Load([]string{}, *eu); err != nil {
logger.Fatalf("failed to load template: %v", err)
}
// set up http server
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/prometheus/api/v1/query":
if err := prometheus.QueryHandler(nil, time.Now(), w, r); err != nil {
httpserver.Errorf(w, r, "%s", err)
}
case "/prometheus/api/v1/write", "/api/v1/write":
if err := promremotewrite.InsertHandler(r); err != nil {
httpserver.Errorf(w, r, "%s", err)
}
default:
}
})
if httpListenPort == "" {
server := httptest.NewServer(handler)
httpListenAddr = strings.Split(server.URL, ":")[2]
defer server.Close()
} else {
httpListenAddr = httpListenPort
ln, err := net.Listen("tcp", fmt.Sprintf(":%s", httpListenPort))
if err != nil {
logger.Fatalf("cannot listen on port %s: %v", httpListenPort, err)
}
go func() {
err = http.Serve(ln, handler)
if err != nil {
logger.Fatalf("cannot start http server: %v", err)
}
defer ln.Close()
}()
}
// adding time.Now().UnixNano() to avoid possible file conflict when multiple processes run on a single host
storagePath = filepath.Join(os.TempDir(), testStoragePath, strconv.FormatInt(time.Now().UnixNano(), 10))
storagePath = filepath.Join(os.TempDir(), testStoragePath)
processFlags()
vminsert.Init()
vmselect.Init()
@@ -135,34 +102,16 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
}
var failed bool
runTest := func() bool {
for fileName, file := range testfiles {
if err := ruleUnitTest(fileName, file, labels); err != nil {
fmt.Println("FAILED")
fmt.Printf("failed to run unit test for file %q: \n%v", fileName, err)
return true
}
fmt.Println("SUCCESS")
for fileName, file := range testfiles {
if err := ruleUnitTest(fileName, file, labels); err != nil {
fmt.Println("FAILED")
fmt.Printf("failed to run unit test for file %q: \n%v", fileName, err)
failed = true
} else {
fmt.Println(" SUCCESS")
}
return false
}
finishCh := make(chan struct{}, 1)
go func() {
failed = runTest()
finishCh <- struct{}{}
}()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
select {
case sig := <-sigs:
fmt.Printf("received signal %s\n", sig)
failed = true
break
case <-finishCh:
break
}
return failed
}
@@ -262,8 +211,8 @@ func processFlags() {
{flag: "search.disableCache", value: "true"},
// set storage retention time to 100 years, allow to store series from 1970-01-01T00:00:00.
{flag: "retentionPeriod", value: "100y"},
{flag: "datasource.url", value: fmt.Sprintf("http://127.0.0.1:%s/prometheus", httpListenAddr)},
{flag: "remoteWrite.url", value: fmt.Sprintf("http://127.0.0.1:%s", httpListenAddr)},
{flag: "datasource.url", value: testDataSourcePath},
{flag: "remoteWrite.url", value: testRemoteWritePath},
{flag: "notifier.blackhole", value: "true"},
} {
// panics if flag doesn't exist
@@ -275,10 +224,27 @@ func processFlags() {
func setUp() {
vmstorage.Init(promql.ResetRollupResultCacheIfNeeded)
var ab flagutil.ArrayBool
go httpserver.Serve([]string{httpListenAddr}, &ab, func(w http.ResponseWriter, r *http.Request) bool {
switch r.URL.Path {
case "/prometheus/api/v1/query":
if err := prometheus.QueryHandler(nil, time.Now(), w, r); err != nil {
httpserver.Errorf(w, r, "%s", err)
}
return true
case "/prometheus/api/v1/write", "/api/v1/write":
if err := promremotewrite.InsertHandler(r); err != nil {
httpserver.Errorf(w, r, "%s", err)
}
return true
default:
}
return false
})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
readyCheckFunc := func() bool {
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%s/health", httpListenAddr))
resp, err := http.Get(testHealthHTTPPath)
if err != nil {
return false
}
@@ -300,6 +266,9 @@ checkCheck:
}
func tearDown() {
if err := httpserver.Stop([]string{httpListenAddr}); err != nil {
logger.Errorf("cannot stop the webservice: %s", err)
}
vmstorage.Stop()
metrics.UnregisterAllMetrics()
fs.MustRemoveAll(storagePath)
@@ -314,7 +283,7 @@ func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]i
if tg.Interval == nil {
tg.Interval = promutils.NewDuration(evalInterval)
}
err := writeInputSeries(tg.InputSeries, tg.Interval, testStartTime, fmt.Sprintf("http://127.0.0.1:%s/api/v1/write", httpListenAddr))
err := writeInputSeries(tg.InputSeries, tg.Interval, testStartTime, testPromWriteHTTPPath)
if err != nil {
return []error{err}
}

View File

@@ -8,7 +8,7 @@ func TestUnitTest_Failure(t *testing.T) {
f := func(files []string) {
t.Helper()
failed := UnitTest(files, false, nil, "", "", "")
failed := UnitTest(files, false, nil, "", "")
if !failed {
t.Fatalf("expecting failed test")
}
@@ -20,20 +20,19 @@ func TestUnitTest_Failure(t *testing.T) {
}
func TestUnitTest_Success(t *testing.T) {
f := func(disableGroupLabel bool, files []string, externalLabels []string, externalURL, httpPort string) {
f := func(disableGroupLabel bool, files []string, externalLabels []string, externalURL string) {
t.Helper()
failed := UnitTest(files, disableGroupLabel, externalLabels, externalURL, httpPort, "")
failed := UnitTest(files, disableGroupLabel, externalLabels, externalURL, "")
if failed {
t.Fatalf("unexpected failed test")
}
}
// run multi files with random http port
f(false, []string{"./testdata/test1.yaml", "./testdata/test2.yaml"}, []string{"cluster=prod"}, "http://grafana:3000", "")
// run multi files
f(false, []string{"./testdata/test1.yaml", "./testdata/test2.yaml"}, []string{"cluster=prod"}, "http://grafana:3000")
// disable group label
// template with null external values
// specify httpListenAddr
f(true, []string{"./testdata/disable-group-label.yaml"}, nil, "", "8880")
f(true, []string{"./testdata/disable-group-label.yaml"}, nil, "")
}

View File

@@ -157,19 +157,6 @@ func TestGroupValidate_Failure(t *testing.T) {
f(&Group{}, false, "group name must be set")
f(&Group{
Name: "both record and alert are not set",
Rules: []Rule{
{
Expr: "sum(up == 0 ) by (host)",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Expr: "sumSeries(time('foo.bar',10))",
},
},
}, false, "invalid rule")
f(&Group{
Name: "negative interval",
Interval: promutils.NewDuration(-1),
@@ -253,6 +240,59 @@ func TestGroupValidate_Failure(t *testing.T) {
},
}, false, "duplicate")
f(&Group{
Name: "test graphite with prometheus expr",
Type: NewGraphiteType(),
Rules: []Rule{
{
Expr: "sum(up == 0 ) by (host)",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Expr: "sumSeries(time('foo.bar',10))",
},
},
}, false, "invalid rule")
f(&Group{
Name: "test graphite inherit",
Type: NewGraphiteType(),
Rules: []Rule{
{
Expr: "sumSeries(time('foo.bar',10))",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Expr: "sum(up == 0 ) by (host)",
},
},
}, false, "either `record` or `alert` must be set")
f(&Group{
Name: "test vlogs with prometheus expr",
Type: NewVLogsType(),
Rules: []Rule{
{
Expr: "sum(up == 0 ) by (host)",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Expr: "sumSeries(time('foo.bar',10))",
},
},
}, false, "invalid rule")
// validate expressions
f(&Group{
Name: "test",
Rules: []Rule{
{
Record: "record",
Expr: "up | 0",
},
},
}, true, "invalid expression")
f(&Group{
Name: "test thanos",
Type: NewRawType("thanos"),
@@ -263,20 +303,8 @@ func TestGroupValidate_Failure(t *testing.T) {
},
}, true, "unknown datasource type")
// validate expressions
f(&Group{
Name: "test prometheus expr",
Type: NewPrometheusType(),
Rules: []Rule{
{
Record: "record",
Expr: "up | 0",
},
},
}, true, "bad prometheus expr")
f(&Group{
Name: "test graphite expr",
Name: "test graphite",
Type: NewGraphiteType(),
Rules: []Rule{
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
@@ -286,63 +314,14 @@ func TestGroupValidate_Failure(t *testing.T) {
}, true, "bad graphite expr")
f(&Group{
Name: "test vlogs expr",
Name: "test vlogs",
Type: NewVLogsType(),
Rules: []Rule{
{Alert: "alert", Expr: "stats count(*) as requests"},
{Alert: "alert", Expr: "stats count(*) as requests", Labels: map[string]string{
"description": "some-description",
}},
},
}, true, "bad LogsQL expr")
f(&Group{
Name: "test vlogs expr",
Type: NewVLogsType(),
Rules: []Rule{
{Alert: "alert", Expr: "_time: 1m | stats by (path, _time: 1m) count(*) as requests"},
},
}, true, "bad LogsQL expr")
f(&Group{
Name: "test graphite with prometheus expr",
Type: NewGraphiteType(),
Rules: []Rule{
{
Record: "r1",
ID: 1,
Expr: "sumSeries(time('foo.bar',10))",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Record: "r2",
ID: 2,
Expr: "sum(up == 0 ) by (host)",
},
},
}, true, "bad graphite expr")
f(&Group{
Name: "test vlogs with prometheus exp",
Type: NewVLogsType(),
Rules: []Rule{
{
Record: "r1",
Expr: "sum(up == 0 ) by (host)",
For: promutils.NewDuration(10 * time.Millisecond),
},
},
}, true, "bad LogsQL expr")
f(&Group{
Name: "test prometheus with vlogs exp",
Type: NewPrometheusType(),
Rules: []Rule{
{
Record: "r1",
Expr: "* | stats by (path) count()",
For: promutils.NewDuration(10 * time.Millisecond),
},
},
}, true, "bad prometheus expr")
}
func TestGroupValidate_Success(t *testing.T) {

View File

@@ -71,18 +71,9 @@ func (t *Type) ValidateExpr(expr string) error {
return fmt.Errorf("bad prometheus expr: %q, err: %w", expr, err)
}
case "vlogs":
q, err := logstorage.ParseStatsQuery(expr, 0)
if err != nil {
if _, err := logstorage.ParseStatsQuery(expr, 0); err != nil {
return fmt.Errorf("bad LogsQL expr: %q, err: %w", expr, err)
}
fields, _ := q.GetStatsByFields()
for i := range fields {
// VictoriaLogs inserts `_time` field as a label in result when query with `stats by (_time:step)`,
// making the result meaningless and may lead to cardinality issues.
if fields[i] == "_time" {
return fmt.Errorf("bad LogsQL expr: %q, err: cannot contain time buckets stats pipe `stats by (_time:step)`", expr)
}
}
default:
return fmt.Errorf("unknown datasource type=%q", t.Name)
}

View File

@@ -54,7 +54,7 @@ var (
"If multiple args are set, then they are applied independently for the corresponding -notifier.url")
oauth2Scopes = flagutil.NewArrayString("notifier.oauth2.scopes", "Optional OAuth2 scopes to use for -notifier.url. Scopes must be delimited by ';'. "+
"If multiple args are set, then they are applied independently for the corresponding -notifier.url")
sendTimeout = flagutil.NewArrayDuration("notifier.sendTimeout", 10*time.Second, "Timeout when sending alerts to the corresponding -notifier.url")
sendTimeout = flagutil.NewArrayDuration("notifier.sendTimeout", time.Second*10, "Timeout for pushing alerts to corresponding -notifier.url.")
)
// cw holds a configWatcher for configPath configuration file

View File

@@ -33,7 +33,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/formatutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)
// go template execution fails when it's tree is empty
@@ -259,7 +259,7 @@ func templateFuncs() textTpl.FuncMap {
// parseDuration parses a duration string such as "1h" into the number of seconds it represents
"parseDuration": func(s string) (float64, error) {
d, err := timeutil.ParseDuration(s)
d, err := promutils.ParseDuration(s)
if err != nil {
return 0, err
}
@@ -268,7 +268,7 @@ func templateFuncs() textTpl.FuncMap {
// same with parseDuration but returns a time.Duration
"parseDurationTime": func(s string) (time.Duration, error) {
d, err := timeutil.ParseDuration(s)
d, err := promutils.ParseDuration(s)
if err != nil {
return 0, err
}

View File

@@ -344,7 +344,7 @@ var (
},
&cli.BoolFlag{
Name: influxSkipDatabaseLabel,
Usage: "Whether to skip adding the label 'db' to timeseries.",
Usage: "Wether to skip adding the label 'db' to timeseries.",
Value: false,
},
&cli.BoolFlag{

View File

@@ -144,9 +144,9 @@ func timeFilter(start, end string) string {
// by checking available (non-empty) tags, fields and measurements
// which unique combination represents all possible
// time series existing in database.
// Explore is required to reduce the load on influx
// The explore required to reduce the load on influx
// by querying field of the exact time series at once,
// instead of fetching all the values over and over.
// instead of fetching all of the values over and over.
//
// May contain non-existing time series.
func (c *Client) Explore() ([]*Series, error) {
@@ -340,7 +340,10 @@ func (c *Client) fieldsByMeasurement() (map[string][]string, error) {
}
func (c *Client) getSeries() ([]*Series, error) {
com := c.getSeriesCommand()
com := "show series"
if c.filterSeries != "" {
com = fmt.Sprintf("%s %s", com, c.filterSeries)
}
q := influx.Query{
Command: com,
Database: c.database,
@@ -386,21 +389,6 @@ func (c *Client) getSeries() ([]*Series, error) {
return result, nil
}
func (c *Client) getSeriesCommand() string {
com := "show series"
if c.filterSeries != "" {
com = fmt.Sprintf("%s %s", com, c.filterSeries)
}
if c.filterTime != "" {
joinStatement := " where "
if strings.Contains(strings.ToLower(com), joinStatement) {
joinStatement = " AND "
}
com = fmt.Sprintf("%s%s%s", com, joinStatement, c.filterTime)
}
return com
}
// getMeasurementTags get the tags for each measurement.
// tags are placed in a map without values (similar to a set) for quick lookups:
// {"measurement1": {"tag1", "tag2"}, "measurement2": {"tag3", "tag4"}}

View File

@@ -103,26 +103,3 @@ func TestTimeFilter(t *testing.T) {
// both start and end filters
f("2020-01-01T20:07:00Z", "2020-01-01T21:07:00Z", "time >= '2020-01-01T20:07:00Z' and time <= '2020-01-01T21:07:00Z'")
}
func TestGetSeriesCommand(t *testing.T) {
f := func(filterSeries, filterTime, expCommand string) {
t.Helper()
c := &Client{
filterTime: filterTime,
filterSeries: filterSeries,
}
gotCommand := c.getSeriesCommand()
if gotCommand != expCommand {
t.Fatalf("unexpected command\ngot\n%s\nwant\n%s", gotCommand, expCommand)
}
}
f("", "", "show series")
f("from cpu", "", "show series from cpu")
f("from cpu where arch='x86'", "", "show series from cpu where arch='x86'")
f("", "time >= '2020-01-01T20:07:00Z'", "show series where time >= '2020-01-01T20:07:00Z'")
f("from cpu", "time >= '2020-01-01T20:07:00Z'", "show series from cpu where time >= '2020-01-01T20:07:00Z'")
f("from cpu where arch='x86'", "time >= '2020-01-01T20:07:00Z'", "show series from cpu where arch='x86' AND time >= '2020-01-01T20:07:00Z'")
f("from cpu where arch='x86' AND hostname='host_2753'", "time >= '2020-01-01T20:07:00Z'", "show series from cpu where arch='x86' AND hostname='host_2753' AND time >= '2020-01-01T20:07:00Z'")
}

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)
const (
@@ -16,7 +16,7 @@ const (
// ParseTime parses time in s string and returns time.Time object
// if parse correctly or error if not
func ParseTime(s string) (time.Time, error) {
msecs, err := timeutil.ParseTimeMsec(s)
msecs, err := promutils.ParseTimeMsec(s)
if err != nil {
return time.Time{}, fmt.Errorf("cannot parse %s: %w", s, err)
}

View File

@@ -2,7 +2,6 @@ package prompush
import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/metrics"
@@ -50,7 +49,6 @@ func push(ctx *common.InsertCtx, tss []prompbmarshal.TimeSeries) {
}
ctx.Reset(rowsLen)
rowsTotal := 0
hasRelabeling := relabel.HasRelabeling()
for i := range tss {
ts := &tss[i]
rowsTotal += len(ts.Samples)
@@ -59,7 +57,7 @@ func push(ctx *common.InsertCtx, tss []prompbmarshal.TimeSeries) {
label := &ts.Labels[j]
ctx.AddLabel(label.Name, label.Value)
}
if !ctx.TryPrepareLabels(hasRelabeling) {
if !ctx.TryPrepareLabels(false) {
continue
}
var metricNameRaw []byte

View File

@@ -373,17 +373,9 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
func(values []float64, timestamps []int64), []*rollupConfig, error) {
preFunc := func(_ []float64, _ []int64) {}
funcName = strings.ToLower(funcName)
// window > lookbackDelta could result in negative delta.
// See issue: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8342
stalenessInterval := lookbackDelta
if stalenessInterval != 0 && stalenessInterval < window {
stalenessInterval = window
}
if rollupFuncsRemoveCounterResets[funcName] {
preFunc = func(values []float64, timestamps []int64) {
removeCounterResets(values, timestamps, stalenessInterval)
removeCounterResets(values, timestamps, lookbackDelta)
}
}
samplesScannedPerCall := rollupFuncsSamplesScannedPerCall[funcName]
@@ -495,7 +487,7 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
if rollupFuncsRemoveCounterResets[aggrFuncName] {
// There is no need to save the previous preFunc, since it is either empty or the same.
preFunc = func(values []float64, timestamps []int64) {
removeCounterResets(values, timestamps, stalenessInterval)
removeCounterResets(values, timestamps, lookbackDelta)
}
}
rf := rollupAggrFuncs[aggrFuncName]

View File

@@ -1,4 +1,4 @@
FROM golang:1.24.0 AS build-web-stage
FROM golang:1.23.6 AS build-web-stage
COPY build /build
WORKDIR /build

View File

@@ -9,7 +9,7 @@ export const useDebugDownsamplingFilters = () => {
const { serverUrl } = useAppState();
const [searchParams, setSearchParams] = useSearchParams();
const [data, setData] = useState<Map<string, string[] | null>>(new Map());
const [data, setData] = useState<Map<string, string[]>>(new Map());
const [loading, setLoading] = useState(false);
const [metricsError, setMetricsError] = useState<ErrorTypes | string>();
const [flagsError, setFlagsError] = useState<ErrorTypes | string>();

View File

@@ -7,7 +7,6 @@ import { PlayIcon, WikiIcon } from "../../components/Main/Icons";
import { useDebugDownsamplingFilters } from "./hooks/useDebugDownsamplingFilters";
import Spinner from "../../components/Main/Spinner/Spinner";
import { useSearchParams } from "react-router-dom";
import classNames from "classnames";
const example = {
flags: `-downsampling.period={env="dev"}:7d:5m,{env="dev"}:30d:30m
@@ -55,14 +54,7 @@ const DownsamplingFilters: FC = () => {
for (const [key, value] of data) {
rows.push(<tr className="vm-table__row">
<td className="vm-table-cell">{key}</td>
<td
className={classNames({
"vm-table-cell": true,
"vm-table-cell_empty": !value,
})}
>
{value ? value.join(" ") : "No matching rules found!"}
</td>
<td className="vm-table-cell">{value.join(" ")}</td>
</tr>);
}
return (

View File

@@ -94,11 +94,6 @@
white-space: nowrap;
width: 100px;
}
&_empty {
color: $color-text-secondary;
font-style: italic;
}
}
&__sort-icon {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,12 @@
# All these commands must run from repository root.
DOCKER_REGISTRIES ?= docker.io quay.io
DOCKER_NAMESPACE ?= victoriametrics
ROOT_IMAGE ?= alpine:3.21.3
ROOT_IMAGE_SCRATCH ?= scratch
CERTS_IMAGE := alpine:3.21.3
GO_BUILDER_IMAGE := golang:1.24.0-alpine
GO_BUILDER_IMAGE := golang:1.23.6-alpine
BUILDER_IMAGE := local/builder:2.0.0-$(shell echo $(GO_BUILDER_IMAGE) | tr :/ __)-1
BASE_IMAGE := local/base:1.1.4-$(shell echo $(ROOT_IMAGE) | tr :/ __)-$(shell echo $(CERTS_IMAGE) | tr :/ __)
DOCKER ?= docker
@@ -93,10 +92,8 @@ publish-via-docker:
--label "org.opencontainers.image.vendor=VictoriaMetrics" \
--label "org.opencontainers.image.version=$(PKG_TAG)" \
--label "org.opencontainers.image.created=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")" \
$(foreach registry,$(DOCKER_REGISTRIES),\
--tag $(registry)/$(DOCKER_NAMESPACE)/$(APP_NAME):$(PKG_TAG)$(RACE) \
--tag $(registry)/$(DOCKER_NAMESPACE)/$(APP_NAME):$(LATEST_TAG)$(RACE) \
) \
--tag $(DOCKER_NAMESPACE)/$(APP_NAME):$(PKG_TAG)$(RACE) \
--tag $(DOCKER_NAMESPACE)/$(APP_NAME):$(LATEST_TAG)$(RACE) \
-o type=image \
--provenance=false \
-f app/$(APP_NAME)/multiarch/Dockerfile \
@@ -113,10 +110,8 @@ publish-via-docker:
--label "org.opencontainers.image.vendor=VictoriaMetrics" \
--label "org.opencontainers.image.version=$(PKG_TAG)" \
--label "org.opencontainers.image.created=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")" \
$(foreach registry,$(DOCKER_REGISTRIES),\
--tag $(registry)/$(DOCKER_NAMESPACE)/$(APP_NAME):$(PKG_TAG)$(RACE)-scratch \
--tag $(registry)/$(DOCKER_NAMESPACE)/$(APP_NAME):$(LATEST_TAG)$(RACE)-scratch \
) \
--tag $(DOCKER_NAMESPACE)/$(APP_NAME):$(PKG_TAG)$(RACE)-scratch \
--tag $(DOCKER_NAMESPACE)/$(APP_NAME):$(LATEST_TAG)$(RACE)-scratch \
-o type=image \
--provenance=false \
-f app/$(APP_NAME)/multiarch/Dockerfile \

View File

@@ -209,5 +209,5 @@ Please see more examples on integration of VictoriaLogs with other log shippers
* [journald](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker/victorialogs/journald)
* [opentelemetry-collector](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker/victorialogs/opentelemetry-collector)
* [telegraf](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker/victorialogs/telegraf)
* [fluentd](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker/victorialogs/fluentd)
* [fluentd]((https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker/victorialogs/fluentd)
* [datadog-serverless](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker/victorialogs/datadog-serverless)

View File

@@ -4,7 +4,7 @@ services:
# And forward them to --remoteWrite.url
vmagent:
container_name: vmagent
image: victoriametrics/vmagent:v1.112.0
image: victoriametrics/vmagent:v1.111.0
depends_on:
- "vminsert"
ports:
@@ -39,7 +39,7 @@ services:
# where N is number of vmstorages (2 in this case).
vmstorage-1:
container_name: vmstorage-1
image: victoriametrics/vmstorage:v1.112.0-cluster
image: victoriametrics/vmstorage:v1.111.0-cluster
ports:
- 8482
- 8400
@@ -51,7 +51,7 @@ services:
restart: always
vmstorage-2:
container_name: vmstorage-2
image: victoriametrics/vmstorage:v1.112.0-cluster
image: victoriametrics/vmstorage:v1.111.0-cluster
ports:
- 8482
- 8400
@@ -66,7 +66,7 @@ services:
# pre-process them and distributes across configured vmstorage shards.
vminsert:
container_name: vminsert
image: victoriametrics/vminsert:v1.112.0-cluster
image: victoriametrics/vminsert:v1.111.0-cluster
depends_on:
- "vmstorage-1"
- "vmstorage-2"
@@ -81,7 +81,7 @@ services:
# vmselect collects results from configured `--storageNode` shards.
vmselect-1:
container_name: vmselect-1
image: victoriametrics/vmselect:v1.112.0-cluster
image: victoriametrics/vmselect:v1.111.0-cluster
depends_on:
- "vmstorage-1"
- "vmstorage-2"
@@ -94,7 +94,7 @@ services:
restart: always
vmselect-2:
container_name: vmselect-2
image: victoriametrics/vmselect:v1.112.0-cluster
image: victoriametrics/vmselect:v1.111.0-cluster
depends_on:
- "vmstorage-1"
- "vmstorage-2"
@@ -112,7 +112,7 @@ services:
# It can be used as an authentication proxy.
vmauth:
container_name: vmauth
image: victoriametrics/vmauth:v1.112.0
image: victoriametrics/vmauth:v1.111.0
depends_on:
- "vmselect-1"
- "vmselect-2"
@@ -127,7 +127,7 @@ services:
# vmalert executes alerting and recording rules
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:v1.112.0
image: victoriametrics/vmalert:v1.111.0
depends_on:
- "vmauth"
ports:

View File

@@ -44,7 +44,7 @@ services:
# storing logs and serving read queries.
victorialogs:
container_name: victorialogs
image: victoriametrics/victoria-logs:v1.15.0-victorialogs
image: victoriametrics/victoria-logs:v1.12.0-victorialogs
command:
- "--storageDataPath=/vlogs"
- "--httpListenAddr=:9428"
@@ -59,7 +59,7 @@ services:
# scraping, storing metrics and serve read requests.
victoriametrics:
container_name: victoriametrics
image: victoriametrics/victoria-metrics:v1.112.0
image: victoriametrics/victoria-metrics:v1.111.0
ports:
- 8428:8428
volumes:
@@ -78,7 +78,7 @@ services:
# depending on the requested path.
vmauth:
container_name: vmauth
image: victoriametrics/vmauth:v1.112.0
image: victoriametrics/vmauth:v1.111.0
depends_on:
- "victoriametrics"
- "victorialogs"
@@ -95,7 +95,7 @@ services:
# vmalert executes alerting and recording rules according to given rule type.
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:v1.112.0
image: victoriametrics/vmalert:v1.111.0
depends_on:
- "vmauth"
- "alertmanager"

View File

@@ -4,7 +4,7 @@ services:
# And forward them to --remoteWrite.url
vmagent:
container_name: vmagent
image: victoriametrics/vmagent:v1.112.0
image: victoriametrics/vmagent:v1.111.0
depends_on:
- "victoriametrics"
ports:
@@ -20,7 +20,7 @@ services:
# storing metrics and serve read requests.
victoriametrics:
container_name: victoriametrics
image: victoriametrics/victoria-metrics:v1.112.0
image: victoriametrics/victoria-metrics:v1.111.0
ports:
- 8428:8428
- 8089:8089
@@ -59,7 +59,7 @@ services:
# vmalert executes alerting and recording rules
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:v1.112.0
image: victoriametrics/vmalert:v1.111.0
depends_on:
- "victoriametrics"
- "alertmanager"

View File

@@ -128,21 +128,3 @@ groups:
summary: "Some rows are rejected on \"{{ $labels.instance }}\" on ingestion attempt"
description: "Ingested rows on instance \"{{ $labels.instance }}\" are rejected due to the
following reason: \"{{ $labels.reason }}\""
- alert: TooHighQueryLoad
expr: increase(vm_concurrent_select_limit_timeout_total[5m]) > 0
for: 15m
labels:
severity: warning
annotations:
summary: "Read queries fail with timeout for {{ $labels.job }} on instance {{ $labels.instance }}"
description: |
Instance {{ $labels.instance }} ({{ $labels.job }}) is failing to serve read queries during last 15m.
Concurrency limit `-search.maxConcurrentRequests` was reached on this instance and extra queries were
put into the queue for `-search.maxQueueDuration` interval. But even after waiting in the queue these queries weren't served.
This happens if instance is overloaded with the current workload, or datasource is too slow to respond.
Possible solutions are the following:
* reduce the query load;
* increase compute resources or number of replicas;
* adjust limits `-search.maxConcurrentRequests` and `-search.maxQueueDuration`.
See more at https://docs.victoriametrics.com/troubleshooting/#slow-queries.

View File

@@ -1,7 +1,7 @@
services:
# meta service will be ignored by compose
.victorialogs:
image: docker.io/victoriametrics/victoria-logs:v1.15.0-victorialogs
image: docker.io/victoriametrics/victoria-logs:v1.12.0-victorialogs
command:
- -storageDataPath=/vlogs
- -loggerFormat=json
@@ -19,7 +19,7 @@ services:
retries: 10
dd-proxy:
image: docker.io/victoriametrics/vmauth:v1.112.0
image: docker.io/victoriametrics/vmauth:v1.111.0
restart: on-failure
volumes:
- ./:/etc/vmauth
@@ -45,7 +45,7 @@ services:
replicas: 0
victoriametrics:
image: victoriametrics/victoria-metrics:v1.112.0
image: victoriametrics/victoria-metrics:v1.111.0
ports:
- '8428:8428'
command:

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
services:
vmagent:
container_name: vmagent
image: victoriametrics/vmagent:v1.112.0
image: victoriametrics/vmagent:v1.111.0
depends_on:
- "victoriametrics"
ports:
@@ -18,7 +18,7 @@ services:
victoriametrics:
container_name: victoriametrics
image: victoriametrics/victoria-metrics:v1.112.0
image: victoriametrics/victoria-metrics:v1.111.0
ports:
- 8428:8428
volumes:
@@ -50,7 +50,7 @@ services:
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:v1.112.0
image: victoriametrics/vmalert:v1.111.0
depends_on:
- "victoriametrics"
ports:

View File

@@ -1,4 +1,4 @@
version: "3"
version: '3'
services:
filebeat-elastic:
@@ -18,7 +18,7 @@ services:
- vlogs
generator:
image: golang:1.24.0-alpine
image: golang:1.23.6-alpine
restart: always
working_dir: /go/src/app
volumes:
@@ -33,7 +33,7 @@ services:
- -syslog.addr=filebeat-elastic:12345
- -syslog.addr2=filebeat-vlogs:12345
- -logs.randomSuffix=false
depends_on: [filebeat-elastic, filebeat-vlogs]
depends_on: [ filebeat-elastic, filebeat-vlogs ]
elastic:
image: docker.elastic.co/elasticsearch/elasticsearch:8.8.0
@@ -48,8 +48,8 @@ services:
volumes:
- ./elk/kibana/kibana.yml:/usr/share/kibana/config/kibana.yml
ports:
- "5601:5601"
depends_on: [elastic]
- '5601:5601'
depends_on: [ elastic ]
beat-exporter-elastic:
image: trustpilot/beat-exporter:0.4.0

View File

@@ -1,8 +1,8 @@
version: "3"
version: '3'
services:
generator:
image: golang:1.24.0-alpine
image: golang:1.23.6-alpine
restart: always
working_dir: /go/src/app
volumes:
@@ -18,7 +18,7 @@ services:
- -syslog.addr=rsyslog:514
- -syslog.addr2=rsyslog:514
- -logs.randomSuffix=false
depends_on: [rsyslog]
depends_on: [ rsyslog ]
loki:
image: grafana/loki:2.9.0
@@ -45,7 +45,8 @@ services:
context: rsyslog
volumes:
- ./rsyslog/rsyslog.conf:/etc/rsyslog.conf
depends_on: [promtail]
depends_on: [ promtail ]
volumes:
loki:

View File

@@ -3,7 +3,7 @@ version: "3"
services:
# Run `make package-victoria-logs` to build victoria-logs image
vlogs:
image: docker.io/victoriametrics/victoria-logs:v1.15.0-victorialogs
image: docker.io/victoriametrics/victoria-logs:v1.12.0-victorialogs
volumes:
- vlogs:/vlogs
ports:

View File

@@ -174,9 +174,9 @@ Also see archives containing the word `cluster`.
Docker images for the cluster version are available here:
- `vminsert` - [Docker Hub](https://hub.docker.com/r/victoriametrics/vminsert/tags) and [Quay](https://quay.io/repository/victoriametrics/vminsert?tab=tags)
- `vmselect` - [Docker Hub](https://hub.docker.com/r/victoriametrics/vmselect/tags) and [Quay](https://quay.io/repository/victoriametrics/vmselect?tab=tags)
- `vmstorage` - [Docker Hub](https://hub.docker.com/r/victoriametrics/vmstorage/tags) and [Quay](https://quay.io/repository/victoriametrics/vmstorage?tab=tags)
- `vminsert` - <https://hub.docker.com/r/victoriametrics/vminsert/tags>
- `vmselect` - <https://hub.docker.com/r/victoriametrics/vmselect/tags>
- `vmstorage` - <https://hub.docker.com/r/victoriametrics/vmstorage/tags>
## Building from sources
@@ -761,7 +761,7 @@ Some workloads may need fine-grained resource usage limits. In these cases the f
- `-search.maxDeleteSeries` at `vmselect` limits the number of unique time
series that can be deleted by a single
[/api/v1/admin/tsdb/delete_series](https://docs.victoriametrics.com/url-examples/#apiv1admintsdbdelete_series)
call. The duration is limited via `-search.maxDeleteDuration` flag{{% available_from "v1.110.0" %}}. Deleting too many time series may require big
call. The duration is limited via `-search.maxDeleteDuration` flag{{% available_from "#tip" %}}. Deleting too many time series may require big
amount of CPU and memory at `vmstorage` and this limit guards against unplanned resource usage spikes.
Also see [How to delete time series](#how-to-delete-time-series) section to
learn about different ways of deleting series.

View File

@@ -22,5 +22,5 @@ to [the latest available releases](https://docs.victoriametrics.com/changelog/).
## Currently supported LTS release lines
- v1.110.x - the latest one is [v1.110.2 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.110.2)
- v1.102.x - the latest one is [v1.102.15 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.102.15)
- v1.110.x - the latest one is [v1.110.1 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.110.1)
- v1.102.x - the latest one is [v1.102.13 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.102.13)

View File

@@ -2349,4 +2349,4 @@ VictoriaMetrics performs the following implicit conversions for incoming queries
is passed to [rollup function](#rollup-functions), then a [subquery](#subqueries) with `1i` lookbehind window and `1i` step is automatically formed.
For example, `rate(sum(up))` is automatically converted to `rate((sum(default_rollup(up)))[1i:1i])`.
This behavior can be disabled or logged via `-search.disableImplicitConversion` and `-search.logImplicitConversion` command-line flags
starting from [`v1.102.0-rc2` release](https://docs.victoriametrics.com/changelog/changelog_2024/#v11020-rc2).
starting from [`v1.101.0` release](https://docs.victoriametrics.com/changelog/).

View File

@@ -23,7 +23,7 @@ VictoriaMetrics is distributed in the following forms:
VictoriaMetrics is available as:
* docker images at [Docker Hub](https://hub.docker.com/r/victoriametrics/victoria-metrics/) and [Quay](https://quay.io/repository/victoriametrics/victoria-metrics?tab=tags)
* [Docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/)
* [Helm Charts](https://github.com/VictoriaMetrics/helm-charts#list-of-charts)
* [Binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest)
* [Ansible Roles](https://github.com/VictoriaMetrics/ansible-playbooks)
@@ -55,8 +55,8 @@ under the current directory:
```sh
docker pull victoriametrics/victoria-metrics:v1.112.0
docker run -it --rm -v `pwd`/victoria-metrics-data:/victoria-metrics-data -p 8428:8428 victoriametrics/victoria-metrics:v1.112.0
docker pull victoriametrics/victoria-metrics:v1.111.0
docker run -it --rm -v `pwd`/victoria-metrics-data:/victoria-metrics-data -p 8428:8428 victoriametrics/victoria-metrics:v1.111.0
```

View File

@@ -1,7 +1,7 @@
![Latest Release](https://img.shields.io/github/v/release/VictoriaMetrics/VictoriaMetrics?sort=semver&label=&filter=!*-victorialogs&logo=github&labelColor=gray&color=gray&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Freleases%2Flatest)
![Docker Pulls](https://img.shields.io/docker/pulls/victoriametrics/victoria-metrics?label=&logo=docker&logoColor=white&labelColor=2496ED&color=2496ED&link=https%3A%2F%2Fhub.docker.com%2Fr%2Fvictoriametrics%2Fvictoria-metrics)
![Go Report](https://goreportcard.com/badge/github.com/VictoriaMetrics/VictoriaMetrics?link=https%3A%2F%2Fgoreportcard.com%2Freport%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics)
![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/actions/workflows/main.yml/badge.svg?branch=master&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Factions)
![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/workflows/main/badge.svg?link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Factions)
![codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/branch/master/graph/badge.svg?link=https%3A%2F%2Fcodecov.io%2Fgh%2FVictoriaMetrics%2FVictoriaMetrics)
![License](https://img.shields.io/github/license/VictoriaMetrics/VictoriaMetrics?labelColor=green&label=&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Fblob%2Fmaster%2FLICENSE)
![Slack](https://img.shields.io/badge/Join-4A154B?logo=slack&link=https%3A%2F%2Fslack.victoriametrics.com)
@@ -12,7 +12,7 @@ VictoriaMetrics is a fast, cost-effective and scalable monitoring solution and t
See [case studies for VictoriaMetrics](https://docs.victoriametrics.com/casestudies/).
VictoriaMetrics is available in [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest),
Docker images at [Docker Hub](https://hub.docker.com/r/victoriametrics/victoria-metrics/) and [Quay](https://quay.io/repository/victoriametrics/victoria-metrics?tab=tags), [source code](https://github.com/VictoriaMetrics/VictoriaMetrics).
[Docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/) and [source code](https://github.com/VictoriaMetrics/VictoriaMetrics).
Documentation for the cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/cluster-victoriametrics/).
@@ -124,7 +124,7 @@ VictoriaMetrics ecosystem contains the following components additionally to [sin
### Install
To quickly try VictoriaMetrics, just download the [VictoriaMetrics executable](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest)
or docker image from [Docker Hub](https://hub.docker.com/r/victoriametrics/victoria-metrics/) or [Quay](https://quay.io/repository/victoriametrics/victoria-metrics?tab=tags) and start it with the desired command-line flags.
or [Docker image](https://hub.docker.com/r/victoriametrics/victoria-metrics/) and start it with the desired command-line flags.
See also [QuickStart guide](https://docs.victoriametrics.com/quick-start/) for additional information.
VictoriaMetrics can also be installed via these installation methods:
@@ -1076,7 +1076,7 @@ VictoriaMetrics supports the following handlers from [Graphite Tags API](https:/
## How to build from sources
We recommend using either [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest) or
docker images ([Docker Hub](https://hub.docker.com/r/victoriametrics/victoria-metrics/) and [Quay](https://quay.io/repository/victoriametrics/victoria-metrics?tab=tags)) instead of building VictoriaMetrics
[docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/) instead of building VictoriaMetrics
from sources. Building from sources is reasonable when developing additional features specific
to your needs or when testing bugfixes.
@@ -1770,7 +1770,7 @@ By default, VictoriaMetrics is tuned for an optimal resource usage under typical
- `-search.maxDeleteSeries` limits the number of unique time series that can be
deleted by a single
[/api/v1/admin/tsdb/delete_series](https://docs.victoriametrics.com/url-examples/#apiv1admintsdbdelete_series)
call. The duration is limited via `-search.maxDeleteDuration` flag{{% available_from "v1.110.0" %}}. Deleting too many time series may require big
call. The duration is limited via `-search.maxDeleteDuration` flag{{% available_from "#tip" %}}. Deleting too many time series may require big
amount of CPU and memory and this limit guards against unplanned resource usage spikes. Also see
[How to delete time series](#how-to-delete-time-series) section to learn about
different ways of deleting series.
@@ -1998,7 +1998,7 @@ based on the time range of the query:
Mappings are added to the indexes during the data ingestion:
- In global index each mapping is created only once per retention period.
- In the per-day index each mapping is created for each unique date that
- In the per-day index each mapping is be created for each unique date that
has been seen in the samples for the corresponding time series.
IndexDB respects [retention period](#retention) and once it is over, the indexes
@@ -2102,9 +2102,9 @@ while leaving the last sample per each 1-hour interval for samples older than 18
VictoriaMetrics supports{{% available_from "v1.100.0" %}} configuring independent downsampling per different sets of [time series](https://docs.victoriametrics.com/keyconcepts/#time-series)
via `-downsampling.period=filter:offset:interval` syntax. In this case the given `offset:interval` downsampling is applied only to time series matching the given `filter`.
The `filter` can contain arbitrary [series filter](https://docs.victoriametrics.com/keyconcepts/#filtering).
For example, `-downsampling.period='{__name__=~"(node|process)_.*"}:1d:1m` instructs VictoriaMetrics to downsample samples older than one day with one minute interval
For example, `-downsampling.period='{__name__=~"(node|process)_.*"}:1d:1m` instructs VictoriaMetrics to deduplicate samples older than one day with one minute interval
only for [time series](https://docs.victoriametrics.com/keyconcepts/#time-series) with names starting with `node_` or `process_` prefixes.
The downsampling for other time series can be configured independently via additional `-downsampling.period` command-line flags.
The deduplication for other time series can be configured independently via additional `-downsampling.period` command-line flags.
Downsampling configuration can be tested in enterprise version of vmui on the page `Tools.Downsampling filters debug`.
If the time series doesn't match any `filter`, then it isn't downsampled. If the time series matches multiple filters, then the downsampling
@@ -2608,7 +2608,7 @@ A prominent example is Kubernetes. Services in k8s expose big number of series w
increasing churn rate. The per-day index speeds up data retrieval in this case.
But if your use case assumes low or no churn rate, then you might benefit from disabling the per-day index by setting
the flag `-disablePerDayIndex`{{% available_from "v1.112.0" %}}. This will improve the time series ingestion speed and decrease disk space usage,
the flag `-disablePerDayIndex`{{% available_from "#tip" %}}. This will improve the time series ingestion speed and decrease disk space usage,
since no time or disk space is spent maintaining the per-day index.
Example use cases:

View File

@@ -17,7 +17,7 @@ git remote add enterprise <url>
1. Make sure you have singing key configured
1. Make sure you have github token with at least `read:org, repo, write:packages` permissions exported under `GITHUB_TOKEN` env variable.
You can create token [here](https://github.com/settings/tokens)
1. Make sure you're [authorized](https://hub.docker.com/orgs/victoriametrics/settings/enforce-sign-in/windows) for pushing docker images to `docker.io` and `quay.io`
1. Make sure you're [authorized](https://hub.docker.com/orgs/victoriametrics/settings/enforce-sign-in/windows) for pushing docker images
### For MacOS users
@@ -69,6 +69,12 @@ Bumping the limits may significantly improve build speed.
* linux/ppc64le
* linux/386
This step can be run manually with the command `make publish` from the needed git tag.
1. Verify that created images are stable and don't introduce regressions on [test environment](https://github.com/VictoriaMetrics/VictoriaMetrics-enterprise/blob/master/Release-Guide.md#testing-releases).
1. Test new images on [sandbox](https://github.com/VictoriaMetrics/VictoriaMetrics-enterprise/blob/master/Release-Guide.md#testing-releases).
1. Push the tags `v1.xx.y` and `v1.xx.y-cluster` created at previous steps to public GitHub repository at https://github.com/VictoriaMetrics/VictoriaMetrics.
Push the tags `v1.xx.y`, `v1.xx.y-cluster`, `v1.xx.y-enterprise` and `v1.xx.y-enterprise-cluster` to the corresponding
branches in private repository.
**Important note:** do not push enterprise tags to public GitHub repository - they must be pushed only to private repository.
1. Run `TAG=v1.xx.y make github-create-release github-upload-assets`. This command performs the following tasks:
a) Create draft GitHub release with the name `TAG`. This step can be run manually
with the command `TAG=v1.xx.y make github-create-release`.
@@ -86,13 +92,6 @@ Bumping the limits may significantly improve build speed.
1. Go to <https://github.com/VictoriaMetrics/VictoriaMetrics/releases> and verify that draft release with the name `TAG` has been created
and this release contains all the needed binaries and checksums.
1. Update the release description with the content of [CHANGELOG](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/docs/changelog/CHANGELOG.md) for this release.
1. Follow the instructions in [LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics-enterprise/blob/master/Release-Guide.md#lts-release).
1. Verify that created images are stable and don't introduce regressions on [test environment](https://github.com/VictoriaMetrics/VictoriaMetrics-enterprise/blob/master/Release-Guide.md#testing-releases).
1. Test new images on [sandbox](https://github.com/VictoriaMetrics/VictoriaMetrics-enterprise/blob/master/Release-Guide.md#testing-releases).
1. Push the tags `v1.xx.y` and `v1.xx.y-cluster` created at previous steps to public GitHub repository at https://github.com/VictoriaMetrics/VictoriaMetrics.
Push the tags `v1.xx.y`, `v1.xx.y-cluster`, `v1.xx.y-enterprise` and `v1.xx.y-enterprise-cluster` to the corresponding
branches in private repository.
**Important note:** do not push enterprise tags to public GitHub repository - they must be pushed only to private repository.
1. Publish release by pressing "Publish release" green button in GitHub's UI.
1. Update GitHub tickets related to the new release. Usually, such tickets have label [waiting for release](https://github.com/VictoriaMetrics/VictoriaMetrics/issues?q=is%3Aopen+is%3Aissue+label%3A%22waiting+for+release%22). Close such tickets by mentioning which release they were included into, and remove the label. See example [here](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6637#issuecomment-2390729511).
1. Bump VictoriaMetrics version at `deployment/docker/docker-compose.yml` and at `deployment/docker/docker-compose-cluster.yml`.

View File

@@ -16,39 +16,6 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
## tip
## [v1.15.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.15.0-victorialogs)
Released at 2025-02-27
* FEATURE: [`pack_json` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#pack_json-pipe): allow packing fields, which start with the given prefixes. For example, `pack_json fields (foo.*, bar.*)` creates a JSON containing all the fields, which start with either `foo.` or `bar.`.
* FEATURE: [`pack_logfmt` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#pack_logfmt-pipe): allow packing fields, which start with the given prefixes. For example, `pack_logfmt fields (foo.*, bar.*)` creates [logfmt](https://brandur.org/logfmt) message containing all the fields, which start with either `foo.` or `bar.`.
* FEATURE: expose `vl_request_duration_seconds` [summaries](https://docs.victoriametrics.com/keyconcepts/#summary) for [select APIs](https://docs.victoriametrics.com/victorialogs/querying/#http-api) at the [/metrics](https://docs.victoriametrics.com/victorialogs/#monitoring) page.
* FEATURE: allow passing `*` as a subquery inside [`in(*)`, `contains_any(*)` and `contains_all(*)` filters](https://docs.victoriametrics.com/victorialogs/logsql/#subquery-filter). Such filters are treated as `match all` aka `*`. This is going to be used by [Grafana plugin for VictoriaLogs](https://docs.victoriametrics.com/victorialogs/victorialogs-datasource/). See [this issue](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/238#issuecomment-2685447673).
* FEATURE: [victorialogs dashboard](https://grafana.com/grafana/dashboards/22084-victorialogs/): add panels to display amount of ingested logs in bytes, latency of [select APIs](https://docs.victoriametrics.com/victorialogs/querying/#http-api) calls, troubleshooting panels.
* FEATURE: provide alternative registry for all VictoriaLogs components at [Quay.io](https://quay.io/organization/victoriametrics): [VictoriaLogs](https://quay.io/repository/victoriametrics/victoria-logs?tab=tags) and [vlogscli](https://quay.io/repository/victoriametrics/vlogscli?tab=tags).
* BUGFIX: do not treat a string containing leading zeros as a number during data ingestion and querying. For example, `00123` string shouldn't be treated as `123` number. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8361).
## [v1.14.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.14.0-victorialogs)
Released at 2025-02-25
* FEATURE: add [`lt_field` filter](https://docs.victoriametrics.com/victorialogs/logsql/#lt_field-filter), which can be used for obtaining logs where the given [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) value doesn't exceed the other field value.
* FEATURE: add [`le_field` filter](https://docs.victoriametrics.com/victorialogs/logsql/#le_field-filter), which can be used for obtaining logs where the given [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) value is smaller or equal to the other field value.
* BUGFIX: [elasticsearch data ingestion](https://docs.victoriametrics.com/victorialogs/data-ingestion/#elasticsearch-bulk-api): support health-check endpoint requested by Jaeger v2. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8353).
* BUGFIX: [`stats_query_range` HTTP endpoint](https://docs.victoriametrics.com/victorialogs/querying/#querying-log-range-stats): fix inconsistent result of `stats_query_range` API. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8312).
## [v1.13.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.13.0-victorialogs)
Released at 2025-02-22
* FEATURE: add [`contains_all` filter](https://docs.victoriametrics.com/victorialogs/logsql/#contains_all-filter), which matches logs containing all the given words / phrases in the given [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
* FEATURE: add [`contains_any` filter](https://docs.victoriametrics.com/victorialogs/logsql/#contains_any-filter), which matches logs containing at least one of the given words / phrases in the given [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
* FEATURE: add [`eq_field` filter](https://docs.victoriametrics.com/victorialogs/logsql/#eq_field-filter), which can be used for obtaining logs with identical values at the given [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
* FEATURE: add [`unpack_words` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#unpack_words-pipe) for unpacking individual [words](https://docs.victoriametrics.com/victorialogs/logsql/#word) from the given [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) into the given destination field as JSON array.
* FEATURE: add [`json_array_len` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#json_array_len-pipe) for calculating the length of JSON array stored in the given [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
## [v1.12.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.12.0-victorialogs)
Released at 2025-02-20

View File

@@ -91,7 +91,8 @@ VictoriaLogs is designed solely for logs. VictoriaLogs uses [similar design idea
VictoriaLogs provides easy to use query language with full-text search specifically optimized
for log analysis - [LogsQL](https://docs.victoriametrics.com/victorialogs/logsql/).
LogsQL is usually easier to use than SQL for typical log analysis tasks - see [these docs](https://docs.victoriametrics.com/victorialogs/sql-to-logsql/).
LogsQL is usually easier to use than SQL for typical log analysis tasks, while some
non-trivial analytics may require SQL power.
- VictoriaLogs accepts logs from popular log shippers out of the box - see [these docs](https://docs.victoriametrics.com/victorialogs/data-ingestion/).
@@ -140,23 +141,28 @@ for limiting the amounts of exported logs.
## I want to ingest logs without message field, is that possible?
VictoriaLogs [accepts](https://docs.victoriametrics.com/victorialogs/data-ingestion/) logs without [`_msg` field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field).
In this case the `_msg` field is set to the default value, which can be configured via `-defaultMsgValue` command-line flag.
Starting from version `v0.30.0`, VictoriaLogs started blocking the ingestion of logs **without a message field**, as it is a requirement of the [VictoriaLogs data model](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field).
Please note that the `_msg` field is **crucial** for VictoriaLogs, so it is highly recommended to fill it with meaningful content.
However, some logs do not have a message field and only contain other fields, such as logs in [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/7056#issuecomment-2434189718) and [this slack thread](https://victoriametrics.slack.com/archives/C05UNTPAEDN/p1730982146818249). Therefore, starting from version `v0.39.0`, logs without a message field are **allowed to be ingested**,
and their message field will be recorded as:
```json
{"_msg": "missing _msg field; see https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field"}
```
The default message field value can be changed using the `-defaultMsgValue` flag, for example, `-defaultMsgValue=foo`.
Please note that the message field is **crucial** for VictoriaLogs, so it is important to fill it with meaningful content.
## What if my logs have multiple message fields candidates?
If you [ingest](https://docs.victoriametrics.com/victorialogs/data-ingestion/) logs into VictoriaLogs
without [`_msg` field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field), then this field
is filled according to the `_msg_field` HTTP query arg and/or `VL-Msg-Field` HTTP header.
See [these docs](https://docs.victoriametrics.com/victorialogs/data-ingestion/#http-parameters) for details.
If the `_msg_field` HTTP query arg and/or `VL-Msg-Field` HTTP header contains a list of comma-separated field names,
then the first non-empty field from this list is used as `_msg` field.
For example, if the following [log entry](https://docs.victoriametrics.com/victorialogs/keyconcepts/)
is ingested into VictoriaLogs with `_msg_field=message,body`:
When ingesting with VictoriaLogs, the message fields is specified through `_msg_field` param, which can accept **multiple fields**, and the **first non-empty field** will be used as the message field.
Here is an example URL when pushing logs to VictoriaLogs with Promtail:
```yaml
clients:
- url: http://localhost:9428/insert/loki/api/v1/push?_stream_fields=instance,job,host,app&_msg=message,body
```
For the following log, its `_msg` will be `foo bar in message`:
```json
{
"message": "foo bar in message",
@@ -164,18 +170,14 @@ is ingested into VictoriaLogs with `_msg_field=message,body`:
}
```
Then `_msg` field is set to `foo bar in message`.
If the following log entry is ingested into VictoriaLogs with `_msg_field=message,body`:
And for the following log, its `_msg` will be `foo bar in body`:
```json
{
"message": "",
"body": "foo bar in body"
}
```
Then `_msg` field is set to `foo bar in body`.
## What length a log record is expected to have?
VictoriaLogs works optimally with log records of up to `10KB`. It works OK with
@@ -305,7 +307,7 @@ _time:1d | count_uniq(_stream)
## Does LogsQL support subqueries?
Yes. See [these docs](https://docs.victoriametrics.com/victorialogs/logsql/#subquery-filter).
LogsQL supports subqieries via [`in(<subquery>)` filter](https://docs.victoriametrics.com/victorialogs/logsql/#multi-exact-filter).
For example, the following query returns the total number of unique values for the `user_id` [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model)
across top 3 [log streams](https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields) with the biggest number of logs during the last hour:
@@ -323,27 +325,4 @@ The query works in the following way:
- Then it selects all the logs across the selected log streams over the last hour with the help of [`_stream_id:...` filter](https://docs.victoriametrics.com/victorialogs/logsql/#_stream_id-filter).
## How to estimate the needed compute resources for the given workload?
The needed storage space depends on the following factors:
- Data compressibility. VictoraLogs compresses the ingested logs before storing them to disk. The compression ratio depends on the "randomness" of the ingested logs.
Less "random" logs with many repeated field values and small differences between log messages compress the best (up to 100x and more).
More "random" logs with many unique field values may have very low compression rate.
- [Data retention](https://docs.victoriametrics.com/victorialogs/#retention). For example, a year-long retention needs 52x more storage space than a week-long retention.
The needed RAM, CPU, storage IO and network bandwidth depends on the type and the rate of queries over the ingested logs.
- "Lightweight" queries over the recently ingested logs with very narrow [log stream filters](https://docs.victoriametrics.com/victorialogs/logsql/#stream-filter)
require very low compute resources, even if they are executed at 1000 rps.
- "Heavy" queries over the long time range, which do not contain [log stream filters](https://docs.victoriametrics.com/victorialogs/logsql/#stream-filter)
or have some heavy [pipe processing](https://docs.victoriametrics.com/victorialogs/logsql/#pipes) such as [analytics' calculations](https://docs.victoriametrics.com/victorialogs/logsql/#stats-pipe)
or [sorting over billions of rows](https://docs.victoriametrics.com/victorialogs/logsql/#sort-pipe) may require hundreds of CPU cores and terabytes of RAM
for fast execution. It is OK to execute such queries on machines with a few CPU cores and a few GiB of RAM - these queries will take more time to execute.
The best approach to estimate the needed compute resources for the given workload is to start a VictoriaLogs, to ingest a share (1%-10%) of your production logs into it,
and to execute typical queries on it, while [measuring](https://docs.victoriametrics.com/victorialogs/#monitoring) the consumed compute resources.
Then you can extrapolate the needed compute resources for the full production workload in your case.
See also [`subquery filters`](https://docs.victoriametrics.com/victorialogs/logsql/#subquery-filter).

View File

@@ -266,8 +266,6 @@ The list of LogsQL filters:
- [Exact prefix filter](#exact-prefix-filter) - matches logs starting with the given prefix for the given [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model)
- [Multi-exact filter](#multi-exact-filter) - matches logs with one of the specified exact values for the given [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model)
- [Subquery filter](#subquery-filter) - matches logs with [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) values matching the results of another query
- [`contains_all` filter](#contains_any-filter) - matches logs with [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) containing all the provided [words](#word) / phrases
- [`contains_any` filter](#contains_any-filter) - matches logs with [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) containing at least one of the provided [words](#word) / phrases
- [Case-insensitive filter](#case-insensitive-filter) - matches logs with the given case-insensitive word, phrase or prefix
- [Sequence filter](#sequence-filter) - matches logs with the given sequence of words or phrases
- [Regexp filter](#regexp-filter) - matches logs for the given regexp
@@ -276,9 +274,6 @@ The list of LogsQL filters:
- [String range filter](#string-range-filter) - matches logs with [field values](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) in the given string range
- [Length range filter](#length-range-filter) - matches logs with [field values](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) of the given length range
- [Value type filter](#value_type-filter) - matches logs with [fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) stored under the given value type
- [Fields' equality filter](#eq_field-filter) - matches logs, which contain identical values in the given [fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model)
- [`Less than` filter](#lt_field-filter) - matches logs where the given field value is smaller than the other field value
- [`Less than or equal` filter](#le_field-filter) - matches logs where the given field value doesn't exceed the other field value
- [Logical filter](#logical-filter) - allows combining other filters
@@ -751,8 +746,6 @@ See also:
- [String range filter](#string-range-filter)
- [Range filter](#range-filter)
- [`le_field` filter](#le_field-filter)
- [`lt_field` filter](#lt_field-filter)
### Empty value filter
@@ -828,7 +821,6 @@ For example, the following query matches the `error` value in the field `log:lev
See also:
- [Fields' equality filter](#eq_field-filter)
- [Exact prefix filter](#exact-prefix-filter)
- [Multi-exact filter](#multi-exact-filter)
- [Word filter](#word-filter)
@@ -911,8 +903,6 @@ See [these docs](#subquery-filter) for details.
See also:
- [`contains_any` filter](#contains_any-filter)
- [`contains_all` filter](#contains_all-filter)
- [Exact filter](#exact-filter)
- [Word filter](#word-filter)
- [Phrase filter](#phrase-filter)
@@ -920,105 +910,27 @@ See also:
- [Logical filter](#logical-filter)
### contains_all filter
If it is needed to find logs, which contain all the given [words](#word) / phrases, then `v1 AND v2 ... AND vN` [logical filter](https://docs.victoriametrics.com/victorialogs/logsql/#logical-filter)
can be used. VictoriaLogs provides an alternative approach with the `contains_all(v1, v2, ..., vN)` filter. For example, the following query matches logs,
which contain both `foo` [word](#word) and `"bar baz"` phrase in the [`_msg` field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field):
```logsql
contains_all(foo, "bar baz")
```
This is equivalent to the following query:
```logsql
foo AND "bar baz"
```
It is possible to pass arbitrary [query](#query-syntax) inside `contains_all(...)` filter in order to match against the results of this query.
See [these docs](#subquery-filter) for details.
See also:
- [`seq` filter](#sequence-filter)
- [word filter](#word-filter)
- [phrase filter](#phrase-filter)
- [`in` filter](#multi-exact-filter)
- [`contains_any` filter](#contains_any-filter)
### contains_any filter
Sometimes it is needed to find logs, which contain at least one [word](#word) or phrase out of many words / phrases.
This can be done with `v1 OR v2 OR ... OR vN` [logical filter](https://docs.victoriametrics.com/victorialogs/logsql/#logical-filter).
VictoriaLogs provides an alternative approach with the `contains_any(v1, v2, ..., vN)` filter. For example, the following query matches logs,
which contain `foo` [word](#word) or `"bar baz"` phrase in the [`_msg` field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field):
```logsql
contains_any(foo, "bar baz")
```
This is equivalent to the following query:
```logsql
foo OR "bar baz"
```
It is possible to pass arbitrary [query](#query-syntax) inside `contains_any(...)` filter in order to match against the results of this query.
See [these docs](#subquery-filter) for details.
See also:
- [word filter](#word-filter)
- [phrase filter](#phrase-filter)
- [`in` filter](#multi-exact-filter)
- [`contains_all` filter](#contains_all-filter)
### Subquery filter
Sometimes it is needed to select logs with [fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) matching values
selected by another [query](#query-syntax) (aka subquery). LogsQL provides such an ability with the following filters:
Sometimes it is needed to select logs with [fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) containing values
selected by another query (aka subquery). LogsQL provides such an ability with the `in(<subquery>)` filter.
For example, the following query selects all the logs for the last 5 minutes for users,
who visited pages with `admin` [word](#word) in the `path` [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model)
during the last day:
- `field:in(<subquery>)` - it returns logs with `field` values matching the values returned by the `<subquery>`.
For example, the following query selects all the logs for the last 5 minutes for users,
who visited pages with `admin` [word](#word) in the `path` [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model)
during the last day:
```logsql
_time:5m AND user_id:in(_time:1d AND path:admin | fields user_id)
```
```logsql
_time:5m AND user_id:in(_time:1d AND path:admin | fields user_id)
```
- `field:contains_all(<subquery>)` - it returns logs with `field` values containing all the [words](#word) and phrases returned by the `<subquery>`.
For example, the following query selects all the logs for the last 5 minutes, which contain all the `user_id` values from admin logs over the last day
in the [`_msg` field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field):
```logsql
_time:5m _msg:contains_all(_time:1d is_admin:true | fields user_id)
```
- `field:contains_any(<subquery>)` - it returns logs with the `field` values containing at least one [word](#word) or phrase returned by the `<subquery>`.
For example, the following query selects all the logs for the last 5 minutes, which contain at least one `user_id` value from admin logs over the last day
in the [`_msg` field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field):
```logsql
_time:5m _msg:contains_any(_time:1d is_admin:true | fields user_id)
```
The `<subquery>` must end with either [`fields` pipe](#fields-pipe) or [`uniq` pipe](#uniq-pipe) containing a single field name,
so VictoriaLogs could use values of this field for matching the given filter.
The subquery inside `in(...)` must end with either [`fields` pipe](#fields-pipe) or [`uniq` pipe](#uniq-pipe) containing a single field name,
so VictoriaLogs could use values of this field for matching the `in(...)` filter.
See also:
- [`in` filter](#multi-exact-filter)
- [`contains_all` filter](#contains_all-filter)
- [`contains_any` filter](#contains_any-filter)
- [multi-exact filter](#multi-exact-filter)
- [`join` pipe](#join-pipe)
- [`union` pipe](#union-pipe)
### Case-insensitive filter
Case-insensitive filter can be applied to any word, phrase or prefix by wrapping the corresponding [word filter](#word-filter),
@@ -1104,7 +1016,6 @@ For example, the following query matches `event:original` field containing `(err
See also:
- [`contains_all` filter](#contains_all-filter)
- [Word filter](#word-filter)
- [Phrase filter](#phrase-filter)
- [Exact-filter](#exact-filter)
@@ -1353,66 +1264,6 @@ See also:
- [Logical filter](#logical-filter)
### eq_field filter
Sometimes it is needed to find logs, which contain identical values in the given [fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
This can be done with `field1:eq_field(field2)` filter.
For example, the following query matches logs with identical values at `user_id` and `customer_id` fields:
```logsql
user_id:eq_field(customer_id)
```
Quick tip: use `NOT user_id:eq_field(customer_id)` for finding logs where `user_id` isn't equal to `customer_id`. It uses [`NOT` logical operator](#logical-filter).
See also:
- [`exact` filter](#exact-filter)
- [`le_field` filter](#le_field-filter)
- [`lt_field` filter](#lt_field-filter)
### le_field filter
Sometimes it is needed to find logs where one [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) value doesn't exceed the other field value.
This can be done with `field1:le_field(field2)` filter.
For example, the following query matches logs where `duration` [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) doesn't exceed the `max_duration` field:
```logsql
duration:le_field(max_duration)
```
Quick tip: use `NOT duration:le_field(max_duration)` for finding logs where `duration` exceeds the `max_duration`.
See also:
- [range comparison filter](#range-comparison-filter)
- [`lt_field` filter](#lt_field-filter)
- [`eq_field` filter](#eq_field-filter)
### lt_field filter
Sometimes it is needed to find logs where one [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) value is smaller than the other field value.
This can be done with `field1:lt_field(field2)` filter.
For example, the following query matches logs where `duration` [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) is smaller than the `max_duration` field:
```logsql
duration:lt_field(max_duration)
```
Quick tip: use `NOT duration:lt_field(max_duration)` for finding logs where `duration` is bigger or equal to the `max_duration`.
See also:
- [range comparison filter](#range-comparison-filter)
- [`le_field` filter](#le_field-filter)
- [`eq_field` filter](#eq_field-filter)
### Logical filter
Basic LogsQL [filters](#filters) can be combined into more complex filters with the following logical operations:
@@ -1421,11 +1272,11 @@ Basic LogsQL [filters](#filters) can be combined into more complex filters with
For example, `error AND file AND app` matches [log messages](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field),
which simultaneously contain `error`, `file` and `app` [words](#word).
The `AND` operation is frequently used in LogsQL queries, so it is allowed to skip the `AND` word.
For example, `error file app` is equivalent to `error AND file AND app`. See also [`contains_all` filter](#contains_all-filter).
For example, `error file app` is equivalent to `error AND file AND app`.
- `q1 OR q2` - merges log entries returned by both `q1` and `q2`. Arbitrary number of [filters](#filters) can be combined with `OR` operation.
For example, `error OR warning OR info` matches [log messages](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field),
which contain at least one of `error`, `warning` or `info` [words](#word). See also [`contains_any` filter](#contains_any-filter).
which contain at least one of `error`, `warning` or `info` [words](#word).
- `NOT q` - returns all the log entries except of those which match `q`. For example, `NOT info` returns all the
[log messages](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field),
@@ -1499,7 +1350,6 @@ LogsQL supports the following pipes:
- [`first`](#first-pipe) returns the first N logs after sorting them by the given [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
- [`format`](#format-pipe) formats output field from input [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
- [`join`](#join-pipe) joins query results by the given [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
- [`json_array_len`](#json_array_len-pipe) returns the length of JSON array stored at the given [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
- [`hash`](#hash-pipe) returns the hash over the given [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) value.
- [`last`](#last-pipe) returns the last N logs after sorting them by the given [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
- [`len`](#len-pipe) returns byte length of the given [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) value.
@@ -1521,8 +1371,7 @@ LogsQL supports the following pipes:
- [`unpack_json`](#unpack_json-pipe) unpacks JSON messages from [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
- [`unpack_logfmt`](#unpack_logfmt-pipe) unpacks [logfmt](https://brandur.org/logfmt) messages from [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
- [`unpack_syslog`](#unpack_syslog-pipe) unpacks [syslog](https://en.wikipedia.org/wiki/Syslog) messages from [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
- [`unpack_words`](#unpack_words-pipe) unpacks [words](#word) from the given [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
- [`unroll`](#unroll-pipe) unrolls JSON arrays from [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) into separate rows.
- [`unroll`](#unroll-pipe) unrolls JSON arrays from [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
### block_stats pipe
@@ -2211,24 +2060,6 @@ See also:
- [conditional `stats`](https://docs.victoriametrics.com/victorialogs/logsql/#stats-with-additional-filters)
- [`filter` pipe](#filter-pipe)
### json_array_len pipe
`<q> | json_array_len(field) as result_field` calculates the length of JSON array at the given [`field`](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model)
and stores it into the `result_field`, for every log entry returned by `<q>` [query](#query-syntax).
For example, the following query returns top 5 logs with contain [log messages](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field)
with the biggest number of [words](#word) across all the logs for the last 5 minutes:
```logsql
_time:5m | unpack_words _msg as words | json_array_len (words) as words_count | first 5 (words_count desc)
```
See also:
- [`len` pipe](#len-pipe)
- [`unpack_words` pipe](#unpack_words-pipe)
- [`first` pipe](#first-pipe)
### hash pipe
`<q> | hash(field) as result_field` calculates hash value for the given [`field`](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model)
@@ -2284,7 +2115,6 @@ _time:5m | len(_msg) as msg_len | sort by (msg_len desc) | limit 5
See also:
- [`json_array_len` pipe](#json_array_len-pipe)
- [`sum_len` stats function](#sum_len-stats)
- [`sort` pipe](#sort-pipe)
- [`limit` pipe](#limit-pipe)
@@ -2437,13 +2267,6 @@ only and stores the result in `baz` field:
_time:5m | pack_json fields (foo, bar) as baz
```
It is possible to pass field prefixes into `fields (...)` in order to pack only the fields, which start with the given prefixes.
For example, the following query builds JSON with all the fields, which start with either `foo.` or `bar.`:
```logsql
_time:5m | pack_json fields (foo.*, bar.*) as baz
```
The `pack_json` doesn't modify or delete other labels. If you do not need them, then add [`| fields ...`](#fields-pipe) after the `pack_json` pipe. For example, the following query
leaves only the `foo` label with the original log fields packed into JSON:
@@ -2483,13 +2306,6 @@ For example, the following query builds [logfmt](https://brandur.org/logfmt) mes
_time:5m | pack_logfmt fields (foo, bar) as baz
```
It is possible to pass field prefixes into `fields (...)` in order to pack only the fields, which start with the given prefixes.
For example, the following query builds `logfmt` message with all the fields, which start with either `foo.` or `bar.`:
```logsql
_time:5m | pack_logfmt fields (foo.*, bar.*) as baz
```
The `pack_logfmt` doesn't modify or delete other labels. If you do not need them, then add [`| fields ...`](#fields-pipe) after the `pack_logfmt` pipe. For example, the following query
leaves only the `foo` label with the original log fields packed into [logfmt](https://brandur.org/logfmt):
@@ -3346,35 +3162,6 @@ only if `hostname` field in the current log entry isn't set or empty:
_time:5m | unpack_syslog if (hostname:"") from foo
```
### unpack_words pipe
`<q> | unpack_words from <src_field> as <dst_field>` [pipe](#pipes) unpacks [words](#word) from the given `<src_field>` [log field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model)
of `<q>` [query](#query-syntax) results into `<dst_field>` as a JSON array.
For example, the following query unpacks words from [log messages](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field) into `words` field:
```logsql
_time:5m | unpack_words from _msg as words
```
By default `unpack_words` pipe unpacks all the words, including duplicates, from the `<src_field>`. It is possible to drop duplicate words by adding `drop_duplicates` suffix to the pipe.
For example, the following query extracts only unique words from every `text` field:
```logsql
_time:5m | unpack_words from text as words drop_duplicates
```
It may be convenient to use [`unroll` pipe](#unroll-pipe) for unrolling the JSON array with unpacked words from the destination field.
For example, the following query returns top 5 most frequently seen words across [log messages](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field) for the last 5 minutes:
```logsql
_time:5m | unpack_words from _msg as words | unroll words | top 5 (words)
```
See also:
- [`unroll` pipe](#unroll-pipe)
### unroll pipe
`<q> | unroll by (field1, ..., fieldN)` [pipe](#pipes) can be used for unrolling JSON arrays from `field1`, ..., `fieldN`
@@ -3392,7 +3179,6 @@ the unrolled array items into separate fields for further processing.
See also:
- [`unpack_json` pipe](#unpack_json-pipe)
- [`unpack_words` pipe](#unpack_words-pipe)
- [`extract` pipe](#extract-pipe)
- [`uniq_values` stats function](#uniq_values-stats)
- [`values` stats function](#values-stats)

View File

@@ -33,8 +33,8 @@ Just download archive for the needed Operating system and architecture, unpack i
For example, the following commands download VictoriaLogs archive for Linux/amd64, unpack and run it:
```sh
curl -L -O https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.15.0-victorialogs/victoria-logs-linux-amd64-v1.15.0-victorialogs.tar.gz
tar xzf victoria-logs-linux-amd64-v1.15.0-victorialogs.tar.gz
curl -L -O https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.12.0-victorialogs/victoria-logs-linux-amd64-v1.12.0-victorialogs.tar.gz
tar xzf victoria-logs-linux-amd64-v1.12.0-victorialogs.tar.gz
./victoria-logs-prod
```
@@ -58,7 +58,7 @@ Here is the command to run VictoriaLogs in a Docker container:
```sh
docker run --rm -it -p 9428:9428 -v ./victoria-logs-data:/victoria-logs-data \
docker.io/victoriametrics/victoria-logs:v1.15.0-victorialogs
docker.io/victoriametrics/victoria-logs:v1.12.0-victorialogs
```
See also:

View File

@@ -18,13 +18,13 @@ It has the following features:
- It supports live tailing - see [these docs](#live-tailing).
This tool can be obtained from the linked release pages at the [changelog](https://docs.victoriametrics.com/victorialogs/changelog/)
or from docker images at [Docker Hub](https://hub.docker.com/r/victoriametrics/vlogscli/tags) and [Quay](https://quay.io/repository/victoriametrics/vlogscli?tab=tags).
or from [docker images](https://hub.docker.com/r/victoriametrics/vlogscli/tags).
### Running `vlogscli` from release binary
```sh
curl -L -O https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.15.0-victorialogs/vlogscli-linux-amd64-v1.15.0-victorialogs.tar.gz
tar xzf vlogscli-linux-amd64-v1.15.0-victorialogs.tar.gz
curl -L -O https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.12.0-victorialogs/vlogscli-linux-amd64-v1.12.0-victorialogs.tar.gz
tar xzf vlogscli-linux-amd64-v1.12.0-victorialogs.tar.gz
./vlogscli-prod
```

View File

@@ -21,16 +21,14 @@ _Note: This page provides only integration instructions for vmalert and Victoria
Run vmalert with the following settings:
```sh
./bin/vmalert -rule=alert.rules \ # Path to the files or http url with alerting and/or recording rules in YAML format.
-datasource.url=http://victorialogs:9428 \ # VictoriaLogs address.
-notifier.url=http://alertmanager:9093 \ # AlertManager URL (required if alerting rules are used)
-remoteWrite.url=http://victoriametrics:8428 \ # Remote write compatible storage to persist recording rules and alerts state info
-remoteRead.url=http://victoriametrics:8428 \ # Prometheus HTTP API compatible datasource to restore alerts state from
./bin/vmalert -rule=alert.rules \ # Path to the files or http url with alerting and/or recording rules in YAML format.
-datasource.url=http://localhost:9428 \ # VictoriaLogs address.
-notifier.url=http://localhost:9093 \ # AlertManager URL (required if alerting rules are used)
-remoteWrite.url=http://localhost:8428 \ # Remote write compatible storage to persist rules and alerts state info (required for recording rules)
-remoteRead.url=http://localhost:8428 \ # Prometheus HTTP API compatible datasource to restore alerts state from
```
> Note: By default, vmalert assumes all configured rules have `prometheus` type and will validate them accordingly.
> For rules in [LogsQL](https://docs.victoriametrics.com/victorialogs/logsql/) specify `type: vlogs` on [Group level](#groups).
> Or set `-rule.defaultRuleType=vlogs` cmd-line flag to apply `type: vlogs` to all configured groups.
> Note: By default, vmalert assumes configured rules have `prometheus` type and will validate them accordingly. For rules in [LogsQL](https://docs.victoriametrics.com/victorialogs/logsql/) specify `type: vlogs` on [Group level](#groups). Or set `-rule.defaultRuleType=vlogs` cmd-line flag to automatically apply `type: vlogs` to all groups.
Each `-rule` file may contain arbitrary number of [groups](https://docs.victoriametrics.com/vmalert/#groups).
See examples in [Groups](#groups) section. See the full list of configuration flags and their descriptions in [configuration](#configuration) section.
@@ -39,11 +37,9 @@ With configuration example above, vmalert will perform the following interaction
![vmalert](vmalert_victorialogs.webp)
1. Rules listed in `-rule` file are executed against VictoriaLogs service configured via `-datasource.url`;
1. Triggered alerting notifications are sent to [Alertmanager](https://github.com/prometheus/alertmanager) service configured via `-notifier.url`;
1. Results of recording rules expressions and alerts state are persisted to Prometheus-compatible remote-write endpoint
(i.e. VictoriaMetrics) configured via `-remoteWrite.url`;
1. On vmalert restarts, alerts state [can be restored](https://docs.victoriametrics.com/vmalert/#alerts-state-on-restarts)
by querying Prometheus-compatible HTTP API endpoint (i.e. VictoriaMetrics) configured via `-remoteRead.url`.
2. Triggered alerting notifications are sent to [Alertmanager](https://github.com/prometheus/alertmanager) service configured via `-notifier.url`;
3. Results of recording rules expressions and alerts state are persisted to Prometheus-compatible remote-write endpoint (i.e. VictoriaMetrics) configured via `-remoteWrite.url`;
4. On vmalert restarts, alerts state [can be restored](https://docs.victoriametrics.com/vmalert/#alerts-state-on-restarts) by querying Prometheus-compatible HTTP API endpoint (i.e. VictoriaMetrics) configured via `-remoteRead.url`.
## Configuration
@@ -86,7 +82,7 @@ The following are key flags related to integration with VictoriaLogs:
Since there is no intentional search delay in VictoriaLogs, `-rule.evalDelay` can be reduced to a few seconds to accommodate network and ingestion time.
```
See full list of configuration options [here](https://docs.victoriametrics.com/vmalert/#configuration).
For more configuration options, such as `notifiers`, visit https://docs.victoriametrics.com/vmalert/#configuration.
### Groups
@@ -152,7 +148,7 @@ groups:
description: "Connection from address {{$labels.ip}} has {{$value}}% failed requests in last 5 minutes"
```
User can specify a customized time filter if needed. For example, rule below will be evaluated every 5 minutes,
User can also specify a customized time filter if needed. For example, rule below will be evaluated every 5 minutes,
but will calculate result over the logs from the last 10 minutes.
```yaml
groups:
@@ -166,7 +162,7 @@ groups:
description: "Connection from address {{$labels.ip}} has {{$value}}% failed requests in last 10 minutes"
```
_Please note, vmalert doesn't support [backfilling](#rules-backfilling) for rules with a customized time filter now. (Might be added in future)._
Please note, vmalert doesn't support [backfilling](#rules-backfilling) for rules with a customized time filter now. (Might be added in future)
## Rules backfilling
@@ -184,8 +180,8 @@ See more details about backfilling [here](https://docs.victoriametrics.com/vmale
## Performance tip
LogsQL allows users to obtain multiple stats from a single expression. For instance, the following query calculates
50th, 90th and 99th percentiles for the `request_duration_seconds` field over logs for the last 5 minutes:
LogsQL allows users to obtain multiple stats from a single expression.
For instance, the following query calculates 50th, 90th and 99th percentiles for the `request_duration_seconds` field over logs for the last 5 minutes:
```logsql
_time:5m | stats
@@ -202,9 +198,8 @@ groups:
interval: 5m
rules:
- record: requestDurationQuantile
expr: '* | stats by (service) quantile(0.5, request_duration_seconds) p50, quantile(0.9, request_duration_seconds) p90, quantile(0.99, request_duration_seconds) p99'
expr: '_time:5m | stats by (service) quantile(0.5, request_duration_seconds) p50, quantile(0.9, request_duration_seconds) p90, quantile(0.99, request_duration_seconds) p99'
```
This creates three metrics for each service:
```
requestDurationQuantile{stats_result="p50", service="service-1"}
@@ -222,7 +217,6 @@ For additional tips on writing LogsQL, refer to this [doc](https://docs.victoria
## Frequently Asked Questions
### How to use [multitenancy](https://docs.victoriametrics.com/victorialogs/#multitenancy) in rules?
vmalert doesn't support multi-tenancy for VictoriaLogs in the same way as it [supports it for VictoriaMetrics in ENT version](https://docs.victoriametrics.com/vmalert/#multitenancy).
However, it is possible to specify the queried tenant from VictoriaLogs datasource via `headers` param in [Group config](https://docs.victoriametrics.com/vmalert/#groups).
For example, the following config will execute all the rules within the group against tenant with `AccountID=1` and `ProjectID=2`:
@@ -294,12 +288,10 @@ To persist different rule results to different tenants in VictoriaMetrics, there
```
### How to use one vmalert for VictoriaLogs and VictoriaMetrics rules in the same time?
We recommend running separate instances of vmalert for VictoriaMetrics and VictoriaLogs.
However, vmalert allows having many groups with different rule types (`vlogs`, `prometheus`, `graphite`).
But only one `-datasource.url` cmd-line flag can be specified, so it can't be configured with more than 1 datasource.
VictoriaMetrics and VictoriaLogs datasources have different query path prefixes, so it is possible to use
[vmauth](https://docs.victoriametrics.com/vmauth/) to route requests of different types between datasources.
However, VictoriaMetrics and VictoriaLogs datasources have different query path prefixes, and it is possible to use [vmauth](https://docs.victoriametrics.com/vmauth/) to route requests of different types between datasources.
See example of vmauth config for such routing below:
```yaml
unauthorized_user:
@@ -311,5 +303,4 @@ See example of vmauth config for such routing below:
- "/select/logsql/.*"
url_prefix: "http://victorialogs:9428"
```
Now, vmalert can be configured with `--datasource.url=http://vmauth:8427/` to send queries to vmauth,
and vmauth will route them to the specified destinations as in configuration example above.
Now, vmalert needs to be configured with `--datasource.url=http://vmauth:8427/` to send queries to vmauth, and vmauth will route them to the specified destinations as in configuration example above.

View File

@@ -84,14 +84,6 @@ models:
## Output produced by vmanomaly
`vmanomaly` models generate [metrics](https://docs.victoriametrics.com/anomaly-detection/components/models#vmanomaly-output) like `anomaly_score`, `yhat`, `yhat_lower`, `yhat_upper`, and `y`. These metrics provide a comprehensive view of the detected anomalies. The service also produces [health check metrics](https://docs.victoriametrics.com/anomaly-detection/components/monitoring#metrics-generated-by-vmanomaly) for monitoring its performance.
## Visualizations
To visualize and interact with both [self-monitoring metrics](https://docs.victoriametrics.com/anomaly-detection/components/monitoring/) and [produced anomaly scores](#what-is-anomaly-score), `vmanomaly` provides respective Grafana dashboards:
- For guidance on using the `vmanomaly` Grafana dashboard and drilling down into anomaly score visualizations, refer to the [default preset section](https://docs.victoriametrics.com/anomaly-detection/presets/#default).
- To monitor `vmanomaly` health, operational performance, and potential issues in real time, visit the [self-monitoring section](https://docs.victoriametrics.com/anomaly-detection/self-monitoring/).
## Choosing the right model for vmanomaly
Selecting the best model for `vmanomaly` depends on the data's nature and the [types of anomalies](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-2/#categories-of-anomalies) to detect. For instance, [Z-score](https://docs.victoriametrics.com/anomaly-detection/components/models#z-score) is suitable for data without trends or seasonality, while more complex patterns might require models like [Prophet](https://docs.victoriametrics.com/anomaly-detection/components/models#prophet).

View File

@@ -13,13 +13,10 @@ menu:
This approach represents a paradigm shift from traditional [static threshold-based alerting rules](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#rule-based-alerting), focused on *raw metric values*, to *static* rules based on [`anomaly_scores`](https://docs.victoriametrics.com/anomaly-detection/faq/#what-is-anomaly-score). These scores offer a consistent, default threshold that remains stable over time, being adjusted for trends, seasonality, data scale, thus, reducing the engineering effort required for maintenance. Anomaly scores are produced by [machine learning models](https://docs.victoriametrics.com/anomaly-detection/components/models/), which are regularly retrained on varying time frames, ensuring alerts remain current and responsive to evolving data patterns.
Additionally, **preset mode** minimizes user input needed to run the service - `vmanomaly` service can be configured by specifying only the preset name and data sources in the [`reader`](https://docs.victoriametrics.com/anomaly-detection/components/reader/) and [`writer`](https://docs.victoriametrics.com/anomaly-detection/components/writer/) sections of the configuration file. All other parameters are already preconfigured.
Each preset, including the [default](#default), comes with premade downstream assets such as **alerting rules** or **Grafana dashboards**, making integration even more straightforward.
Additionally, **preset mode** minimizes user input needed to run the service. You can configure `vmanomaly` by specifying only the preset name and data sources in the [`reader`](https://docs.victoriametrics.com/anomaly-detection/components/reader/) and [`writer`](https://docs.victoriametrics.com/anomaly-detection/components/writer/) sections of the configuration file. All other parameters are already preconfigured.
**Available presets:**
- [Default](#default)
Available presets:
- [Node-Exporter](#node-exporter)
To enable preset mode, `preset` arg should be set to particular preset name:
@@ -36,154 +33,6 @@ After you run `vmanomaly` with `preset` arg specified, available assets can be v
![preset-localhost](presets-localhost.webp)
## Default
The default preset indicates that the `vmanomaly` service is running in its standard mode — either because the `preset` section is not specified in the configuration or is explicitly set to `default` or `default:vX.Y`.
Since this mode is designed to support **any** configuration, it requires the user to fully define all [necessary configuration sections](https://docs.victoriametrics.com/anomaly-detection/components/), including:
- [Reader](https://docs.victoriametrics.com/anomaly-detection/components/reader) (with the required `queries` section)
- [Writer](https://docs.victoriametrics.com/anomaly-detection/components/writer)
- [Models](https://docs.victoriametrics.com/anomaly-detection/components/models)
- [Schedulers](https://docs.victoriametrics.com/anomaly-detection/components/scheduler)
Although this mode is designed to be as flexible as possible, it includes a pre-built [Grafana dashboard](https://grafana.com/grafana/dashboards/22922). This dashboard is based on the [service output](https://docs.victoriametrics.com/anomaly-detection/components/models/#vmanomaly-output), specifically [anomaly scores](https://docs.victoriametrics.com/anomaly-detection/faq/#what-is-anomaly-score), and helps streamline the anomaly drill-down process.
### Grafana Dashboard
> Note: [For additional benefits](https://docs.victoriametrics.com/victoriametrics-datasource/#motivation), this dashboard is based on [VictoriaMetrics datasource](https://docs.victoriametrics.com/victoriametrics-datasource/) rather than on `Prometheus` datasource. Please follow [these instructions](https://docs.victoriametrics.com/victoriametrics-datasource/#installation) to enable datasource in Grafana.
You can find the Grafana dashboard `.json` file either [here](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker/vmanomaly/vmanomaly-default/dashboard.json) or, for already running `vmanomaly` instance: `http://localhost:8490/presets/dashboard.json`.
Alternatively, you can [download it from Grafana cloud](https://grafana.com/grafana/dashboards/22922) or import directly in Grafana by ID `22922`. Below is the navigation guide.
#### Filters
![dashboard-as-filter-section](vmanomaly-default-dashboard-filters.webp)
**P.S.** Expand the **"Dashboard Hints"** panel to view suggested usage tips.
- Hover over the **(i) icons** in the top-right corner of each visualization and variable to see a detailed description of its specifics.
- **Dashboard-level filters** apply to all visualizations, unless otherwise stated
- The most generic filters are already added, e.g.`preset`, `model_alias`, `scheduler_alias`, which are the labels that all `anomaly_score` metrics have by default
- To **apply additional filters** on one or more labels, use `Ad hoc filters` [ad hoc Grafana filter](https://grafana.com/docs/grafana/latest/dashboards/use-dashboards/#filter-dashboard-data).
- Adjust `Metrics prefix` (by default - no prefix) according to output format, defined in `writer.metric_format` [config section](https://docs.victoriametrics.com/anomaly-detection/components/writer/?highlight=metric_format#config-parameters). <br>Example: `__name__: 'vmanomaly_$VAR'` will result in metric names 'vmanomaly_anomaly_score', 'vmanomaly_y', 'vmanomaly_yhat', 'vmanomaly_yhat_upper', etc. - put `vmanomaly_` in the field for such metrics to be properly fetched in visualizations
- The **`Group by`** dashboard filter allows you to segment visualizations based on selected label (applicable to [Global Anomaly Score Statistics](#global-anomaly-score-statistics) section).
- For example, setting it to `model_alias` will create separate sub-visualizations for each unique model alias.
- This helps in analyzing anomaly patterns across different dimensions.
- It's recommended to start with `for` label (which stands for query alias defined in `reader.queries` and can be also configured in `writer.metric_format` [config section](https://docs.victoriametrics.com/anomaly-detection/components/writer/?highlight=metric_format#config-parameters))
- The **`Threshold`** dashboard constant (default: `1.0`) determines anomaly classification:
- **Non-anomalous** data points: `anomaly_score``Threshold`
- **Anomalous** data points: `anomaly_score` > `Threshold`
- Set the threshold value consistently with your **alerting rules** for accurate anomaly detection visualizations.
- The **`Min. Anomaly%`** dashboard constant (from `[0,1]` range) helps to filter the visualizations in [Local Anomaly Score Statistics](#local-anomaly-score-statistics) dashboard section, leaving only timeseries with **% of anomalies over time > selected threshold**. <br>Set it to 0 to disable filtering on anomaly percentage.
- The **`Show series`** dashboard custom multi-valued list allows to specify what [vmanomaly output](https://docs.victoriametrics.com/anomaly-detection/components/models/#vmanomaly-output) series to plot on [Local Anomaly Score Statistics](#local-anomaly-score-statistics) dashboard section. Defaults to `y` - raw metric values model saw during prediction phase. Setting it to (`y`, `yhat`, `yhat_lower`, `yhat_upper`) may ease the debugging of produced anomaly scores.
#### Global Anomaly Score Statistics
![dashboard-as-sections-global](vmanomaly-default-dashboard-sections-global.webp)
This section provides aggregated anomaly score statistics, grouped by the selected `Group by` filter label (e.g. by `for`):
- A table summarizing key statistics over the selected data range, including:
- The number of unique time series in each group
- The total number of data points
- The total number of anomalies (data points where the anomaly score exceeds the selected `Threshold` filter)
- The percentage of anomalous data points (as % of the total number of data points)
- The minimum, maximum, and key quantiles (25th, 50th, 75th, and 99th percentiles) of anomaly scores
- A time series graphs for the respective group, displaying:
- The percentage of anomalies over time
- The 99th percentile (`q99`) of anomaly scores over time
- A histogram visualizing the distribution of individual anomaly scores (for scores exceeding the `Threshold` filter), helping to identify a suitable cutoff for alerting rules and distinguish real business anomalies from minor noise.
#### Local Anomaly Score Statistics
![dashboard-as-sections-local](vmanomaly-default-dashboard-sections-local.webp)
This section provides anomaly score statistics at the individual time series level:
- A table displaying raw time series data, including:
- The percentage of anomalies over time
- The associated label set (each row represents an individual time series, with columns for label names and values)
- Sorted in descending order by anomaly percentage, with the most anomalous time series at the top
- A time series graph visualizing anomaly scores over time, allowing you to pinpoint when and where anomalies occurred.
- A time series graph with respective raw metric values over time. **Note**: this panel has data if these conditions were met:
- [univariate models](https://docs.victoriametrics.com/anomaly-detection/components/models/#univariate-models) were used under respective `model_alias` label
- The `provide_series` [argument](https://docs.victoriametrics.com/anomaly-detection/components/models/#provide-series) explicitly or implicitly included `y` <br>(input data of original timeseries, observed by the model at prediction time).
- In `writer.metric_format` special label `for` was configured to point out to query alias used in `reader.queries` section. <br>See `metric_format` argument description on [writer page](https://docs.victoriametrics.com/anomaly-detection/components/writer/?highlight=metric_format#config-parameters) for the details.
Use **`Show series`** dashboard variable to specify what [vmanomaly output](https://docs.victoriametrics.com/anomaly-detection/components/models/#vmanomaly-output) series to plot on [Local Anomaly Score Statistics](#local-anomaly-score-statistics) dashboard section corresponding to raw metrics scale. Defaults to `y` - raw metric values model saw during prediction phase. Setting it to (`y`, `yhat`, `yhat_lower`, `yhat_upper`) may ease the debugging of produced anomaly scores.
### Example
To analyze anomalies effectively, start from a high-level overview and progressively narrow down to specific time series.
1. **Select a time range:**
Choose the relevant time window (e.g., the last `12h`) on dashboard's time range filter.
2. **Apply grouping:**
Use an appropriate `Group by` label (e.g., `for` to group by `reader.queries`). See the [filters section](#filters) for details.
3. **Sort groups:**
- Sort the table by `q99` or the maximum anomaly percentage in descending order (if not already).
- Identify groupings with the highest values.
4. **Analyze visual trends:**
- Observe patterns within grouped anomalies.
- For example, the `context_switch` metric in the anomaly percentage graph shows a steady increase over time.
5. **Zoom in on the most anomalous groups:**
- Focus on the most affected category (`context_switch` in this case).
- Notice when the anomaly score first exceeded the threshold—around 15:35 in the example.
![dashboard-as-example-step-1](vmanomaly-default-dashboard-example-step-1.webp)
6. **Refine the view:**
- Before switching to the [Local Anomaly Score Statistics](#local-anomaly-score-statistics) section, apply an ad-hoc filter (e.g., `for=context_switch`) to exclude unrelated data.
![dashboard-as-example-step-2](vmanomaly-default-dashboard-example-step-2.webp)
---
In the **Local View** section, identify the most anomalous time series by:
- Checking the table, which is sorted in descending order by anomaly percentage.
- Analyzing the graph to observe anomaly scores over time.
To focus on the most significant anomalies, set the `Min anomaly %` dashboard constant (e.g., `5%` in this example). This will filter the table and graph to display only time series with anomaly percentages over selected range above the threshold.
![dashboard-as-example-step-3](vmanomaly-default-dashboard-sections-local.webp)
To further refine the results, you can:
- **(Global effect)** Filter table columns (e.g., by a specific `instance`).
**Note:** This applies an additional ad-hoc filter at the dashboard level, so remember to remove it after your investigation.
![dashboard-as-example-step-4](vmanomaly-default-dashboard-example-step-3.webp)
- **(Local effect)** Click on legend values to isolate specific series (`CTRL` or `CMD` + click to select multiple items).
For 1 or more filtered timeseries, explore the label set directly in the table or via the popup.<br>
To determine whether the anomaly is a true or [false positive](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#false-positive), check the raw metric values:
- **(Preferred method)** Directly within the dashboard, if [univariate models](https://docs.victoriametrics.com/anomaly-detection/components/models/#univariate-models) were used and the `provide_series` [argument](https://docs.victoriametrics.com/anomaly-detection/components/models/#provide-series) explicitly or implicitly included `y` (i.e., the original values observed by the model at prediction time). Also, using **Show series** filter to add `yhat`, `yhat_lower`, `yhat_upper` to the visualizations (if respective columns were defined in `provide_series`) might help to understand model behavior and the magnitude of produced anomaly scores.
![dashboard-as-example-step-4a](vmanomaly-default-dashboard-example-step-4a.webp)
- **(Alternative method)** Use the `Explore` tab in Grafana:
![dashboard-as-example-step-4b](vmanomaly-default-dashboard-example-step-4b.webp)
- Copy the corresponding `for` query MetricsQL expression from the `reader.queries` section of the `vmanomaly` config (e.g., `rate(node_context_switches_total[3m])` in this particular example).
- Paste that expression into the `Metrics browser` field.
- Apply filters to isolate specific series, either by adding `{label=value}` filters to the query or by clicking on legend items as shown in the screenshot.
**Final Analysis:** This anomaly appears to be a **pattern shift**, potentially caused by:
- **Application-related factors**: Increased time series ingestion by `vmagent`, a higher number of scrapers, or more remote write activity.
- **Instance-level factors**: Increased CPU contention from other processes, higher soft interrupts, or changes in CPU affinity affecting `vmagent`.
- **Business-related factors**: Scheduled workloads or expected operational changes that should be accounted for. If confirmed as non-anomalous, these patterns can be excluded from detection by adjusting the configuration for seasonality or refining alerting rules to ignore predefined time ranges from triggering actual alerts.
Further investigation is needed to determine the exact cause.
---
## Node-Exporter
@@ -191,7 +40,7 @@ The Node-Exporter preset simplifies the monitoring and anomaly detection of key
> **Note: Node-Exporter preset assets can be also found [here](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker/vmanomaly/vmanomaly-node-exporter-preset/)**
For enabling Node-Exporter in config file set the `preset` arg accordingly. Also, include at least `datasource_url`-s (and `tenant_id` if using [cluster version of VictoriaMetrics](https://docs.victoriametrics.com/cluster-victoriametrics/)) in `reader` and `writer` sections, like that:
For enabling Node-Exporter in config file set the `preset` arg accordingly. Also, include at least `datasource_url`-s (and `tenant_id` if using cluster version of VictoriaMetrics) in reader and writer sections, like that:
```yaml
preset: "node-exporter"
@@ -207,7 +56,7 @@ Run a service using such config file with one of the [available options](https:/
### Generated anomaly scores
Machine learning models will be fit for each timeseries, returned by underlying [MetricsQL](https://docs.victoriametrics.com/metricsql/) queries.
In addition to preset label, anomaly score metric labels will also contain [model](https://docs.victoriametrics.com/anomaly-detection/components/models/) and [scheduler](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/) aliases for labelset uniqueness (`preset`, `model_alias`, `scheduler_alias` labels, respectively).
Anomaly score metric labels will also contain [model classes](https://docs.victoriametrics.com/anomaly-detection/components/models/) and [schedulers](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/) for labelset uniqueness.
Here's an example of produced metrics:
@@ -359,7 +208,7 @@ Disk latency. The total read/write time spent in seconds. / The total number of
</tbody>
</table>
### Example
## Example
Here's how attached [Grafana dashboard](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker/vmanomaly/vmanomaly-node-exporter-preset/dashboard.json) can be used to drill down anomalies:
@@ -387,3 +236,4 @@ For this node from the timestamp `2024-06-03 10:35:00` CPU time spent handling s
At the same time `cpu_seconds_total` for `steal` mode started to grow as well.
![steal](presets_cpu_seconds_steal.webp)

View File

@@ -216,30 +216,20 @@ writer:
```
### Recommended steps
**Schedulers**:
Next steps:
- Define how often to run and make inferences in the [scheduler](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/) section of a config file.
**Reader**:
- Setup the datasource to read data from in the [reader](https://docs.victoriametrics.com/anomaly-detection/components/reader/) section. Include tenant ID if using a [cluster version of VictoriaMetrics](https://docs.victoriametrics.com/cluster-victoriametrics/) for reading the data.
- Define queries for input data using [MetricsQL](https://docs.victoriametrics.com/metricsql/) under `reader.queries` section.
**Writer**:
- Setup the datasource to read data from in the [reader](https://docs.victoriametrics.com/anomaly-detection/components/reader/) section.
- Specify where and how to store anomaly detection metrics in the [writer](https://docs.victoriametrics.com/anomaly-detection/components/writer/) section.
- Include tenant ID if using a [cluster version of VictoriaMetrics](https://docs.victoriametrics.com/cluster-victoriametrics/) for writing the results.
- Adding `for` label to `metric_format` argument is recommended for smoother visual experience in the [anomaly score dashboard](https://docs.victoriametrics.com/anomaly-detection/presets/#default).
- Configure built-in models parameters according to your needs in the [models](https://docs.victoriametrics.com/anomaly-detection/components/models/) section.
- Integrate your [custom models](https://docs.victoriametrics.com/anomaly-detection/components/models/#custom-model-guide) with `vmanomaly`.
- Define queries for input data using [MetricsQL](https://docs.victoriametrics.com/metricsql/).
**Models**:
- Configure built-in models parameters according to your needs in the [models](https://docs.victoriametrics.com/anomaly-detection/components/models/) section.
- (Optionally) Develop or integrate your [custom models](https://docs.victoriametrics.com/anomaly-detection/components/models/#custom-model-guide) with `vmanomaly`.
- Adding `y` to `provide_series` arg values is recommended for smoother visual experience in the [anomaly score dashboard](https://docs.victoriametrics.com/anomaly-detection/presets/#default). Also, other `vmanomaly` [output](https://docs.victoriametrics.com/anomaly-detection/components/models#vmanomaly-output) can be used in `provide_series`. <br>**Note:** Only [univariate models](https://docs.victoriametrics.com/anomaly-detection/components/models/#univariate-models) support the generation of such output.
## Check also
Here are the links for further deep dive into Anomaly Detection in general and `vmanomaly` in particular:
Here are other materials that you might find useful:
- [Guide: Anomaly Detection and Alerting Setup](https://docs.victoriametrics.com/anomaly-detection/guides/guide-vmanomaly-vmalert/)
- [FAQ](https://docs.victoriametrics.com/anomaly-detection/faq/)
- [CHANGELOG](https://docs.victoriametrics.com/anomaly-detection/changelog/)
- [Changelog](https://docs.victoriametrics.com/anomaly-detection/changelog/)
- [Anomaly Detection Blog](https://victoriametrics.com/tags/anomaly-detection/)

View File

@@ -2,9 +2,9 @@
- To use *vmanomaly*, part of the enterprise package, a license key is required. Obtain your key [here](https://victoriametrics.com/products/enterprise/trial/) for this tutorial or for enterprise use.
- In the tutorial, we'll be using the following VictoriaMetrics components:
- [VictoriaMetrics Single-Node](https://docs.victoriametrics.com/single-server-victoriametrics) (v1.112.0)
- [vmalert](https://docs.victoriametrics.com/vmalert/) (v1.112.0)
- [vmagent](https://docs.victoriametrics.com/vmagent/) (v1.112.0)
- [VictoriaMetrics Single-Node](https://docs.victoriametrics.com/single-server-victoriametrics) (v1.111.0)
- [vmalert](https://docs.victoriametrics.com/vmalert/) (v1.111.0)
- [vmagent](https://docs.victoriametrics.com/vmagent/) (v1.111.0)
- [Grafana](https://grafana.com/) (v.10.2.1)
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/)
- [Node exporter](https://github.com/prometheus/node_exporter#node-exporter) (v1.7.0) and [Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/) (v0.27.0)
@@ -315,7 +315,7 @@ Let's wrap it all up together into the `docker-compose.yml` file.
services:
vmagent:
container_name: vmagent
image: victoriametrics/vmagent:v1.112.0
image: victoriametrics/vmagent:v1.111.0
depends_on:
- "victoriametrics"
ports:
@@ -332,7 +332,7 @@ services:
victoriametrics:
container_name: victoriametrics
image: victoriametrics/victoria-metrics:v1.112.0
image: victoriametrics/victoria-metrics:v1.111.0
ports:
- 8428:8428
volumes:
@@ -365,7 +365,7 @@ services:
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:v1.112.0
image: victoriametrics/vmalert:v1.111.0
depends_on:
- "victoriametrics"
ports:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

View File

@@ -18,54 +18,14 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/).
## tip
**Update note 1: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/) and [vmagent](https://docs.victoriametrics.com/vmagent/) include a fix which enforces IPv6 addresses escaping for containers discovered with [Kubernetes service-discovery](https://docs.victoriametrics.com/sd_configs/#kubernetes_sd_configs) and `role: pod` which do not have exposed ports defined. This means that `address` for these containers will always be wrapped in square brackets, this might affect some relabeling rules which were relying on previous behaviour.**
**Update note 2: [vmalert](https://docs.victoriametrics.com/vmalert/) disallow using [time buckets stats pipe](https://docs.victoriametrics.com/victorialogs/logsql/#stats-by-time-buckets) in alerting or recording rules with VictoriaLogs as datasource. Time buckets used with [stats query API](https://docs.victoriametrics.com/victorialogs/querying/#querying-log-stats) may produce unexpected results for user and result into cardinality issues.**
* FEATURE: upgrade Go builder from Go1.23.6 to Go1.24. See [Go1.24 release notes](https://tip.golang.org/doc/go1.24).
* FEATURE: [alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules/alerts-vmalert.yml): add alerting rule `TooHighQueryLoad` to notify user when VictoriaMetrics or vmselect weren't able to serve requests in timely manner during last 15min.
* FEATURE: [dashboards/single](https://grafana.com/grafana/dashboards/10229) and [dashboards/cluster](https://grafana.com/grafana/dashboards/11176): add panel `Deduplication rate` that shows how many samples are [deduplicated](https://docs.victoriametrics.com/#deduplication) during merges or read queries by VictoriaMetrics components.
* FEATURE: [dashboards/single](https://grafana.com/grafana/dashboards/10229) and [dashboards/cluster](https://grafana.com/grafana/dashboards/11176): add panel `Number of snapshots` that shows the max number of [snapshots](https://docs.victoriametrics.com/#how-to-work-with-snapshots) across vmstorage nodes. This panel should help in disk usage [troubleshooting](https://docs.victoriametrics.com/#snapshot-troubleshooting).
* FEATURE: [dashboards/single](https://grafana.com/grafana/dashboards/10229) and [dashboards/cluster](https://grafana.com/grafana/dashboards/11176): account for samples dropped according to [relabeling config](https://docs.victoriametrics.com/#relabeling) in `Samples dropped for last 1h` panel.
* FEATURE: [dashboards/single](https://grafana.com/grafana/dashboards/10229) and [dashboards/cluster](https://grafana.com/grafana/dashboards/11176): show number of parts in the last partition on `LSM parts max by type` panel. Before, the resulting graph could be skewed by the max number of parts across all partitions. Displaying parts for the latest partition is the correct way to show if storage is currently impacted by merge delays.
* FEATURE: [dashboards/cluster](https://grafana.com/grafana/dashboards/11176): add panel `Partial query results` that shows the number of served [partial responses](https://docs.victoriametrics.com/cluster-victoriametrics/#cluster-availability) by vmselects.
* FEATURE: provide alternative registry for all VictoriaMetrics components at [Quay.io](https://quay.io/organization/victoriametrics).
* FEATURE: [vmalert-tool](https://docs.victoriametrics.com/vmalert-tool/): add command-line flag `-httpListenPort` to specify the port used during testing. If not provided, a random unoccupied port will be assigned. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8393).
* FEATURE: [vmalert-tool](https://docs.victoriametrics.com/vmalert-tool/): make the temporary storage path for unittest unique, allowing user to run multiple vmalert-tool processes on a single host simultaneously. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8393).
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert/): disallow using [time buckets stats pipe](https://docs.victoriametrics.com/victorialogs/logsql/#stats-by-time-buckets) in VictoriaLogs rule expressions. Such construction produces meaningless results for [stats query API](https://docs.victoriametrics.com/victorialogs/querying/#querying-log-stats) and may lead to cardinality issues.
* BUGFIX: [Single-node VictoriaMetrics](https://docs.victoriametrics.com/) and [vmstorage](https://docs.victoriametrics.com/victoriametrics/): fix the incorrect caching of extMetricsIDs when a query timeout error occurs. This can lead to incorrect query results. Thanks to @changshun-shi for [the bug report issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8345).
* BUGFIX: [vmctl](https://docs.victoriametrics.com/vmctl/): respect time filter when exploring time series for [influxdb mode](https://docs.victoriametrics.com/vmctl/#migrating-data-from-influxdb-1x). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8259) for details.
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/): properly apply global relabeling configuration, defined with `-relabelConfig` flag, for metrics scrapped with `-promscrape.config`. Bug was introduces in [v1.108.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.108.0). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8389).
* BUGFIX: [vmbackupmanager](https://docs.victoriametrics.com/vmbackupmanager/): properly propagate an error message when applying retention policy fails. Previously, an actual error messages was discarded.
* BUGFIX: [vmgateway](https://docs.victoriametrics.com/vmgateway): fix data query in [rate limiter](https://docs.victoriametrics.com/vmgateway/#rate-limiter). The bug was introduced in [this commit](https://github.com/VictoriaMetrics/VictoriaMetrics/commit/68bad22fd26d1436ad0236b1f3ced8604c5d851c) starting from [v1.106.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.106.0).
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/) and [vmagent](https://docs.victoriametrics.com/vmagent/): properly escape IPv6 address in [Kubernetes service-discovery](https://docs.victoriametrics.com/sd_configs/#kubernetes_sd_configs) with `role: pod` for containers without exposed ports. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8374).
* BUGFIX: [vmalert-tool](https://docs.victoriametrics.com/vmalert-tool/): clean up the temporary storage path when process is terminated by SIGTERM or SIGINT. Previously, unclean shut down might affect the next run.
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix an infinite loader on the [Downsampling filters debug page](https://docs.victoriametrics.com/#vmui) when provided configuration matches no series. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8339).
## [v1.102.15](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.102.15)
Released at 2025-02-28
**v1.102.x is a line of [LTS releases](https://docs.victoriametrics.com/lts-releases/). It contains important up-to-date bugfixes for [VictoriaMetrics enterprise](https://docs.victoriametrics.com/enterprise.html).
All these fixes are also included in [the latest community release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest).
The v1.102.x line will be supported for at least 12 months since [v1.102.0](https://docs.victoriametrics.com/changelog/#v11020) release**
**Update note 1: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/) and [vmagent](https://docs.victoriametrics.com/vmagent/) include a fix which enforces IPv6 addresses escaping for containers discovered with [Kubernetes service-discovery](https://docs.victoriametrics.com/sd_configs/#kubernetes_sd_configs) and `role: pod` which do not have exposed ports defined. This means that `address` for these containers will always be wrapped in square brackets, this might affect some relabeling rules which were relying on previous behaviour.**
* SECURITY: upgrade golang.org/x/net from v0.31.0 to v0.33.0 to address [GHSA-w32m-9786-jp63](https://github.com/advisories/GHSA-w32m-9786-jp63).
* BUGFIX: [Single-node VictoriaMetrics](https://docs.victoriametrics.com/) and [vmstorage](https://docs.victoriametrics.com/victoriametrics/): fix the incorrect caching of extMetricsIDs when a query timeout error occurs. This can lead to incorrect query results. Thanks to @changshun-shi for [the bug report issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8345).
* BUGFIX: [vmctl](https://docs.victoriametrics.com/vmctl/): respect time filter when exploring time series for [influxdb mode](https://docs.victoriametrics.com/vmctl/#migrating-data-from-influxdb-1x). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8259) for details.
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/) and [vmagent](https://docs.victoriametrics.com/vmagent/): properly escape IPv6 address in [Kubernetes service-discovery](https://docs.victoriametrics.com/sd_configs/#kubernetes_sd_configs) with `role: pod` for containers without exposed ports. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8374).
## [v1.112.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.112.0)
Released at 2025-02-21
Released at 2025-02-10
* SECURITY: upgrade Go builder from Go1.23.5 to Go1.23.6. See the list of issues addressed in [Go1.23.6](https://github.com/golang/go/issues?q=milestone%3AGo1.23.6+label%3ACherryPickApproved).
* SECURITY: upgrade base docker image (Alpine) from 3.21.2 to 3.21.3. See [Alpine 3.21.3 release notes](https://alpinelinux.org/posts/Alpine-3.18.12-3.19.7-3.20.6-3.21.3-released.html).
* FEATURE: [Single-node VictoriaMetrics](https://docs.victoriametrics.com/) and [vmstorage](https://docs.victoriametrics.com/victoriametrics/): allow [disabling per-day indexes](https://docs.victoriametrics.com/#index-tuning-for-low-churn-rate) for workloads with low or zero churn rates via `-disablePerDayIndex` cmd-line flag. This option works the best for IoT cases and significantly loads resource and disk usage. See [this PR](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6976).
* FEATURE: [Single-node VictoriaMetrics](https://docs.victoriametrics.com/) and [vmstorage](https://docs.victoriametrics.com/victoriametrics/): allow disabling per-day indexes for workloads with [low or zero churn rates](https://docs.victoriametrics.com/#index-tuning-for-low-churn-rate) via `-disablePerDayIndex` cmd-line flag. This option works the best for IoT cases and significantly loads resource and disk usage. See [this PR](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6976).
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert/): add command-line flag `-notifier.sendTimeout(default 10s)` to allow configuring request timeout when sending alerts to the corresponding `-notifier.url`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8287) for details. Thanks to @gjkim42 for the [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8297).
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent/) and [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/): add support of [aggregation windows](https://docs.victoriametrics.com/stream-aggregation/#aggregation-windows) to improve accuracy of stream aggregation. This feature is especially important for aggregation of [histrograms](https://docs.victoriametrics.com/keyconcepts/#histogram), but enabling this feature requires additional memory. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4580).
* FEATURE: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/), `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/) and [vmagent](https://docs.victoriametrics.com/vmagent/): add `-influx.forceStreamMode` cmd-line flag to force stream processing for data ingested via [InfluxDB protocol](https://docs.victoriametrics.com/#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8269).
@@ -77,45 +37,13 @@ Released at 2025-02-21
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): print full error messages for failed queries on the `Explore Cardinality` page. Before, only response status code was printed.
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): move values representing changes relative to the previous day to a separate column for easier sorting on the `Explore Cardinality` page.
* FEATURE: [MetricsQL](https://docs.victoriametrics.com/metricsql/): support auto-format (prettify) for expressions that use quoted metric or label names. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7703) for details.
* FEATURE: [MetricsQL](https://docs.victoriametrics.com/metricsql/): parse `$__interval` and `$__rate_interval` inside square brackets as missing square brackets. For example, `rate(m[$__interval])` is parsed as `rate(m)` instead of `rate(m[1i])`. This enables automatic detection of the lookbehind window for [rollup functions](https://docs.victoriametrics.com/metricsql/#rollup-functions) by VictoriaMetrics, which usually returns the most expected result.
* BUGFIX: all the VictoriaMetrics components: properly override basic authorization for API endpoints protected with `authKey`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7345#issuecomment-2662595807) for details.
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert/): fix polluted alert messages when multiple Alertmanager instances are configured with `alert_relabel_configs`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8040), and thanks to @evkuzin for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8258).
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert/): fix the auto-generated metrics for alerts and groups. Previously, metrics might be missing after reload. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8229) for the details.
* BUGFIX: `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/): properly return parsing errors during data ingestion in [multi-level cluster setup](https://docs.victoriametrics.com/cluster-victoriametrics/#multi-level-cluster-setup). Before, some of the errors could have been silently ignored.
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/), `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/): fix negative rate result when the lookbehind window is longer than `-search.maxLookback` or `-search.maxStalenessInterval` and data contains gap. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8342).
* BUGFIX: [Single-node VictoriaMetrics](https://docs.victoriametrics.com/) and [vmselect](https://docs.victoriametrics.com/cluster-victoriametrics/): properly enforce presence of default `-retentionPeriod` configuration for [retention filters](https://docs.victoriametrics.com/#retention-filters) debug interface. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8343).
## [v1.110.2](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.110.2)
Released at 2025-02-21
**v1.110.x is a line of [LTS releases](https://docs.victoriametrics.com/lts-releases/). It contains important up-to-date bugfixes for [VictoriaMetrics enterprise](https://docs.victoriametrics.com/enterprise.html).
All these fixes are also included in [the latest community release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest).
The v1.110.x line will be supported for at least 12 months since [v1.110.0](https://docs.victoriametrics.com/changelog/#v11100) release**
* SECURITY: upgrade Go builder from Go1.23.5 to Go1.23.6. See the list of issues addressed in [Go1.23.6](https://github.com/golang/go/issues?q=milestone%3AGo1.23.6+label%3ACherryPickApproved).
* SECURITY: upgrade base docker image (Alpine) from 3.21.2 to 3.21.3. See [Alpine 3.21.3 release notes](https://alpinelinux.org/posts/Alpine-3.18.12-3.19.7-3.20.6-3.21.3-released.html).
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert/): fix polluted alert messages when multiple Alertmanager instances are configured with `alert_relabel_configs`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8040), and thanks to @evkuzin for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8258).
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert/): fix the auto-generated metrics for alerts and groups. Previously, metrics might be missing after reload. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8229) for the details.
* BUGFIX: `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/): properly return parsing errors during data ingestion in [multi-level cluster setup](https://docs.victoriametrics.com/cluster-victoriametrics/#multi-level-cluster-setup). Before, some of the errors could have been silently ignored.
* BUGFIX: [Single-node VictoriaMetrics](https://docs.victoriametrics.com/) and [vmselect](https://docs.victoriametrics.com/cluster-victoriametrics/): properly enforce presence of default `-retentionPeriod` configuration for [retention filters](https://docs.victoriametrics.com/#retention-filters) debug interface. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8343).
## [v1.102.14](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.102.14)
Released at 2025-02-21
**v1.102.x is a line of [LTS releases](https://docs.victoriametrics.com/lts-releases/). It contains important up-to-date bugfixes for [VictoriaMetrics enterprise](https://docs.victoriametrics.com/enterprise.html).
All these fixes are also included in [the latest community release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest).
The v1.102.x line will be supported for at least 12 months since [v1.102.0](https://docs.victoriametrics.com/changelog/#v11020) release**
* SECURITY: upgrade Go builder from Go1.23.5 to Go1.23.6. See the list of issues addressed in [Go1.23.6](https://github.com/golang/go/issues?q=milestone%3AGo1.23.6+label%3ACherryPickApproved).
* SECURITY: upgrade base docker image (Alpine) from 3.21.2 to 3.21.3. See [Alpine 3.21.3 release notes](https://alpinelinux.org/posts/Alpine-3.18.12-3.19.7-3.20.6-3.21.3-released.html).
* BUGFIX: `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/): properly return parsing error for data ingestion.
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert/): fix the auto-generated metrics for alerts and groups. Previously, metrics might be missing after reload. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8229) for the details.
## [v1.111.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.111.0)
Released at 2025-02-10

View File

@@ -16,7 +16,7 @@ VictoriaMetrics community components are open source and are free to use - see [
and [the license](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/LICENSE).
VictoriaMetrics Enterprise components are available in binary form at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest)
and at [Docker Hub](https://hub.docker.com/u/victoriametrics) and [Quay](https://quay.io/organization/victoriametrics). Enterprise binaries and packages have `enterprise` suffix in their names.
and at [docker hub](https://hub.docker.com/u/victoriametrics). Enterprise binaries and packages have `enterprise` suffix in their names.
## Valid cases for VictoriaMetrics Enterprise
@@ -82,7 +82,7 @@ VictoriaMetrics Enterprise components are available in the following forms:
It is allowed to run VictoriaMetrics Enterprise components in [cases listed here](#valid-cases-for-victoriametrics-enterprise).
Binary releases of VictoriaMetrics Enterprise are available [at the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest).
Enterprise binaries and packages have `enterprise` suffix in their names. For example, `victoria-metrics-linux-amd64-v1.112.0-enterprise.tar.gz`.
Enterprise binaries and packages have `enterprise` suffix in their names. For example, `victoria-metrics-linux-amd64-v1.111.0-enterprise.tar.gz`.
In order to run binary release of VictoriaMetrics Enterprise component, please download the `*-enterprise.tar.gz` archive for your OS and architecture
from the [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest) and unpack it. Then run the unpacked binary.
@@ -100,8 +100,8 @@ For example, the following command runs VictoriaMetrics Enterprise binary with t
obtained at [this page](https://victoriametrics.com/products/enterprise/trial/):
```sh
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.112.0/victoria-metrics-linux-amd64-v1.112.0-enterprise.tar.gz
tar -xzf victoria-metrics-linux-amd64-v1.112.0-enterprise.tar.gz
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.111.0/victoria-metrics-linux-amd64-v1.111.0-enterprise.tar.gz
tar -xzf victoria-metrics-linux-amd64-v1.111.0-enterprise.tar.gz
./victoria-metrics-prod -license=BASE64_ENCODED_LICENSE_KEY
```
@@ -115,8 +115,8 @@ Alternatively, VictoriaMetrics Enterprise license can be stored in the file and
It is allowed to run VictoriaMetrics Enterprise components in [cases listed here](#valid-cases-for-victoriametrics-enterprise).
Docker images for VictoriaMetrics Enterprise are available at VictoriaMetrics [Docker Hub](https://hub.docker.com/u/victoriametrics) and [Quay](https://quay.io/organization/victoriametrics).
Enterprise docker images have `enterprise` suffix in their names. For example, `victoriametrics/victoria-metrics:v1.112.0-enterprise`.
Docker images for VictoriaMetrics Enterprise are available [at VictoriaMetrics DockerHub](https://hub.docker.com/u/victoriametrics).
Enterprise docker images have `enterprise` suffix in their names. For example, `victoriametrics/victoria-metrics:v1.111.0-enterprise`.
In order to run Docker image of VictoriaMetrics Enterprise component, it is required to provide the license key via command-line
flag as described [here](#binary-releases).
@@ -126,13 +126,13 @@ Enterprise license key can be obtained at [this page](https://victoriametrics.co
For example, the following command runs VictoriaMetrics Enterprise Docker image with the specified license key:
```sh
docker run --name=victoria-metrics victoriametrics/victoria-metrics:v1.112.0-enterprise -license=BASE64_ENCODED_LICENSE_KEY
docker run --name=victoria-metrics victoriametrics/victoria-metrics:v1.111.0-enterprise -license=BASE64_ENCODED_LICENSE_KEY
```
Alternatively, the license code can be stored in the file and then referred via `-licenseFile` command-line flag:
```sh
docker run --name=victoria-metrics -v /vm-license:/vm-license victoriametrics/victoria-metrics:v1.112.0-enterprise -licenseFile=/path/to/vm-license
docker run --name=victoria-metrics -v /vm-license:/vm-license victoriametrics/victoria-metrics:v1.111.0-enterprise -licenseFile=/path/to/vm-license
```
Example docker-compose configuration:
@@ -141,7 +141,7 @@ version: "3.5"
services:
victoriametrics:
container_name: victoriametrics
image: victoriametrics/victoria-metrics:v1.112.0
image: victoriametrics/victoria-metrics:v1.111.0
ports:
- 8428:8428
volumes:
@@ -173,7 +173,7 @@ is used to provide key in plain-text:
```yaml
server:
image:
tag: v1.112.0-enterprise
tag: v1.111.0-enterprise
license:
key: {BASE64_ENCODED_LICENSE_KEY}
@@ -184,7 +184,7 @@ In order to provide key via existing secret, the following values file is used:
```yaml
server:
image:
tag: v1.112.0-enterprise
tag: v1.111.0-enterprise
license:
secret:
@@ -233,7 +233,7 @@ spec:
license:
key: {BASE64_ENCODED_LICENSE_KEY}
image:
tag: v1.112.0-enterprise
tag: v1.111.0-enterprise
```
In order to provide key via existing secret, the following custom resource is used:
@@ -250,7 +250,7 @@ spec:
name: vm-license
key: license
image:
tag: v1.112.0-enterprise
tag: v1.111.0-enterprise
```
Example secret with license key:

View File

@@ -236,27 +236,27 @@ services:
- grafana_data:/var/lib/grafana/
vmsingle:
image: victoriametrics/victoria-metrics:v1.112.0
image: victoriametrics/victoria-metrics:v1.111.0
command:
- -httpListenAddr=0.0.0.0:8429
vmstorage:
image: victoriametrics/vmstorage:v1.112.0-cluster
image: victoriametrics/vmstorage:v1.111.0-cluster
vminsert:
image: victoriametrics/vminsert:v1.112.0-cluster
image: victoriametrics/vminsert:v1.111.0-cluster
command:
- -storageNode=vmstorage:8400
- -httpListenAddr=0.0.0.0:8480
vmselect:
image: victoriametrics/vmselect:v1.112.0-cluster
image: victoriametrics/vmselect:v1.111.0-cluster
command:
- -storageNode=vmstorage:8401
- -httpListenAddr=0.0.0.0:8481
vmagent:
image: victoriametrics/vmagent:v1.112.0
image: victoriametrics/vmagent:v1.111.0
volumes:
- ./scrape.yaml:/etc/vmagent/config.yaml
command:
@@ -265,7 +265,7 @@ services:
- -remoteWrite.url=http://vmsingle:8429/api/v1/write
vmgateway-cluster:
image: victoriametrics/vmgateway:v1.112.0-enterprise
image: victoriametrics/vmgateway:v1.111.0-enterprise
ports:
- 8431:8431
volumes:
@@ -281,7 +281,7 @@ services:
- -auth.oidcDiscoveryEndpoints=http://keycloak:8080/realms/master/.well-known/openid-configuration
vmgateway-single:
image: victoriametrics/vmgateway:v1.112.0-enterprise
image: victoriametrics/vmgateway:v1.111.0-enterprise
ports:
- 8432:8431
volumes:
@@ -393,7 +393,7 @@ Once iDP configuration is done, vmagent configuration needs to be updated to use
```yaml
vmagent:
image: victoriametrics/vmagent:v1.112.0
image: victoriametrics/vmagent:v1.111.0
volumes:
- ./scrape.yaml:/etc/vmagent/config.yaml
- ./vmagent-client-secret:/etc/vmagent/oauth2-client-secret

View File

@@ -30,8 +30,8 @@ scrape_configs:
After you created the `scrape.yaml` file, download and unpack [single-node VictoriaMetrics](https://docs.victoriametrics.com/) to the same directory:
```
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.112.0/victoria-metrics-linux-amd64-v1.112.0.tar.gz
tar xzf victoria-metrics-linux-amd64-v1.112.0.tar.gz
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.111.0/victoria-metrics-linux-amd64-v1.111.0.tar.gz
tar xzf victoria-metrics-linux-amd64-v1.111.0.tar.gz
```
Then start VictoriaMetrics and instruct it to scrape targets defined in `scrape.yaml` and save scraped metrics
@@ -146,8 +146,8 @@ Then start [single-node VictoriaMetrics](https://docs.victoriametrics.com/) acco
```yaml
# Download and unpack single-node VictoriaMetrics
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.112.0/victoria-metrics-linux-amd64-v1.112.0.tar.gz
tar xzf victoria-metrics-linux-amd64-v1.112.0.tar.gz
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.111.0/victoria-metrics-linux-amd64-v1.111.0.tar.gz
tar xzf victoria-metrics-linux-amd64-v1.111.0.tar.gz
# Run single-node VictoriaMetrics with the given scrape.yaml
./victoria-metrics-prod -promscrape.config=scrape.yaml

View File

@@ -1164,8 +1164,9 @@ It depends on network lag, load, clock synchronization, etc. In most scenarios i
deduplication results, which are consistent within margin of error. But for metrics represented as a collection of series,
like [histograms](https://docs.victoriametrics.com/keyconcepts/#histogram), such inaccuracy leads to invalid aggregation results.
For this case, streaming aggregation and deduplication support mode with aggregation windows for current and previous state.
With this mode, flush doesn't happen immediately but is shifted by a calculated samples lag that improves correctness for delayed data. {{% available_from "v1.112.0" %}}
For this case, streaming aggregation and deduplication support mode with aggregation windows {{% available_from "#tip" %}}
for current and previous state. With this mode, flush doesn't happen immediately but is shifted by a calculated samples
lag that improves correctness for delayed data.
Enabling of this mode has increased resource usage: memory usage is expected to double as aggregation will store two states
instead of one. However, this significantly improves accuracy of calculations. Aggregation windows can be enabled via

View File

@@ -16,7 +16,7 @@ This guide explains the different ways in which you can use vmalert in conjuncti
## Preconditions
* [vmalert](https://docs.victoriametrics.com/vmalert/) is installed. You can obtain it by building it from [source](https://docs.victoriametrics.com/vmalert/#quickstart), downloading it from the [GitHub releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest), or using the docker image [Docker Hub](https://hub.docker.com/r/victoriametrics/vmalert) or [Quay](https://quay.io/repository/victoriametrics/vmalert?tab=tags) for the container ecosystem (such as docker, k8s, etc.).
* [vmalert](https://docs.victoriametrics.com/vmalert/) is installed. You can obtain it by building it from [source](https://docs.victoriametrics.com/vmalert/#quickstart), downloading it from the [GitHub releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest), or using the [docker image](https://hub.docker.com/r/victoriametrics/vmalert) for the container ecosystem (such as docker, k8s, etc.).
* [Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/) is installed.
* You have a [single or cluster](https://docs.victoriametrics.com/victoriametrics-cloud/quickstart/#creating-deployment) deployment in [VictoriaMetrics Cloud](https://docs.victoriametrics.com/victoriametrics-cloud/overview/).
* If you are using helm, add the [VictoriaMetrics helm chart](https://docs.victoriametrics.com/helm/victoriametrics-alert#how-to-install) repository to your helm repositories. This step is optional.

View File

@@ -55,7 +55,7 @@ additionally to [discovering Prometheus-compatible targets and scraping metrics
## Quick Start
Please download `vmutils-*` archive from [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest) (
`vmagent` is also available in docker images [Docker Hub](https://hub.docker.com/r/victoriametrics/vmagent/tags) and [Quay](https://quay.io/repository/victoriametrics/vmagent?tab=tags)),
`vmagent` is also available in [docker images](https://hub.docker.com/r/victoriametrics/vmagent/tags)),
unpack it and pass the following flags to the `vmagent` binary in order to start scraping Prometheus-compatible targets
and sending the data to the Prometheus-compatible remote storage:
@@ -1368,7 +1368,7 @@ and gzipped [JSON line](https://docs.victoriametrics.com/#json-line-format) mess
These command-line flags are available only in [enterprise](https://docs.victoriametrics.com/enterprise/) version of `vmagent`,
which can be downloaded for evaluation from [releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest) page
(see `vmutils-...-enterprise.tar.gz` archives) and from docker images [Docker Hub](https://hub.docker.com/r/victoriametrics/vmagent/tags) and [Quay](https://quay.io/repository/victoriametrics/vmagent?tab=tags) with tags containing `enterprise` suffix.
(see `vmutils-...-enterprise.tar.gz` archives) and from [docker images](https://hub.docker.com/r/victoriametrics/vmagent/tags) with tags containing `enterprise` suffix.
```sh
-gcp.pubsub.subscribe.credentialsFile string
@@ -1402,7 +1402,7 @@ These messages can be read later from Google PubSub by another `vmagent` instanc
These command-line flags are available only in [enterprise](https://docs.victoriametrics.com/enterprise/) version of `vmagent`,
which can be downloaded for evaluation from [releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest) page
(see `vmutils-...-enterprise.tar.gz` archives) and from docker images [Docker Hub](https://hub.docker.com/r/victoriametrics/vmagent/tags) and [Quay](https://quay.io/repository/victoriametrics/vmagent?tab=tags) with tags containing `enterprise` suffix.
(see `vmutils-...-enterprise.tar.gz` archives) and from [docker images](https://hub.docker.com/r/victoriametrics/vmagent/tags) with tags containing `enterprise` suffix.
```sh
-gcp.pubsub.publish.byteThreshold int
@@ -1431,7 +1431,7 @@ which can be downloaded for evaluation from [releases](https://github.com/Victor
* [Writing metrics to Kafka](#writing-metrics-to-kafka)
The enterprise version of vmagent is available for evaluation at [releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest) page
in `vmutils-...-enterprise.tar.gz` archives and in docker images [Docker Hub](https://hub.docker.com/r/victoriametrics/vmagent/tags) and [Quay](https://quay.io/repository/victoriametrics/vmagent?tab=tags) with tags containing `enterprise` suffix.
in `vmutils-...-enterprise.tar.gz` archives and in [docker images](https://hub.docker.com/r/victoriametrics/vmagent/tags) with tags containing `enterprise` suffix.
See how to request a free trial license [here](https://victoriametrics.com/products/enterprise/trial/).
### Reading metrics from Kafka
@@ -1507,7 +1507,7 @@ See also [how to write metrics to multiple distinct tenants](https://docs.victor
These command-line flags are available only in [enterprise](https://docs.victoriametrics.com/enterprise/) version of `vmagent`,
which can be downloaded for evaluation from [releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest) page
(see `vmutils-...-enterprise.tar.gz` archives) and from docker images [Docker Hub](https://hub.docker.com/r/victoriametrics/vmagent/tags) and [Quay](https://quay.io/repository/victoriametrics/vmagent?tab=tags) with tags containing `enterprise` suffix.
(see `vmutils-...-enterprise.tar.gz` archives) and from [docker images](https://hub.docker.com/r/victoriametrics/vmagent/tags) with tags containing `enterprise` suffix.
```sh
-kafka.consumer.topic array

View File

@@ -312,8 +312,6 @@ Run `vmalert-tool unittest --help` to get all configuration options:
Optional label in the form 'name=value' to add to all generated recording rules and alerts. Supports an array of values separated by comma or specified via multiple flags.
-external.url
Optional external URL to template in rule's labels or annotations.
-httpListenPort
Optional local port for incoming HTTP requests. If not specified, a random unoccupied port will be used.
-loggerLevel
Minimum level of errors to log. Possible values: INFO, WARN, ERROR, FATAL, PANIC (default "ERROR").
```

View File

@@ -8,8 +8,8 @@ title: vmalert
aliases:
- /vmalert.html
---
`vmalert` executes a list of the given [alerting](https://docs.victoriametrics.com/vmalert/#alerting-rules)
or [recording](https://docs.victoriametrics.com/vmalert/#recording-rules)
`vmalert` executes a list of the given [alerting](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/)
or [recording](https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/)
rules against configured `-datasource.url`. For sending alerting notifications
`vmalert` relies on [Alertmanager](https://github.com/prometheus/alertmanager) configured via `-notifier.url` flag.
Recording rules results are persisted via [remote write](https://prometheus.io/docs/prometheus/latest/storage/#remote-storage-integrations)
@@ -521,7 +521,7 @@ is obtained from `-defaultTenant.prometheus` or `-defaultTenant.graphite` depend
The enterprise version of vmalert is available in `vmutils-*-enterprise.tar.gz` files
at [release page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest) and in `*-enterprise`
tags at [Docker Hub](https://hub.docker.com/r/victoriametrics/vmalert/tags) and [Quay](https://quay.io/repository/victoriametrics/vmalert?tab=tags).
tags at [Docker Hub](https://hub.docker.com/r/victoriametrics/vmalert/tags).
### Reading rules from object storage
@@ -952,7 +952,7 @@ Sensitive info is stripped from the `curl` examples - see [security](#security)
### Never-firing alerts
vmalert can detect{{% available_from "v1.90.0" %}} if alert's expression doesn't match any time series in runtime
vmalert can detect if alert's expression doesn't match any time series in runtime
starting from [v1.91](https://docs.victoriametrics.com/changelog/#v1910). This problem usually happens
when alerting expression selects time series which aren't present in the datasource (i.e. wrong `job` label)
or there is a typo in the series selector (i.e. `env=prod`). Such alerting rules will be marked with special icon in
@@ -964,8 +964,8 @@ used to detect rules matching no series:
max(vmalert_alerting_rules_last_evaluation_series_fetched) by(group, alertname) == 0
```
See more details [here](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4039) and
read [Never-firing alerts](https://victoriametrics.com/blog/never-firing-alerts/) blogpost.
See more details [here](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4039).
This feature is available only if vmalert is using VictoriaMetrics v1.90 or higher as a datasource.
### Series with the same labelset
@@ -1269,7 +1269,7 @@ The shortlist of configuration flags is the following:
Supports an array of values separated by comma or specified via multiple flags.
Value can contain comma inside single-quoted or double-quoted string, {}, [] and () braces.
-notifier.sendTimeout
Timeout when sending alerts to the corresponding -notifier.url. (default 10s)
Timeout for sending alerts to the configured -notifier.url. (default 10s)
-notifier.showURL
Whether to avoid stripping sensitive information such as passwords from URL in log messages or UI for -notifier.url. It is hidden by default, since it can contain sensitive info such as auth key
-notifier.suppressDuplicateTargetErrors

View File

@@ -28,7 +28,7 @@ The port can be modified via `-httpListenAddr` command-line flag.
See [how to reload config without restart](#config-reload).
Docker images for `vmauth` are available at [Docker Hub](https://hub.docker.com/r/victoriametrics/vmauth/tags) and [Quay](https://quay.io/repository/victoriametrics/vmauth?tab=tags).
Docker images for `vmauth` are available [here](https://hub.docker.com/r/victoriametrics/vmauth/tags).
See how `vmauth` used in [docker-compose env](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/README.md#victoriametrics-cluster).
Pass `-help` to `vmauth` in order to see all the supported command-line flags with their descriptions.

View File

@@ -1191,7 +1191,7 @@ Flags available only for the `opentsdb` command:
--influx-measurement-field-separator value
The {separator} symbol used to concatenate {measurement} and {field} names into series name {measurement}{separator}{field}. (default: "_")
--influx-skip-database-label
Whether to skip adding the label 'db' to timeseries. (default: false)
Wether to skip adding the label 'db' to timeseries. (default: false)
--influx-prometheus-mode
Whether to restore the original timeseries name previously written from Prometheus to InfluxDB v1 via remote_write. (default: false)
--influx-cert-file value

4
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/VictoriaMetrics/VictoriaMetrics
go 1.24.0
go 1.23.6
// This is needed in order to avoid vmbackup and vmrestore binary size increase by 20MB
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8008
@@ -15,7 +15,7 @@ require (
github.com/VictoriaMetrics/easyproto v0.1.4
github.com/VictoriaMetrics/fastcache v1.12.2
github.com/VictoriaMetrics/metrics v1.35.2
github.com/VictoriaMetrics/metricsql v0.84.0
github.com/VictoriaMetrics/metricsql v0.83.1
github.com/aws/aws-sdk-go-v2 v1.36.1
github.com/aws/aws-sdk-go-v2/config v1.29.6
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.61

6
go.sum
View File

@@ -43,8 +43,10 @@ github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkT
github.com/VictoriaMetrics/metrics v1.34.0/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
github.com/VictoriaMetrics/metrics v1.35.2 h1:Bj6L6ExfnakZKYPpi7mGUnkJP4NGQz2v5wiChhXNyWQ=
github.com/VictoriaMetrics/metrics v1.35.2/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
github.com/VictoriaMetrics/metricsql v0.84.0 h1:rVZapkXHiM4dR979La3tk8u2equ57Insbr1+Hm6yUew=
github.com/VictoriaMetrics/metricsql v0.84.0/go.mod h1:1g4hdCwlbJZ851PU9VN65xy9Rdlzupo6fx3SNZ8Z64U=
github.com/VictoriaMetrics/metricsql v0.83.0 h1:3460jZ97XWD+595jpPdtVTVoKJflbxqBgeCbzPCiSgU=
github.com/VictoriaMetrics/metricsql v0.83.0/go.mod h1:1g4hdCwlbJZ851PU9VN65xy9Rdlzupo6fx3SNZ8Z64U=
github.com/VictoriaMetrics/metricsql v0.83.1 h1:+eHBVsYJvOPw8oy25gh1VT7OnT+jtfHb31eGvfYNEkY=
github.com/VictoriaMetrics/metricsql v0.83.1/go.mod h1:1g4hdCwlbJZ851PU9VN65xy9Rdlzupo6fx3SNZ8Z64U=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=

View File

@@ -20,7 +20,7 @@ var (
)
func init() {
r := make(map[zstd.EncoderLevel]*zstd.Encoder)
r := make(map[int]*zstd.Encoder)
av.Store(r)
var err error
@@ -39,19 +39,12 @@ func Decompress(dst, src []byte) ([]byte, error) {
//
// The given compressionLevel is used for the compression.
func CompressLevel(dst, src []byte, compressionLevel int) []byte {
// Convert the compressionLevel to the real compression level supported by github.com/klauspost/compress/zstd
// This allows saving memory on caching zstd.Encoder instances per each level,
// since the number of real compression levels at github.com/klauspost/compress/zstd
// is smaller than the number of zstd compression levels.
// See https://github.com/klauspost/compress/discussions/1025
realCompressionLevel := zstd.EncoderLevelFromZstd(compressionLevel)
e := getEncoder(realCompressionLevel)
e := getEncoder(compressionLevel)
return e.EncodeAll(src, dst)
}
func getEncoder(compressionLevel zstd.EncoderLevel) *zstd.Encoder {
r := av.Load().(map[zstd.EncoderLevel]*zstd.Encoder)
func getEncoder(compressionLevel int) *zstd.Encoder {
r := av.Load().(map[int]*zstd.Encoder)
e := r[compressionLevel]
if e != nil {
return e
@@ -60,10 +53,10 @@ func getEncoder(compressionLevel zstd.EncoderLevel) *zstd.Encoder {
mu.Lock()
// Create the encoder under lock in order to prevent from wasted work
// when concurrent goroutines create encoder for the same compressionLevel.
r1 := av.Load().(map[zstd.EncoderLevel]*zstd.Encoder)
r1 := av.Load().(map[int]*zstd.Encoder)
if e = r1[compressionLevel]; e == nil {
e = newEncoder(compressionLevel)
r2 := make(map[zstd.EncoderLevel]*zstd.Encoder)
r2 := make(map[int]*zstd.Encoder)
for k, v := range r1 {
r2[k] = v
}
@@ -75,10 +68,11 @@ func getEncoder(compressionLevel zstd.EncoderLevel) *zstd.Encoder {
return e
}
func newEncoder(compressionLevel zstd.EncoderLevel) *zstd.Encoder {
func newEncoder(compressionLevel int) *zstd.Encoder {
level := zstd.EncoderLevelFromZstd(compressionLevel)
e, err := zstd.NewWriter(nil,
zstd.WithEncoderCRC(false), // Disable CRC for performance reasons.
zstd.WithEncoderLevel(compressionLevel))
zstd.WithEncoderLevel(level))
if err != nil {
logger.Panicf("BUG: failed to create ZSTD writer: %s", err)
}

View File

@@ -5,7 +5,7 @@ import (
"net/http"
"strconv"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)
// GetDuration returns duration in milliseconds from the given argKey query arg.
@@ -21,7 +21,7 @@ func GetDuration(r *http.Request, argKey string, defaultValue int64) (int64, err
secs, err := strconv.ParseFloat(argValue, 64)
if err != nil {
// Try parsing string format
d, err := timeutil.ParseDuration(argValue)
d, err := promutils.ParseDuration(argValue)
if err != nil {
return 0, fmt.Errorf("cannot parse %q=%q: %w", argKey, argValue, err)
}

View File

@@ -6,7 +6,7 @@ import (
"net/http"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)
// GetTime returns time in milliseconds from the given argKey query arg.
@@ -28,7 +28,7 @@ func GetTime(r *http.Request, argKey string, defaultMs int64) (int64, error) {
return maxTimeMsecs, nil
}
// Parse argValue
msecs, err := timeutil.ParseTimeMsec(argValue)
msecs, err := promutils.ParseTimeMsec(argValue)
if err != nil {
return 0, fmt.Errorf("cannot parse %s=%s: %w", argKey, argValue, err)
}

View File

@@ -14,7 +14,7 @@ func getBitmap(bitsLen int) *bitmap {
v = &bitmap{}
}
bm := v.(*bitmap)
bm.resizeNoInit(bitsLen)
bm.init(bitsLen)
return bm
}
@@ -46,17 +46,19 @@ func (bm *bitmap) copyFrom(src *bitmap) {
func (bm *bitmap) init(bitsLen int) {
bm.reset()
bm.resizeNoInit(bitsLen)
}
func (bm *bitmap) resizeNoInit(bitsLen int) {
a := bm.a
wordsLen := (bitsLen + 63) / 64
bm.a = slicesutil.SetLength(bm.a, wordsLen)
a = slicesutil.SetLength(a, wordsLen)
bm.a = a
bm.bitsLen = bitsLen
}
func (bm *bitmap) resetBits() {
clear(bm.a)
a := bm.a
for i := range a {
a[i] = 0
}
}
func (bm *bitmap) setBits() {
@@ -100,9 +102,6 @@ func (bm *bitmap) andNot(x *bitmap) {
if bm.bitsLen != x.bitsLen {
logger.Panicf("BUG: cannot merge bitmaps with distinct lengths; %d vs %d", bm.bitsLen, x.bitsLen)
}
if x.isZero() {
return
}
a := bm.a
b := x.a
for i := range a {
@@ -110,6 +109,17 @@ func (bm *bitmap) andNot(x *bitmap) {
}
}
func (bm *bitmap) or(x *bitmap) {
if bm.bitsLen != x.bitsLen {
logger.Panicf("BUG: cannot merge bitmaps with distinct lengths; %d vs %d", bm.bitsLen, x.bitsLen)
}
a := bm.a
b := x.a
for i := range a {
a[i] |= b[i]
}
}
func (bm *bitmap) setBit(i int) {
wordIdx := uint(i) / 64
wordOffset := uint(i) % 64

View File

@@ -312,8 +312,10 @@ func (br *blockResult) addResultColumnConst(rc *resultColumn) {
})
}
// initAllColumns initializes all the columns in br except of unneededColumnNames.
func (br *blockResult) initAllColumns(unneededColumnNames []string) {
// initAllColumns initializes all the columns in br.
func (br *blockResult) initAllColumns() {
unneededColumnNames := br.bs.bsw.so.unneededColumnNames
if !slices.Contains(unneededColumnNames, "_time") {
// Add _time column
br.addTimeColumn()
@@ -371,9 +373,9 @@ func (br *blockResult) initAllColumns(unneededColumnNames []string) {
br.csInitFast()
}
// initRequestedColumns initializes neededColumnNames at br.
func (br *blockResult) initRequestedColumns(neededColumnNames []string) {
for _, columnName := range neededColumnNames {
// initRequestedColumns initialized only requested columns in br.
func (br *blockResult) initRequestedColumns() {
for _, columnName := range br.bs.bsw.so.neededColumnNames {
switch columnName {
case "_stream_id":
br.addStreamIDColumn()
@@ -2487,13 +2489,7 @@ func getEmptyStrings(rowsLen int) []string {
return values
}
values := *p
needStore := cap(values) < rowsLen
values = slicesutil.SetLength(values, rowsLen)
if needStore {
valuesLocal := values
emptyStrings.Store(&valuesLocal)
}
return values
return slicesutil.SetLength(values, rowsLen)
}
var emptyStrings atomic.Pointer[[]string]

View File

@@ -223,9 +223,9 @@ func (bs *blockSearch) search(bsw *blockSearchWork, bm *bitmap) {
// fetch the requested columns to bs.br.
if bs.bsw.so.needAllColumns {
bs.br.initAllColumns(bsw.so.unneededColumnNames)
bs.br.initAllColumns()
} else {
bs.br.initRequestedColumns(bsw.so.neededColumnNames)
bs.br.initRequestedColumns()
}
}

View File

@@ -122,7 +122,7 @@ func initBloomFilter(bits, hashes []uint64) {
// appendTokensHashes appends hashes for the given tokens to dst and returns the result.
//
// The appended hashes can be then passed to bloomFilter.containsAll().
// the appended hashes can be then passed to bloomFilter.containsAll().
func appendTokensHashes(dst []uint64, tokens []string) []uint64 {
dstLen := len(dst)
hashesCount := len(tokens) * bloomFilterHashesCount
@@ -145,10 +145,7 @@ func appendTokensHashes(dst []uint64, tokens []string) []uint64 {
// appendHashesHashes appends hashes for the given hashes to dst and returns the result.
//
// The hashes must be generated from tokens by tokenizeHashes().
// See also appendTokensHashes().
//
// The appended hashes can be then passed to bloomFilter.containsAll().
// the appended hashes can be then passed to bloomFilter.containsAll().
func appendHashesHashes(dst, hashes []uint64) []uint64 {
dstLen := len(dst)
hashesCount := len(hashes) * bloomFilterHashesCount
@@ -169,7 +166,7 @@ func appendHashesHashes(dst, hashes []uint64) []uint64 {
return dst
}
// containsAll returns true if bf contains all the given tokens hashes generated by appendTokensHashes or appendHashesHashes
// containsAll returns true if bf contains all the given tokens hashes generated by appendTokensHashes.
func (bf *bloomFilter) containsAll(hashes []uint64) bool {
bits := bf.bits
if len(bits) == 0 {

View File

@@ -29,6 +29,10 @@ const maxBigPartSize = 1e12
// cannot keep up with the rate of creating new in-memory parts.
const maxInmemoryPartsPerPartition = 20
// The interval for guaranteed flush of recently ingested data from memory to on-disk parts,
// so they survive process crash.
var dataFlushInterval = 5 * time.Second
// Default number of parts to merge at once.
//
// This number has been obtained empirically - it gives the lowest possible overhead.
@@ -268,7 +272,7 @@ func (ddb *datadb) startInmemoryPartsFlusher() {
func (ddb *datadb) inmemoryPartsFlusher() {
// Do not add jitter to d in order to guarantee the flush interval
ticker := time.NewTicker(ddb.flushInterval)
ticker := time.NewTicker(dataFlushInterval)
defer ticker.Stop()
for {
select {

View File

@@ -1,319 +0,0 @@
package logstorage
import (
"fmt"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
// filterContainsAll matches logs containing all the given values.
//
// Example LogsQL: `fieldName:contains_all("foo", "bar baz")`
type filterContainsAll struct {
fieldName string
values inValues
}
func (fi *filterContainsAll) String() string {
args := fi.values.String()
return fmt.Sprintf("%scontains_all(%s)", quoteFieldNameIfNeeded(fi.fieldName), args)
}
func (fi *filterContainsAll) updateNeededFields(neededFields fieldsSet) {
neededFields.add(fi.fieldName)
}
func (fi *filterContainsAll) applyToBlockResult(br *blockResult, bm *bitmap) {
if fi.values.isEmpty() || fi.values.isOnlyEmptyValue() {
return
}
c := br.getColumnByName(fi.fieldName)
if c.isConst {
v := c.valuesEncoded[0]
if !matchAllPhrases(v, fi.values.values) {
bm.resetBits()
}
return
}
if c.isTime {
fi.matchColumnByStringValues(br, bm, c)
return
}
switch c.valueType {
case valueTypeString:
fi.matchColumnByStringValues(br, bm, c)
case valueTypeDict:
phrases := fi.values.values
bb := bbPool.Get()
for _, v := range c.dictValues {
c := byte(0)
if matchAllPhrases(v, phrases) {
c = 1
}
bb.B = append(bb.B, c)
}
valuesEncoded := c.getValuesEncoded(br)
bm.forEachSetBit(func(idx int) bool {
n := valuesEncoded[idx][0]
return bb.B[n] == 1
})
bbPool.Put(bb)
case valueTypeUint8:
binValues := fi.values.getUint8Values()
nonEmptyValuesLen := fi.values.getNonEmptyValuesLen()
matchColumnByAllBinValues(br, bm, c, binValues, nonEmptyValuesLen)
case valueTypeUint16:
binValues := fi.values.getUint16Values()
nonEmptyValuesLen := fi.values.getNonEmptyValuesLen()
matchColumnByAllBinValues(br, bm, c, binValues, nonEmptyValuesLen)
case valueTypeUint32:
binValues := fi.values.getUint32Values()
nonEmptyValuesLen := fi.values.getNonEmptyValuesLen()
matchColumnByAllBinValues(br, bm, c, binValues, nonEmptyValuesLen)
case valueTypeUint64:
binValues := fi.values.getUint64Values()
nonEmptyValuesLen := fi.values.getNonEmptyValuesLen()
matchColumnByAllBinValues(br, bm, c, binValues, nonEmptyValuesLen)
case valueTypeInt64:
fi.matchColumnByStringValues(br, bm, c)
case valueTypeFloat64:
fi.matchColumnByStringValues(br, bm, c)
case valueTypeIPv4:
fi.matchColumnByStringValues(br, bm, c)
case valueTypeTimestampISO8601:
fi.matchColumnByStringValues(br, bm, c)
default:
logger.Panicf("FATAL: unknown valueType=%d", c.valueType)
}
}
func matchColumnByAllBinValues(br *blockResult, bm *bitmap, c *blockResultColumn, binValues map[string]struct{}, nonEmptyValuesLen int) {
if nonEmptyValuesLen == 0 {
return
}
if nonEmptyValuesLen != 1 || nonEmptyValuesLen != len(binValues) {
bm.resetBits()
return
}
binValue := ""
for k := range binValues {
binValue = k
}
valuesEncoded := c.getValuesEncoded(br)
bm.forEachSetBit(func(idx int) bool {
return valuesEncoded[idx] == binValue
})
}
func (fi *filterContainsAll) matchColumnByStringValues(br *blockResult, bm *bitmap, c *blockResultColumn) {
phrases := fi.values.values
values := c.getValues(br)
bm.forEachSetBit(func(idx int) bool {
return matchAllPhrases(values[idx], phrases)
})
}
func (fi *filterContainsAll) applyToBlockSearch(bs *blockSearch, bm *bitmap) {
if fi.values.isEmpty() || fi.values.isOnlyEmptyValue() {
return
}
v := bs.getConstColumnValue(fi.fieldName)
if v != "" {
if !matchAllPhrases(v, fi.values.values) {
bm.resetBits()
}
return
}
// Verify whether filter matches other columns
ch := bs.getColumnHeader(fi.fieldName)
if ch == nil {
// Fast path - there are no matching columns.
// It matches anything only for empty phrase.
if !matchAllPhrases("", fi.values.values) {
bm.resetBits()
}
return
}
tokens := fi.values.getTokensHashesAll()
switch ch.valueType {
case valueTypeString:
matchAllPhrasesString(bs, ch, bm, fi.values.values, tokens)
case valueTypeDict:
matchAllPhrasesDict(bs, ch, bm, fi.values.values)
case valueTypeUint8:
binValues := fi.values.getUint8Values()
nonEmptyValuesLen := fi.values.getNonEmptyValuesLen()
matchAllValues(bs, ch, bm, binValues, nonEmptyValuesLen, tokens)
case valueTypeUint16:
binValues := fi.values.getUint16Values()
nonEmptyValuesLen := fi.values.getNonEmptyValuesLen()
matchAllValues(bs, ch, bm, binValues, nonEmptyValuesLen, tokens)
case valueTypeUint32:
binValues := fi.values.getUint32Values()
nonEmptyValuesLen := fi.values.getNonEmptyValuesLen()
matchAllValues(bs, ch, bm, binValues, nonEmptyValuesLen, tokens)
case valueTypeUint64:
binValues := fi.values.getUint64Values()
nonEmptyValuesLen := fi.values.getNonEmptyValuesLen()
matchAllValues(bs, ch, bm, binValues, nonEmptyValuesLen, tokens)
case valueTypeInt64:
matchAllPhrasesInt64(bs, ch, bm, fi.values.values, tokens)
case valueTypeFloat64:
matchAllPhrasesFloat64(bs, ch, bm, fi.values.values, tokens)
case valueTypeIPv4:
matchAllPhrasesIPv4(bs, ch, bm, fi.values.values, tokens)
case valueTypeTimestampISO8601:
matchAllPhrasesTimestampISO8601(bs, ch, bm, fi.values.values, tokens)
default:
logger.Panicf("FATAL: %s: unknown valueType=%d", bs.partPath(), ch.valueType)
}
}
func matchAllValues(bs *blockSearch, ch *columnHeader, bm *bitmap, binValues map[string]struct{}, nonEmptyValuesLen int, tokens []uint64) {
if nonEmptyValuesLen == 0 {
return
}
if nonEmptyValuesLen != 1 || nonEmptyValuesLen != len(binValues) {
bm.resetBits()
return
}
if !matchBloomFilterAllTokens(bs, ch, tokens) {
bm.resetBits()
return
}
binValue := ""
for k := range binValues {
binValue = k
}
visitValues(bs, ch, bm, func(v string) bool {
return v == binValue
})
}
func matchAllPhrasesString(bs *blockSearch, ch *columnHeader, bm *bitmap, phrases []string, tokens []uint64) {
if len(phrases) == 0 {
return
}
if !matchBloomFilterAllTokens(bs, ch, tokens) {
bm.resetBits()
return
}
values := bs.getValuesForColumn(ch)
bm.forEachSetBit(func(idx int) bool {
return matchAllPhrases(values[idx], phrases)
})
}
func matchAllPhrasesInt64(bs *blockSearch, ch *columnHeader, bm *bitmap, phrases []string, tokens []uint64) {
if len(phrases) == 0 {
return
}
if !matchBloomFilterAllTokens(bs, ch, tokens) {
bm.resetBits()
return
}
bb := bbPool.Get()
visitValues(bs, ch, bm, func(v string) bool {
n := unmarshalInt64(v)
bb.B = marshalInt64String(bb.B[:0], n)
s := bytesutil.ToUnsafeString(bb.B)
return matchAllPhrases(s, phrases)
})
bbPool.Put(bb)
}
func matchAllPhrasesFloat64(bs *blockSearch, ch *columnHeader, bm *bitmap, phrases []string, tokens []uint64) {
if len(phrases) == 0 {
return
}
if !matchBloomFilterAllTokens(bs, ch, tokens) {
bm.resetBits()
return
}
bb := bbPool.Get()
visitValues(bs, ch, bm, func(v string) bool {
n := unmarshalFloat64(v)
bb.B = marshalFloat64String(bb.B[:0], n)
s := bytesutil.ToUnsafeString(bb.B)
return matchAllPhrases(s, phrases)
})
bbPool.Put(bb)
}
func matchAllPhrasesIPv4(bs *blockSearch, ch *columnHeader, bm *bitmap, phrases []string, tokens []uint64) {
if len(phrases) == 0 {
return
}
if !matchBloomFilterAllTokens(bs, ch, tokens) {
bm.resetBits()
return
}
bb := bbPool.Get()
visitValues(bs, ch, bm, func(v string) bool {
n := unmarshalIPv4(v)
bb.B = marshalIPv4String(bb.B[:0], n)
s := bytesutil.ToUnsafeString(bb.B)
return matchAllPhrases(s, phrases)
})
bbPool.Put(bb)
}
func matchAllPhrasesTimestampISO8601(bs *blockSearch, ch *columnHeader, bm *bitmap, phrases []string, tokens []uint64) {
if len(phrases) == 0 {
return
}
if !matchBloomFilterAllTokens(bs, ch, tokens) {
bm.resetBits()
return
}
bb := bbPool.Get()
visitValues(bs, ch, bm, func(v string) bool {
n := unmarshalTimestampISO8601(v)
bb.B = marshalTimestampISO8601String(bb.B[:0], n)
s := bytesutil.ToUnsafeString(bb.B)
return matchAllPhrases(s, phrases)
})
bbPool.Put(bb)
}
func matchAllPhrasesDict(bs *blockSearch, ch *columnHeader, bm *bitmap, phrases []string) {
bb := bbPool.Get()
for _, v := range ch.valuesDict.values {
c := byte(0)
if matchAllPhrases(v, phrases) {
c = 1
}
bb.B = append(bb.B, c)
}
matchEncodedValuesDict(bs, ch, bm, bb.B)
bbPool.Put(bb)
}
func matchAllPhrases(v string, phrases []string) bool {
for _, phrase := range phrases {
if phrase == "" {
// Special case - empty phrase matches everything
continue
}
if !matchPhrase(v, phrase) {
return false
}
}
return true
}

View File

@@ -1,894 +0,0 @@
package logstorage
import (
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
)
func TestFilterContainsAll(t *testing.T) {
t.Parallel()
t.Run("single-row", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"abc def",
},
},
{
name: "other column",
values: []string{
"asdfdsf",
},
},
}
// match
fi := &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"def", "abc"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0})
// mismatch
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"foo", "abc"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{"abc", "def", ""}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("const-column", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"abc def",
"abc def",
"abc def",
},
},
}
// match
fi := &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"abc", "def", "abc def"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2})
// mismatch
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"abc def", "foobar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "non-existing column",
}
fi.values.values = []string{"x"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("dict", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"",
"foobar",
"abc",
"afdf foobar baz",
"fddf foobarbaz",
"afoobarbaz",
"foobar",
},
},
}
// match
fi := &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"foobar", "afdf", ""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6})
// mismatch
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"bar", "aaaa"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "non-existing column",
}
fi.values.values = []string{"foobar", "aaaa"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("strings", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"a foo",
"a foobar",
"aa abc a",
"ca afdf a,foobar baz",
"a fddf foobarbaz",
"a afoobarbaz",
"a foobar",
"a kjlkjf dfff",
"a ТЕСТЙЦУК НГКШ ",
"a !!,23.(!1)",
},
},
}
// match
fi := &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"a", "", " ", "foobar"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 3, 6})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
// mismatch
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"aa a", "adfwer"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{"abc"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("uint8", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"123",
"12",
"32",
"0",
"0",
"12",
"1",
"2",
"3",
"4",
"5",
},
},
}
// match
fi := &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"12"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 5})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"12", "12", "", "12"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 5})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"0", "12"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{"12"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("uint16", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"123",
"12",
"32",
"0",
"0",
"12",
"256",
"2",
"3",
"4",
"5",
},
},
}
// match
fi := &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"12"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 5})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"12", "12"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 5})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"12", "0"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{"12", "0"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("uint32", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"123",
"12",
"32",
"0",
"0",
"12",
"65536",
"2",
"3",
"4",
"5",
},
},
}
// match
fi := &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"12"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 5})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"12", "12"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 5})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"12", "0"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{"12"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("uint64", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"123",
"12",
"32",
"0",
"0",
"12",
"12345678901",
"2",
"3",
"4",
"5",
},
},
}
// match
fi := &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"12"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 5})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"12", ""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 5})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"12", "12"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 5})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"0", "12"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{"12"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("int64", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"123",
"12",
"-32",
"0",
"0",
"12",
"12345678901",
"2",
"3",
"4",
"5",
},
},
}
// match
fi := &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"12"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 5})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"12", "", "12"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 5})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"0", "12"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{"12"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("float64", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"1234",
"0",
"3454",
"-65536",
"1234.5678901",
"1",
"2",
"3",
"4",
},
},
}
// match
fi := &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"1234"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 4})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"1234", "", "1234"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 4})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"5678901", ".", "1234"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{4})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"-65536", "-"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
// mismatch
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"655361"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{"0"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("ipv4", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"1.2.3.4",
"0.0.0.0",
"127.0.0.1",
"254.255.255.255",
"127.0.0.1",
"127.0.0.1",
"127.0.4.2",
"127.0.0.1",
"12.0.127.6",
"55.55.55.55",
"66.66.66.66",
"7.7.7.7",
},
},
}
// match
fi := &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"127.0.0.1", ".0.0.", "127.0"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{2, 4, 5, 7})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11})
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11})
// mismatch
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "foo",
}
fi.values.values = []string{"5", "127"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAll{
fieldName: "non-existing-field",
}
fi.values.values = []string{"0"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("timestamp-iso8601", func(t *testing.T) {
columns := []column{
{
name: "_msg",
values: []string{
"2006-01-02T15:04:05.001Z",
"2006-01-02T15:04:05.002Z",
"2006-01-02T15:04:05.003Z",
"2006-01-02T15:04:05.004Z",
"2006-01-02T15:04:05.005Z",
"2006-01-02T15:04:05.006Z",
"2006-01-02T15:04:05.007Z",
"2006-01-02T15:04:05.008Z",
"2006-01-02T15:04:05.009Z",
},
},
}
// match
fi := &filterContainsAll{
fieldName: "_msg",
}
fi.values.values = []string{"04:05.005Z", "", "2006-01"}
testFilterMatchForColumns(t, columns, fi, "_msg", []int{4})
fi = &filterContainsAll{
fieldName: "_msg",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "_msg", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
fi = &filterContainsAll{
fieldName: "_msg",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "_msg", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "_msg", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "_msg", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
// mimatch
fi = &filterContainsAll{
fieldName: "_msg",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "_msg", nil)
fi = &filterContainsAll{
fieldName: "_msg",
}
fi.values.values = []string{"2006-04-02T15:04:05.005Z"}
testFilterMatchForColumns(t, columns, fi, "_msg", nil)
fi = &filterContainsAll{
fieldName: "non-existing-column",
}
fi.values.values = []string{"2006"}
testFilterMatchForColumns(t, columns, fi, "_msg", nil)
})
// Remove the remaining data files for the test
fs.MustRemoveAll(t.Name())
}

View File

@@ -1,298 +0,0 @@
package logstorage
import (
"fmt"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
// filterContainsAny matches any value from the values.
//
// Example LogsQL: `fieldName:contains_any("foo", "bar baz")`
type filterContainsAny struct {
fieldName string
values inValues
}
func (fi *filterContainsAny) String() string {
args := fi.values.String()
return fmt.Sprintf("%scontains_any(%s)", quoteFieldNameIfNeeded(fi.fieldName), args)
}
func (fi *filterContainsAny) updateNeededFields(neededFields fieldsSet) {
neededFields.add(fi.fieldName)
}
func (fi *filterContainsAny) applyToBlockResult(br *blockResult, bm *bitmap) {
if fi.values.isEmpty() {
bm.resetBits()
return
}
if fi.values.hasEmptyValue() {
// Special case - empty value matches everything
return
}
c := br.getColumnByName(fi.fieldName)
if c.isConst {
v := c.valuesEncoded[0]
if !matchAnyPhrase(v, fi.values.values) {
bm.resetBits()
}
return
}
if c.isTime {
fi.matchColumnByStringValues(br, bm, c)
return
}
switch c.valueType {
case valueTypeString:
fi.matchColumnByStringValues(br, bm, c)
case valueTypeDict:
phrases := fi.values.values
bb := bbPool.Get()
for _, v := range c.dictValues {
c := byte(0)
if matchAnyPhrase(v, phrases) {
c = 1
}
bb.B = append(bb.B, c)
}
valuesEncoded := c.getValuesEncoded(br)
bm.forEachSetBit(func(idx int) bool {
n := valuesEncoded[idx][0]
return bb.B[n] == 1
})
bbPool.Put(bb)
case valueTypeUint8:
binValues := fi.values.getUint8Values()
matchColumnByBinValues(br, bm, c, binValues)
case valueTypeUint16:
binValues := fi.values.getUint16Values()
matchColumnByBinValues(br, bm, c, binValues)
case valueTypeUint32:
binValues := fi.values.getUint32Values()
matchColumnByBinValues(br, bm, c, binValues)
case valueTypeUint64:
binValues := fi.values.getUint64Values()
matchColumnByBinValues(br, bm, c, binValues)
case valueTypeInt64:
fi.matchColumnByStringValues(br, bm, c)
case valueTypeFloat64:
fi.matchColumnByStringValues(br, bm, c)
case valueTypeIPv4:
fi.matchColumnByStringValues(br, bm, c)
case valueTypeTimestampISO8601:
fi.matchColumnByStringValues(br, bm, c)
default:
logger.Panicf("FATAL: unknown valueType=%d", c.valueType)
}
}
func (fi *filterContainsAny) matchColumnByStringValues(br *blockResult, bm *bitmap, c *blockResultColumn) {
phrases := fi.values.values
values := c.getValues(br)
bm.forEachSetBit(func(idx int) bool {
return matchAnyPhrase(values[idx], phrases)
})
}
func (fi *filterContainsAny) applyToBlockSearch(bs *blockSearch, bm *bitmap) {
if fi.values.isEmpty() {
bm.resetBits()
return
}
if fi.values.hasEmptyValue() {
// Special case - empty value matches everything
return
}
v := bs.getConstColumnValue(fi.fieldName)
if v != "" {
if !matchAnyPhrase(v, fi.values.values) {
bm.resetBits()
}
return
}
// Verify whether filter matches other columns
ch := bs.getColumnHeader(fi.fieldName)
if ch == nil {
// Fast path - there are no matching columns.
// It matches anything only for empty phrase.
if !matchAnyPhrase("", fi.values.values) {
bm.resetBits()
}
return
}
commonTokens, tokenSets := fi.values.getTokensHashesAny()
switch ch.valueType {
case valueTypeString:
matchAnyPhraseString(bs, ch, bm, fi.values.values, commonTokens, tokenSets)
case valueTypeDict:
matchAnyPhraseDict(bs, ch, bm, fi.values.values)
case valueTypeUint8:
binValues := fi.values.getUint8Values()
matchAnyValue(bs, ch, bm, binValues, commonTokens, tokenSets)
case valueTypeUint16:
binValues := fi.values.getUint16Values()
matchAnyValue(bs, ch, bm, binValues, commonTokens, tokenSets)
case valueTypeUint32:
binValues := fi.values.getUint32Values()
matchAnyValue(bs, ch, bm, binValues, commonTokens, tokenSets)
case valueTypeUint64:
binValues := fi.values.getUint64Values()
matchAnyValue(bs, ch, bm, binValues, commonTokens, tokenSets)
case valueTypeInt64:
matchAnyPhraseInt64(bs, ch, bm, fi.values.values, commonTokens, tokenSets)
case valueTypeFloat64:
matchAnyPhraseFloat64(bs, ch, bm, fi.values.values, commonTokens, tokenSets)
case valueTypeIPv4:
matchAnyPhraseIPv4(bs, ch, bm, fi.values.values, commonTokens, tokenSets)
case valueTypeTimestampISO8601:
matchAnyPhraseTimestampISO8601(bs, ch, bm, fi.values.values, commonTokens, tokenSets)
default:
logger.Panicf("FATAL: %s: unknown valueType=%d", bs.partPath(), ch.valueType)
}
}
func matchAnyPhraseString(bs *blockSearch, ch *columnHeader, bm *bitmap, phrases []string, commonTokens []uint64, tokenSets [][]uint64) {
if len(phrases) == 0 {
bm.resetBits()
return
}
if !matchBloomFilterAllTokens(bs, ch, commonTokens) {
bm.resetBits()
return
}
matchValuesAnyPhrase(bs, ch, bm, phrases, tokenSets, matchAnyPhrase)
}
func matchValuesAnyPhrase(bs *blockSearch, ch *columnHeader, bm *bitmap, phrases []string, tokenSets [][]uint64, matchPhraseFunc func(value string, phrases []string) bool) {
bf := bs.getBloomFilterForColumn(ch)
sb := getStringBucket()
for i := range phrases {
if bf.containsAll(tokenSets[i]) {
sb.a = append(sb.a, phrases[i])
}
}
if len(sb.a) > 0 {
values := bs.getValuesForColumn(ch)
bm.forEachSetBit(func(idx int) bool {
return matchPhraseFunc(values[idx], sb.a)
})
} else {
bm.resetBits()
}
putStringBucket(sb)
}
func matchAnyPhraseInt64(bs *blockSearch, ch *columnHeader, bm *bitmap, phrases []string, commonTokens []uint64, tokenSets [][]uint64) {
if len(phrases) == 0 {
bm.resetBits()
return
}
if !matchBloomFilterAllTokens(bs, ch, commonTokens) {
bm.resetBits()
return
}
bb := bbPool.Get()
matchValuesAnyPhrase(bs, ch, bm, phrases, tokenSets, func(v string, phrases []string) bool {
n := unmarshalInt64(v)
bb.B = marshalInt64String(bb.B[:0], n)
s := bytesutil.ToUnsafeString(bb.B)
return matchAnyPhrase(s, phrases)
})
bbPool.Put(bb)
}
func matchAnyPhraseFloat64(bs *blockSearch, ch *columnHeader, bm *bitmap, phrases []string, commonTokens []uint64, tokenSets [][]uint64) {
if len(phrases) == 0 {
bm.resetBits()
return
}
if !matchBloomFilterAllTokens(bs, ch, commonTokens) {
bm.resetBits()
return
}
bb := bbPool.Get()
matchValuesAnyPhrase(bs, ch, bm, phrases, tokenSets, func(v string, phrases []string) bool {
n := unmarshalFloat64(v)
bb.B = marshalFloat64String(bb.B[:0], n)
s := bytesutil.ToUnsafeString(bb.B)
return matchAnyPhrase(s, phrases)
})
bbPool.Put(bb)
}
func matchAnyPhraseIPv4(bs *blockSearch, ch *columnHeader, bm *bitmap, phrases []string, commonTokens []uint64, tokenSets [][]uint64) {
if len(phrases) == 0 {
bm.resetBits()
return
}
if !matchBloomFilterAllTokens(bs, ch, commonTokens) {
bm.resetBits()
return
}
bb := bbPool.Get()
matchValuesAnyPhrase(bs, ch, bm, phrases, tokenSets, func(v string, phrases []string) bool {
n := unmarshalIPv4(v)
bb.B = marshalIPv4String(bb.B[:0], n)
s := bytesutil.ToUnsafeString(bb.B)
return matchAnyPhrase(s, phrases)
})
bbPool.Put(bb)
}
func matchAnyPhraseTimestampISO8601(bs *blockSearch, ch *columnHeader, bm *bitmap, phrases []string, commonTokens []uint64, tokenSets [][]uint64) {
if len(phrases) == 0 {
bm.resetBits()
return
}
if !matchBloomFilterAllTokens(bs, ch, commonTokens) {
bm.resetBits()
return
}
bb := bbPool.Get()
matchValuesAnyPhrase(bs, ch, bm, phrases, tokenSets, func(v string, phrases []string) bool {
n := unmarshalTimestampISO8601(v)
bb.B = marshalTimestampISO8601String(bb.B[:0], n)
s := bytesutil.ToUnsafeString(bb.B)
return matchAnyPhrase(s, phrases)
})
bbPool.Put(bb)
}
func matchAnyPhraseDict(bs *blockSearch, ch *columnHeader, bm *bitmap, phrases []string) {
bb := bbPool.Get()
for _, v := range ch.valuesDict.values {
c := byte(0)
if matchAnyPhrase(v, phrases) {
c = 1
}
bb.B = append(bb.B, c)
}
matchEncodedValuesDict(bs, ch, bm, bb.B)
bbPool.Put(bb)
}
func matchAnyPhrase(v string, phrases []string) bool {
for _, phrase := range phrases {
if matchPhrase(v, phrase) {
return true
}
}
return false
}

View File

@@ -1,798 +0,0 @@
package logstorage
import (
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
)
func TestFilterContainsAny(t *testing.T) {
t.Parallel()
t.Run("single-row", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"abc def",
},
},
{
name: "other column",
values: []string{
"asdfdsf",
},
},
}
// match
fi := &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"def", "abc", "foobar"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0})
fi = &filterContainsAny{
fieldName: "other column",
}
fi.values.values = []string{"asdfdsf", ""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"", "bar"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0})
fi = &filterContainsAny{
fieldName: "non-existing-column",
}
fi.values.values = []string{"", "foo"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0})
// mismatch
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"xabc", "deff"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"xabc"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "other column",
}
fi.values.values = []string{"sd"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "non-existing column",
}
fi.values.values = []string{"abc", "def"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("const-column", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"abc def",
"abc def",
"abc def",
},
},
}
// match
fi := &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"aaaa", "abc", "foobar"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2})
fi = &filterContainsAny{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2})
// mismatch
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"abc def ", "foobar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "non-existing column",
}
fi.values.values = []string{"x"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("dict", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"",
"foobar",
"abc",
"afdf foobar baz",
"fddf foobarbaz",
"afoobarbaz",
"foobar",
},
},
}
// match
fi := &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"foobar", "aaaa", "abc", "bazz"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 3, 6})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"bbbb", "", "aaaa"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6})
fi = &filterContainsAny{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6})
// mismatch
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"bar", "aaaa"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "non-existing column",
}
fi.values.values = []string{"foobar", "aaaa"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("strings", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"a foo",
"a foobar",
"aa abc a",
"ca afdf a,foobar baz",
"a fddf foobarbaz",
"a afoobarbaz",
"a foobar",
"a kjlkjf dfff",
"a ТЕСТЙЦУК НГКШ ",
"a !!,23.(!1)",
},
},
}
// match
fi := &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"foobar", "abc a"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 3, 6})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
fi = &filterContainsAny{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
// mismatch
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"aa a"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("uint8", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"123",
"12",
"32",
"0",
"0",
"12",
"1",
"2",
"3",
"4",
"5",
},
},
}
// match
fi := &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"12", "32"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 5})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"0"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3, 4})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAny{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"1234"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("uint16", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"123",
"12",
"32",
"0",
"0",
"12",
"256",
"2",
"3",
"4",
"5",
},
},
}
// match
fi := &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"12", "32"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 5})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"0"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3, 4})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAny{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"123456"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("uint32", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"123",
"12",
"32",
"0",
"0",
"12",
"65536",
"2",
"3",
"4",
"5",
},
},
}
// match
fi := &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"12", "32"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 5})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"0"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3, 4})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAny{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"12345678901"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("uint64", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"123",
"12",
"32",
"0",
"0",
"12",
"12345678901",
"2",
"3",
"4",
"5",
},
},
}
// match
fi := &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"12", "32"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 5})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"0"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3, 4})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAny{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("int64", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"123",
"12",
"-32",
"0",
"0",
"12",
"12345678901",
"2",
"3",
"4",
"5",
},
},
}
// match
fi := &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"12", "32"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 5})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"0"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3, 4})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
fi = &filterContainsAny{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("float64", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"1234",
"0",
"3454",
"-65536",
"1234.5678901",
"1",
"2",
"3",
"4",
},
},
}
// match
fi := &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"1234", "1", "foobar", "123211"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 4, 5})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"5678901"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{4})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"-65536"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
fi = &filterContainsAny{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
// mismatch
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"655361"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"123"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"12345678901234567890"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("ipv4", func(t *testing.T) {
columns := []column{
{
name: "foo",
values: []string{
"1.2.3.4",
"0.0.0.0",
"127.0.0.1",
"254.255.255.255",
"127.0.0.1",
"127.0.0.1",
"127.0.4.2",
"127.0.0.1",
"12.0.127.6",
"55.55.55.55",
"66.66.66.66",
"7.7.7.7",
},
},
}
// match
fi := &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"127.0.0.1", "24.54.1.2", ".0.4."}
testFilterMatchForColumns(t, columns, fi, "foo", []int{2, 4, 5, 6, 7})
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11})
fi = &filterContainsAny{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11})
// mismatch
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"5"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterContainsAny{
fieldName: "foo",
}
fi.values.values = []string{"255.255.255.255"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
t.Run("timestamp-iso8601", func(t *testing.T) {
columns := []column{
{
name: "_msg",
values: []string{
"2006-01-02T15:04:05.001Z",
"2006-01-02T15:04:05.002Z",
"2006-01-02T15:04:05.003Z",
"2006-01-02T15:04:05.004Z",
"2006-01-02T15:04:05.005Z",
"2006-01-02T15:04:05.006Z",
"2006-01-02T15:04:05.007Z",
"2006-01-02T15:04:05.008Z",
"2006-01-02T15:04:05.009Z",
},
},
}
// match
fi := &filterContainsAny{
fieldName: "_msg",
}
fi.values.values = []string{"04:05.005Z", "foobar"}
testFilterMatchForColumns(t, columns, fi, "_msg", []int{4})
fi = &filterContainsAny{
fieldName: "_msg",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "_msg", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
fi = &filterContainsAny{
fieldName: "non-existing-column",
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "_msg", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
// mimatch
fi = &filterContainsAny{
fieldName: "_msg",
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "_msg", nil)
fi = &filterContainsAny{
fieldName: "_msg",
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "_msg", nil)
fi = &filterContainsAny{
fieldName: "_msg",
}
fi.values.values = []string{"2006-04-02T15:04:05.005Z"}
testFilterMatchForColumns(t, columns, fi, "_msg", nil)
})
// Remove the remaining data files for the test
fs.MustRemoveAll(t.Name())
}

View File

@@ -1,222 +0,0 @@
package logstorage
import (
"fmt"
"sync"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
// filterEqField matches if the given fields have equivalent values.
//
// Example LogsQL: `fieldName:eq_field(otherField)`
type filterEqField struct {
fieldName string
otherFieldName string
}
func (fe *filterEqField) String() string {
return fmt.Sprintf("%seq_field(%s)", quoteFieldNameIfNeeded(fe.fieldName), quoteTokenIfNeeded(fe.otherFieldName))
}
func (fe *filterEqField) updateNeededFields(neededFields fieldsSet) {
neededFields.add(fe.fieldName)
neededFields.add(fe.otherFieldName)
}
func (fe *filterEqField) applyToBlockResult(br *blockResult, bm *bitmap) {
if fe.fieldName == fe.otherFieldName {
return
}
c := br.getColumnByName(fe.fieldName)
cOther := br.getColumnByName(fe.otherFieldName)
if c.isConst && cOther.isConst {
v := c.valuesEncoded[0]
vOther := cOther.valuesEncoded[0]
if v != vOther {
bm.resetBits()
}
return
}
if c.isTime && cOther.isTime {
// c and cOther point to the same _time column, since only a single _time column may exist
return
}
if c.valueType != cOther.valueType {
// Slow path - c and cOther have different valueType, so convert them to string values and compare them
applyFilterEqString(br, bm, c, cOther)
return
}
switch c.valueType {
case valueTypeString:
applyFilterEqString(br, bm, c, cOther)
case valueTypeDict:
applyFilterEqDict(br, bm, c, cOther)
case valueTypeUint8:
applyFilterEqBinValues(br, bm, c, cOther)
case valueTypeUint16:
applyFilterEqBinValues(br, bm, c, cOther)
case valueTypeUint32:
applyFilterEqBinValues(br, bm, c, cOther)
case valueTypeUint64:
applyFilterEqBinValues(br, bm, c, cOther)
case valueTypeInt64:
applyFilterEqBinValues(br, bm, c, cOther)
case valueTypeFloat64:
applyFilterEqBinValues(br, bm, c, cOther)
case valueTypeIPv4:
applyFilterEqBinValues(br, bm, c, cOther)
case valueTypeTimestampISO8601:
applyFilterEqBinValues(br, bm, c, cOther)
default:
logger.Panicf("FATAL: unknown valueType=%d", c.valueType)
}
}
func applyFilterEqString(br *blockResult, bm *bitmap, c, cOther *blockResultColumn) {
values := c.getValues(br)
valuesOther := cOther.getValues(br)
bm.forEachSetBit(func(idx int) bool {
return values[idx] == valuesOther[idx]
})
}
func applyFilterEqDict(br *blockResult, bm *bitmap, c, cOther *blockResultColumn) {
valuesEncoded := c.getValuesEncoded(br)
valuesEncodedOther := cOther.getValuesEncoded(br)
bm.forEachSetBit(func(idx int) bool {
dictIdx := valuesEncoded[idx][0]
dictIdxOther := valuesEncodedOther[idx][0]
v := c.dictValues[dictIdx]
vOther := cOther.dictValues[dictIdxOther]
return v == vOther
})
}
func applyFilterEqBinValues(br *blockResult, bm *bitmap, c, cOther *blockResultColumn) {
valuesEncoded := c.getValuesEncoded(br)
valuesEncodedOther := cOther.getValuesEncoded(br)
bm.forEachSetBit(func(idx int) bool {
return valuesEncoded[idx] == valuesEncodedOther[idx]
})
}
func (fe *filterEqField) applyToBlockSearch(bs *blockSearch, bm *bitmap) {
if fe.fieldName == fe.otherFieldName {
return
}
v := bs.getConstColumnValue(fe.fieldName)
vOther := bs.getConstColumnValue(fe.otherFieldName)
if v != "" || vOther != "" {
if v != "" && vOther != "" {
if v != vOther {
bm.resetBits()
}
return
}
fe.applyFilterString(bs, bm)
return
}
ch := bs.getColumnHeader(fe.fieldName)
chOther := bs.getColumnHeader(fe.otherFieldName)
if ch == nil || chOther == nil {
if ch == nil && chOther == nil {
return
}
fe.applyFilterString(bs, bm)
return
}
if ch.valueType != chOther.valueType {
// Slow path - c and cOther have different valueType, so convert them to string values and compare them
fe.applyFilterString(bs, bm)
return
}
switch ch.valueType {
case valueTypeString:
fe.applyFilterString(bs, bm)
case valueTypeDict:
fe.applyFilterDict(bs, bm, ch, chOther)
case valueTypeUint8:
fe.applyFilterBinValue(bs, bm, ch, chOther)
case valueTypeUint16:
fe.applyFilterBinValue(bs, bm, ch, chOther)
case valueTypeUint32:
fe.applyFilterBinValue(bs, bm, ch, chOther)
case valueTypeUint64:
fe.applyFilterBinValue(bs, bm, ch, chOther)
case valueTypeInt64:
fe.applyFilterBinValue(bs, bm, ch, chOther)
case valueTypeFloat64:
fe.applyFilterBinValue(bs, bm, ch, chOther)
case valueTypeIPv4:
fe.applyFilterBinValue(bs, bm, ch, chOther)
case valueTypeTimestampISO8601:
fe.applyFilterBinValue(bs, bm, ch, chOther)
default:
logger.Panicf("FATAL: %s: unknown valueType=%d", bs.partPath(), ch.valueType)
}
}
func (fe *filterEqField) applyFilterString(bs *blockSearch, bm *bitmap) {
br := getBlockResult()
br.mustInit(bs, bm)
br.initRequestedColumns([]string{fe.fieldName, fe.otherFieldName})
c := br.getColumnByName(fe.fieldName)
cOther := br.getColumnByName(fe.otherFieldName)
values := c.getValues(br)
valuesOther := cOther.getValues(br)
srcIdx := 0
bm.forEachSetBit(func(_ int) bool {
ok := values[srcIdx] == valuesOther[srcIdx]
srcIdx++
return ok
})
putBlockResult(br)
}
func (fe *filterEqField) applyFilterDict(bs *blockSearch, bm *bitmap, ch, chOther *columnHeader) {
valuesEncoded := bs.getValuesForColumn(ch)
valuesEncodedOther := bs.getValuesForColumn(chOther)
bm.forEachSetBit(func(idx int) bool {
dictIdx := valuesEncoded[idx][0]
dictIdxOther := valuesEncodedOther[idx][0]
v := ch.valuesDict.values[dictIdx]
vOther := chOther.valuesDict.values[dictIdxOther]
return v == vOther
})
}
func (fe *filterEqField) applyFilterBinValue(bs *blockSearch, bm *bitmap, ch, chOther *columnHeader) {
valuesEncoded := bs.getValuesForColumn(ch)
valuesEncodedOther := bs.getValuesForColumn(chOther)
bm.forEachSetBit(func(idx int) bool {
return valuesEncoded[idx] == valuesEncodedOther[idx]
})
}
func getBlockResult() *blockResult {
v := brPool.Get()
if v == nil {
return &blockResult{}
}
return v.(*blockResult)
}
func putBlockResult(br *blockResult) {
br.reset()
brPool.Put(br)
}
var brPool sync.Pool

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,14 @@ package logstorage
import (
"fmt"
"math"
"slices"
"strings"
"sync"
"unsafe"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
@@ -11,12 +18,58 @@ import (
// Example LogsQL: `fieldName:in("foo", "bar baz")`
type filterIn struct {
fieldName string
values []string
values inValues
// If q is non-nil, then values must be populated from q before filter execution.
q *Query
// qFieldName must be set to field name for obtaining values from if q is non-nil.
qFieldName string
tokensOnce sync.Once
commonTokensHashes []uint64
tokenSetsHashes [][]uint64
stringValuesOnce sync.Once
stringValues map[string]struct{}
uint8ValuesOnce sync.Once
uint8Values map[string]struct{}
uint16ValuesOnce sync.Once
uint16Values map[string]struct{}
uint32ValuesOnce sync.Once
uint32Values map[string]struct{}
uint64ValuesOnce sync.Once
uint64Values map[string]struct{}
int64ValuesOnce sync.Once
int64Values map[string]struct{}
float64ValuesOnce sync.Once
float64Values map[string]struct{}
ipv4ValuesOnce sync.Once
ipv4Values map[string]struct{}
timestampISO8601ValuesOnce sync.Once
timestampISO8601Values map[string]struct{}
}
func (fi *filterIn) String() string {
args := fi.values.String()
args := ""
if fi.q != nil {
args = fi.q.String()
} else {
values := fi.values
a := make([]string, len(values))
for i, value := range values {
a[i] = quoteTokenIfNeeded(value)
}
args = strings.Join(a, ",")
}
return fmt.Sprintf("%sin(%s)", quoteFieldNameIfNeeded(fi.fieldName), args)
}
@@ -24,15 +77,229 @@ func (fi *filterIn) updateNeededFields(neededFields fieldsSet) {
neededFields.add(fi.fieldName)
}
func (fi *filterIn) getTokensHashes() ([]uint64, [][]uint64) {
fi.tokensOnce.Do(fi.initTokens)
return fi.commonTokensHashes, fi.tokenSetsHashes
}
func (fi *filterIn) initTokens() {
commonTokens, tokenSets := getCommonTokensAndTokenSets(fi.values)
fi.commonTokensHashes = appendTokensHashes(nil, commonTokens)
var hashesBuf []uint64
tokenSetsHashes := make([][]uint64, len(tokenSets))
for i, tokens := range tokenSets {
if hashesBuf == nil || len(hashesBuf) > 60_000/int(unsafe.Sizeof(hashesBuf[0])) {
hashesBuf = make([]uint64, 0, 64*1024/int(unsafe.Sizeof(hashesBuf[0])))
}
hashesBufLen := len(hashesBuf)
hashesBuf = appendTokensHashes(hashesBuf, tokens)
tokenSetsHashes[i] = hashesBuf[hashesBufLen:]
}
fi.tokenSetsHashes = tokenSetsHashes
}
func (fi *filterIn) getStringValues() map[string]struct{} {
fi.stringValuesOnce.Do(fi.initStringValues)
return fi.stringValues
}
func (fi *filterIn) initStringValues() {
values := fi.values
m := make(map[string]struct{}, len(values))
for _, v := range values {
m[v] = struct{}{}
}
fi.stringValues = m
}
func (fi *filterIn) getUint8Values() map[string]struct{} {
fi.uint8ValuesOnce.Do(fi.initUint8Values)
return fi.uint8Values
}
func (fi *filterIn) initUint8Values() {
values := fi.values
m := make(map[string]struct{}, len(values))
buf := make([]byte, 0, len(values)*1)
for _, v := range values {
n, ok := tryParseUint64(v)
if !ok || n >= (1<<8) {
continue
}
bufLen := len(buf)
buf = append(buf, byte(n))
s := bytesutil.ToUnsafeString(buf[bufLen:])
m[s] = struct{}{}
}
fi.uint8Values = m
}
func (fi *filterIn) getUint16Values() map[string]struct{} {
fi.uint16ValuesOnce.Do(fi.initUint16Values)
return fi.uint16Values
}
func (fi *filterIn) initUint16Values() {
values := fi.values
m := make(map[string]struct{}, len(values))
buf := make([]byte, 0, len(values)*2)
for _, v := range values {
n, ok := tryParseUint64(v)
if !ok || n >= (1<<16) {
continue
}
bufLen := len(buf)
buf = encoding.MarshalUint16(buf, uint16(n))
s := bytesutil.ToUnsafeString(buf[bufLen:])
m[s] = struct{}{}
}
fi.uint16Values = m
}
func (fi *filterIn) getUint32Values() map[string]struct{} {
fi.uint32ValuesOnce.Do(fi.initUint32Values)
return fi.uint32Values
}
func (fi *filterIn) initUint32Values() {
values := fi.values
m := make(map[string]struct{}, len(values))
buf := make([]byte, 0, len(values)*4)
for _, v := range values {
n, ok := tryParseUint64(v)
if !ok || n >= (1<<32) {
continue
}
bufLen := len(buf)
buf = encoding.MarshalUint32(buf, uint32(n))
s := bytesutil.ToUnsafeString(buf[bufLen:])
m[s] = struct{}{}
}
fi.uint32Values = m
}
func (fi *filterIn) getUint64Values() map[string]struct{} {
fi.uint64ValuesOnce.Do(fi.initUint64Values)
return fi.uint64Values
}
func (fi *filterIn) getInt64Values() map[string]struct{} {
fi.int64ValuesOnce.Do(fi.initInt64Values)
return fi.int64Values
}
func (fi *filterIn) initUint64Values() {
values := fi.values
m := make(map[string]struct{}, len(values))
buf := make([]byte, 0, len(values)*8)
for _, v := range values {
n, ok := tryParseUint64(v)
if !ok {
continue
}
bufLen := len(buf)
buf = encoding.MarshalUint64(buf, n)
s := bytesutil.ToUnsafeString(buf[bufLen:])
m[s] = struct{}{}
}
fi.uint64Values = m
}
func (fi *filterIn) initInt64Values() {
values := fi.values
m := make(map[string]struct{}, len(values))
buf := make([]byte, 0, len(values)*8)
for _, v := range values {
n, ok := tryParseInt64(v)
if !ok {
continue
}
bufLen := len(buf)
buf = encoding.MarshalInt64(buf, n)
s := bytesutil.ToUnsafeString(buf[bufLen:])
m[s] = struct{}{}
}
fi.int64Values = m
}
func (fi *filterIn) getFloat64Values() map[string]struct{} {
fi.float64ValuesOnce.Do(fi.initFloat64Values)
return fi.float64Values
}
func (fi *filterIn) initFloat64Values() {
values := fi.values
m := make(map[string]struct{}, len(values))
buf := make([]byte, 0, len(values)*8)
for _, v := range values {
f, ok := tryParseFloat64Exact(v)
if !ok {
continue
}
n := math.Float64bits(f)
bufLen := len(buf)
buf = encoding.MarshalUint64(buf, n)
s := bytesutil.ToUnsafeString(buf[bufLen:])
m[s] = struct{}{}
}
fi.float64Values = m
}
func (fi *filterIn) getIPv4Values() map[string]struct{} {
fi.ipv4ValuesOnce.Do(fi.initIPv4Values)
return fi.ipv4Values
}
func (fi *filterIn) initIPv4Values() {
values := fi.values
m := make(map[string]struct{}, len(values))
buf := make([]byte, 0, len(values)*4)
for _, v := range values {
n, ok := tryParseIPv4(v)
if !ok {
continue
}
bufLen := len(buf)
buf = encoding.MarshalUint32(buf, uint32(n))
s := bytesutil.ToUnsafeString(buf[bufLen:])
m[s] = struct{}{}
}
fi.ipv4Values = m
}
func (fi *filterIn) getTimestampISO8601Values() map[string]struct{} {
fi.timestampISO8601ValuesOnce.Do(fi.initTimestampISO8601Values)
return fi.timestampISO8601Values
}
func (fi *filterIn) initTimestampISO8601Values() {
values := fi.values
m := make(map[string]struct{}, len(values))
buf := make([]byte, 0, len(values)*8)
for _, v := range values {
n, ok := tryParseTimestampISO8601(v)
if !ok {
continue
}
bufLen := len(buf)
buf = encoding.MarshalUint64(buf, uint64(n))
s := bytesutil.ToUnsafeString(buf[bufLen:])
m[s] = struct{}{}
}
fi.timestampISO8601Values = m
}
func (fi *filterIn) applyToBlockResult(br *blockResult, bm *bitmap) {
if fi.values.isEmpty() {
if len(fi.values) == 0 {
bm.resetBits()
return
}
c := br.getColumnByName(fi.fieldName)
if c.isConst {
stringValues := fi.values.getStringValues()
stringValues := fi.getStringValues()
v := c.valuesEncoded[0]
if _, ok := stringValues[v]; !ok {
bm.resetBits()
@@ -48,7 +315,7 @@ func (fi *filterIn) applyToBlockResult(br *blockResult, bm *bitmap) {
case valueTypeString:
fi.matchColumnByStringValues(br, bm, c)
case valueTypeDict:
stringValues := fi.values.getStringValues()
stringValues := fi.getStringValues()
bb := bbPool.Get()
for _, v := range c.dictValues {
c := byte(0)
@@ -64,28 +331,28 @@ func (fi *filterIn) applyToBlockResult(br *blockResult, bm *bitmap) {
})
bbPool.Put(bb)
case valueTypeUint8:
binValues := fi.values.getUint8Values()
binValues := fi.getUint8Values()
matchColumnByBinValues(br, bm, c, binValues)
case valueTypeUint16:
binValues := fi.values.getUint16Values()
binValues := fi.getUint16Values()
matchColumnByBinValues(br, bm, c, binValues)
case valueTypeUint32:
binValues := fi.values.getUint32Values()
binValues := fi.getUint32Values()
matchColumnByBinValues(br, bm, c, binValues)
case valueTypeUint64:
binValues := fi.values.getUint64Values()
binValues := fi.getUint64Values()
matchColumnByBinValues(br, bm, c, binValues)
case valueTypeInt64:
binValues := fi.values.getInt64Values()
binValues := fi.getInt64Values()
matchColumnByBinValues(br, bm, c, binValues)
case valueTypeFloat64:
binValues := fi.values.getFloat64Values()
binValues := fi.getFloat64Values()
matchColumnByBinValues(br, bm, c, binValues)
case valueTypeIPv4:
binValues := fi.values.getIPv4Values()
binValues := fi.getIPv4Values()
matchColumnByBinValues(br, bm, c, binValues)
case valueTypeTimestampISO8601:
binValues := fi.values.getTimestampISO8601Values()
binValues := fi.getTimestampISO8601Values()
matchColumnByBinValues(br, bm, c, binValues)
default:
logger.Panicf("FATAL: unknown valueType=%d", c.valueType)
@@ -93,7 +360,7 @@ func (fi *filterIn) applyToBlockResult(br *blockResult, bm *bitmap) {
}
func (fi *filterIn) matchColumnByStringValues(br *blockResult, bm *bitmap, c *blockResultColumn) {
stringValues := fi.values.getStringValues()
stringValues := fi.getStringValues()
values := c.getValues(br)
bm.forEachSetBit(func(idx int) bool {
v := values[idx]
@@ -118,14 +385,14 @@ func matchColumnByBinValues(br *blockResult, bm *bitmap, c *blockResultColumn, b
func (fi *filterIn) applyToBlockSearch(bs *blockSearch, bm *bitmap) {
fieldName := fi.fieldName
if fi.values.isEmpty() {
if len(fi.values) == 0 {
bm.resetBits()
return
}
v := bs.getConstColumnValue(fieldName)
if v != "" {
stringValues := fi.values.getStringValues()
stringValues := fi.getStringValues()
if _, ok := stringValues[v]; !ok {
bm.resetBits()
}
@@ -137,53 +404,53 @@ func (fi *filterIn) applyToBlockSearch(bs *blockSearch, bm *bitmap) {
if ch == nil {
// Fast path - there are no matching columns.
// It matches anything only for empty phrase.
stringValues := fi.values.getStringValues()
stringValues := fi.getStringValues()
if _, ok := stringValues[""]; !ok {
bm.resetBits()
}
return
}
commonTokens, tokenSets := fi.values.getTokensHashesAny()
commonTokens, tokenSets := fi.getTokensHashes()
switch ch.valueType {
case valueTypeString:
stringValues := fi.values.getStringValues()
stringValues := fi.getStringValues()
matchAnyValue(bs, ch, bm, stringValues, commonTokens, tokenSets)
case valueTypeDict:
stringValues := fi.values.getStringValues()
stringValues := fi.getStringValues()
matchValuesDictByAnyValue(bs, ch, bm, stringValues)
case valueTypeUint8:
binValues := fi.values.getUint8Values()
binValues := fi.getUint8Values()
matchAnyValue(bs, ch, bm, binValues, commonTokens, tokenSets)
case valueTypeUint16:
binValues := fi.values.getUint16Values()
binValues := fi.getUint16Values()
matchAnyValue(bs, ch, bm, binValues, commonTokens, tokenSets)
case valueTypeUint32:
binValues := fi.values.getUint32Values()
binValues := fi.getUint32Values()
matchAnyValue(bs, ch, bm, binValues, commonTokens, tokenSets)
case valueTypeUint64:
binValues := fi.values.getUint64Values()
binValues := fi.getUint64Values()
matchAnyValue(bs, ch, bm, binValues, commonTokens, tokenSets)
case valueTypeInt64:
binValues := fi.values.getInt64Values()
binValues := fi.getInt64Values()
matchAnyValue(bs, ch, bm, binValues, commonTokens, tokenSets)
case valueTypeFloat64:
binValues := fi.values.getFloat64Values()
binValues := fi.getFloat64Values()
matchAnyValue(bs, ch, bm, binValues, commonTokens, tokenSets)
case valueTypeIPv4:
binValues := fi.values.getIPv4Values()
binValues := fi.getIPv4Values()
matchAnyValue(bs, ch, bm, binValues, commonTokens, tokenSets)
case valueTypeTimestampISO8601:
binValues := fi.values.getTimestampISO8601Values()
binValues := fi.getTimestampISO8601Values()
matchAnyValue(bs, ch, bm, binValues, commonTokens, tokenSets)
default:
logger.Panicf("FATAL: %s: unknown valueType=%d", bs.partPath(), ch.valueType)
}
}
func matchAnyValue(bs *blockSearch, ch *columnHeader, bm *bitmap, binValues map[string]struct{}, commonTokens []uint64, tokenSets [][]uint64) {
if len(binValues) == 0 {
func matchAnyValue(bs *blockSearch, ch *columnHeader, bm *bitmap, values map[string]struct{}, commonTokens []uint64, tokenSets [][]uint64) {
if len(values) == 0 {
bm.resetBits()
return
}
@@ -192,14 +459,19 @@ func matchAnyValue(bs *blockSearch, ch *columnHeader, bm *bitmap, binValues map[
return
}
visitValues(bs, ch, bm, func(v string) bool {
_, ok := binValues[v]
_, ok := values[v]
return ok
})
}
func matchBloomFilterAnyTokenSet(bs *blockSearch, ch *columnHeader, commonTokens []uint64, tokenSets [][]uint64) bool {
if !matchBloomFilterAllTokens(bs, ch, commonTokens) {
return false
if len(commonTokens) > 0 {
if !matchBloomFilterAllTokens(bs, ch, commonTokens) {
return false
}
}
if len(tokenSets) == 0 {
return len(commonTokens) > 0
}
if len(tokenSets) > maxTokenSetsToInit || uint64(len(tokenSets)) > 10*bs.bsw.bh.rowsCount {
// It is faster to match every row in the block against all the values
@@ -230,3 +502,62 @@ func matchValuesDictByAnyValue(bs *blockSearch, ch *columnHeader, bm *bitmap, va
matchEncodedValuesDict(bs, ch, bm, bb.B)
bbPool.Put(bb)
}
func getCommonTokensAndTokenSets(values []string) ([]string, [][]string) {
var tokensBuf []string
tokenSets := make([][]string, len(values))
for i, v := range values {
if tokensBuf == nil || len(tokensBuf) > 60_000/int(unsafe.Sizeof(tokensBuf[0])) {
tokensBuf = make([]string, 0, 64*1024/int(unsafe.Sizeof(tokensBuf[0])))
}
tokensBufLen := len(tokensBuf)
tokensBuf = tokenizeStrings(tokensBuf, []string{v})
tokenSets[i] = tokensBuf[tokensBufLen:]
}
commonTokens := getCommonTokens(tokenSets)
if len(commonTokens) == 0 {
return nil, tokenSets
}
// remove commonTokens from tokenSets
for i, tokens := range tokenSets {
dstTokens := tokens[:0]
for _, token := range tokens {
if !slices.Contains(commonTokens, token) {
dstTokens = append(dstTokens, token)
}
}
if len(dstTokens) == 0 {
return commonTokens, nil
}
tokenSets[i] = dstTokens
}
return commonTokens, tokenSets
}
// getCommonTokens returns common tokens seen at every set of tokens inside tokenSets.
//
// The returned common tokens preserve the original order seen in tokenSets.
func getCommonTokens(tokenSets [][]string) []string {
if len(tokenSets) == 0 {
return nil
}
commonTokens := append([]string{}, tokenSets[0]...)
for _, tokens := range tokenSets[1:] {
if len(commonTokens) == 0 {
return nil
}
dst := commonTokens[:0]
for _, token := range commonTokens {
if slices.Contains(tokens, token) {
dst = append(dst, token)
}
}
commonTokens = dst
}
return commonTokens
}

View File

@@ -1,6 +1,8 @@
package logstorage
import (
"reflect"
"slices"
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
@@ -28,51 +30,51 @@ func TestFilterIn(t *testing.T) {
// match
fi := &filterIn{
fieldName: "foo",
values: []string{"abc def", "abc", "foobar"},
}
fi.values.values = []string{"abc def", "abc", "foobar"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0})
fi = &filterIn{
fieldName: "other column",
values: []string{"asdfdsf", ""},
}
fi.values.values = []string{"asdfdsf", ""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0})
fi = &filterIn{
fieldName: "non-existing-column",
values: []string{"", "foo"},
}
fi.values.values = []string{"", "foo"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0})
// mismatch
fi = &filterIn{
fieldName: "foo",
values: []string{"abc", "def"},
}
fi.values.values = []string{"abc", "def"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{},
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"", "abc"},
}
fi.values.values = []string{"", "abc"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "other column",
values: []string{"sd"},
}
fi.values.values = []string{"sd"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "non-existing column",
values: []string{"abc", "def"},
}
fi.values.values = []string{"abc", "def"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
@@ -91,39 +93,39 @@ func TestFilterIn(t *testing.T) {
// match
fi := &filterIn{
fieldName: "foo",
values: []string{"aaaa", "abc def", "foobar"},
}
fi.values.values = []string{"aaaa", "abc def", "foobar"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2})
fi = &filterIn{
fieldName: "non-existing-column",
values: []string{"", "abc"},
}
fi.values.values = []string{"", "abc"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2})
// mismatch
fi = &filterIn{
fieldName: "foo",
values: []string{"abc def ", "foobar"},
}
fi.values.values = []string{"abc def ", "foobar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{},
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "non-existing column",
values: []string{"x"},
}
fi.values.values = []string{"x"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
@@ -146,39 +148,39 @@ func TestFilterIn(t *testing.T) {
// match
fi := &filterIn{
fieldName: "foo",
values: []string{"foobar", "aaaa", "abc", "baz"},
}
fi.values.values = []string{"foobar", "aaaa", "abc", "baz"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 6})
fi = &filterIn{
fieldName: "foo",
values: []string{"bbbb", "", "aaaa"},
}
fi.values.values = []string{"bbbb", "", "aaaa"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0})
fi = &filterIn{
fieldName: "non-existing-column",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6})
// mismatch
fi = &filterIn{
fieldName: "foo",
values: []string{"bar", "aaaa"},
}
fi.values.values = []string{"bar", "aaaa"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{},
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "non-existing column",
values: []string{"foobar", "aaaa"},
}
fi.values.values = []string{"foobar", "aaaa"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
@@ -204,33 +206,33 @@ func TestFilterIn(t *testing.T) {
// match
fi := &filterIn{
fieldName: "foo",
values: []string{"a foobar", "aa abc a"},
}
fi.values.values = []string{"a foobar", "aa abc a"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 6})
fi = &filterIn{
fieldName: "non-existing-column",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
// mismatch
fi = &filterIn{
fieldName: "foo",
values: []string{"aa a"},
}
fi.values.values = []string{"aa a"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{},
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
@@ -257,45 +259,45 @@ func TestFilterIn(t *testing.T) {
// match
fi := &filterIn{
fieldName: "foo",
values: []string{"12", "32"},
}
fi.values.values = []string{"12", "32"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 5})
fi = &filterIn{
fieldName: "foo",
values: []string{"0"},
}
fi.values.values = []string{"0"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3, 4})
fi = &filterIn{
fieldName: "non-existing-column",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterIn{
fieldName: "foo",
values: []string{"bar"},
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{},
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"33"},
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"1234"},
}
fi.values.values = []string{"1234"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
@@ -322,45 +324,45 @@ func TestFilterIn(t *testing.T) {
// match
fi := &filterIn{
fieldName: "foo",
values: []string{"12", "32"},
}
fi.values.values = []string{"12", "32"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 5})
fi = &filterIn{
fieldName: "foo",
values: []string{"0"},
}
fi.values.values = []string{"0"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3, 4})
fi = &filterIn{
fieldName: "non-existing-column",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterIn{
fieldName: "foo",
values: []string{"bar"},
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{},
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"33"},
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"123456"},
}
fi.values.values = []string{"123456"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
@@ -387,45 +389,45 @@ func TestFilterIn(t *testing.T) {
// match
fi := &filterIn{
fieldName: "foo",
values: []string{"12", "32"},
}
fi.values.values = []string{"12", "32"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 5})
fi = &filterIn{
fieldName: "foo",
values: []string{"0"},
}
fi.values.values = []string{"0"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3, 4})
fi = &filterIn{
fieldName: "non-existing-column",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterIn{
fieldName: "foo",
values: []string{"bar"},
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{},
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"33"},
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"12345678901"},
}
fi.values.values = []string{"12345678901"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
@@ -452,39 +454,39 @@ func TestFilterIn(t *testing.T) {
// match
fi := &filterIn{
fieldName: "foo",
values: []string{"12", "32"},
}
fi.values.values = []string{"12", "32"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 5})
fi = &filterIn{
fieldName: "foo",
values: []string{"0"},
}
fi.values.values = []string{"0"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3, 4})
fi = &filterIn{
fieldName: "non-existing-column",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterIn{
fieldName: "foo",
values: []string{"bar"},
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{},
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"33"},
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
@@ -511,39 +513,39 @@ func TestFilterIn(t *testing.T) {
// match
fi := &filterIn{
fieldName: "foo",
values: []string{"12", "-32"},
}
fi.values.values = []string{"12", "-32"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{1, 2, 5})
fi = &filterIn{
fieldName: "foo",
values: []string{"0"},
}
fi.values.values = []string{"0"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3, 4})
fi = &filterIn{
fieldName: "non-existing-column",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// mismatch
fi = &filterIn{
fieldName: "foo",
values: []string{"bar"},
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{},
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"33"},
}
fi.values.values = []string{"33"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
@@ -568,63 +570,63 @@ func TestFilterIn(t *testing.T) {
// match
fi := &filterIn{
fieldName: "foo",
values: []string{"1234", "1", "foobar", "123211"},
}
fi.values.values = []string{"1234", "1", "foobar", "123211"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 5})
fi = &filterIn{
fieldName: "foo",
values: []string{"1234.5678901"},
}
fi.values.values = []string{"1234.5678901"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{4})
fi = &filterIn{
fieldName: "foo",
values: []string{"-65536"},
}
fi.values.values = []string{"-65536"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{3})
fi = &filterIn{
fieldName: "non-existing-column",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
// mismatch
fi = &filterIn{
fieldName: "foo",
values: []string{"bar"},
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"65536"},
}
fi.values.values = []string{"65536"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{},
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"123"},
}
fi.values.values = []string{"123"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"12345678901234567890"},
}
fi.values.values = []string{"12345678901234567890"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
@@ -652,45 +654,45 @@ func TestFilterIn(t *testing.T) {
// match
fi := &filterIn{
fieldName: "foo",
values: []string{"127.0.0.1", "24.54.1.2", "127.0.4.2"},
}
fi.values.values = []string{"127.0.0.1", "24.54.1.2", "127.0.4.2"}
testFilterMatchForColumns(t, columns, fi, "foo", []int{2, 4, 5, 6, 7})
fi = &filterIn{
fieldName: "non-existing-column",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11})
// mismatch
fi = &filterIn{
fieldName: "foo",
values: []string{"bar"},
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{},
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"5"},
}
fi.values.values = []string{"5"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
fi = &filterIn{
fieldName: "foo",
values: []string{"255.255.255.255"},
}
fi.values.values = []string{"255.255.255.255"}
testFilterMatchForColumns(t, columns, fi, "foo", nil)
})
@@ -715,42 +717,71 @@ func TestFilterIn(t *testing.T) {
// match
fi := &filterIn{
fieldName: "_msg",
values: []string{"2006-01-02T15:04:05.005Z", "foobar"},
}
fi.values.values = []string{"2006-01-02T15:04:05.005Z", "foobar"}
testFilterMatchForColumns(t, columns, fi, "_msg", []int{4})
fi = &filterIn{
fieldName: "non-existing-column",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "_msg", []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
// mimatch
fi = &filterIn{
fieldName: "_msg",
values: []string{"bar"},
}
fi.values.values = []string{"bar"}
testFilterMatchForColumns(t, columns, fi, "_msg", nil)
fi = &filterIn{
fieldName: "_msg",
values: []string{},
}
fi.values.values = []string{}
testFilterMatchForColumns(t, columns, fi, "_msg", nil)
fi = &filterIn{
fieldName: "_msg",
values: []string{""},
}
fi.values.values = []string{""}
testFilterMatchForColumns(t, columns, fi, "_msg", nil)
fi = &filterIn{
fieldName: "_msg",
values: []string{"2006-04-02T15:04:05.005Z"},
}
fi.values.values = []string{"2006-04-02T15:04:05.005Z"}
testFilterMatchForColumns(t, columns, fi, "_msg", nil)
})
// Remove the remaining data files for the test
fs.MustRemoveAll(t.Name())
}
func TestGetCommonTokensAndTokenSets(t *testing.T) {
f := func(values []string, commonTokensExpected []string, tokenSetsExpected [][]string) {
t.Helper()
commonTokens, tokenSets := getCommonTokensAndTokenSets(values)
slices.Sort(commonTokens)
if !reflect.DeepEqual(commonTokens, commonTokensExpected) {
t.Fatalf("unexpected commonTokens for values=%q\ngot\n%q\nwant\n%q", values, commonTokens, commonTokensExpected)
}
for i, tokens := range tokenSets {
slices.Sort(tokens)
tokensExpected := tokenSetsExpected[i]
if !reflect.DeepEqual(tokens, tokensExpected) {
t.Fatalf("unexpected tokens for value=%q\ngot\n%q\nwant\n%q", values[i], tokens, tokensExpected)
}
}
}
f(nil, nil, nil)
f([]string{"foo"}, []string{"foo"}, nil)
f([]string{"foo", "foo"}, []string{"foo"}, nil)
f([]string{"foo", "bar", "bar", "foo"}, nil, [][]string{{"foo"}, {"bar"}, {"bar"}, {"foo"}})
f([]string{"foo", "foo bar", "bar foo"}, []string{"foo"}, nil)
f([]string{"a foo bar", "bar abc foo", "foo abc a bar"}, []string{"bar", "foo"}, [][]string{{"a"}, {"abc"}, {"a", "abc"}})
f([]string{"a xfoo bar", "xbar abc foo", "foo abc a bar"}, nil, [][]string{{"a", "bar", "xfoo"}, {"abc", "foo", "xbar"}, {"a", "abc", "bar", "foo"}})
}

View File

@@ -1,296 +0,0 @@
package logstorage
import (
"fmt"
"math"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
// filterLeField matches if the fieldName field is smaller or equal to the otherFieldName field
//
// Example LogsQL: `fieldName:le_field(otherField)`
type filterLeField struct {
fieldName string
otherFieldName string
excludeEqualValues bool
}
func (fe *filterLeField) String() string {
funcName := "le_field"
if fe.excludeEqualValues {
funcName = "lt_field"
}
return fmt.Sprintf("%s%s(%s)", quoteFieldNameIfNeeded(fe.fieldName), funcName, quoteTokenIfNeeded(fe.otherFieldName))
}
func (fe *filterLeField) updateNeededFields(neededFields fieldsSet) {
neededFields.add(fe.fieldName)
neededFields.add(fe.otherFieldName)
}
func (fe *filterLeField) applyToBlockResult(br *blockResult, bm *bitmap) {
if fe.fieldName == fe.otherFieldName {
if fe.excludeEqualValues {
bm.resetBits()
}
return
}
c := br.getColumnByName(fe.fieldName)
cOther := br.getColumnByName(fe.otherFieldName)
if c.isConst && cOther.isConst {
v := c.valuesEncoded[0]
vOther := cOther.valuesEncoded[0]
if !leValuesString(v, vOther, fe.excludeEqualValues) {
bm.resetBits()
}
return
}
if c.isTime && cOther.isTime {
// c and cOther point to the same _time column, since only a single _time column may exist
if fe.excludeEqualValues {
bm.resetBits()
}
return
}
if c.valueType != cOther.valueType {
// Slow path - c and cOther have different valueType, so convert them to string values and compare them
applyFilterLeString(br, bm, c, cOther, fe.excludeEqualValues)
return
}
switch c.valueType {
case valueTypeString:
applyFilterLeString(br, bm, c, cOther, fe.excludeEqualValues)
case valueTypeDict:
applyFilterLeDict(br, bm, c, cOther, fe.excludeEqualValues)
case valueTypeUint8:
applyFilterLeUint(br, bm, c, cOther, fe.excludeEqualValues)
case valueTypeUint16:
applyFilterLeUint(br, bm, c, cOther, fe.excludeEqualValues)
case valueTypeUint32:
applyFilterLeUint(br, bm, c, cOther, fe.excludeEqualValues)
case valueTypeUint64:
applyFilterLeUint(br, bm, c, cOther, fe.excludeEqualValues)
case valueTypeInt64:
applyFilterLeInt64(br, bm, c, cOther, fe.excludeEqualValues)
case valueTypeFloat64:
applyFilterLeFloat64(br, bm, c, cOther, fe.excludeEqualValues)
case valueTypeIPv4:
applyFilterLeUint(br, bm, c, cOther, fe.excludeEqualValues)
case valueTypeTimestampISO8601:
applyFilterLeUint(br, bm, c, cOther, fe.excludeEqualValues)
default:
logger.Panicf("FATAL: unknown valueType=%d", c.valueType)
}
}
func applyFilterLeString(br *blockResult, bm *bitmap, c, cOther *blockResultColumn, excludeEqualValues bool) {
values := c.getValues(br)
valuesOther := cOther.getValues(br)
bm.forEachSetBit(func(idx int) bool {
return leValuesString(values[idx], valuesOther[idx], excludeEqualValues)
})
}
func applyFilterLeDict(br *blockResult, bm *bitmap, c, cOther *blockResultColumn, excludeEqualValues bool) {
valuesEncoded := c.getValuesEncoded(br)
valuesEncodedOther := cOther.getValuesEncoded(br)
bm.forEachSetBit(func(idx int) bool {
dictIdx := valuesEncoded[idx][0]
dictIdxOther := valuesEncodedOther[idx][0]
v := c.dictValues[dictIdx]
vOther := cOther.dictValues[dictIdxOther]
return leValuesString(v, vOther, excludeEqualValues)
})
}
func applyFilterLeUint(br *blockResult, bm *bitmap, c, cOther *blockResultColumn, excludeEqualValues bool) {
valuesEncoded := c.getValuesEncoded(br)
valuesEncodedOther := cOther.getValuesEncoded(br)
bm.forEachSetBit(func(idx int) bool {
return leValuesString(valuesEncoded[idx], valuesEncodedOther[idx], excludeEqualValues)
})
}
func applyFilterLeInt64(br *blockResult, bm *bitmap, c, cOther *blockResultColumn, excludeEqualValues bool) {
valuesEncoded := c.getValuesEncoded(br)
valuesEncodedOther := cOther.getValuesEncoded(br)
bm.forEachSetBit(func(idx int) bool {
n := unmarshalInt64(valuesEncoded[idx])
nOther := unmarshalInt64(valuesEncodedOther[idx])
return leValuesInt64(n, nOther, excludeEqualValues)
})
}
func applyFilterLeFloat64(br *blockResult, bm *bitmap, c, cOther *blockResultColumn, excludeEqualValues bool) {
valuesEncoded := c.getValuesEncoded(br)
valuesEncodedOther := cOther.getValuesEncoded(br)
bm.forEachSetBit(func(idx int) bool {
f := unmarshalFloat64(valuesEncoded[idx])
fOther := unmarshalFloat64(valuesEncodedOther[idx])
return leValuesFloat64(f, fOther, excludeEqualValues)
})
}
func (fe *filterLeField) applyToBlockSearch(bs *blockSearch, bm *bitmap) {
if fe.fieldName == fe.otherFieldName {
if fe.excludeEqualValues {
bm.resetBits()
}
return
}
v := bs.getConstColumnValue(fe.fieldName)
vOther := bs.getConstColumnValue(fe.otherFieldName)
if v != "" || vOther != "" {
if v != "" && vOther != "" {
if !leValuesString(v, vOther, fe.excludeEqualValues) {
bm.resetBits()
}
return
}
fe.applyFilterString(bs, bm)
return
}
ch := bs.getColumnHeader(fe.fieldName)
chOther := bs.getColumnHeader(fe.otherFieldName)
if ch == nil || chOther == nil {
if ch == nil && chOther == nil {
if fe.excludeEqualValues {
bm.resetBits()
}
return
}
fe.applyFilterString(bs, bm)
return
}
if ch.valueType != chOther.valueType {
// Slow path - c and cOther have different valueType, so convert them to string values and compare them
fe.applyFilterString(bs, bm)
return
}
switch ch.valueType {
case valueTypeString:
fe.applyFilterString(bs, bm)
case valueTypeDict:
fe.applyFilterDict(bs, bm, ch, chOther)
case valueTypeUint8:
fe.applyFilterUint(bs, bm, ch, chOther)
case valueTypeUint16:
fe.applyFilterUint(bs, bm, ch, chOther)
case valueTypeUint32:
fe.applyFilterUint(bs, bm, ch, chOther)
case valueTypeUint64:
fe.applyFilterUint(bs, bm, ch, chOther)
case valueTypeInt64:
fe.applyFilterInt64(bs, bm, ch, chOther)
case valueTypeFloat64:
fe.applyFilterFloat64(bs, bm, ch, chOther)
case valueTypeIPv4:
fe.applyFilterUint(bs, bm, ch, chOther)
case valueTypeTimestampISO8601:
fe.applyFilterUint(bs, bm, ch, chOther)
default:
logger.Panicf("FATAL: %s: unknown valueType=%d", bs.partPath(), ch.valueType)
}
}
func (fe *filterLeField) applyFilterString(bs *blockSearch, bm *bitmap) {
br := getBlockResult()
br.mustInit(bs, bm)
br.initRequestedColumns([]string{fe.fieldName, fe.otherFieldName})
c := br.getColumnByName(fe.fieldName)
cOther := br.getColumnByName(fe.otherFieldName)
values := c.getValues(br)
valuesOther := cOther.getValues(br)
srcIdx := 0
bm.forEachSetBit(func(_ int) bool {
ok := leValuesString(values[srcIdx], valuesOther[srcIdx], fe.excludeEqualValues)
srcIdx++
return ok
})
putBlockResult(br)
}
func (fe *filterLeField) applyFilterDict(bs *blockSearch, bm *bitmap, ch, chOther *columnHeader) {
valuesEncoded := bs.getValuesForColumn(ch)
valuesEncodedOther := bs.getValuesForColumn(chOther)
bm.forEachSetBit(func(idx int) bool {
dictIdx := valuesEncoded[idx][0]
dictIdxOther := valuesEncodedOther[idx][0]
v := ch.valuesDict.values[dictIdx]
vOther := chOther.valuesDict.values[dictIdxOther]
return leValuesString(v, vOther, fe.excludeEqualValues)
})
}
func (fe *filterLeField) applyFilterUint(bs *blockSearch, bm *bitmap, ch, chOther *columnHeader) {
valuesEncoded := bs.getValuesForColumn(ch)
valuesEncodedOther := bs.getValuesForColumn(chOther)
bm.forEachSetBit(func(idx int) bool {
return leValuesString(valuesEncoded[idx], valuesEncodedOther[idx], fe.excludeEqualValues)
})
}
func (fe *filterLeField) applyFilterInt64(bs *blockSearch, bm *bitmap, ch, chOther *columnHeader) {
valuesEncoded := bs.getValuesForColumn(ch)
valuesEncodedOther := bs.getValuesForColumn(chOther)
bm.forEachSetBit(func(idx int) bool {
n := unmarshalInt64(valuesEncoded[idx])
nOther := unmarshalInt64(valuesEncodedOther[idx])
return leValuesInt64(n, nOther, fe.excludeEqualValues)
})
}
func (fe *filterLeField) applyFilterFloat64(bs *blockSearch, bm *bitmap, ch, chOther *columnHeader) {
valuesEncoded := bs.getValuesForColumn(ch)
valuesEncodedOther := bs.getValuesForColumn(chOther)
bm.forEachSetBit(func(idx int) bool {
f := unmarshalFloat64(valuesEncoded[idx])
fOther := unmarshalFloat64(valuesEncodedOther[idx])
return leValuesFloat64(f, fOther, fe.excludeEqualValues)
})
}
func leValuesString(a, b string, excludeEqualValues bool) bool {
fA := parseMathNumber(a)
if !math.IsNaN(fA) {
fB := parseMathNumber(b)
if !math.IsNaN(fB) {
if excludeEqualValues {
return fA < fB
}
return fA <= fB
}
}
if excludeEqualValues {
return a < b
}
return a <= b
}
func leValuesInt64(a, b int64, excludeEqualValues bool) bool {
if excludeEqualValues {
return a < b
}
return a <= b
}
func leValuesFloat64(a, b float64, excludeEqualValues bool) bool {
if excludeEqualValues {
return a < b
}
return a <= b
}

File diff suppressed because it is too large Load Diff

View File

@@ -34,19 +34,23 @@ func (fo *filterOr) updateNeededFields(neededFields fieldsSet) {
func (fo *filterOr) applyToBlockResult(br *blockResult, bm *bitmap) {
bmResult := getBitmap(bm.bitsLen)
bmTmp := getBitmap(bm.bitsLen)
bmResult.copyFrom(bm)
for _, f := range fo.filters {
bmTmp.copyFrom(bmResult)
f.applyToBlockResult(br, bmTmp)
bmResult.andNot(bmTmp)
if bmResult.isZero() {
putBitmap(bmTmp)
putBitmap(bmResult)
return
// Minimize the number of rows to check by the filter by checking only
// the rows, which may change the output bm:
// - bm matches them, e.g. the caller wants to get them
// - bmResult doesn't match them, e.g. all the previous OR filters didn't match them
bmTmp.copyFrom(bm)
bmTmp.andNot(bmResult)
if bmTmp.isZero() {
// Shortcut - there is no need in applying the remaining filters,
// since the result already matches all the values from the block.
break
}
f.applyToBlockResult(br, bmTmp)
bmResult.or(bmTmp)
}
bm.andNot(bmResult)
putBitmap(bmTmp)
bm.copyFrom(bmResult)
putBitmap(bmResult)
}
@@ -59,19 +63,23 @@ func (fo *filterOr) applyToBlockSearch(bs *blockSearch, bm *bitmap) {
bmResult := getBitmap(bm.bitsLen)
bmTmp := getBitmap(bm.bitsLen)
bmResult.copyFrom(bm)
for _, f := range fo.filters {
bmTmp.copyFrom(bmResult)
f.applyToBlockSearch(bs, bmTmp)
bmResult.andNot(bmTmp)
if bmResult.isZero() {
putBitmap(bmTmp)
putBitmap(bmResult)
return
// Minimize the number of rows to check by the filter by checking only
// the rows, which may change the output bm:
// - bm matches them, e.g. the caller wants to get them
// - bmResult doesn't match them, e.g. all the previous OR filters didn't match them
bmTmp.copyFrom(bm)
bmTmp.andNot(bmResult)
if bmTmp.isZero() {
// Shortcut - there is no need in applying the remaining filters,
// since the result already matches all the values from the block.
break
}
f.applyToBlockSearch(bs, bmTmp)
bmResult.or(bmTmp)
}
bm.andNot(bmResult)
putBitmap(bmTmp)
bm.copyFrom(bmResult)
putBitmap(bmResult)
}

View File

@@ -10,8 +10,7 @@ import (
// tokenizeHashes extracts word tokens from a, hashes them, appends hashes to dst and returns the result.
//
// The returned hashes must be passed to bloomFilterMarshalHashes in order to build bloom filters.
// The returned hashes must be passed to appendHashesHashes before being passed to bloomFilter.containsAll.
// The returned hashes can be used for building bloom filters.
func tokenizeHashes(dst []uint64, a []string) []uint64 {
t := getHashTokenizer()
for i, s := range a {

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