mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-29 23:00:51 +03:00
Compare commits
2 Commits
v1.113.0
...
vmctl-prop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a07f4078de | ||
|
|
de96a937dd |
@@ -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/).
|
||||
File diff suppressed because one or more lines are too long
1
app/vlselect/vmui/assets/index-CEiptoJw.css
Normal file
1
app/vlselect/vmui/assets/index-CEiptoJw.css
Normal file
File diff suppressed because one or more lines are too long
203
app/vlselect/vmui/assets/index-DuTUAk-m.js
Normal file
203
app/vlselect/vmui/assets/index-DuTUAk-m.js
Normal file
File diff suppressed because one or more lines are too long
@@ -35,10 +35,10 @@
|
||||
<meta property="og:title" content="UI for VictoriaLogs">
|
||||
<meta property="og:url" content="https://victoriametrics.com/products/victorialogs/">
|
||||
<meta property="og:description" content="Explore your log data with VictoriaLogs UI">
|
||||
<script type="module" crossorigin src="./assets/index-C68hz-qY.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-DuTUAk-m.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-DojlIpLz.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-B_R5bdPN.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CEiptoJw.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -88,9 +88,6 @@ func (g *Group) Validate(validateTplFn ValidateTplFn, validateExpressions bool)
|
||||
if g.EvalOffset.Duration() > g.Interval.Duration() {
|
||||
return fmt.Errorf("eval_offset should be smaller than interval; now eval_offset: %v, interval: %v", g.EvalOffset.Duration(), g.Interval.Duration())
|
||||
}
|
||||
if g.EvalOffset != nil && g.EvalDelay != nil {
|
||||
return fmt.Errorf("eval_offset cannot be used with eval_delay")
|
||||
}
|
||||
if g.Limit < 0 {
|
||||
return fmt.Errorf("invalid limit %d, shouldn't be less than 0", g.Limit)
|
||||
}
|
||||
|
||||
@@ -27,15 +27,14 @@ import (
|
||||
var (
|
||||
ruleUpdateEntriesLimit = flag.Int("rule.updateEntriesLimit", 20, "Defines the max number of rule's state updates stored in-memory. "+
|
||||
"Rule's updates are available on rule's Details page and are used for debugging purposes. The number of stored updates can be overridden per rule via update_entries_limit param.")
|
||||
resendDelay = flag.Duration("rule.resendDelay", 0, "MiniMum amount of time to wait before resending an alert to notifier.")
|
||||
resendDelay = flag.Duration("rule.resendDelay", 0, "MiniMum amount of time to wait before resending an alert to notifier")
|
||||
maxResolveDuration = flag.Duration("rule.maxResolveDuration", 0, "Limits the maxiMum duration for automatic alert expiration, "+
|
||||
"which by default is 4 times evaluationInterval of the parent group")
|
||||
evalDelay = flag.Duration("rule.evalDelay", 30*time.Second, "Adjustment of the `time` parameter for rule evaluation requests to compensate intentional data delay from the datasource. "+
|
||||
"Normally, should be equal to `-search.latencyOffset` (cmd-line flag configured for VictoriaMetrics single-node or vmselect). "+
|
||||
"This doesn't apply to groups with eval_offset specified.")
|
||||
evalDelay = flag.Duration("rule.evalDelay", 30*time.Second, "Adjustment of the `time` parameter for rule evaluation requests to compensate intentional data delay from the datasource."+
|
||||
"Normally, should be equal to `-search.latencyOffset` (cmd-line flag configured for VictoriaMetrics single-node or vmselect).")
|
||||
disableAlertGroupLabel = flag.Bool("disableAlertgroupLabel", false, "Whether to disable adding group's Name as label to generated alerts and time series.")
|
||||
remoteReadLookBack = flag.Duration("remoteRead.lookback", time.Hour, "Lookback defines how far to look into past for alerts timeseries. "+
|
||||
"For example, if lookback=1h then range from now() to now()-1h will be scanned.")
|
||||
remoteReadLookBack = flag.Duration("remoteRead.lookback", time.Hour, "Lookback defines how far to look into past for alerts timeseries."+
|
||||
" For example, if lookback=1h then range from now() to now()-1h will be scanned.")
|
||||
)
|
||||
|
||||
// Group is an entity for grouping rules
|
||||
@@ -89,10 +88,10 @@ func newGroupMetrics(g *Group) *groupMetrics {
|
||||
m.set = metrics.NewSet()
|
||||
|
||||
labels := fmt.Sprintf(`group=%q, file=%q`, g.Name, g.File)
|
||||
m.iterationTotal = m.set.NewCounter(fmt.Sprintf(`vmalert_iteration_total{%s}`, labels))
|
||||
m.iterationDuration = m.set.NewSummary(fmt.Sprintf(`vmalert_iteration_duration_seconds{%s}`, labels))
|
||||
m.iterationMissed = m.set.NewCounter(fmt.Sprintf(`vmalert_iteration_missed_total{%s}`, labels))
|
||||
m.iterationInterval = m.set.NewGauge(fmt.Sprintf(`vmalert_iteration_interval_seconds{%s}`, labels), func() float64 {
|
||||
m.iterationTotal = m.set.GetOrCreateCounter(fmt.Sprintf(`vmalert_iteration_total{%s}`, labels))
|
||||
m.iterationDuration = m.set.GetOrCreateSummary(fmt.Sprintf(`vmalert_iteration_duration_seconds{%s}`, labels))
|
||||
m.iterationMissed = m.set.GetOrCreateCounter(fmt.Sprintf(`vmalert_iteration_missed_total{%s}`, labels))
|
||||
m.iterationInterval = m.set.GetOrCreateGauge(fmt.Sprintf(`vmalert_iteration_interval_seconds{%s}`, labels), func() float64 {
|
||||
g.mu.RLock()
|
||||
i := g.Interval.Seconds()
|
||||
g.mu.RUnlock()
|
||||
@@ -376,7 +375,6 @@ func (g *Group) Start(ctx context.Context, nts func() []notifier.Notifier, rw re
|
||||
}
|
||||
|
||||
resolveDuration := getResolveDuration(g.Interval, *resendDelay, *maxResolveDuration)
|
||||
// adjust request timestamp using evalDelay and evalAlignment if necessary
|
||||
ts = g.adjustReqTimestamp(ts)
|
||||
errs := e.execConcurrently(ctx, g.Rules, ts, g.Concurrency, resolveDuration, g.Limit)
|
||||
for err := range errs {
|
||||
@@ -470,18 +468,10 @@ func (g *Group) DeepCopy() *Group {
|
||||
return &newG
|
||||
}
|
||||
|
||||
// if offset is specified, delayBeforeStart returns a duration to help aligning timestamp with offset;
|
||||
// otherwise, it returns a random duration between [0..interval] based on group key.
|
||||
// delayBeforeStart returns a duration on the interval between [ts..ts+interval].
|
||||
// delayBeforeStart accounts for `offset`, so returned duration should be always
|
||||
// bigger than the `offset`.
|
||||
func delayBeforeStart(ts time.Time, key uint64, interval time.Duration, offset *time.Duration) time.Duration {
|
||||
if offset != nil {
|
||||
currentOffsetPoint := ts.Truncate(interval).Add(*offset)
|
||||
if currentOffsetPoint.Before(ts) {
|
||||
// wait until the next offset point
|
||||
return currentOffsetPoint.Add(interval).Sub(ts)
|
||||
}
|
||||
return currentOffsetPoint.Sub(ts)
|
||||
}
|
||||
|
||||
var randSleep time.Duration
|
||||
randSleep = time.Duration(float64(interval) * (float64(key) / (1 << 64)))
|
||||
sleepOffset := time.Duration(ts.UnixNano() % interval.Nanoseconds())
|
||||
@@ -489,6 +479,15 @@ func delayBeforeStart(ts time.Time, key uint64, interval time.Duration, offset *
|
||||
randSleep += interval
|
||||
}
|
||||
randSleep -= sleepOffset
|
||||
// check if `ts` after randSleep is before `offset`,
|
||||
// if it is, add extra eval_offset to randSleep.
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3409.
|
||||
if offset != nil {
|
||||
tmpEvalTS := ts.Add(randSleep)
|
||||
if tmpEvalTS.Before(tmpEvalTS.Truncate(interval).Add(*offset)) {
|
||||
randSleep += *offset
|
||||
}
|
||||
}
|
||||
return randSleep
|
||||
}
|
||||
|
||||
@@ -594,14 +593,26 @@ func getResolveDuration(groupInterval, delta, maxDuration time.Duration) time.Du
|
||||
}
|
||||
|
||||
func (g *Group) adjustReqTimestamp(timestamp time.Time) time.Time {
|
||||
// if `eval_offset` is specified, timestamp is already aligned with offset, do nothing
|
||||
if g.EvalOffset != nil {
|
||||
return timestamp
|
||||
// calculate the min timestamp on the evaluationInterval
|
||||
intervalStart := timestamp.Truncate(g.Interval)
|
||||
ts := intervalStart.Add(*g.EvalOffset)
|
||||
if timestamp.Before(ts) {
|
||||
// if passed timestamp is before the expected evaluation offset,
|
||||
// then we should adjust it to the previous evaluation round.
|
||||
// E.g. request with evaluationInterval=1h and evaluationOffset=30m
|
||||
// was evaluated at 11:20. Then the timestamp should be adjusted
|
||||
// to 10:30, to the previous evaluationInterval.
|
||||
return ts.Add(-g.Interval)
|
||||
}
|
||||
// when `eval_offset` is using, ts shouldn't be effect by `eval_alignment` and `eval_delay`
|
||||
// since it should be always aligned.
|
||||
return ts
|
||||
}
|
||||
|
||||
timestamp = timestamp.Add(-g.getEvalDelay())
|
||||
|
||||
// apply the alignment as the last step
|
||||
// always apply the alignment as a last step
|
||||
if g.evalAlignment == nil || *g.evalAlignment {
|
||||
// align query time with interval to get similar result with grafana when plotting time series.
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5049
|
||||
|
||||
@@ -533,14 +533,34 @@ func TestGroupStartDelay(t *testing.T) {
|
||||
f("2023-01-01T00:00:29.000+00:00", "2023-01-01T00:00:30.000+00:00")
|
||||
f("2023-01-01T00:00:31.000+00:00", "2023-01-01T00:05:30.000+00:00")
|
||||
|
||||
// test group with offset
|
||||
offset := 3 * time.Minute
|
||||
// test group with offset smaller than above fixed randSleep,
|
||||
// this way randSleep will always be enough
|
||||
offset := 20 * time.Second
|
||||
g.EvalOffset = &offset
|
||||
|
||||
f("2023-01-01T00:00:15.000+00:00", "2023-01-01T00:03:00.000+00:00")
|
||||
f("2023-01-01T00:01:00.000+00:00", "2023-01-01T00:03:00.000+00:00")
|
||||
f("2023-01-01T00:03:30.000+00:00", "2023-01-01T00:08:00.000+00:00")
|
||||
f("2023-01-01T00:08:00.000+00:00", "2023-01-01T00:08:00.000+00:00")
|
||||
f("2023-01-01T00:00:00.000+00:00", "2023-01-01T00:00:30.000+00:00")
|
||||
f("2023-01-01T00:00:29.000+00:00", "2023-01-01T00:00:30.000+00:00")
|
||||
f("2023-01-01T00:00:31.000+00:00", "2023-01-01T00:05:30.000+00:00")
|
||||
|
||||
// test group with offset bigger than above fixed randSleep,
|
||||
// this way offset will be added to delay
|
||||
offset = 3 * time.Minute
|
||||
g.EvalOffset = &offset
|
||||
|
||||
f("2023-01-01T00:00:00.000+00:00", "2023-01-01T00:03:30.000+00:00")
|
||||
f("2023-01-01T00:00:29.000+00:00", "2023-01-01T00:03:30.000+00:00")
|
||||
f("2023-01-01T00:01:00.000+00:00", "2023-01-01T00:08:30.000+00:00")
|
||||
f("2023-01-01T00:03:30.000+00:00", "2023-01-01T00:08:30.000+00:00")
|
||||
f("2023-01-01T00:07:30.000+00:00", "2023-01-01T00:13:30.000+00:00")
|
||||
|
||||
offset = 10 * time.Minute
|
||||
g.EvalOffset = &offset
|
||||
// interval of 1h and key generate a static delay of 6m
|
||||
g.Interval = time.Hour
|
||||
|
||||
f("2023-01-01T00:00:00.000+00:00", "2023-01-01T00:16:00.000+00:00")
|
||||
f("2023-01-01T00:05:00.000+00:00", "2023-01-01T00:16:00.000+00:00")
|
||||
f("2023-01-01T00:30:00.000+00:00", "2023-01-01T01:16:00.000+00:00")
|
||||
}
|
||||
|
||||
func TestGetPrometheusReqTimestamp(t *testing.T) {
|
||||
@@ -570,11 +590,17 @@ func TestGetPrometheusReqTimestamp(t *testing.T) {
|
||||
evalAlignment: &disableAlign,
|
||||
}, "2023-08-28T11:11:00+00:00", "2023-08-28T11:10:30+00:00")
|
||||
|
||||
// with eval_offset
|
||||
// with eval_offset, find previous offset point + default evalDelay
|
||||
f(&Group{
|
||||
EvalOffset: &offset,
|
||||
Interval: time.Hour,
|
||||
}, "2023-08-28T11:30:00+00:00", "2023-08-28T11:30:00+00:00")
|
||||
}, "2023-08-28T11:11:00+00:00", "2023-08-28T10:30:00+00:00")
|
||||
|
||||
// with eval_offset + default evalDelay
|
||||
f(&Group{
|
||||
EvalOffset: &offset,
|
||||
Interval: time.Hour,
|
||||
}, "2023-08-28T11:41:00+00:00", "2023-08-28T11:30:00+00:00")
|
||||
|
||||
// 1h interval with eval_delay
|
||||
f(&Group{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -37,7 +38,7 @@ func newInfluxProcessor(ic *influx.Client, im *vm.Importer, cc int, separator st
|
||||
}
|
||||
}
|
||||
|
||||
func (ip *influxProcessor) run() error {
|
||||
func (ip *influxProcessor) run(ctx context.Context) error {
|
||||
series, err := ip.ic.Explore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("explore query failed: %s", err)
|
||||
@@ -67,7 +68,7 @@ func (ip *influxProcessor) run() error {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for s := range seriesCh {
|
||||
if err := ip.do(s); err != nil {
|
||||
if err := ip.do(ctx, s); err != nil {
|
||||
errCh <- fmt.Errorf("request failed for %q.%q: %s", s.Measurement, s.Field, err)
|
||||
return
|
||||
}
|
||||
@@ -110,7 +111,7 @@ const dbLabel = "db"
|
||||
const nameLabel = "__name__"
|
||||
const valueField = "value"
|
||||
|
||||
func (ip *influxProcessor) do(s *influx.Series) error {
|
||||
func (ip *influxProcessor) do(ctx context.Context, s *influx.Series) error {
|
||||
cr, err := ip.ic.FetchDataPoints(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch datapoints: %s", err)
|
||||
@@ -163,7 +164,7 @@ func (ip *influxProcessor) do(s *influx.Series) error {
|
||||
Timestamps: time,
|
||||
Values: values,
|
||||
}
|
||||
if err := ip.im.Input(&ts); err != nil {
|
||||
if err := ip.im.Input(ctx, &ts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func main() {
|
||||
}
|
||||
|
||||
otsdbProcessor := newOtsdbProcessor(otsdbClient, importer, c.Int(otsdbConcurrency), c.Bool(globalVerbose))
|
||||
return otsdbProcessor.run()
|
||||
return otsdbProcessor.run(ctx)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -158,7 +158,7 @@ func main() {
|
||||
c.Bool(influxSkipDatabaseLabel),
|
||||
c.Bool(influxPrometheusMode),
|
||||
c.Bool(globalVerbose))
|
||||
return processor.run()
|
||||
return processor.run(ctx)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -261,7 +261,7 @@ func main() {
|
||||
cc: c.Int(promConcurrency),
|
||||
isVerbose: c.Bool(globalVerbose),
|
||||
}
|
||||
return pp.run()
|
||||
return pp.run(ctx)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/opentsdb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
)
|
||||
|
||||
type otsdbProcessor struct {
|
||||
@@ -37,7 +39,7 @@ func newOtsdbProcessor(oc *opentsdb.Client, im *vm.Importer, otsdbcc int, verbos
|
||||
}
|
||||
}
|
||||
|
||||
func (op *otsdbProcessor) run() error {
|
||||
func (op *otsdbProcessor) run(ctx context.Context) error {
|
||||
log.Println("Loading all metrics from OpenTSDB for filters: ", op.oc.Filters)
|
||||
var metrics []string
|
||||
for _, filter := range op.oc.Filters {
|
||||
@@ -93,7 +95,7 @@ func (op *otsdbProcessor) run() error {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for s := range seriesCh {
|
||||
if err := op.do(s); err != nil {
|
||||
if err := op.do(ctx, s); err != nil {
|
||||
errCh <- fmt.Errorf("couldn't retrieve series for %s : %s", metric, err)
|
||||
return
|
||||
}
|
||||
@@ -148,7 +150,7 @@ func (op *otsdbProcessor) run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (op *otsdbProcessor) do(s queryObj) error {
|
||||
func (op *otsdbProcessor) do(ctx context.Context, s queryObj) error {
|
||||
start := s.StartTime - s.Tr.Start
|
||||
end := s.StartTime - s.Tr.End
|
||||
data, err := op.oc.GetData(s.Series, s.Rt, start, end, op.oc.MsecsTime)
|
||||
@@ -168,5 +170,5 @@ func (op *otsdbProcessor) do(s queryObj) error {
|
||||
Timestamps: data.Timestamps,
|
||||
Values: data.Values,
|
||||
}
|
||||
return op.im.Input(&ts)
|
||||
return op.im.Input(ctx, &ts)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
@@ -30,7 +31,7 @@ type prometheusProcessor struct {
|
||||
isVerbose bool
|
||||
}
|
||||
|
||||
func (pp *prometheusProcessor) run() error {
|
||||
func (pp *prometheusProcessor) run(ctx context.Context) error {
|
||||
blocks, err := pp.cl.Explore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("explore failed: %s", err)
|
||||
@@ -59,7 +60,7 @@ func (pp *prometheusProcessor) run() error {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for br := range blockReadersCh {
|
||||
if err := pp.do(br); err != nil {
|
||||
if err := pp.do(ctx, br); err != nil {
|
||||
errCh <- fmt.Errorf("read failed for block %q: %s", br.Meta().ULID, err)
|
||||
return
|
||||
}
|
||||
@@ -100,7 +101,7 @@ func (pp *prometheusProcessor) run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pp *prometheusProcessor) do(b tsdb.BlockReader) error {
|
||||
func (pp *prometheusProcessor) do(ctx context.Context, b tsdb.BlockReader) error {
|
||||
ss, err := pp.cl.Read(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read block: %s", err)
|
||||
@@ -150,7 +151,7 @@ func (pp *prometheusProcessor) do(b tsdb.BlockReader) error {
|
||||
Timestamps: timestamps,
|
||||
Values: values,
|
||||
}
|
||||
if err := pp.im.Input(&ts); err != nil {
|
||||
if err := pp.im.Input(ctx, &ts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ func TestPrometheusProcessorRun(t *testing.T) {
|
||||
go tt.fields.closer(importer)
|
||||
}
|
||||
|
||||
if err := pp.run(); (err != nil) != tt.wantErr {
|
||||
if err := pp.run(context.Background()); (err != nil) != tt.wantErr {
|
||||
t.Fatalf("run() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -112,7 +112,7 @@ func (rrp *remoteReadProcessor) run(ctx context.Context) error {
|
||||
|
||||
func (rrp *remoteReadProcessor) do(ctx context.Context, filter *remoteread.Filter) error {
|
||||
return rrp.src.Read(ctx, filter, func(series *vm.TimeSeries) error {
|
||||
if err := rrp.dst.Input(series); err != nil {
|
||||
if err := rrp.dst.Input(ctx, series); err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to read data for time range start: %d, end: %d, %s",
|
||||
filter.StartTimestampMs, filter.EndTimestampMs, err)
|
||||
|
||||
@@ -69,7 +69,6 @@ type Importer struct {
|
||||
user string
|
||||
password string
|
||||
|
||||
close chan struct{}
|
||||
input chan *TimeSeries
|
||||
errors chan *ImportError
|
||||
|
||||
@@ -143,7 +142,6 @@ func NewImporter(ctx context.Context, cfg Config) (*Importer, error) {
|
||||
user: cfg.User,
|
||||
password: cfg.Password,
|
||||
rl: limiter.NewLimiter(cfg.RateLimit),
|
||||
close: make(chan struct{}),
|
||||
input: make(chan *TimeSeries, cfg.Concurrency*4),
|
||||
errors: make(chan *ImportError, cfg.Concurrency),
|
||||
backoff: cfg.Backoff,
|
||||
@@ -189,10 +187,10 @@ func (im *Importer) Errors() chan *ImportError { return im.errors }
|
||||
|
||||
// Input returns a channel for sending timeseries
|
||||
// that need to be imported
|
||||
func (im *Importer) Input(ts *TimeSeries) error {
|
||||
func (im *Importer) Input(ctx context.Context, ts *TimeSeries) error {
|
||||
select {
|
||||
case <-im.close:
|
||||
return fmt.Errorf("importer is closed")
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case im.input <- ts:
|
||||
return nil
|
||||
case err := <-im.errors:
|
||||
@@ -207,7 +205,6 @@ func (im *Importer) Input(ts *TimeSeries) error {
|
||||
// and waits until they are finished
|
||||
func (im *Importer) Close() {
|
||||
im.once.Do(func() {
|
||||
close(im.close)
|
||||
close(im.input)
|
||||
im.wg.Wait()
|
||||
close(im.errors)
|
||||
@@ -220,24 +217,34 @@ func (im *Importer) startWorker(ctx context.Context, bar barpool.Bar, batchSize,
|
||||
var waitForBatch time.Time
|
||||
for {
|
||||
select {
|
||||
case <-im.close:
|
||||
case <-ctx.Done():
|
||||
for ts := range im.input {
|
||||
ts = roundTimeseriesValue(ts, significantFigures, roundDigits)
|
||||
batch = append(batch, ts)
|
||||
exitErr := &ImportError{
|
||||
Batch: batch,
|
||||
}
|
||||
retryableFunc := func() error { return im.Import(batch) }
|
||||
_, err := im.backoff.Retry(ctx, retryableFunc)
|
||||
if err != nil {
|
||||
exitErr.Err = err
|
||||
}
|
||||
im.errors <- exitErr
|
||||
}
|
||||
exitErr := &ImportError{
|
||||
Batch: batch,
|
||||
}
|
||||
retryableFunc := func() error { return im.Import(batch) }
|
||||
_, err := im.backoff.Retry(ctx, retryableFunc)
|
||||
if err != nil {
|
||||
exitErr.Err = err
|
||||
}
|
||||
im.errors <- exitErr
|
||||
return
|
||||
case ts, ok := <-im.input:
|
||||
if !ok {
|
||||
continue
|
||||
// drain all batches before exit
|
||||
exitErr := &ImportError{
|
||||
Batch: batch,
|
||||
}
|
||||
retryableFunc := func() error { return im.Import(batch) }
|
||||
_, err := im.backoff.Retry(ctx, retryableFunc)
|
||||
if err != nil {
|
||||
exitErr.Err = err
|
||||
}
|
||||
im.errors <- exitErr
|
||||
return
|
||||
}
|
||||
// init waitForBatch when first
|
||||
// value was received
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/prometheus"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/stats"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
@@ -30,10 +29,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
deleteAuthKey = flagutil.NewPassword("deleteAuthKey", "authKey for metrics' deletion via /api/v1/admin/tsdb/delete_series and /tags/delSeries. It could be passed via authKey query arg. It overrides -httpAuth.*")
|
||||
metricNamesStatsResetAuthKey = flagutil.NewPassword("metricNamesStatsResetAuthKey", "authKey for reseting metric names usage cache via /api/v1/admin/status/metric_names_stats/reset. It overrides -httpAuth.*. "+
|
||||
"See https://docs.victoriametrics.com/#track-ingested-metrics-usage")
|
||||
|
||||
deleteAuthKey = flagutil.NewPassword("deleteAuthKey", "authKey for metrics' deletion via /api/v1/admin/tsdb/delete_series and /tags/delSeries. It could be passed via authKey query arg. It overrides -httpAuth.*")
|
||||
maxConcurrentRequests = flag.Int("search.maxConcurrentRequests", getDefaultMaxConcurrentRequests(), "The maximum number of concurrent search requests. "+
|
||||
"It shouldn't be high, since a single request can saturate all the CPU cores, while many concurrently executed requests may require high amounts of memory. "+
|
||||
"See also -search.maxQueueDuration and -search.maxMemoryPerQuery")
|
||||
@@ -182,6 +178,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
promql.ResetRollupResultCache()
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "/api/v1/label/") {
|
||||
s := path[len("/api/v1/label/"):]
|
||||
if strings.HasSuffix(s, "/values") {
|
||||
@@ -402,26 +399,6 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return true
|
||||
case "/api/v1/status/metric_names_stats":
|
||||
metricNamesStatsRequests.Inc()
|
||||
if err := stats.MetricNamesStatsHandler(qt, w, r); err != nil {
|
||||
metricNamesStatsErrors.Inc()
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
case "/api/v1/admin/status/metric_names_stats/reset":
|
||||
metricNamesStatsResetRequests.Inc()
|
||||
if !httpserver.CheckAuthFlag(w, r, metricNamesStatsResetAuthKey) {
|
||||
return true
|
||||
}
|
||||
if err := stats.ResetMetricNamesStatsHandler(qt); err != nil {
|
||||
metricNamesStatsResetErrors.Inc()
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -697,12 +674,6 @@ var (
|
||||
metadataRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/metadata"}`)
|
||||
buildInfoRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/buildinfo"}`)
|
||||
queryExemplarsRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/query_exemplars"}`)
|
||||
|
||||
metricNamesStatsRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/status/metric_names_stats"}`)
|
||||
metricNamesStatsErrors = metrics.NewCounter(`vm_http_request_errors_total{path="/api/v1/status/metric_names_stats"}`)
|
||||
|
||||
metricNamesStatsResetRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/admin/status/metric_names_stats/reset"}`)
|
||||
metricNamesStatsResetErrors = metrics.NewCounter(`vm_http_request_errors_total{path="/api/v1/admin/status/metric_names_stats/reset"}`)
|
||||
)
|
||||
|
||||
func proxyVMAlertRequests(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -1367,18 +1367,3 @@ func applyGraphiteRegexpFilter(filter string, ss []string) ([]string, error) {
|
||||
//
|
||||
// See https://github.com/golang/go/blob/704401ffa06c60e059c9e6e4048045b4ff42530a/src/runtime/malloc.go#L11
|
||||
const maxFastAllocBlockSize = 32 * 1024
|
||||
|
||||
// GetMetricNamesStats returns statistic for timeseries metric names usage.
|
||||
func GetMetricNamesStats(qt *querytracer.Tracer, limit, le int, matchPattern string) (storage.MetricNamesStatsResponse, error) {
|
||||
qt = qt.NewChild("get metric names usage statistics with limit: %d, less or equal to: %d, match pattern=%q", limit, le, matchPattern)
|
||||
defer qt.Done()
|
||||
return vmstorage.GetMetricNamesStats(qt, limit, le, matchPattern)
|
||||
}
|
||||
|
||||
// ResetMetricNamesStats resets state of metric names usage
|
||||
func ResetMetricNamesStats(qt *querytracer.Tracer) error {
|
||||
qt = qt.NewChild("reset metric names usage stats")
|
||||
defer qt.Done()
|
||||
vmstorage.ResetMetricNamesStats(qt)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,9 +12,6 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
@@ -28,6 +25,8 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/stringsutil"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -815,19 +814,7 @@ func evalRollupFunc(qt *querytracer.Tracer, ec *EvalConfig, funcName string, rf
|
||||
Err: fmt.Errorf("`@` modifier must return a single series; it returns %d series instead", len(tssAt)),
|
||||
}
|
||||
}
|
||||
atValue := math.NaN()
|
||||
for _, v := range tssAt[0].Values {
|
||||
if !math.IsNaN(v) {
|
||||
atValue = v
|
||||
break
|
||||
}
|
||||
}
|
||||
if math.IsNaN(atValue) {
|
||||
return nil, &httpserver.UserReadableError{
|
||||
Err: fmt.Errorf("`@` modifier must return a non-NaN value"),
|
||||
}
|
||||
}
|
||||
atTimestamp := int64(atValue * 1000)
|
||||
atTimestamp := int64(tssAt[0].Values[0] * 1000)
|
||||
ecNew := copyEvalConfig(ec)
|
||||
ecNew.Start = atTimestamp
|
||||
ecNew.End = atTimestamp
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
{% import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
) %}
|
||||
|
||||
{% stripspace %}
|
||||
MetricNamesStatsResponse generates response for /api/v1/status/metric_names_stats .
|
||||
{% func MetricNamesStatsResponse(stats *storage.MetricNamesStatsResponse, qt *querytracer.Tracer) %}
|
||||
{
|
||||
"status":"success",
|
||||
"statsCollectedSince": {%dul= stats.CollectedSinceTs %},
|
||||
"statsCollectedRecordsTotal": {%dul= stats.TotalRecords %},
|
||||
"trackerMemoryMaxSizeBytes": {%dul= stats.MaxSizeBytes %},
|
||||
"trackerCurrentMemoryUsageBytes": {%dul= stats.CurrentSizeBytes %},
|
||||
"records":
|
||||
[
|
||||
{% for i, r := range stats.Records %}
|
||||
{
|
||||
"metricName":{%q= r.MetricName %},
|
||||
"queryRequestsCount":{%dul= r.RequestsCount %},
|
||||
"lastQueryRequestTimestamp":{%dul= r.LastRequestTs %}
|
||||
}
|
||||
{% if i+1 < len(stats.Records) %},{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
{% code qt.Done() %}
|
||||
{% code traceJSON := qt.ToJSON() %}
|
||||
{% if traceJSON != "" %},"trace":{%s= traceJSON %}{% endif %}
|
||||
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% endstripspace %}
|
||||
@@ -1,117 +0,0 @@
|
||||
// Code generated by qtc from "metric_names_usage_response.qtpl". DO NOT EDIT.
|
||||
// See https://github.com/valyala/quicktemplate for details.
|
||||
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:1
|
||||
package stats
|
||||
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:1
|
||||
import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
)
|
||||
|
||||
// MetricNamesStatsResponse generates response for /api/v1/status/metric_names_stats .
|
||||
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:8
|
||||
import (
|
||||
qtio422016 "io"
|
||||
|
||||
qt422016 "github.com/valyala/quicktemplate"
|
||||
)
|
||||
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:8
|
||||
var (
|
||||
_ = qtio422016.Copy
|
||||
_ = qt422016.AcquireByteBuffer
|
||||
)
|
||||
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:8
|
||||
func StreamMetricNamesStatsResponse(qw422016 *qt422016.Writer, stats *storage.MetricNamesStatsResponse, qt *querytracer.Tracer) {
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:8
|
||||
qw422016.N().S(`{"status":"success","statsCollectedSince":`)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:11
|
||||
qw422016.N().DUL(stats.CollectedSinceTs)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:11
|
||||
qw422016.N().S(`,"statsCollectedRecordsTotal":`)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:12
|
||||
qw422016.N().DUL(stats.TotalRecords)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:12
|
||||
qw422016.N().S(`,"trackerMemoryMaxSizeBytes":`)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:13
|
||||
qw422016.N().DUL(stats.MaxSizeBytes)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:13
|
||||
qw422016.N().S(`,"trackerCurrentMemoryUsageBytes":`)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:14
|
||||
qw422016.N().DUL(stats.CurrentSizeBytes)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:14
|
||||
qw422016.N().S(`,"records":[`)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:17
|
||||
for i, r := range stats.Records {
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:17
|
||||
qw422016.N().S(`{"metricName":`)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:19
|
||||
qw422016.N().Q(r.MetricName)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:19
|
||||
qw422016.N().S(`,"queryRequestsCount":`)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:20
|
||||
qw422016.N().DUL(r.RequestsCount)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:20
|
||||
qw422016.N().S(`,"lastQueryRequestTimestamp":`)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:21
|
||||
qw422016.N().DUL(r.LastRequestTs)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:21
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:23
|
||||
if i+1 < len(stats.Records) {
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:23
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:23
|
||||
}
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:24
|
||||
}
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:24
|
||||
qw422016.N().S(`]`)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:26
|
||||
qt.Done()
|
||||
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:27
|
||||
traceJSON := qt.ToJSON()
|
||||
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:28
|
||||
if traceJSON != "" {
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:28
|
||||
qw422016.N().S(`,"trace":`)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:28
|
||||
qw422016.N().S(traceJSON)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:28
|
||||
}
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:28
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:31
|
||||
}
|
||||
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:31
|
||||
func WriteMetricNamesStatsResponse(qq422016 qtio422016.Writer, stats *storage.MetricNamesStatsResponse, qt *querytracer.Tracer) {
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:31
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:31
|
||||
StreamMetricNamesStatsResponse(qw422016, stats, qt)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:31
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:31
|
||||
}
|
||||
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:31
|
||||
func MetricNamesStatsResponse(stats *storage.MetricNamesStatsResponse, qt *querytracer.Tracer) string {
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:31
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:31
|
||||
WriteMetricNamesStatsResponse(qb422016, stats, qt)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:31
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:31
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:31
|
||||
return qs422016
|
||||
//line app/vmselect/stats/metric_names_usage_response.qtpl:31
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
)
|
||||
|
||||
// MetricNamesStatsHandler returns timeseries metric names usage statistics
|
||||
func MetricNamesStatsHandler(qt *querytracer.Tracer, w http.ResponseWriter, r *http.Request) error {
|
||||
limit := 1000
|
||||
limitStr := r.FormValue("limit")
|
||||
if len(limitStr) > 0 {
|
||||
n, err := strconv.Atoi(limitStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse `limit` arg %q: %w", limitStr, err)
|
||||
}
|
||||
if n > 0 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
// by default display all values
|
||||
le := -1
|
||||
leStr := r.FormValue("le")
|
||||
if len(leStr) > 0 {
|
||||
n, err := strconv.Atoi(leStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse `le` arg %q: %w", leStr, err)
|
||||
}
|
||||
le = n
|
||||
}
|
||||
matchPattern := r.FormValue("match_pattern")
|
||||
stats, err := netstorage.GetMetricNamesStats(qt, limit, le, matchPattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
WriteMetricNamesStatsResponse(w, &stats, qt)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetMetricNamesStatsHandler resets metric names usage state
|
||||
func ResetMetricNamesStatsHandler(qt *querytracer.Tracer) error {
|
||||
if err := netstorage.ResetMetricNamesStats(qt); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -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/).
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -36,10 +36,10 @@
|
||||
<meta property="og:title" content="UI for VictoriaMetrics">
|
||||
<meta property="og:url" content="https://victoriametrics.com/">
|
||||
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<script type="module" crossorigin src="./assets/index-C4jrb8hY.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-DzehQsnZ.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-DojlIpLz.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-B_R5bdPN.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-Cqbobgy7.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -76,11 +76,6 @@ var (
|
||||
"This may improve performance and decrease disk space usage for the use cases with fixed set of timeseries scattered across a "+
|
||||
"big time range (for example, when loading years of historical data). "+
|
||||
"See https://docs.victoriametrics.com/single-server-victoriametrics/#index-tuning")
|
||||
trackMetricNamesStats = flag.Bool("storage.trackMetricNamesStats", false, "Whether to track ingest and query requests for timeseries metric names. "+
|
||||
"This feature allows to track metric names unused at query requests. "+
|
||||
"See https://docs.victoriametrics.com/#track-ingested-metrics-usage")
|
||||
cacheSizeMetricNamesStats = flagutil.NewBytes("storage.cacheSizeMetricNamesStats", 0, "Overrides max size for storage/metricNamesStatsTracker cache. "+
|
||||
"See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning")
|
||||
)
|
||||
|
||||
// CheckTimeRange returns true if the given tr is denied for querying.
|
||||
@@ -110,7 +105,6 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
|
||||
storage.SetFreeDiskSpaceLimit(minFreeDiskSpaceBytes.N)
|
||||
storage.SetTSIDCacheSize(cacheSizeStorageTSID.IntN())
|
||||
storage.SetTagFiltersCacheSize(cacheSizeIndexDBTagFilters.IntN())
|
||||
storage.SetMetricNamesStatsCacheSize(cacheSizeMetricNamesStats.IntN())
|
||||
mergeset.SetIndexBlocksCacheSize(cacheSizeIndexDBIndexBlocks.IntN())
|
||||
mergeset.SetDataBlocksCacheSize(cacheSizeIndexDBDataBlocks.IntN())
|
||||
mergeset.SetDataBlocksSparseCacheSize(cacheSizeIndexDBDataBlocksSparse.IntN())
|
||||
@@ -121,12 +115,12 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
|
||||
logger.Infof("opening storage at %q with -retentionPeriod=%s", *DataPath, retentionPeriod)
|
||||
startTime := time.Now()
|
||||
WG = syncwg.WaitGroup{}
|
||||
|
||||
opts := storage.OpenOptions{
|
||||
Retention: retentionPeriod.Duration(),
|
||||
MaxHourlySeries: *maxHourlySeries,
|
||||
MaxDailySeries: *maxDailySeries,
|
||||
DisablePerDayIndex: *disablePerDayIndex,
|
||||
TrackMetricNamesStats: *trackMetricNamesStats,
|
||||
Retention: retentionPeriod.Duration(),
|
||||
MaxHourlySeries: *maxHourlySeries,
|
||||
MaxDailySeries: *maxDailySeries,
|
||||
DisablePerDayIndex: *disablePerDayIndex,
|
||||
}
|
||||
strg := storage.MustOpenStorage(*DataPath, opts)
|
||||
Storage = strg
|
||||
@@ -199,21 +193,6 @@ func DeleteSeries(qt *querytracer.Tracer, tfss []*storage.TagFilters, maxMetrics
|
||||
return n, err
|
||||
}
|
||||
|
||||
// GetMetricNamesStats returns metric names usage stats with give limit and lte predicate
|
||||
func GetMetricNamesStats(qt *querytracer.Tracer, limit, le int, matchPattern string) (storage.MetricNamesStatsResponse, error) {
|
||||
WG.Add(1)
|
||||
r := Storage.GetMetricNamesStats(qt, limit, le, matchPattern)
|
||||
WG.Done()
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// ResetMetricNamesStats resets state for metric names usage tracker
|
||||
func ResetMetricNamesStats(qt *querytracer.Tracer) {
|
||||
WG.Add(1)
|
||||
Storage.ResetMetricNamesStats(qt)
|
||||
WG.Done()
|
||||
}
|
||||
|
||||
// SearchMetricNames returns metric names for the given tfss on the given tr.
|
||||
func SearchMetricNames(qt *querytracer.Tracer, tfss []*storage.TagFilters, tr storage.TimeRange, maxMetrics int, deadline uint64) ([]string, error) {
|
||||
WG.Add(1)
|
||||
@@ -678,12 +657,6 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
|
||||
|
||||
metrics.WriteGaugeUint64(w, `vm_next_retention_seconds`, m.NextRetentionSeconds)
|
||||
|
||||
if *trackMetricNamesStats {
|
||||
metrics.WriteCounterUint64(w, `vm_cache_size_bytes{type="storage/metricNamesStatsTracker"}`, m.MetricNamesUsageTrackerSizeBytes)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_size{type="storage/metricNamesStatsTracker"}`, m.MetricNamesUsageTrackerSize)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_size_max_bytes{type="storage/metricNamesStatsTracker"}`, m.MetricNamesUsageTrackerSizeMaxBytes)
|
||||
}
|
||||
|
||||
metrics.WriteGaugeUint64(w, `vm_downsampling_partitions_scheduled`, tm.ScheduledDownsamplingPartitions)
|
||||
metrics.WriteGaugeUint64(w, `vm_downsampling_partitions_scheduled_size_bytes`, tm.ScheduledDownsamplingPartitionsSize)
|
||||
}
|
||||
|
||||
@@ -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/).
|
||||
|
||||
@@ -258,15 +258,3 @@ func (t *Trace) Contains(s string) int {
|
||||
}
|
||||
return times
|
||||
}
|
||||
|
||||
// MetricNamesStatsResponse is an inmemory representation of the
|
||||
// /api/v1/status/metric_names_stats API response
|
||||
type MetricNamesStatsResponse struct {
|
||||
Records []MetricNamesStatsRecord
|
||||
}
|
||||
|
||||
// MetricNamesStatsRecord is a record item for MetricNamesStatsResponse
|
||||
type MetricNamesStatsRecord struct {
|
||||
MetricName string
|
||||
QueryRequestsCount uint64
|
||||
}
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
)
|
||||
|
||||
func TestSingleMetricNamesStats(t *testing.T) {
|
||||
os.RemoveAll(t.Name())
|
||||
tc := at.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
sut := tc.MustStartVmsingle("vmsingle", []string{"-storage.trackMetricNamesStats=true", "-retentionPeriod=100y"})
|
||||
|
||||
const ingestDateTime = `2024-02-05T08:57:36.700Z`
|
||||
const ingestTimestamp = ` 1707123456700`
|
||||
dataSet := []string{
|
||||
`metric_name_1{label="foo"} 10`,
|
||||
`metric_name_1{label="bar"} 10`,
|
||||
`metric_name_2{label="baz"} 20`,
|
||||
`metric_name_1{label="baz"} 10`,
|
||||
`metric_name_3{label="baz"} 30`,
|
||||
}
|
||||
for idx := range dataSet {
|
||||
dataSet[idx] += ingestTimestamp
|
||||
}
|
||||
|
||||
sut.PrometheusAPIV1ImportPrometheus(t, dataSet, at.QueryOpts{})
|
||||
sut.ForceFlush(t)
|
||||
|
||||
// verify ingest request correctly registered
|
||||
expected := apptest.MetricNamesStatsResponse{
|
||||
Records: []at.MetricNamesStatsRecord{
|
||||
{MetricName: "metric_name_1"},
|
||||
{MetricName: "metric_name_2"},
|
||||
{MetricName: "metric_name_3"},
|
||||
},
|
||||
}
|
||||
got := sut.APIV1StatusMetricNamesStats(t, "", "", "", at.QueryOpts{})
|
||||
if diff := cmp.Diff(expected, got); diff != "" {
|
||||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// verify query request correctly registered
|
||||
sut.PrometheusAPIV1Query(t, `{__name__!=""}`, at.QueryOpts{Time: ingestDateTime})
|
||||
expected = apptest.MetricNamesStatsResponse{
|
||||
Records: []at.MetricNamesStatsRecord{
|
||||
{MetricName: "metric_name_1", QueryRequestsCount: 3},
|
||||
{MetricName: "metric_name_2", QueryRequestsCount: 1},
|
||||
{MetricName: "metric_name_3", QueryRequestsCount: 1},
|
||||
},
|
||||
}
|
||||
got = sut.APIV1StatusMetricNamesStats(t, "", "", "", at.QueryOpts{})
|
||||
if diff := cmp.Diff(expected, got); diff != "" {
|
||||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// perform query request for single metric and check counter increase
|
||||
sut.PrometheusAPIV1Query(t, `metric_name_2`, at.QueryOpts{Time: ingestDateTime})
|
||||
expected = apptest.MetricNamesStatsResponse{
|
||||
Records: []at.MetricNamesStatsRecord{
|
||||
{MetricName: "metric_name_1", QueryRequestsCount: 3},
|
||||
{MetricName: "metric_name_2", QueryRequestsCount: 2},
|
||||
{MetricName: "metric_name_3", QueryRequestsCount: 1},
|
||||
},
|
||||
}
|
||||
got = sut.APIV1StatusMetricNamesStats(t, "", "", "", at.QueryOpts{})
|
||||
if diff := cmp.Diff(expected, got); diff != "" {
|
||||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// verify le filter
|
||||
expected = apptest.MetricNamesStatsResponse{
|
||||
Records: []at.MetricNamesStatsRecord{
|
||||
{MetricName: "metric_name_2", QueryRequestsCount: 2},
|
||||
{MetricName: "metric_name_3", QueryRequestsCount: 1},
|
||||
},
|
||||
}
|
||||
got = sut.APIV1StatusMetricNamesStats(t, "", "2", "", at.QueryOpts{})
|
||||
if diff := cmp.Diff(expected, got); diff != "" {
|
||||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// reset state and check empty request response
|
||||
sut.APIV1AdminStatusMetricNamesStatsReset(t, at.QueryOpts{})
|
||||
expected = apptest.MetricNamesStatsResponse{
|
||||
Records: []at.MetricNamesStatsRecord{},
|
||||
}
|
||||
got = sut.APIV1StatusMetricNamesStats(t, "", "", "", at.QueryOpts{})
|
||||
if diff := cmp.Diff(expected, got); diff != "" {
|
||||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestClusterMetricNamesStats(t *testing.T) {
|
||||
|
||||
os.RemoveAll(t.Name())
|
||||
|
||||
tc := apptest.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
vmstorage1 := tc.MustStartVmstorage("vmstorage-1", []string{
|
||||
"-storageDataPath=" + tc.Dir() + "/vmstorage-1",
|
||||
"-retentionPeriod=100y",
|
||||
"-storage.trackMetricNamesStats",
|
||||
})
|
||||
vmstorage2 := tc.MustStartVmstorage("vmstorage-2", []string{
|
||||
"-storageDataPath=" + tc.Dir() + "/vmstorage-2",
|
||||
"-retentionPeriod=100y",
|
||||
"-storage.trackMetricNamesStats",
|
||||
})
|
||||
|
||||
vminsert := tc.MustStartVminsert("vminsert", []string{
|
||||
fmt.Sprintf("-storageNode=%s,%s", vmstorage1.VminsertAddr(), vmstorage2.VminsertAddr()),
|
||||
})
|
||||
vmselect := tc.MustStartVmselect("vmselect", []string{
|
||||
fmt.Sprintf("-storageNode=%s,%s", vmstorage1.VmselectAddr(), vmstorage2.VmselectAddr()),
|
||||
})
|
||||
// verify empty stats
|
||||
resp := vmselect.MetricNamesStats(t, "", "", "", apptest.QueryOpts{Tenant: "0:0"})
|
||||
if len(resp.Records) != 0 {
|
||||
t.Fatalf("unexpected resp Records: %d, want: %d", len(resp.Records), 0)
|
||||
}
|
||||
|
||||
const ingestDateTime = `2024-02-05T08:57:36.700Z`
|
||||
const ingestTimestamp = ` 1707123456700`
|
||||
dataSet := []string{
|
||||
`metric_name_1{label="foo"} 10`,
|
||||
`metric_name_1{label="bar"} 10`,
|
||||
`metric_name_2{label="baz"} 20`,
|
||||
`metric_name_1{label="baz"} 10`,
|
||||
`metric_name_3{label="baz"} 30`,
|
||||
}
|
||||
for idx := range dataSet {
|
||||
dataSet[idx] += ingestTimestamp
|
||||
}
|
||||
|
||||
// ingest per tenant data and verify it with search
|
||||
tenantIDs := []string{"1:1", "1:15", "15:15"}
|
||||
for _, tenantID := range tenantIDs {
|
||||
vminsert.PrometheusAPIV1ImportPrometheus(t, dataSet, apptest.QueryOpts{Tenant: tenantID})
|
||||
vmstorage1.ForceFlush(t)
|
||||
vmstorage2.ForceFlush(t)
|
||||
|
||||
// verify ingest request correctly registered
|
||||
expected := apptest.MetricNamesStatsResponse{
|
||||
Records: []at.MetricNamesStatsRecord{
|
||||
{MetricName: "metric_name_1"},
|
||||
{MetricName: "metric_name_2"},
|
||||
{MetricName: "metric_name_3"},
|
||||
},
|
||||
}
|
||||
gotStats := vmselect.MetricNamesStats(t, "", "", "", apptest.QueryOpts{Tenant: tenantID})
|
||||
if diff := cmp.Diff(expected, gotStats); diff != "" {
|
||||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// verify query request registered correctly
|
||||
vmselect.PrometheusAPIV1Query(t, `{__name__!=""}`, apptest.QueryOpts{
|
||||
Tenant: tenantID, Time: ingestDateTime,
|
||||
})
|
||||
|
||||
expected = apptest.MetricNamesStatsResponse{
|
||||
Records: []at.MetricNamesStatsRecord{
|
||||
{MetricName: "metric_name_2", QueryRequestsCount: 1},
|
||||
{MetricName: "metric_name_3", QueryRequestsCount: 1},
|
||||
{MetricName: "metric_name_1", QueryRequestsCount: 3},
|
||||
},
|
||||
}
|
||||
gotStats = vmselect.MetricNamesStats(t, "", "", "", apptest.QueryOpts{Tenant: tenantID})
|
||||
if diff := cmp.Diff(expected, gotStats); diff != "" {
|
||||
t.Errorf("unexpected response tenant: %s (-want, +got):\n%s", tenantID, diff)
|
||||
}
|
||||
}
|
||||
|
||||
// verify multitenant stats
|
||||
expected := apptest.MetricNamesStatsResponse{
|
||||
Records: []at.MetricNamesStatsRecord{
|
||||
{MetricName: "metric_name_2", QueryRequestsCount: 3},
|
||||
{MetricName: "metric_name_3", QueryRequestsCount: 3},
|
||||
{MetricName: "metric_name_1", QueryRequestsCount: 9},
|
||||
},
|
||||
}
|
||||
gotStats := vmselect.MetricNamesStats(t, "", "", "", apptest.QueryOpts{Tenant: "multitenant"})
|
||||
if diff := cmp.Diff(expected, gotStats); diff != "" {
|
||||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// reset cache and check empty state
|
||||
vmselect.MetricNamesStatsReset(t, at.QueryOpts{})
|
||||
resp = vmselect.MetricNamesStats(t, "", "", "", apptest.QueryOpts{Tenant: "multitenant"})
|
||||
if len(resp.Records) != 0 {
|
||||
t.Fatalf("want 0 records, got: %d", len(resp.Records))
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,14 @@ package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
|
||||
pb "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
)
|
||||
|
||||
func millis(s string) int64 {
|
||||
@@ -30,8 +28,6 @@ func TestSingleInstantQuery(t *testing.T) {
|
||||
|
||||
testInstantQueryWithUTFNames(t, sut)
|
||||
testInstantQueryDoesNotReturnStaleNaNs(t, sut)
|
||||
|
||||
testQueryRangeWithAtModifier(t, sut)
|
||||
}
|
||||
|
||||
func TestClusterInstantQuery(t *testing.T) {
|
||||
@@ -42,8 +38,6 @@ func TestClusterInstantQuery(t *testing.T) {
|
||||
|
||||
testInstantQueryWithUTFNames(t, sut)
|
||||
testInstantQueryDoesNotReturnStaleNaNs(t, sut)
|
||||
|
||||
testQueryRangeWithAtModifier(t, sut)
|
||||
}
|
||||
|
||||
func testInstantQueryWithUTFNames(t *testing.T, sut apptest.PrometheusWriteQuerier) {
|
||||
@@ -179,54 +173,3 @@ func testInstantQueryDoesNotReturnStaleNaNs(t *testing.T, sut apptest.Prometheus
|
||||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// This test checks absence of panic after conversion of math.NaN to int64 in vmselect.
|
||||
// See: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8444
|
||||
// However, conversion of math.NaN to int64 could behave differently depending on platform and Go version.
|
||||
// Hence, this test could succeed for some platforms even if fix is rolled back.
|
||||
func testQueryRangeWithAtModifier(t *testing.T, sut apptest.PrometheusWriteQuerier) {
|
||||
data := []pb.TimeSeries{
|
||||
{
|
||||
Labels: []pb.Label{
|
||||
{Name: "__name__", Value: "up"},
|
||||
},
|
||||
Samples: []pb.Sample{
|
||||
{Value: 1, Timestamp: millis("2025-01-01T00:01:00Z")},
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: []pb.Label{
|
||||
{Name: "__name__", Value: "metricNaN"},
|
||||
},
|
||||
Samples: []pb.Sample{
|
||||
{Value: decimal.StaleNaN, Timestamp: millis("2025-01-01T00:01:00Z")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sut.PrometheusAPIV1Write(t, data, apptest.QueryOpts{})
|
||||
sut.ForceFlush(t)
|
||||
|
||||
resp := sut.PrometheusAPIV1QueryRange(t, `vector(1) @ up`, apptest.QueryOpts{
|
||||
Start: "2025-01-01T00:00:00Z",
|
||||
End: "2025-01-01T00:02:00Z",
|
||||
Step: "10s",
|
||||
})
|
||||
|
||||
if resp.Status != "success" {
|
||||
t.Fatalf("unexpected status: %q", resp.Status)
|
||||
}
|
||||
|
||||
resp = sut.PrometheusAPIV1QueryRange(t, `vector(1) @ metricNaN`, apptest.QueryOpts{
|
||||
Start: "2025-01-01T00:00:00Z",
|
||||
End: "2025-01-01T00:02:00Z",
|
||||
Step: "10s",
|
||||
})
|
||||
|
||||
if resp.Status != "error" {
|
||||
t.Fatalf("unexpected status: %q", resp.Status)
|
||||
}
|
||||
if !strings.Contains(resp.Error, "modifier must return a non-NaN value") {
|
||||
t.Fatalf("unexpected error: %q", resp.Error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package apptest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
@@ -135,45 +133,6 @@ func (app *Vmselect) DeleteSeries(t *testing.T, matchQuery string, opts QueryOpt
|
||||
}
|
||||
}
|
||||
|
||||
// MetricNamesStats sends a query to a /select/tenant/prometheus/api/v1/status/metric_names_stats endpoint
|
||||
// and returns the statistics response for given params.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/#Trackingestedmetricsusage
|
||||
func (app *Vmselect) MetricNamesStats(t *testing.T, limit, le, matchPattern string, opts QueryOpts) MetricNamesStatsResponse {
|
||||
t.Helper()
|
||||
|
||||
values := opts.asURLValues()
|
||||
values.Add("limit", limit)
|
||||
values.Add("le", le)
|
||||
values.Add("match_pattern", matchPattern)
|
||||
queryURL := fmt.Sprintf("http://%s/select/%s/prometheus/api/v1/status/metric_names_stats", app.httpListenAddr, opts.getTenant())
|
||||
|
||||
res, statusCode := app.cli.PostForm(t, queryURL, values)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, res)
|
||||
}
|
||||
var resp MetricNamesStatsResponse
|
||||
if err := json.Unmarshal([]byte(res), &resp); err != nil {
|
||||
t.Fatalf("could not unmarshal series response data:\n%s\n err: %v", res, err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// MetricNamesStatsReset sends a query to a /admin/api/v1/status/metric_names_stats/reset endpoint
|
||||
//
|
||||
// See https://docs.victoriametrics.com/#Trackingestedmetricsusage
|
||||
func (app *Vmselect) MetricNamesStatsReset(t *testing.T, opts QueryOpts) {
|
||||
t.Helper()
|
||||
|
||||
values := opts.asURLValues()
|
||||
queryURL := fmt.Sprintf("http://%s/admin/api/v1/admin/status/metric_names_stats/reset", app.httpListenAddr)
|
||||
|
||||
res, statusCode := app.cli.PostForm(t, queryURL, values)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusNoContent, res)
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation of the vmselect app state.
|
||||
func (app *Vmselect) String() string {
|
||||
return fmt.Sprintf("{app: %s httpListenAddr: %q}", app.app, app.httpListenAddr)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package apptest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -189,45 +188,6 @@ func (app *Vmsingle) PrometheusAPIV1Series(t *testing.T, matchQuery string, opts
|
||||
return NewPrometheusAPIV1SeriesResponse(t, res)
|
||||
}
|
||||
|
||||
// APIV1StatusMetricNamesStats sends a query to a /api/v1/status/metric_names_stats endpoint
|
||||
// and returns the statistics response for given params.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/#track-ingested-metrics-usage
|
||||
func (app *Vmsingle) APIV1StatusMetricNamesStats(t *testing.T, limit, le, matchPattern string, opts QueryOpts) MetricNamesStatsResponse {
|
||||
t.Helper()
|
||||
|
||||
values := opts.asURLValues()
|
||||
values.Add("limit", limit)
|
||||
values.Add("le", le)
|
||||
values.Add("match_pattern", matchPattern)
|
||||
queryURL := fmt.Sprintf("http://%s/api/v1/status/metric_names_stats", app.httpListenAddr)
|
||||
|
||||
res, statusCode := app.cli.PostForm(t, queryURL, values)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, res)
|
||||
}
|
||||
var resp MetricNamesStatsResponse
|
||||
if err := json.Unmarshal([]byte(res), &resp); err != nil {
|
||||
t.Fatalf("could not unmarshal metric names stats response data:\n%s\n err: %v", res, err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// APIV1AdminStatusMetricNamesStatsReset sends a query to a /api/v1/admin/status/metric_names_stats/reset endpoint
|
||||
//
|
||||
// See https://docs.victoriametrics.com/#Trackingestedmetricsusage
|
||||
func (app *Vmsingle) APIV1AdminStatusMetricNamesStatsReset(t *testing.T, opts QueryOpts) {
|
||||
t.Helper()
|
||||
|
||||
values := opts.asURLValues()
|
||||
queryURL := fmt.Sprintf("http://%s/api/v1/admin/status/metric_names_stats/reset", app.httpListenAddr)
|
||||
|
||||
res, statusCode := app.cli.PostForm(t, queryURL, values)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusNoContent, res)
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation of the vmsingle app state.
|
||||
func (app *Vmsingle) String() string {
|
||||
return fmt.Sprintf("{app: %s storageDataPath: %q httpListenAddr: %q}", []any{
|
||||
|
||||
@@ -1264,11 +1264,6 @@ Below is the output for `/path/to/vminsert -help`:
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
-memory.allowedPercent float
|
||||
Allowed percent of system memory VictoriaMetrics caches may occupy. See also -memory.allowedBytes. Too low a value may increase cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from the OS page cache which will result in higher disk IO usage (default 60)
|
||||
-metricNamesStatsResetAuthKey value
|
||||
AuthKey for reseting metric names usage cache via /api/v1/admin/status/metric_names_stats/reset. It overrides -httpAuth.*
|
||||
See https://docs.victoriametrics.com/#track-ingested-metrics-usage
|
||||
Flag value can be read from the given file when using -metricNamesStatsResetAuthKey=file:///abs/path/to/file or -metricNamesStatsResetAuthKey=file://./relative/path/to/file . Flag value can be read from the given http/https
|
||||
url when using -metricNamesStatsResetAuthKey=http://host/path or -metricNamesStatsResetAuthKey=https://host/path
|
||||
-metrics.exposeMetadata
|
||||
Whether to expose TYPE and HELP metadata at the /metrics page, which is exposed at -httpListenAddr . The metadata may be needed when the /metrics page is consumed by systems, which require this information. For example, Managed Prometheus in Google Cloud - https://cloud.google.com/stackdriver/docs/managed-prometheus/troubleshooting#missing-metric-type
|
||||
-metricsAuthKey value
|
||||
@@ -1941,9 +1936,6 @@ Below is the output for `/path/to/vmstorage -help`:
|
||||
-storage.cacheSizeIndexDBTagFilters size
|
||||
Overrides max size for indexdb/tagFiltersToMetricIDs cache. See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
-storage.cacheSizeMetricNamesStats size
|
||||
Overrides max size for storage/metricNamesStatsTracker cache. See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
-storage.cacheSizeStorageTSID size
|
||||
Overrides max size for storage/tsid cache. See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
@@ -1958,9 +1950,6 @@ Below is the output for `/path/to/vmstorage -help`:
|
||||
-storage.minFreeDiskSpaceBytes size
|
||||
The minimum free disk space at -storageDataPath after which the storage stops accepting new data
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 10000000)
|
||||
-storage.trackMetricNamesStats
|
||||
Whether to track ingest and query requests for timeseries metric names. This feature allows to track metric names unused at query requests.
|
||||
See https://docs.victoriametrics.com/#track-ingested-metrics-usage
|
||||
-storage.vminsertConnsShutdownDuration duration
|
||||
The time needed for gradual closing of vminsert connections during graceful shutdown. Bigger duration reduces spikes in CPU, RAM and disk IO load on the remaining vmstorage nodes during rolling restart. Smaller duration reduces the time needed to close all the vminsert connections, thus reducing the time for graceful shutdown. See https://docs.victoriametrics.com/cluster-victoriametrics/#improving-re-routing-performance-during-restart (default 25s)
|
||||
-storageDataPath string
|
||||
|
||||
@@ -461,58 +461,6 @@ vmselect requests stats via [/api/v1/status/tsdb](#tsdb-stats) API from each vms
|
||||
This may lead to inflated values when samples for the same time series are spread across multiple vmstorage nodes
|
||||
due to [replication](#replication) or [rerouting](https://docs.victoriametrics.com/cluster-victoriametrics/?highlight=re-routes#cluster-availability).
|
||||
|
||||
### Track ingested metrics usage
|
||||
|
||||
VictoriaMetrics provides the ability to record statistics of fetched [metric names](https://docs.victoriametrics.com/keyconcepts/#structure-of-a-metric) during [querying](https://docs.victoriametrics.com/keyconcepts/#query-data). This feature can be enabled via the flag `--storage.trackMetricNamesStats` (disabled by default) on a single-node VictoriaMetrics or [vmstorage](https://docs.victoriametrics.com/cluster-victoriametrics/#architecture-overview). Querying a metric with non-matching filters doesn't increase the counter for this particular metric name.
|
||||
For example, querying for `vm_log_messages_total{level!="info"}` won't increment usage counter for `vm_log_messages_total` if there are no `{level="error"}` or `{level="warning"}` series yet.
|
||||
VictoriaMetrics tracks metric names query statistics for `/api/v1/query`, `/api/v1/query_range`, `/render`, `/federate` and `/api/v1/export` API calls.
|
||||
|
||||
To get metric names usage statistics, use the `/prometheus/api/v1/status/metric_names_stats` API endpoint. It accepts the following query parameters:
|
||||
|
||||
* `limit` - integer value to limit the number of metric names in response. By default, API returns 1000 records.
|
||||
* `le` - `less than or equal`, is an integer threshold for filtering metric names by their usage count in queries. For example, with `?le=1` API returns metric names that were queried <=1 times.
|
||||
* `match_pattern` - a substring pattern to match metric names. For example, `?match_pattern=vm_` will match any metric names with `vm_` pattern, like `vm_http_requests`, `max_vm_memory_available`. It doesn't support regex syntax.
|
||||
|
||||
The API endpoint returns the following `JSON` response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"statsSollectedSince": 1737534094,
|
||||
"statsCollectedRecordsTotal": 2,
|
||||
"records": [
|
||||
{
|
||||
"metricName": "node_disk_writes_completed_total",
|
||||
"queryRequests": 50,
|
||||
"lastRequestTimestamp": 1737534262
|
||||
},
|
||||
{
|
||||
"metricName": "node_network_transmit_errs_total",
|
||||
"queryRequestsCount": 100,
|
||||
"lastRequestTimestamp": 1737534262
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
VictoriaMetrics stores tracked metric names in memory and saves the state to disk in the data/cache folder during restarts.
|
||||
The size of the in-memory state is limited to 1% of the available memory by default.
|
||||
This limit can be adjusted using the `-storage.cacheSizeMetricNamesStats` flag.
|
||||
|
||||
When the maximum state capacity is reached, VictoriaMetrics will stop tracking stats for newly registered time series.
|
||||
However, read request statistics for already tracked time series will continue to work as expected.
|
||||
|
||||
VictoriaMetrics exposes the following metrics for the metric name tracker:
|
||||
* vm_cache_size_bytes{type="storage/metricNamesStatsTracker"}
|
||||
* vm_cache_size{type="storage/metricNamesStatsTracker"}
|
||||
* vm_cache_size_max_bytes{type="storage/metricNamesStatsTracker"}
|
||||
|
||||
|
||||
An alerting rule with query `vm_cache_size_bytes{type="storage/metricNamesStatsTracker"} \ vm_cache_size_max_bytes{type="storage/metricNamesStatsTracker"} > 0.9` can be used to notify the user of cache utilization exceeding 90%.
|
||||
|
||||
The metric name tracker state can be reset via the API endpoint /api/v1/admin/status/metric_names_stats/reset or
|
||||
via [cache removal](#cache-removal) procedure.
|
||||
|
||||
## How to apply new config to VictoriaMetrics
|
||||
|
||||
VictoriaMetrics is configured via command-line flags, so it must be restarted when new command-line flags should be applied:
|
||||
@@ -3261,11 +3209,6 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-reloadAuthKey value
|
||||
Auth key for /-/reload http endpoint. It must be passed via authKey query arg. It overrides -httpAuth.*
|
||||
Flag value can be read from the given file when using -reloadAuthKey=file:///abs/path/to/file or -reloadAuthKey=file://./relative/path/to/file . Flag value can be read from the given http/https url when using -reloadAuthKey=http://host/path or -reloadAuthKey=https://host/path
|
||||
-metricNamesStatsResetAuthKey value
|
||||
AuthKey for reseting metric names usage cache via /api/v1/admin/status/metric_names_stats/reset. It overrides -httpAuth.*
|
||||
See https://docs.victoriametrics.com/#track-ingested-metrics-usage
|
||||
Flag value can be read from the given file when using -metricNamesStatsResetAuthKey=file:///abs/path/to/file or -metricNamesStatsResetAuthKey=file://./relative/path/to/file . Flag value can be read from the given http/https
|
||||
url when using -metricNamesStatsResetAuthKey=http://host/path or -metricNamesStatsResetAuthKey=https://host/path
|
||||
-retentionFilter array
|
||||
Retention filter in the format 'filter:retention'. For example, '{env="dev"}:3d' configures the retention for time series with env="dev" label to 3 days. See https://docs.victoriametrics.com/#retention-filters for details. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise/
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
@@ -3414,9 +3357,6 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-storage.cacheSizeIndexDBTagFilters size
|
||||
Overrides max size for indexdb/tagFiltersToMetricIDs cache. See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
-storage.cacheSizeMetricNamesStats size
|
||||
Overrides max size for storage/metricNamesStatsTracker cache. See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
-storage.cacheSizeStorageTSID size
|
||||
Overrides max size for storage/tsid cache. See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
@@ -3431,9 +3371,6 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-storage.minFreeDiskSpaceBytes size
|
||||
The minimum free disk space at -storageDataPath after which the storage stops accepting new data
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 10000000)
|
||||
-storage.trackMetricNamesStats
|
||||
Whether to track ingest and query requests for timeseries metric names. This feature allows to track metric names unused at query requests.
|
||||
See https://docs.victoriametrics.com/#track-ingested-metrics-usage
|
||||
-storageDataPath string
|
||||
Path to storage data (default "victoria-metrics-data")
|
||||
-streamAggr.config string
|
||||
|
||||
@@ -26,7 +26,7 @@ clients:
|
||||
Substitute `localhost:9428` address inside `clients` with the real TCP address of VictoriaLogs.
|
||||
|
||||
VictoriaLogs uses [log streams](https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields) defined at the client side,
|
||||
e.g. at Promtail, Grafana Agent or Grafana Alloy. Sometimes it may be needed overriding the set of these fields. This can be done via `_stream_fields`
|
||||
e.g. at Promtail, Grafana Agent or Grafana Allow. Sometimes it may be needed overriding the set of these fields. This can be done via `_stream_fields`
|
||||
query arg. For example, the following config instructs using only the `instance` and `job` labels as log stream fields, while other labels
|
||||
will be stored as [usual log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model):
|
||||
|
||||
|
||||
@@ -18,30 +18,22 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/).
|
||||
|
||||
## tip
|
||||
|
||||
## [v1.113.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.113.0)
|
||||
|
||||
Released at 2025-03-07
|
||||
|
||||
**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/) disallows 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.**
|
||||
|
||||
**Update note 3: [vmalert](https://docs.victoriametrics.com/vmalert/) disallows specifying `eval_offset` and `eval_delay` options in the same [group](https://docs.victoriametrics.com/vmalert/#groups). The `eval_offset` option ensures the group is evaluated at the exact offset in the range of [0...interval]. However, with `eval_delay`, this behavior cannot be guaranteed without further adjusting the evaluation time, which could lead to more confusion.**
|
||||
**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: provide alternative registry for all VictoriaMetrics components at [Quay.io](https://quay.io/organization/victoriametrics).
|
||||
* FEATURE: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/): add a new flag `--storage.trackMetricNamesStats` and a new HTTP API - `/api/v1/status/metric_names_stats`. It allows to track how frequent ingested [metric names](https://docs.victoriametrics.com/keyconcepts/#structure-of-a-metric) are used during [querying](https://docs.victoriametrics.com/keyconcepts/#query-data). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4458) for details and related [docs](https://docs.victoriametrics.com/#track-ingested-metrics-usage)
|
||||
* FEATURE: [data ingestion](https://docs.victoriametrics.com/victorialogs/data-ingestion/): make `KeyValueList`, `ArrayValue` [OpenTelemetry protocol for metrics](https://docs.victoriametrics.com/#sending-data-via-opentelemetry) attributes label values compatible with open-telemetry-collector format. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8384).
|
||||
* 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.
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert/): remove random sleep before a group starts when `eval_offset` is specified, because `eval_offset` already disperses the group evaluation time, serving the same purpose as the random sleep. This change also enables chaining groups, see [this doc](https://docs.victoriametrics.com/vmalert/#chaining-groups) and [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/860).
|
||||
* 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: [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.
|
||||
* FEATURE: [data ingestion](https://docs.victoriametrics.com/victorialogs/data-ingestion/): make `KeyValueList`, `ArrayValue` [OpenTelemetry protocol for metrics](https://docs.victoriametrics.com/#sending-data-via-opentelemetry) attributes label values compatible with open-telemetry-collector format. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8384).
|
||||
|
||||
* 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.
|
||||
@@ -53,9 +45,6 @@ Released at 2025-03-07
|
||||
* 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).
|
||||
* BUGFIX: [MetricsQL](https://docs.victoriametrics.com/metricsql/): fix filters pushdown logic for expression like `foo{a="a"} ifnot bar{a="b"}`. Previously, filters from right operand were incorrectly propagated to the left operand and could result in empty query results even if `foo{a="a"}` matches time series. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8435).
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/), `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/): prevent possible panic for `foo @ bar` expression when first sample in `bar` starts with `NaN` or starts long after first sample in `foo`. Now, VM will try to find first non-NaN value in `bar` and could yield an error `@ modifier must return a non-NaN value` if it won't find it. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8444).
|
||||
* BUGFIX: [Single-node VictoriaMetrics](https://docs.victoriametrics.com/) and [vmstorage](https://docs.victoriametrics.com/victoriametrics/): prevent panic when using with rules that have zero interval: `-downsampling.period=5m:5m,0s:0s`. Such rule configuration shouldn't be rejected and cause an error when used. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8454).
|
||||
|
||||
## [v1.102.15](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.102.15)
|
||||
|
||||
@@ -91,8 +80,8 @@ Released at 2025-02-21
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): improve numbers formatting for better readability on the `Explore Cardinality` page. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8318).
|
||||
* 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/): 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.
|
||||
* 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).
|
||||
|
||||
@@ -49,9 +49,9 @@ please refer to the [VictoriaMetrics Cloud documentation](https://docs.victoriam
|
||||
* `vmalert` execute queries against remote datasource which has reliability risks because of the network.
|
||||
It is recommended to configure alerts thresholds and rules expressions with the understanding that network
|
||||
requests may fail;
|
||||
* `vmalert` executes rules within a group sequentially, but persistence of execution results to remote
|
||||
* by default, rules execution is sequential within one group, but persistence of execution results to remote
|
||||
storage is asynchronous. Hence, user shouldn't rely on chaining of recording rules when result of previous
|
||||
recording rule is reused in the next one. See how to chain groups [here](https://docs.victoriametrics.com/vmalert/#chaining-groups).
|
||||
recording rule is reused in the next one;
|
||||
|
||||
## QuickStart
|
||||
|
||||
@@ -138,8 +138,7 @@ name: <string>
|
||||
# Group will be evaluated at the exact offset in the range of [0...interval].
|
||||
# E.g. for Group with `interval: 1h` and `eval_offset: 5m` the evaluation will
|
||||
# start at 5th minute of the hour. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3409
|
||||
# `interval` must be specified if `eval_offset` is used, and `eval_offset` cannot exceed `interval`.
|
||||
# `eval_offset` cannot be used with `eval_delay`, as group will be executed at the exact offset and `eval_delay` is ignored.
|
||||
# `eval_offset` can't be bigger than `interval`.
|
||||
[ eval_offset: <duration> ]
|
||||
|
||||
# Optional
|
||||
@@ -1461,7 +1460,7 @@ The shortlist of configuration flags is the following:
|
||||
-rule.defaultRuleType string
|
||||
Default type for rule expressions, can be overridden by type parameter inside the rule group. Supported values: "graphite", "prometheus" and "vlogs". (default: "prometheus")
|
||||
-rule.evalDelay time
|
||||
Adjustment of the time parameter for rule evaluation requests to compensate intentional data delay from the datasource.Normally, should be equal to `-search.latencyOffset` (cmd-line flag configured for VictoriaMetrics single-node or vmselect). This doesn't apply to groups with eval_offset specified. (default 30s)
|
||||
Adjustment of the time parameter for rule evaluation requests to compensate intentional data delay from the datasource.Normally, should be equal to `-search.latencyOffset` (cmd-line flag configured for VictoriaMetrics single-node or vmselect). (default 30s)
|
||||
-rule.maxResolveDuration duration
|
||||
Limits the maxiMum duration for automatic alert expiration, which by default is 4 times evaluationInterval of the parent group
|
||||
-rule.resendDelay duration
|
||||
@@ -1555,62 +1554,6 @@ Please note, `params` are used only for executing rules expressions (requests to
|
||||
If there would be a conflict between URL params set in `datasource.url` flag and params in group definition
|
||||
the latter will have higher priority.
|
||||
|
||||
### Chaining groups
|
||||
|
||||
For chaining groups, they must be executed in a specific order, and the next group should be executed after
|
||||
the results from previous group are available in the datasource.
|
||||
In `vmalert`, user can specify `eval_offset` to achieve that{{% available_from "v1.113.0" %}}.
|
||||
|
||||
For example:
|
||||
```yaml
|
||||
groups:
|
||||
- name: BaseGroup
|
||||
interval: 1m
|
||||
eval_offset: 10s
|
||||
rules:
|
||||
- record: http_server_request_duration_seconds:sum_rate:5m:http_get
|
||||
expr: |
|
||||
sum without(instance, pod) (
|
||||
rate(
|
||||
http_server_request_duration_seconds{
|
||||
http_request_method="GET"
|
||||
}[5m]
|
||||
)
|
||||
)
|
||||
- record: http_server_request_duration_seconds:sum_rate:5m:http_post
|
||||
expr: |
|
||||
sum without(instance, pod) (
|
||||
rate(
|
||||
http_server_request_duration_seconds{
|
||||
http_request_method="POST"
|
||||
}[5m]
|
||||
)
|
||||
)
|
||||
- name: TopGroup
|
||||
interval: 1m
|
||||
eval_offset: 40s
|
||||
rules:
|
||||
- record: http_server_request_duration_seconds:sum_rate:5m:merged
|
||||
expr: |
|
||||
http_server_request_duration_seconds:sum_rate:5m:http_get
|
||||
or
|
||||
http_server_request_duration_seconds:sum_rate:5m:http_post
|
||||
```
|
||||
|
||||
This configuration ensures that rules in `BaseGroup` are exectuted at(assuming vmalert starts at `12:00:00`):
|
||||
```
|
||||
[12:00:10, 12:01:10, 12:02:10, 12:03:10...]
|
||||
```
|
||||
while rules in group `TopGroup` are exectuted at:
|
||||
```
|
||||
[12:00:40, 12:01:40, 12:02:40, 12:03:40...]
|
||||
```
|
||||
As a result, `TopGroup` always gets the latest results of `BaseGroup`.
|
||||
|
||||
By default, the `eval_offset` values should be at least 30 seconds apart to accommodate the
|
||||
`-search.latencyOffset(default 30s)` command-line flag at vmselect or VictoriaMetrics single-node.
|
||||
The mininum `eval_offset` gap can be adjusted accordingly with `-search.latencyOffset`.
|
||||
|
||||
### Notifier configuration file
|
||||
|
||||
Notifier also supports configuration via file specified with flag `notifier.config`:
|
||||
|
||||
2
go.mod
2
go.mod
@@ -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.1
|
||||
github.com/VictoriaMetrics/metricsql v0.84.0
|
||||
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
|
||||
|
||||
8
go.sum
8
go.sum
@@ -43,8 +43,8 @@ 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.1 h1:ts0fJBcmClFRmO7Ibn/YG2ctT698aX/TxPbTfax8eTA=
|
||||
github.com/VictoriaMetrics/metricsql v0.84.1/go.mod h1:1g4hdCwlbJZ851PU9VN65xy9Rdlzupo6fx3SNZ8Z64U=
|
||||
github.com/VictoriaMetrics/metricsql v0.84.0 h1:rVZapkXHiM4dR979La3tk8u2equ57Insbr1+Hm6yUew=
|
||||
github.com/VictoriaMetrics/metricsql v0.84.0/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=
|
||||
@@ -130,8 +130,8 @@ github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
|
||||
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.3 h1:c1EIw4vwYCaovxRZtyycws8aX6dJ9W2p+4bCi7mcDgw=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:c955gQjaXHsMxMjHjEZ7nwIzMJYxXpN+sJIGufsSbg4=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.3 h1:hVEaommgvzTjTd4xCaFd+kEQ2iYBtGxP6luyLrx6uOk=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
|
||||
github.com/ergochat/readline v0.1.3 h1:/DytGTmwdUJcLAe3k3VJgowh5vNnsdifYT6uVaf4pSo=
|
||||
|
||||
@@ -479,7 +479,7 @@ func isProtectedByAuthFlag(path string) bool {
|
||||
return strings.HasSuffix(path, "/config") || strings.HasSuffix(path, "/reload") ||
|
||||
strings.HasSuffix(path, "/resetRollupResultCache") || strings.HasSuffix(path, "/delSeries") || strings.HasSuffix(path, "/delete_series") ||
|
||||
strings.HasSuffix(path, "/force_merge") || strings.HasSuffix(path, "/force_flush") || strings.HasSuffix(path, "/snapshot") ||
|
||||
strings.HasPrefix(path, "/snapshot/") || strings.HasSuffix(path, "/admin/status/metric_names_stats/reset")
|
||||
strings.HasPrefix(path, "/snapshot/")
|
||||
}
|
||||
|
||||
// CheckAuthFlag checks whether the given authKey is set and valid
|
||||
|
||||
@@ -1,605 +0,0 @@
|
||||
package metricnamestats
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// metricNameBufSize can hold up to 64 metric name values
|
||||
// max size of metric name label value is 256
|
||||
// but usual size of metric name is 16-32
|
||||
metricNameBufSize = 16 * 1024
|
||||
statItemBufSize = 1024
|
||||
// statKey + statItem + approx key-value at map in-memory size
|
||||
storeOverhead = 24 + 16 + 24
|
||||
)
|
||||
|
||||
// Tracker implements in-memory tracker for timeseries metric names
|
||||
// it tracks ingest and query requests for metric names
|
||||
// and collects statistics
|
||||
//
|
||||
// main purpose of this tracker is to provide insights about metrics that have never been queried
|
||||
type Tracker struct {
|
||||
maxSizeBytes uint64
|
||||
cachePath string
|
||||
|
||||
creationTs atomic.Uint64
|
||||
currentSizeBytes atomic.Uint64
|
||||
currentItemsCount atomic.Uint64
|
||||
|
||||
// mu protect fields below
|
||||
mu sync.RWMutex
|
||||
|
||||
store map[statKey]*statItem
|
||||
// holds batch allocations for statItems at store
|
||||
statItemBuf []statItem
|
||||
// holds batch allocations for metric names at statKey
|
||||
metricNamesBuf []byte
|
||||
|
||||
// helper for tests
|
||||
getCurrentTs func() uint64
|
||||
}
|
||||
|
||||
type statKey struct {
|
||||
accountID uint32
|
||||
projectID uint32
|
||||
metricName string
|
||||
}
|
||||
|
||||
type statItem struct {
|
||||
requestsCount atomic.Uint64
|
||||
lastRequestTs atomic.Uint64
|
||||
}
|
||||
|
||||
type recordForStore struct {
|
||||
AccountID uint32
|
||||
ProjectID uint32
|
||||
MetricName string
|
||||
RequestsCount uint64
|
||||
LastRequestTs uint64
|
||||
}
|
||||
|
||||
// MustLoadFrom inits tracker from the given on-disk path
|
||||
func MustLoadFrom(loadPath string, maxSizeBytes uint64) *Tracker {
|
||||
mt, err := loadFrom(loadPath, maxSizeBytes)
|
||||
if err != nil {
|
||||
logger.Fatalf("unexpected error at tracker state load from path=%q: %s", loadPath, err)
|
||||
}
|
||||
return mt
|
||||
}
|
||||
|
||||
func loadFrom(loadPath string, maxSizeBytes uint64) (*Tracker, error) {
|
||||
mt := &Tracker{
|
||||
maxSizeBytes: maxSizeBytes,
|
||||
cachePath: loadPath,
|
||||
getCurrentTs: fasttime.UnixTimestamp,
|
||||
}
|
||||
mt.initEmpty()
|
||||
|
||||
f, err := os.Open(loadPath)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("cannot access file content: %w", err)
|
||||
}
|
||||
// fast path
|
||||
if f == nil {
|
||||
return mt, nil
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
zr, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create new gzip reader: %w", err)
|
||||
}
|
||||
reader := json.NewDecoder(zr)
|
||||
var storedMaxSizeBytes uint64
|
||||
if err := reader.Decode(&storedMaxSizeBytes); err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return mt, nil
|
||||
}
|
||||
return nil, fmt.Errorf("cannot parse maxSizeBytes: %w", err)
|
||||
}
|
||||
if storedMaxSizeBytes > maxSizeBytes {
|
||||
logger.Infof("Reseting tracker state due to changed maxSizeBytes from %d to %d.", storedMaxSizeBytes, maxSizeBytes)
|
||||
return mt, nil
|
||||
}
|
||||
var creationTs uint64
|
||||
if err := reader.Decode(&creationTs); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse creation timestamp: %w", err)
|
||||
}
|
||||
mt.creationTs.Store(creationTs)
|
||||
var cnt uint64
|
||||
var size uint64
|
||||
var r recordForStore
|
||||
for {
|
||||
if err := reader.Decode(&r); err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("cannot parse state record: %w", err)
|
||||
}
|
||||
// during cache load, there is no need to hold lock
|
||||
si := mt.nextRecordLocked()
|
||||
|
||||
si.lastRequestTs.Store(r.LastRequestTs)
|
||||
si.requestsCount.Store(r.RequestsCount)
|
||||
|
||||
key := statKey{
|
||||
projectID: r.ProjectID,
|
||||
accountID: r.AccountID,
|
||||
metricName: mt.cloneMetricNameLocked([]byte(r.MetricName)),
|
||||
}
|
||||
mt.store[key] = si
|
||||
size += uint64(len(r.MetricName)) + storeOverhead
|
||||
cnt++
|
||||
}
|
||||
if err := zr.Close(); err != nil {
|
||||
return nil, fmt.Errorf("cannot close gzip reader: %w", err)
|
||||
}
|
||||
|
||||
mt.currentSizeBytes.Store(size)
|
||||
mt.currentItemsCount.Store(cnt)
|
||||
logger.Infof("loaded state from disk, records: %d, total size: %d", cnt, size)
|
||||
return mt, nil
|
||||
}
|
||||
|
||||
func (mt *Tracker) nextRecordLocked() *statItem {
|
||||
n := len(mt.statItemBuf) + 1
|
||||
if n > cap(mt.statItemBuf) {
|
||||
// allocate a new slice instead of reallocating exist
|
||||
// it saves memory and reduces GC pressure
|
||||
mt.statItemBuf = make([]statItem, 0, statItemBufSize)
|
||||
n = 1
|
||||
}
|
||||
mt.statItemBuf = mt.statItemBuf[:n]
|
||||
st := &mt.statItemBuf[n-1]
|
||||
|
||||
return st
|
||||
}
|
||||
|
||||
// cloneMetricNameLocked uses the same idea as strings.Clone.
|
||||
// But instead of direct []byte allocation for each cloned string,
|
||||
// it allocates metricNamesBuf, copies provide metricGroup into it
|
||||
// and uses string *byte references for it via subslice.
|
||||
func (mt *Tracker) cloneMetricNameLocked(metricName []byte) string {
|
||||
idx := len(mt.metricNamesBuf)
|
||||
n := len(metricName) + len(mt.metricNamesBuf)
|
||||
if n > cap(mt.metricNamesBuf) {
|
||||
// allocate a new slice instead of reallocting exist
|
||||
// it saves memory and reduces GC pressure
|
||||
mt.metricNamesBuf = make([]byte, 0, metricNameBufSize)
|
||||
idx = 0
|
||||
}
|
||||
mt.metricNamesBuf = append(mt.metricNamesBuf, metricName...)
|
||||
return bytesutil.ToUnsafeString(mt.metricNamesBuf[idx:])
|
||||
}
|
||||
|
||||
// MustClose closes tracker and saves state on disk
|
||||
func (mt *Tracker) MustClose() {
|
||||
if mt == nil {
|
||||
return
|
||||
}
|
||||
if err := mt.saveLocked(); err != nil {
|
||||
logger.Panicf("cannot save tracker state at path=%q: %s", mt.cachePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// saveLocked stores in-memory state of tracker on disk
|
||||
func (mt *Tracker) saveLocked() error {
|
||||
// Create dir if it doesn't exist in the same manner as other caches doing
|
||||
dir, fileName := filepath.Split(mt.cachePath)
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("cannot stat %q: %s", dir, err)
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("cannot create dir %q: %s", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// create temp directory in the same directory where original file located
|
||||
// it's needed to mitigate cross block-device rename error.
|
||||
tempDir, err := os.MkdirTemp(dir, "metricnamestats.tmp.")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create tempDir for state save: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if tempDir != "" {
|
||||
_ = os.RemoveAll(tempDir)
|
||||
}
|
||||
}()
|
||||
|
||||
f, err := os.Create(filepath.Join(tempDir, fileName))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open file for state save: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
zw := gzip.NewWriter(f)
|
||||
writer := json.NewEncoder(zw)
|
||||
if err := writer.Encode(mt.maxSizeBytes); err != nil {
|
||||
return fmt.Errorf("cannot save encoded maxSizeBytes: %w", err)
|
||||
}
|
||||
if err := writer.Encode(mt.creationTs.Load()); err != nil {
|
||||
return fmt.Errorf("cannot save encoded creation timestamp: %w", err)
|
||||
}
|
||||
|
||||
var r recordForStore
|
||||
for sk, si := range mt.store {
|
||||
r.AccountID = sk.accountID
|
||||
r.ProjectID = sk.projectID
|
||||
r.MetricName = sk.metricName
|
||||
r.LastRequestTs = si.lastRequestTs.Load()
|
||||
r.RequestsCount = si.requestsCount.Load()
|
||||
if err := writer.Encode(r); err != nil {
|
||||
return fmt.Errorf("cannot save encoded state record: %w", err)
|
||||
}
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
return fmt.Errorf("cannot flush writer state: %w", err)
|
||||
}
|
||||
// atomically save result
|
||||
if err := os.Rename(f.Name(), mt.cachePath); err != nil {
|
||||
return fmt.Errorf("cannot move temporary file %q to %q: %s", f.Name(), mt.cachePath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TrackerMetrics holds metrics to report
|
||||
type TrackerMetrics struct {
|
||||
CurrentSizeBytes uint64
|
||||
CurrentItemsCount uint64
|
||||
MaxSizeBytes uint64
|
||||
}
|
||||
|
||||
// UpdateMetrics writes internal metrics to the provided object
|
||||
func (mt *Tracker) UpdateMetrics(dst *TrackerMetrics) {
|
||||
if mt == nil {
|
||||
return
|
||||
}
|
||||
dst.CurrentSizeBytes = mt.currentSizeBytes.Load()
|
||||
dst.CurrentItemsCount = mt.currentItemsCount.Load()
|
||||
dst.MaxSizeBytes = mt.maxSizeBytes
|
||||
}
|
||||
|
||||
// IsEmpty checks if internal state has any records
|
||||
func (mt *Tracker) IsEmpty() bool {
|
||||
return mt.currentItemsCount.Load() == 0
|
||||
}
|
||||
|
||||
// Reset cleans stats, saves cache state and executes provided func
|
||||
func (mt *Tracker) Reset(onReset func()) {
|
||||
if mt == nil {
|
||||
return
|
||||
}
|
||||
logger.Infof("reseting metric names tracker state")
|
||||
mt.mu.Lock()
|
||||
defer mt.mu.Unlock()
|
||||
mt.initEmpty()
|
||||
if err := mt.saveLocked(); err != nil {
|
||||
logger.Panicf("during Tracker reset cannot save state: %s", err)
|
||||
}
|
||||
onReset()
|
||||
}
|
||||
|
||||
func (mt *Tracker) initEmpty() {
|
||||
mt.store = make(map[statKey]*statItem)
|
||||
mt.metricNamesBuf = make([]byte, 0, metricNameBufSize)
|
||||
mt.statItemBuf = make([]statItem, 0, statItemBufSize)
|
||||
mt.currentSizeBytes.Store(0)
|
||||
mt.currentItemsCount.Store(0)
|
||||
mt.creationTs.Store(mt.getCurrentTs())
|
||||
}
|
||||
|
||||
// RegisterIngestRequest tracks metric name ingestion
|
||||
func (mt *Tracker) RegisterIngestRequest(accountID, projectID uint32, metricName []byte) {
|
||||
if mt == nil {
|
||||
return
|
||||
}
|
||||
if mt.cacheIsFull() {
|
||||
return
|
||||
}
|
||||
|
||||
sk := statKey{
|
||||
accountID: accountID,
|
||||
projectID: projectID,
|
||||
metricName: bytesutil.ToUnsafeString(metricName),
|
||||
}
|
||||
mt.mu.RLock()
|
||||
_, ok := mt.store[sk]
|
||||
mt.mu.RUnlock()
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
|
||||
mt.mu.Lock()
|
||||
// key could be already ingested concurrently
|
||||
_, ok = mt.store[sk]
|
||||
if ok {
|
||||
mt.mu.Unlock()
|
||||
return
|
||||
}
|
||||
si := mt.nextRecordLocked()
|
||||
sk.metricName = mt.cloneMetricNameLocked(metricName)
|
||||
mt.store[sk] = si
|
||||
mt.mu.Unlock()
|
||||
|
||||
mt.currentSizeBytes.Add(uint64(len(metricName)) + storeOverhead)
|
||||
mt.currentItemsCount.Add(1)
|
||||
}
|
||||
|
||||
// RegisterQueryRequest tracks metric name at query request
|
||||
func (mt *Tracker) RegisterQueryRequest(accountID, projectID uint32, metricName []byte) {
|
||||
if mt == nil {
|
||||
return
|
||||
}
|
||||
mt.mu.RLock()
|
||||
key := statKey{
|
||||
accountID: accountID,
|
||||
projectID: projectID,
|
||||
metricName: bytesutil.ToUnsafeString(metricName),
|
||||
}
|
||||
si, ok := mt.store[key]
|
||||
mt.mu.RUnlock()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
si.lastRequestTs.Store(mt.getCurrentTs())
|
||||
si.requestsCount.Add(1)
|
||||
}
|
||||
|
||||
func (mt *Tracker) cacheIsFull() bool {
|
||||
return mt.currentSizeBytes.Load() > mt.maxSizeBytes
|
||||
}
|
||||
|
||||
// GetStatsForTenant returns stats response for the tracked metrics for given tenant
|
||||
func (mt *Tracker) GetStatsForTenant(accountID, projectID uint32, limit, le int, matchPattern string) StatsResult {
|
||||
var result StatsResult
|
||||
if mt == nil {
|
||||
return result
|
||||
}
|
||||
mt.mu.RLock()
|
||||
|
||||
result = mt.getStatsLocked(limit, func(sk *statKey, si *statItem) bool {
|
||||
if sk.accountID != accountID || sk.projectID != projectID {
|
||||
return false
|
||||
}
|
||||
if le >= 0 && int(si.requestsCount.Load()) > le {
|
||||
return false
|
||||
}
|
||||
if len(matchPattern) > 0 && !strings.Contains(sk.metricName, matchPattern) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
mt.mu.RUnlock()
|
||||
|
||||
result.sort()
|
||||
return result
|
||||
}
|
||||
|
||||
// GetStats returns stats response for the tracked metrics
|
||||
//
|
||||
// DeduplicateMergeRecords must be called at cluster version on returned result.
|
||||
func (mt *Tracker) GetStats(limit, le int, matchPattern string) StatsResult {
|
||||
var result StatsResult
|
||||
if mt == nil {
|
||||
return result
|
||||
}
|
||||
mt.mu.RLock()
|
||||
|
||||
result = mt.getStatsLocked(limit, func(sk *statKey, si *statItem) bool {
|
||||
if le >= 0 && int(si.requestsCount.Load()) > le {
|
||||
return false
|
||||
}
|
||||
if len(matchPattern) > 0 && !strings.Contains(sk.metricName, matchPattern) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
mt.mu.RUnlock()
|
||||
|
||||
result.sort()
|
||||
return result
|
||||
}
|
||||
|
||||
func (mt *Tracker) getStatsLocked(limit int, predicate func(sk *statKey, si *statItem) bool) StatsResult {
|
||||
var result StatsResult
|
||||
|
||||
result.CollectedSinceTs = mt.creationTs.Load()
|
||||
result.TotalRecords = mt.currentItemsCount.Load()
|
||||
result.MaxSizeBytes = mt.maxSizeBytes
|
||||
result.CurrentSizeBytes = mt.currentSizeBytes.Load()
|
||||
|
||||
for sk, si := range mt.store {
|
||||
if len(result.Records) >= limit {
|
||||
return result
|
||||
}
|
||||
if predicate(&sk, si) {
|
||||
result.Records = append(result.Records, StatRecord{
|
||||
MetricName: sk.metricName,
|
||||
RequestsCount: si.requestsCount.Load(),
|
||||
LastRequestTs: si.lastRequestTs.Load(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// StatsResult defines stats result for GetStats request
|
||||
type StatsResult struct {
|
||||
CollectedSinceTs uint64
|
||||
TotalRecords uint64
|
||||
MaxSizeBytes uint64
|
||||
CurrentSizeBytes uint64
|
||||
Records []StatRecord
|
||||
}
|
||||
|
||||
// StatRecord defines stat record for given metric name
|
||||
type StatRecord struct {
|
||||
MetricName string
|
||||
RequestsCount uint64
|
||||
LastRequestTs uint64
|
||||
}
|
||||
|
||||
func (sr *StatsResult) sort() {
|
||||
sort.Slice(sr.Records, func(i, j int) bool {
|
||||
return sr.Records[i].MetricName < sr.Records[j].MetricName
|
||||
})
|
||||
}
|
||||
|
||||
// DeduplicateMergeRecords performs merging duplicate records by metric name
|
||||
//
|
||||
// It is usual case for global tenant request at cluster version.
|
||||
func (sr *StatsResult) DeduplicateMergeRecords() {
|
||||
if len(sr.Records) < 2 {
|
||||
return
|
||||
}
|
||||
tmp := sr.Records[:0]
|
||||
// deduplication uses sliding indexes
|
||||
//
|
||||
// records:
|
||||
// [ 0 1 2 3 4 5 6 ]
|
||||
//
|
||||
// [ mn1, mn2, mn2, mn2, mn3, mn4, mn4 ]
|
||||
//
|
||||
// 0 1
|
||||
// 0 2
|
||||
// 2 3
|
||||
// 2 4
|
||||
// 2 5
|
||||
// 5 6
|
||||
//
|
||||
// result:
|
||||
//
|
||||
// [0,1,4,5]
|
||||
|
||||
i := 0
|
||||
j := 1
|
||||
rCurr := sr.Records[i]
|
||||
rNext := sr.Records[j]
|
||||
for {
|
||||
if rCurr.MetricName == rNext.MetricName {
|
||||
rCurr.RequestsCount += rNext.RequestsCount
|
||||
if rCurr.LastRequestTs < rNext.LastRequestTs {
|
||||
rCurr.LastRequestTs = rNext.LastRequestTs
|
||||
}
|
||||
j++
|
||||
if j >= len(sr.Records) {
|
||||
tmp = append(tmp, rCurr)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
tmp = append(tmp, rCurr)
|
||||
i = j
|
||||
rCurr = sr.Records[i]
|
||||
j++
|
||||
if j >= len(sr.Records) {
|
||||
tmp = append(tmp, rNext)
|
||||
break
|
||||
}
|
||||
}
|
||||
rNext = sr.Records[j]
|
||||
}
|
||||
sr.Records = tmp
|
||||
}
|
||||
|
||||
// Sort sorts records by metric name and requests count
|
||||
func (sr *StatsResult) Sort() {
|
||||
sort.Slice(sr.Records, func(i, j int) bool {
|
||||
if sr.Records[i].RequestsCount == sr.Records[j].RequestsCount {
|
||||
return sr.Records[i].MetricName < sr.Records[j].MetricName
|
||||
}
|
||||
return sr.Records[i].RequestsCount < sr.Records[j].RequestsCount
|
||||
})
|
||||
}
|
||||
|
||||
// Merge adds records from given src
|
||||
//
|
||||
// It expected src to be sorted by metricName
|
||||
func (sr *StatsResult) Merge(src *StatsResult) {
|
||||
if sr.CollectedSinceTs < src.CollectedSinceTs {
|
||||
sr.CollectedSinceTs = src.CollectedSinceTs
|
||||
}
|
||||
sr.TotalRecords += src.TotalRecords
|
||||
sr.CurrentSizeBytes += src.CurrentSizeBytes
|
||||
sr.MaxSizeBytes += src.MaxSizeBytes
|
||||
|
||||
if len(src.Records) == 0 {
|
||||
return
|
||||
}
|
||||
if len(sr.Records) == 0 {
|
||||
sr.Records = append(sr.Records, src.Records...)
|
||||
return
|
||||
}
|
||||
// merge sorted elements into new slice
|
||||
// records:
|
||||
// [ mn1, mn2, mn3, mn4, mn6 ]
|
||||
// [ mn2, mn4, mn5 ]
|
||||
// 0
|
||||
// 0
|
||||
// [ ]
|
||||
// 1
|
||||
// 0
|
||||
// [ mn1 ]
|
||||
// 2
|
||||
// 1
|
||||
// [ mn1, mn2 ]
|
||||
// 3
|
||||
// 1
|
||||
// [ mn1, mn2, mn3 ]
|
||||
// 4
|
||||
// 2
|
||||
// [ mn1, mn2, mn3, mn4 ]
|
||||
// 4
|
||||
// -
|
||||
// [ mn1, mn2, mn3, mn4, mn5 ]
|
||||
//
|
||||
// [ mn1, mn2, mn3, mn4, mn5, mn6 ]
|
||||
i := 0
|
||||
j := 0
|
||||
// TODO: probably, we can append src records to sr instead of allocating new slice
|
||||
// it will require to perform sort on sr and probably will use more CPU, but less memory
|
||||
result := make([]StatRecord, 0, len(sr.Records))
|
||||
for {
|
||||
if i >= len(sr.Records) {
|
||||
result = append(result, src.Records[j:]...)
|
||||
break
|
||||
}
|
||||
if j >= len(src.Records) {
|
||||
result = append(result, sr.Records[i:]...)
|
||||
break
|
||||
}
|
||||
left, right := sr.Records[i], src.Records[j]
|
||||
switch {
|
||||
case left.MetricName == right.MetricName:
|
||||
left.RequestsCount += right.RequestsCount
|
||||
if left.LastRequestTs < right.LastRequestTs {
|
||||
left.LastRequestTs = right.LastRequestTs
|
||||
}
|
||||
result = append(result, left)
|
||||
i++
|
||||
j++
|
||||
case left.MetricName < right.MetricName:
|
||||
result = append(result, left)
|
||||
i++
|
||||
case left.MetricName > right.MetricName:
|
||||
result = append(result, right)
|
||||
j++
|
||||
}
|
||||
}
|
||||
sr.Records = result
|
||||
}
|
||||
@@ -1,564 +0,0 @@
|
||||
package metricnamestats
|
||||
|
||||
import (
|
||||
"path"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
)
|
||||
|
||||
var statsResultCmpOpts = cmpopts.IgnoreFields(StatsResult{}, "CollectedSinceTs", "MaxSizeBytes", "CurrentSizeBytes")
|
||||
|
||||
func TestMetricsTracker(t *testing.T) {
|
||||
type testOp struct {
|
||||
aID uint32
|
||||
pID uint32
|
||||
o byte
|
||||
mg string
|
||||
ts uint64
|
||||
}
|
||||
type queryOpts struct {
|
||||
accountID uint32
|
||||
projectID uint32
|
||||
isTenantEmpty bool
|
||||
limit int
|
||||
lte int
|
||||
matchPattern string
|
||||
}
|
||||
cmpOpts := cmpopts.IgnoreFields(StatsResult{}, "CollectedSinceTs", "MaxSizeBytes", "CurrentSizeBytes")
|
||||
cachePath := path.Join(t.TempDir(), t.Name())
|
||||
f := func(ops []testOp, qo queryOpts, expected StatsResult) {
|
||||
t.Helper()
|
||||
expected.sort()
|
||||
mt, err := loadFrom(cachePath, 100_000)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot load state from disk on init: %s", err)
|
||||
}
|
||||
for _, op := range ops {
|
||||
mt.getCurrentTs = func() uint64 {
|
||||
return op.ts
|
||||
}
|
||||
switch op.o {
|
||||
case 'i':
|
||||
mt.RegisterIngestRequest(op.aID, op.pID, []byte(op.mg))
|
||||
case 'r':
|
||||
mt.RegisterQueryRequest(op.aID, op.pID, []byte(op.mg))
|
||||
}
|
||||
}
|
||||
var got StatsResult
|
||||
if qo.isTenantEmpty {
|
||||
got = mt.GetStats(qo.limit, qo.lte, qo.matchPattern)
|
||||
got.sort()
|
||||
got.DeduplicateMergeRecords()
|
||||
} else {
|
||||
got = mt.GetStatsForTenant(qo.accountID, qo.projectID, qo.limit, qo.lte, qo.matchPattern)
|
||||
got.sort()
|
||||
}
|
||||
if !cmp.Equal(expected, got, cmpOpts) {
|
||||
t.Fatalf("unexpected GetStatsForTenant result: %s", cmp.Diff(expected, got, cmpOpts))
|
||||
}
|
||||
if err := mt.saveLocked(); err != nil {
|
||||
t.Fatalf("cannot save in-memory state: %s", err)
|
||||
}
|
||||
loadedUmt, err := loadFrom(cachePath, 100_000)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot load restore state from disk: %s", err)
|
||||
}
|
||||
if qo.isTenantEmpty {
|
||||
got = loadedUmt.GetStats(qo.limit, qo.lte, qo.matchPattern)
|
||||
got.sort()
|
||||
got.DeduplicateMergeRecords()
|
||||
} else {
|
||||
got = loadedUmt.GetStatsForTenant(qo.accountID, qo.projectID, qo.limit, qo.lte, qo.matchPattern)
|
||||
got.sort()
|
||||
}
|
||||
if !cmp.Equal(expected, got, cmpOpts) {
|
||||
t.Fatalf("unexpected GetStatsForTenant result after load state from disk: %s", cmp.Diff(expected, got, cmpOpts))
|
||||
}
|
||||
mt.Reset(func() {})
|
||||
}
|
||||
|
||||
dataSet := []testOp{
|
||||
{1, 1, 'i', "metric_1", 1},
|
||||
{1, 1, 'i', "metric_1", 1},
|
||||
{1, 1, 'r', "metric_1", 1},
|
||||
{1, 1, 'i', "metric_2", 1},
|
||||
{1, 1, 'r', "metric_2", 1},
|
||||
{1, 1, 'r', "metric_2", 1},
|
||||
{15, 15, 'i', "metric_1", 1},
|
||||
{15, 15, 'i', "metric_2", 1},
|
||||
{15, 15, 'i', "metric_3", 1},
|
||||
{15, 15, 'r', "metric_3", 1},
|
||||
{15, 15, 'r', "metric_2", 1},
|
||||
}
|
||||
qOpts := queryOpts{
|
||||
limit: 100,
|
||||
lte: -1,
|
||||
}
|
||||
// query empty tenant
|
||||
expected := StatsResult{
|
||||
TotalRecords: 5,
|
||||
}
|
||||
f(dataSet, qOpts, expected)
|
||||
|
||||
// query single tenant
|
||||
qOpts = queryOpts{
|
||||
accountID: 1,
|
||||
projectID: 1,
|
||||
limit: 100,
|
||||
lte: -1,
|
||||
}
|
||||
expected = StatsResult{
|
||||
TotalRecords: 5,
|
||||
Records: []StatRecord{
|
||||
{"metric_1", 1, 1},
|
||||
{"metric_2", 2, 1},
|
||||
},
|
||||
}
|
||||
f(dataSet, qOpts, expected)
|
||||
|
||||
// query all tenants
|
||||
qOpts = queryOpts{
|
||||
isTenantEmpty: true,
|
||||
limit: 100,
|
||||
lte: -1,
|
||||
}
|
||||
expected = StatsResult{
|
||||
TotalRecords: 5,
|
||||
Records: []StatRecord{
|
||||
{"metric_1", 1, 1},
|
||||
{"metric_2", 3, 1},
|
||||
{"metric_3", 1, 1},
|
||||
},
|
||||
}
|
||||
f(dataSet, qOpts, expected)
|
||||
}
|
||||
|
||||
func TestMetricsTrackerConcurrent(t *testing.T) {
|
||||
type testOp struct {
|
||||
o byte
|
||||
mg string
|
||||
}
|
||||
const concurrency = 3
|
||||
f := func(ops []testOp, predicate int, expected StatsResult) {
|
||||
t.Helper()
|
||||
umt, err := loadFrom(t.TempDir()+t.Name(), 1024)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot load: %s", err)
|
||||
}
|
||||
umt.creationTs.Store(0)
|
||||
umt.getCurrentTs = func() uint64 { return 1 }
|
||||
for _, op := range ops {
|
||||
switch op.o {
|
||||
case 'i':
|
||||
umt.RegisterIngestRequest(0, 0, []byte(op.mg))
|
||||
case 'r':
|
||||
umt.RegisterQueryRequest(0, 0, []byte(op.mg))
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range concurrency {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for _, op := range ops {
|
||||
switch op.o {
|
||||
case 'i':
|
||||
umt.RegisterIngestRequest(0, 0, []byte(op.mg))
|
||||
case 'r':
|
||||
umt.RegisterQueryRequest(0, 0, []byte(op.mg))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
got := umt.GetStats(100, predicate, "")
|
||||
got.sort()
|
||||
expected.sort()
|
||||
if !cmp.Equal(expected.Records, got.Records) {
|
||||
t.Fatalf("unexpected unusedMetricNames result: %s", cmp.Diff(expected.Records, got.Records))
|
||||
}
|
||||
}
|
||||
f([]testOp{{'i', "metric_1"}, {'r', "metric_2"}, {'r', "metric_1"}, {'i', "metric_3"}},
|
||||
0,
|
||||
StatsResult{
|
||||
Records: []StatRecord{
|
||||
{
|
||||
MetricName: "metric_3",
|
||||
},
|
||||
},
|
||||
})
|
||||
f([]testOp{{'i', "metric_1"}, {'i', "metric_2"}, {'r', "metric_2"}, {'r', "metric_2"}, {'r', "metric_1"}, {'i', "metric_3"}},
|
||||
10,
|
||||
StatsResult{
|
||||
Records: []StatRecord{
|
||||
{
|
||||
MetricName: "metric_1",
|
||||
RequestsCount: 1 + concurrency,
|
||||
LastRequestTs: 1,
|
||||
},
|
||||
{
|
||||
MetricName: "metric_2",
|
||||
RequestsCount: 2 + 2*concurrency,
|
||||
LastRequestTs: 1,
|
||||
},
|
||||
{
|
||||
MetricName: "metric_3",
|
||||
LastRequestTs: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestMetricsTrackerMaxSize(t *testing.T) {
|
||||
type testOp struct {
|
||||
o byte
|
||||
mg string
|
||||
}
|
||||
|
||||
umt, err := loadFrom(t.TempDir()+t.Name(), storeOverhead+10*2)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot load tracker: %s", err)
|
||||
}
|
||||
umt.getCurrentTs = func() uint64 { return 1 }
|
||||
ops := []testOp{
|
||||
{'i', "metric_1"},
|
||||
{'r', "metric_2"},
|
||||
{'r', "metric_1"},
|
||||
{'i', "metric_2"},
|
||||
{'i', "metric_3"},
|
||||
{'i', "metric_4"},
|
||||
{'r', "metric_1"},
|
||||
{'r', "metric_2"},
|
||||
{'r', "metric_2"},
|
||||
{'r', "metric_2"},
|
||||
}
|
||||
for _, op := range ops {
|
||||
switch op.o {
|
||||
case 'i':
|
||||
umt.RegisterIngestRequest(0, 0, []byte(op.mg))
|
||||
case 'r':
|
||||
umt.RegisterQueryRequest(0, 0, []byte(op.mg))
|
||||
}
|
||||
}
|
||||
got := umt.GetStats(100, -1, "")
|
||||
got.sort()
|
||||
expected := StatsResult{
|
||||
Records: []StatRecord{
|
||||
{
|
||||
MetricName: "metric_1",
|
||||
RequestsCount: 2,
|
||||
LastRequestTs: 1,
|
||||
},
|
||||
{
|
||||
MetricName: "metric_2",
|
||||
RequestsCount: 3,
|
||||
LastRequestTs: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if !cmp.Equal(expected.Records, got.Records) {
|
||||
t.Fatalf("unexpected unusedMetricNames result: %s", cmp.Diff(expected.Records, got.Records))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeduplicateRecords(t *testing.T) {
|
||||
f := func(result StatsResult, expected StatsResult) {
|
||||
t.Helper()
|
||||
expected.sort()
|
||||
result.sort()
|
||||
result.DeduplicateMergeRecords()
|
||||
if !cmp.Equal(result, expected, statsResultCmpOpts) {
|
||||
t.Fatalf("unexpected deduplicate result: %s", cmp.Diff(result, expected, statsResultCmpOpts))
|
||||
}
|
||||
}
|
||||
|
||||
// single record
|
||||
dataSet := StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
},
|
||||
}
|
||||
expected := StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
},
|
||||
}
|
||||
f(dataSet, expected)
|
||||
|
||||
// no duplicates
|
||||
dataSet = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
{MetricName: "mn2", RequestsCount: 12, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 13, LastRequestTs: 3},
|
||||
{MetricName: "mn4", RequestsCount: 15, LastRequestTs: 4},
|
||||
},
|
||||
}
|
||||
expected = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
{MetricName: "mn2", RequestsCount: 12, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 13, LastRequestTs: 3},
|
||||
{MetricName: "mn4", RequestsCount: 15, LastRequestTs: 4},
|
||||
},
|
||||
}
|
||||
f(dataSet, expected)
|
||||
|
||||
// 2 duplicates
|
||||
dataSet = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
},
|
||||
}
|
||||
expected = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 20, LastRequestTs: 1},
|
||||
},
|
||||
}
|
||||
f(dataSet, expected)
|
||||
|
||||
// duplicates on start
|
||||
dataSet = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
{MetricName: "mn2", RequestsCount: 12, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 13, LastRequestTs: 3},
|
||||
{MetricName: "mn4", RequestsCount: 15, LastRequestTs: 4},
|
||||
},
|
||||
}
|
||||
expected = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 20, LastRequestTs: 1},
|
||||
{MetricName: "mn2", RequestsCount: 12, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 13, LastRequestTs: 3},
|
||||
{MetricName: "mn4", RequestsCount: 15, LastRequestTs: 4},
|
||||
},
|
||||
}
|
||||
f(dataSet, expected)
|
||||
|
||||
// duplicates on end
|
||||
dataSet = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
{MetricName: "mn2", RequestsCount: 12, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 13, LastRequestTs: 3},
|
||||
{MetricName: "mn4", RequestsCount: 15, LastRequestTs: 4},
|
||||
{MetricName: "mn4", RequestsCount: 15, LastRequestTs: 4},
|
||||
},
|
||||
}
|
||||
expected = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
{MetricName: "mn2", RequestsCount: 12, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 13, LastRequestTs: 3},
|
||||
{MetricName: "mn4", RequestsCount: 30, LastRequestTs: 4},
|
||||
},
|
||||
}
|
||||
f(dataSet, expected)
|
||||
|
||||
// duplicates start end
|
||||
dataSet = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
{MetricName: "mn2", RequestsCount: 12, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 13, LastRequestTs: 3},
|
||||
{MetricName: "mn4", RequestsCount: 15, LastRequestTs: 4},
|
||||
{MetricName: "mn4", RequestsCount: 15, LastRequestTs: 4},
|
||||
},
|
||||
}
|
||||
expected = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 20, LastRequestTs: 1},
|
||||
{MetricName: "mn2", RequestsCount: 12, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 13, LastRequestTs: 3},
|
||||
{MetricName: "mn4", RequestsCount: 30, LastRequestTs: 4},
|
||||
},
|
||||
}
|
||||
f(dataSet, expected)
|
||||
|
||||
// duplicates mixed
|
||||
dataSet = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 1},
|
||||
{MetricName: "mn2", RequestsCount: 12, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 10, LastRequestTs: 3},
|
||||
{MetricName: "mn3", RequestsCount: 10, LastRequestTs: 3},
|
||||
{MetricName: "mn4", RequestsCount: 15, LastRequestTs: 4},
|
||||
{MetricName: "mn4", RequestsCount: 15, LastRequestTs: 4},
|
||||
{MetricName: "mn5", RequestsCount: 15, LastRequestTs: 4},
|
||||
},
|
||||
}
|
||||
expected = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 20, LastRequestTs: 1},
|
||||
{MetricName: "mn2", RequestsCount: 12, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 30, LastRequestTs: 3},
|
||||
{MetricName: "mn4", RequestsCount: 30, LastRequestTs: 4},
|
||||
{MetricName: "mn5", RequestsCount: 15, LastRequestTs: 4},
|
||||
},
|
||||
}
|
||||
f(dataSet, expected)
|
||||
}
|
||||
|
||||
func TestStatsResultMerge(t *testing.T) {
|
||||
f := func(left, right StatsResult, expected StatsResult) {
|
||||
t.Helper()
|
||||
expected.sort()
|
||||
left.sort()
|
||||
right.sort()
|
||||
left.Merge(&right)
|
||||
if !cmp.Equal(left, expected, statsResultCmpOpts) {
|
||||
t.Fatalf("unexpected deduplicate result: %s", cmp.Diff(left, expected, statsResultCmpOpts))
|
||||
}
|
||||
}
|
||||
|
||||
// empty src
|
||||
dst := StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
src := StatsResult{}
|
||||
expected := StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
f(dst, src, expected)
|
||||
|
||||
// empty dst
|
||||
dst = StatsResult{}
|
||||
src = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
expected = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
f(dst, src, expected)
|
||||
|
||||
// all duplicates
|
||||
dst = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn2", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 30, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
src = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn2", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 30, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
expected = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn2", RequestsCount: 40, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 60, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
f(dst, src, expected)
|
||||
|
||||
// no duplicates
|
||||
dst = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn2", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 30, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
src = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn4", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn5", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn6", RequestsCount: 30, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
expected = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn2", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 30, LastRequestTs: 2},
|
||||
{MetricName: "mn4", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn5", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn6", RequestsCount: 30, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
f(dst, src, expected)
|
||||
|
||||
// mixed
|
||||
dst = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 30, LastRequestTs: 2},
|
||||
{MetricName: "mn4", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn5", RequestsCount: 20, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
src = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn2", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn5", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn6", RequestsCount: 30, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
expected = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn2", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 30, LastRequestTs: 2},
|
||||
{MetricName: "mn4", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn5", RequestsCount: 40, LastRequestTs: 2},
|
||||
{MetricName: "mn6", RequestsCount: 30, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
f(dst, src, expected)
|
||||
|
||||
// mixed
|
||||
dst = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 30, LastRequestTs: 2},
|
||||
{MetricName: "mn4", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn5", RequestsCount: 20, LastRequestTs: 1},
|
||||
{MetricName: "mn6", RequestsCount: 30, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
src = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn2", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn5", RequestsCount: 20, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
expected = StatsResult{
|
||||
Records: []StatRecord{
|
||||
{MetricName: "mn1", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn2", RequestsCount: 20, LastRequestTs: 2},
|
||||
{MetricName: "mn3", RequestsCount: 30, LastRequestTs: 2},
|
||||
{MetricName: "mn4", RequestsCount: 10, LastRequestTs: 2},
|
||||
{MetricName: "mn5", RequestsCount: 40, LastRequestTs: 2},
|
||||
{MetricName: "mn6", RequestsCount: 30, LastRequestTs: 2},
|
||||
},
|
||||
}
|
||||
f(dst, src, expected)
|
||||
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package metricnamestats
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func BenchmarkTracker(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
mt := MustLoadFrom("testdata/"+b.Name(), 100_000_000)
|
||||
mt.getCurrentTs = func() uint64 {
|
||||
return 1
|
||||
}
|
||||
type testOp struct {
|
||||
t byte
|
||||
metricName []byte
|
||||
}
|
||||
dataSet := []testOp{
|
||||
{'i', []byte("metric_2")},
|
||||
{'i', []byte("metric_3")},
|
||||
{'i', []byte("metric_3")},
|
||||
{'i', []byte("metric_4")},
|
||||
{'r', []byte("metric_3")},
|
||||
{'r', []byte("metric_3")},
|
||||
{'r', []byte("metric_3")},
|
||||
{'i', []byte("metric_1")},
|
||||
{'r', []byte("metric_1")},
|
||||
}
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
for _, op := range dataSet {
|
||||
switch op.t {
|
||||
case 'i':
|
||||
mt.RegisterIngestRequest(0, 0, op.metricName)
|
||||
case 'r':
|
||||
mt.RegisterQueryRequest(0, 0, op.metricName)
|
||||
}
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
got := mt.GetStats(100, -1, "")
|
||||
got.sort()
|
||||
expected := StatsResult{
|
||||
TotalRecords: 4,
|
||||
Records: []StatRecord{
|
||||
{"metric_2", 0, 0},
|
||||
{"metric_4", 0, 0},
|
||||
{"metric_1", uint64(b.N), 1},
|
||||
{"metric_3", 3 * uint64(b.N), 1},
|
||||
},
|
||||
}
|
||||
expected.sort()
|
||||
if !cmp.Equal(expected, got, statsResultCmpOpts) {
|
||||
b.Fatalf("unexpected result: %s", cmp.Diff(expected, got, statsResultCmpOpts))
|
||||
}
|
||||
}
|
||||
@@ -118,9 +118,6 @@ type Search struct {
|
||||
loops int
|
||||
|
||||
prevMetricID uint64
|
||||
|
||||
// metricGroupBuf holds metricGroup used for metric names tracker
|
||||
metricGroupBuf []byte
|
||||
}
|
||||
|
||||
func (s *Search) reset() {
|
||||
@@ -137,7 +134,6 @@ func (s *Search) reset() {
|
||||
s.needClosing = false
|
||||
s.loops = 0
|
||||
s.prevMetricID = 0
|
||||
s.metricGroupBuf = nil
|
||||
}
|
||||
|
||||
// Init initializes s from the given storage, tfss and tr.
|
||||
@@ -228,18 +224,6 @@ func (s *Search) NextMetricBlock() bool {
|
||||
// It should be automatically fixed. See indexDB.searchMetricNameWithCache for details.
|
||||
continue
|
||||
}
|
||||
// for perfomance reasons parse metricGroup conditionally
|
||||
if s.idb.s.metricsTracker != nil {
|
||||
var err error
|
||||
// MetricName must be sorted and marshalled with MetricName.Marshal()
|
||||
// it guarantees that first tag is metricGroup
|
||||
_, s.metricGroupBuf, err = unmarshalTagValue(s.metricGroupBuf[:0], s.MetricBlockRef.MetricName)
|
||||
if err != nil {
|
||||
s.err = fmt.Errorf("cannot unmarshal metricGroup from MetricBlockRef.MetricName: %w", err)
|
||||
return false
|
||||
}
|
||||
s.idb.s.metricsTracker.RegisterQueryRequest(0, 0, s.metricGroupBuf)
|
||||
}
|
||||
s.prevMetricID = tsid.MetricID
|
||||
}
|
||||
s.MetricBlockRef.BlockRef = s.ts.BlockRef
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/snapshot/snapshotutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage/metricnamestats"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/uint64set"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/workingsetcache"
|
||||
@@ -166,17 +165,14 @@ type Storage struct {
|
||||
|
||||
// isReadOnly is set to true when the storage is in read-only mode.
|
||||
isReadOnly atomic.Bool
|
||||
|
||||
metricsTracker *metricnamestats.Tracker
|
||||
}
|
||||
|
||||
// OpenOptions optional args for MustOpenStorage
|
||||
type OpenOptions struct {
|
||||
Retention time.Duration
|
||||
MaxHourlySeries int
|
||||
MaxDailySeries int
|
||||
DisablePerDayIndex bool
|
||||
TrackMetricNamesStats bool
|
||||
Retention time.Duration
|
||||
MaxHourlySeries int
|
||||
MaxDailySeries int
|
||||
DisablePerDayIndex bool
|
||||
}
|
||||
|
||||
// MustOpenStorage opens storage on the given path with the given retentionMsecs.
|
||||
@@ -248,16 +244,6 @@ func MustOpenStorage(path string, opts OpenOptions) *Storage {
|
||||
s.pendingNextDayMetricIDs = &uint64set.Set{}
|
||||
|
||||
s.prefetchedMetricIDs = &uint64set.Set{}
|
||||
if opts.TrackMetricNamesStats {
|
||||
mnt := metricnamestats.MustLoadFrom(filepath.Join(s.cachePath, "metric_usage_tracker"), uint64(getMetricNamesStatsCacheSize()))
|
||||
s.metricsTracker = mnt
|
||||
if mnt.IsEmpty() {
|
||||
// metric names tracker performs attemp to track timeseries during ingestion only at tsid cache miss.
|
||||
// It allows to do not decrease storage performance.
|
||||
logger.Infof("reseting tsidCache in order to properly track metric names stats usage")
|
||||
s.tsidCache.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Load metadata
|
||||
metadataDir := filepath.Join(path, metadataDirname)
|
||||
@@ -333,20 +319,6 @@ func getTSIDCacheSize() int {
|
||||
return maxTSIDCacheSize
|
||||
}
|
||||
|
||||
var maxMetricNamesStatsCacheSize int
|
||||
|
||||
// SetMetricNamesStatsCacheSize overrides the default size of storage/metricNamesStatsTracker
|
||||
func SetMetricNamesStatsCacheSize(size int) {
|
||||
maxMetricNamesStatsCacheSize = size
|
||||
}
|
||||
|
||||
func getMetricNamesStatsCacheSize() int {
|
||||
if maxMetricNamesStatsCacheSize <= 0 {
|
||||
return memory.Allowed() / 100
|
||||
}
|
||||
return maxMetricNamesStatsCacheSize
|
||||
}
|
||||
|
||||
func (s *Storage) getDeletedMetricIDs() *uint64set.Set {
|
||||
return s.deletedMetricIDs.Load()
|
||||
}
|
||||
@@ -589,10 +561,6 @@ type Metrics struct {
|
||||
|
||||
NextRetentionSeconds uint64
|
||||
|
||||
MetricNamesUsageTrackerSize uint64
|
||||
MetricNamesUsageTrackerSizeBytes uint64
|
||||
MetricNamesUsageTrackerSizeMaxBytes uint64
|
||||
|
||||
IndexDBMetrics IndexDBMetrics
|
||||
TableMetrics TableMetrics
|
||||
}
|
||||
@@ -687,12 +655,6 @@ func (s *Storage) UpdateMetrics(m *Metrics) {
|
||||
m.PrefetchedMetricIDsSizeBytes += uint64(prefetchedMetricIDs.SizeBytes())
|
||||
s.prefetchedMetricIDsLock.Unlock()
|
||||
|
||||
var tm metricnamestats.TrackerMetrics
|
||||
s.metricsTracker.UpdateMetrics(&tm)
|
||||
m.MetricNamesUsageTrackerSizeBytes = tm.CurrentSizeBytes
|
||||
m.MetricNamesUsageTrackerSize = tm.CurrentItemsCount
|
||||
m.MetricNamesUsageTrackerSizeMaxBytes = tm.MaxSizeBytes
|
||||
|
||||
d := s.nextRetentionSeconds()
|
||||
if d < 0 {
|
||||
d = 0
|
||||
@@ -942,7 +904,6 @@ func (s *Storage) MustClose() {
|
||||
nextDayMetricIDs := s.nextDayMetricIDs.Load()
|
||||
s.mustSaveNextDayMetricIDs(nextDayMetricIDs)
|
||||
|
||||
s.metricsTracker.MustClose()
|
||||
// Release lock file.
|
||||
fs.MustClose(s.flockF)
|
||||
s.flockF = nil
|
||||
@@ -2067,11 +2028,6 @@ func (s *Storage) add(rows []rawRow, dstMrs []*MetricRow, mrs []MetricRow, preci
|
||||
mn.sortTags()
|
||||
metricNameBuf = mn.Marshal(metricNameBuf[:0])
|
||||
|
||||
// register metric name on tsid cache miss
|
||||
// it allows to track metric names since last tsid cache reset
|
||||
// and skip index scan to fill metrics tracker
|
||||
s.metricsTracker.RegisterIngestRequest(0, 0, mn.MetricGroup)
|
||||
|
||||
// Search for TSID for the given mr.MetricNameRaw in the indexdb.
|
||||
if is.getTSIDByMetricName(&genTSID, metricNameBuf, date) {
|
||||
// Slower path - the TSID has been found in indexdb.
|
||||
@@ -2131,7 +2087,6 @@ func (s *Storage) add(rows []rawRow, dstMrs []*MetricRow, mrs []MetricRow, preci
|
||||
firstWarn = fmt.Errorf("cannot prefill next indexdb: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.updatePerDateData(rows, dstMrs); err != nil {
|
||||
if firstWarn == nil {
|
||||
firstWarn = fmt.Errorf("cannot not update per-day index: %w", err)
|
||||
@@ -2890,16 +2845,3 @@ func (s *Storage) wasMetricIDMissingBefore(metricID uint64) bool {
|
||||
}
|
||||
return ct > deleteDeadline
|
||||
}
|
||||
|
||||
// MetricNamesStatsResponse contains metric names usage stats API response
|
||||
type MetricNamesStatsResponse = metricnamestats.StatsResult
|
||||
|
||||
// GetMetricNamesStats returns metric names usage stats with given limit and le predicate
|
||||
func (s *Storage) GetMetricNamesStats(_ *querytracer.Tracer, limit, le int, matchPattern string) MetricNamesStatsResponse {
|
||||
return s.metricsTracker.GetStats(limit, le, matchPattern)
|
||||
}
|
||||
|
||||
// ResetMetricNamesStats resets state for metric names usage tracker
|
||||
func (s *Storage) ResetMetricNamesStats(_ *querytracer.Tracer) {
|
||||
s.metricsTracker.Reset(s.tsidCache.Reset)
|
||||
}
|
||||
|
||||
@@ -2892,51 +2892,3 @@ func testGenerateMetricRowBatches(opts *batchOptions) ([][]MetricRow, *counts) {
|
||||
}
|
||||
return batches, &want
|
||||
}
|
||||
|
||||
func TestStorageMetricTracker(t *testing.T) {
|
||||
defer testRemoveAll(t)
|
||||
rng := rand.New(rand.NewSource(1))
|
||||
numRows := uint64(1000)
|
||||
minTimestamp := time.Now().UnixMilli()
|
||||
maxTimestamp := minTimestamp + 1000
|
||||
mrs := testGenerateMetricRows(rng, numRows, minTimestamp, maxTimestamp)
|
||||
|
||||
var gotMetrics Metrics
|
||||
s := MustOpenStorage(t.Name(), OpenOptions{TrackMetricNamesStats: true})
|
||||
defer s.MustClose()
|
||||
s.AddRows(mrs, defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
s.UpdateMetrics(&gotMetrics)
|
||||
|
||||
var sr Search
|
||||
tr := TimeRange{
|
||||
MinTimestamp: minTimestamp,
|
||||
MaxTimestamp: maxTimestamp,
|
||||
}
|
||||
|
||||
// check stats for metrics with 0 requests count
|
||||
mus := s.GetMetricNamesStats(nil, 10_000, 0, "")
|
||||
if len(mus.Records) != int(numRows) {
|
||||
t.Fatalf("unexpected Stats records count=%d, want %d records", len(mus.Records), numRows)
|
||||
}
|
||||
|
||||
// search query for all ingested metrics
|
||||
tfs := NewTagFilters()
|
||||
if err := tfs.Add(nil, []byte("metric_.+"), false, true); err != nil {
|
||||
t.Fatalf("unexpected error at tfs add: %s", err)
|
||||
}
|
||||
|
||||
sr.Init(nil, s, []*TagFilters{tfs}, tr, 1e5, noDeadline)
|
||||
for sr.NextMetricBlock() {
|
||||
}
|
||||
sr.MustClose()
|
||||
|
||||
mus = s.GetMetricNamesStats(nil, 10_000, 0, "")
|
||||
if len(mus.Records) != 0 {
|
||||
t.Fatalf("unexpected Stats records count=%d; want 0 records", len(mus.Records))
|
||||
}
|
||||
mus = s.GetMetricNamesStats(nil, 10_000, 1, "")
|
||||
if len(mus.Records) != int(numRows) {
|
||||
t.Fatalf("unexpected Stats records count=%d, want %d records", len(mus.Records), numRows)
|
||||
}
|
||||
}
|
||||
|
||||
5
vendor/github.com/VictoriaMetrics/metricsql/optimizer.go
generated
vendored
5
vendor/github.com/VictoriaMetrics/metricsql/optimizer.go
generated
vendored
@@ -149,11 +149,6 @@ func getCommonLabelFilters(e Expr) []LabelFilter {
|
||||
// {f1} unless on(f1, f2) {f2} -> {f1}
|
||||
// {f1} unless on(f3) {f2} -> {}
|
||||
return TrimFiltersByGroupModifier(lfsLeft, t)
|
||||
case "ifnot":
|
||||
// remove right from left, so filter in left can be pushed down to right.
|
||||
// {f1} ifnot `any` -> {f1}
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8435
|
||||
return TrimFiltersByGroupModifier(lfsLeft, t)
|
||||
default:
|
||||
switch strings.ToLower(t.JoinModifier.Op) {
|
||||
case "group_left":
|
||||
|
||||
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@@ -119,7 +119,7 @@ github.com/VictoriaMetrics/fastcache
|
||||
# github.com/VictoriaMetrics/metrics v1.35.2
|
||||
## explicit; go 1.17
|
||||
github.com/VictoriaMetrics/metrics
|
||||
# github.com/VictoriaMetrics/metricsql v0.84.1
|
||||
# github.com/VictoriaMetrics/metricsql v0.84.0
|
||||
## explicit; go 1.13
|
||||
github.com/VictoriaMetrics/metricsql
|
||||
github.com/VictoriaMetrics/metricsql/binaryop
|
||||
|
||||
Reference in New Issue
Block a user