Merge branch 'public-single-node' into pmm-6401-read-prometheus-data-files

This commit is contained in:
Aliaksandr Valialkin 2021-02-26 23:00:40 +02:00
commit 0ad887fd4d
76 changed files with 3150 additions and 2095 deletions

View file

@ -605,7 +605,8 @@ and it is easier to use when migrating from Graphite to VictoriaMetrics.
### Graphite Render API usage ### Graphite Render API usage
[VictoriaMetrics Enterprise](https://victoriametrics.com/enterprise.html) supports [Graphite Render API](https://graphite.readthedocs.io/en/stable/render_api.html) subset [VictoriaMetrics Enterprise](https://victoriametrics.com/enterprise.html) supports [Graphite Render API](https://graphite.readthedocs.io/en/stable/render_api.html) subset
at `/render` endpoint. This subset is required for [Graphite datasource in Grafana](https://grafana.com/docs/grafana/latest/datasources/graphite/). at `/render` endpoint, which is used by [Graphite datasource in Grafana](https://grafana.com/docs/grafana/latest/datasources/graphite/).
It supports `Storage-Step` http request header, which must be set to a step between data points stored in VictoriaMetrics when configuring Graphite datasource in Grafana.
### Graphite Metrics API usage ### Graphite Metrics API usage

View file

@ -298,6 +298,16 @@ It may be useful for performing `vmagent` rolling update without scrape loss.
the url may contain sensitive information such as auth tokens or passwords. the url may contain sensitive information such as auth tokens or passwords.
Pass `-remoteWrite.showURL` command-line flag when starting `vmagent` in order to see all the valid urls. Pass `-remoteWrite.showURL` command-line flag when starting `vmagent` in order to see all the valid urls.
* If scrapes must be aligned in time (for instance, if they must be performed at the beginning of every hour), then set `scrape_align_interval` option
in the corresponding scrape config. For example, the following config aligns hourly scrapes to the nearest 10 minutes:
```yml
scrape_configs:
- job_name: foo
scrape_interval: 1h
scrape_align_interval: 10m
```
* If you see `skipping duplicate scrape target with identical labels` errors when scraping Kubernetes pods, then it is likely these pods listen multiple ports * If you see `skipping duplicate scrape target with identical labels` errors when scraping Kubernetes pods, then it is likely these pods listen multiple ports
or they use init container. These errors can be either fixed or suppressed with `-promscrape.suppressDuplicateScrapeTargetErrors` command-line flag. or they use init container. These errors can be either fixed or suppressed with `-promscrape.suppressDuplicateScrapeTargetErrors` command-line flag.
See available options below if you prefer fixing the root cause of the error: See available options below if you prefer fixing the root cause of the error:

View file

@ -41,7 +41,7 @@ func loadRelabelConfigs() (*relabelConfigs, error) {
return nil, fmt.Errorf("too many -remoteWrite.urlRelabelConfig args: %d; it mustn't exceed the number of -remoteWrite.url args: %d", return nil, fmt.Errorf("too many -remoteWrite.urlRelabelConfig args: %d; it mustn't exceed the number of -remoteWrite.url args: %d",
len(*relabelConfigPaths), len(*remoteWriteURLs)) len(*relabelConfigPaths), len(*remoteWriteURLs))
} }
rcs.perURL = make([][]promrelabel.ParsedRelabelConfig, len(*remoteWriteURLs)) rcs.perURL = make([]*promrelabel.ParsedConfigs, len(*remoteWriteURLs))
for i, path := range *relabelConfigPaths { for i, path := range *relabelConfigPaths {
if len(path) == 0 { if len(path) == 0 {
// Skip empty relabel config. // Skip empty relabel config.
@ -57,8 +57,8 @@ func loadRelabelConfigs() (*relabelConfigs, error) {
} }
type relabelConfigs struct { type relabelConfigs struct {
global []promrelabel.ParsedRelabelConfig global *promrelabel.ParsedConfigs
perURL [][]promrelabel.ParsedRelabelConfig perURL []*promrelabel.ParsedConfigs
} }
// initLabelsGlobal must be called after parsing command-line flags. // initLabelsGlobal must be called after parsing command-line flags.
@ -79,8 +79,8 @@ func initLabelsGlobal() {
} }
} }
func (rctx *relabelCtx) applyRelabeling(tss []prompbmarshal.TimeSeries, extraLabels []prompbmarshal.Label, prcs []promrelabel.ParsedRelabelConfig) []prompbmarshal.TimeSeries { func (rctx *relabelCtx) applyRelabeling(tss []prompbmarshal.TimeSeries, extraLabels []prompbmarshal.Label, pcs *promrelabel.ParsedConfigs) []prompbmarshal.TimeSeries {
if len(extraLabels) == 0 && len(prcs) == 0 { if len(extraLabels) == 0 && pcs.Len() == 0 {
// Nothing to change. // Nothing to change.
return tss return tss
} }
@ -100,7 +100,7 @@ func (rctx *relabelCtx) applyRelabeling(tss []prompbmarshal.TimeSeries, extraLab
labels = append(labels, *extraLabel) labels = append(labels, *extraLabel)
} }
} }
labels = promrelabel.ApplyRelabelConfigs(labels, labelsLen, prcs, true) labels = pcs.Apply(labels, labelsLen, true)
if len(labels) == labelsLen { if len(labels) == labelsLen {
// Drop the current time series, since relabeling removed all the labels. // Drop the current time series, since relabeling removed all the labels.
continue continue

View file

@ -142,8 +142,8 @@ func Stop() {
func Push(wr *prompbmarshal.WriteRequest) { func Push(wr *prompbmarshal.WriteRequest) {
var rctx *relabelCtx var rctx *relabelCtx
rcs := allRelabelConfigs.Load().(*relabelConfigs) rcs := allRelabelConfigs.Load().(*relabelConfigs)
prcsGlobal := rcs.global pcsGlobal := rcs.global
if len(prcsGlobal) > 0 || len(labelsGlobal) > 0 { if pcsGlobal.Len() > 0 || len(labelsGlobal) > 0 {
rctx = getRelabelCtx() rctx = getRelabelCtx()
} }
tss := wr.Timeseries tss := wr.Timeseries
@ -167,7 +167,7 @@ func Push(wr *prompbmarshal.WriteRequest) {
} }
if rctx != nil { if rctx != nil {
tssBlockLen := len(tssBlock) tssBlockLen := len(tssBlock)
tssBlock = rctx.applyRelabeling(tssBlock, labelsGlobal, prcsGlobal) tssBlock = rctx.applyRelabeling(tssBlock, labelsGlobal, pcsGlobal)
globalRelabelMetricsDropped.Add(tssBlockLen - len(tssBlock)) globalRelabelMetricsDropped.Add(tssBlockLen - len(tssBlock))
} }
for _, rwctx := range rwctxs { for _, rwctx := range rwctxs {
@ -227,6 +227,7 @@ func (rwctx *remoteWriteCtx) MustStop() {
} }
rwctx.idx = 0 rwctx.idx = 0
rwctx.pss = nil rwctx.pss = nil
rwctx.fq.UnblockAllReaders()
rwctx.c.MustStop() rwctx.c.MustStop()
rwctx.c = nil rwctx.c = nil
rwctx.fq.MustClose() rwctx.fq.MustClose()
@ -239,8 +240,8 @@ func (rwctx *remoteWriteCtx) Push(tss []prompbmarshal.TimeSeries) {
var rctx *relabelCtx var rctx *relabelCtx
var v *[]prompbmarshal.TimeSeries var v *[]prompbmarshal.TimeSeries
rcs := allRelabelConfigs.Load().(*relabelConfigs) rcs := allRelabelConfigs.Load().(*relabelConfigs)
prcs := rcs.perURL[rwctx.idx] pcs := rcs.perURL[rwctx.idx]
if len(prcs) > 0 { if pcs.Len() > 0 {
rctx = getRelabelCtx() rctx = getRelabelCtx()
// Make a copy of tss before applying relabeling in order to prevent // Make a copy of tss before applying relabeling in order to prevent
// from affecting time series for other remoteWrite.url configs. // from affecting time series for other remoteWrite.url configs.
@ -249,7 +250,7 @@ func (rwctx *remoteWriteCtx) Push(tss []prompbmarshal.TimeSeries) {
v = tssRelabelPool.Get().(*[]prompbmarshal.TimeSeries) v = tssRelabelPool.Get().(*[]prompbmarshal.TimeSeries)
tss = append(*v, tss...) tss = append(*v, tss...)
tssLen := len(tss) tssLen := len(tss)
tss = rctx.applyRelabeling(tss, nil, prcs) tss = rctx.applyRelabeling(tss, nil, pcs)
rwctx.relabelMetricsDropped.Add(tssLen - len(tss)) rwctx.relabelMetricsDropped.Add(tssLen - len(tss))
} }
pss := rwctx.pss pss := rwctx.pss

View file

@ -19,11 +19,11 @@ var relabelConfig = flag.String("relabelConfig", "", "Optional path to a file wi
// Init must be called after flag.Parse and before using the relabel package. // Init must be called after flag.Parse and before using the relabel package.
func Init() { func Init() {
prcs, err := loadRelabelConfig() pcs, err := loadRelabelConfig()
if err != nil { if err != nil {
logger.Fatalf("cannot load relabelConfig: %s", err) logger.Fatalf("cannot load relabelConfig: %s", err)
} }
prcsGlobal.Store(&prcs) pcsGlobal.Store(pcs)
if len(*relabelConfig) == 0 { if len(*relabelConfig) == 0 {
return return
} }
@ -31,34 +31,34 @@ func Init() {
go func() { go func() {
for range sighupCh { for range sighupCh {
logger.Infof("received SIGHUP; reloading -relabelConfig=%q...", *relabelConfig) logger.Infof("received SIGHUP; reloading -relabelConfig=%q...", *relabelConfig)
prcs, err := loadRelabelConfig() pcs, err := loadRelabelConfig()
if err != nil { if err != nil {
logger.Errorf("cannot load the updated relabelConfig: %s; preserving the previous config", err) logger.Errorf("cannot load the updated relabelConfig: %s; preserving the previous config", err)
continue continue
} }
prcsGlobal.Store(&prcs) pcsGlobal.Store(pcs)
logger.Infof("successfully reloaded -relabelConfig=%q", *relabelConfig) logger.Infof("successfully reloaded -relabelConfig=%q", *relabelConfig)
} }
}() }()
} }
var prcsGlobal atomic.Value var pcsGlobal atomic.Value
func loadRelabelConfig() ([]promrelabel.ParsedRelabelConfig, error) { func loadRelabelConfig() (*promrelabel.ParsedConfigs, error) {
if len(*relabelConfig) == 0 { if len(*relabelConfig) == 0 {
return nil, nil return nil, nil
} }
prcs, err := promrelabel.LoadRelabelConfigs(*relabelConfig) pcs, err := promrelabel.LoadRelabelConfigs(*relabelConfig)
if err != nil { if err != nil {
return nil, fmt.Errorf("error when reading -relabelConfig=%q: %w", *relabelConfig, err) return nil, fmt.Errorf("error when reading -relabelConfig=%q: %w", *relabelConfig, err)
} }
return prcs, nil return pcs, nil
} }
// HasRelabeling returns true if there is global relabeling. // HasRelabeling returns true if there is global relabeling.
func HasRelabeling() bool { func HasRelabeling() bool {
prcs := prcsGlobal.Load().(*[]promrelabel.ParsedRelabelConfig) pcs := pcsGlobal.Load().(*promrelabel.ParsedConfigs)
return len(*prcs) > 0 return pcs.Len() > 0
} }
// Ctx holds relabeling context. // Ctx holds relabeling context.
@ -77,8 +77,8 @@ func (ctx *Ctx) Reset() {
// //
// The returned labels are valid until the next call to ApplyRelabeling. // The returned labels are valid until the next call to ApplyRelabeling.
func (ctx *Ctx) ApplyRelabeling(labels []prompb.Label) []prompb.Label { func (ctx *Ctx) ApplyRelabeling(labels []prompb.Label) []prompb.Label {
prcs := prcsGlobal.Load().(*[]promrelabel.ParsedRelabelConfig) pcs := pcsGlobal.Load().(*promrelabel.ParsedConfigs)
if len(*prcs) == 0 { if pcs.Len() == 0 {
// There are no relabeling rules. // There are no relabeling rules.
return labels return labels
} }
@ -97,7 +97,7 @@ func (ctx *Ctx) ApplyRelabeling(labels []prompb.Label) []prompb.Label {
} }
// Apply relabeling // Apply relabeling
tmpLabels = promrelabel.ApplyRelabelConfigs(tmpLabels, 0, *prcs, true) tmpLabels = pcs.Apply(tmpLabels, 0, true)
ctx.tmpLabels = tmpLabels ctx.tmpLabels = tmpLabels
if len(tmpLabels) == 0 { if len(tmpLabels) == 0 {
metricsDropped.Inc() metricsDropped.Inc()

View file

@ -252,16 +252,21 @@ func mergeNonOverlappingTimeseries(dst, src *timeseries) bool {
// Verify whether the time series can be merged. // Verify whether the time series can be merged.
srcValues := src.Values srcValues := src.Values
dstValues := dst.Values dstValues := dst.Values
overlaps := 0
_ = dstValues[len(srcValues)-1] _ = dstValues[len(srcValues)-1]
for i, v := range srcValues { for i, v := range srcValues {
if math.IsNaN(v) { if math.IsNaN(v) {
continue continue
} }
if !math.IsNaN(dstValues[i]) { if !math.IsNaN(dstValues[i]) {
return false overlaps++
} }
} }
// Allow up to two overlapping datapoints, which can appear due to staleness algorithm,
// which can add a few datapoints in the end of time series.
if overlaps > 2 {
return false
}
// Time series can be merged. Merge them. // Time series can be merged. Merge them.
for i, v := range srcValues { for i, v := range srcValues {
if math.IsNaN(v) { if math.IsNaN(v) {

View file

@ -673,6 +673,17 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r} resultExpected := []netstorage.Result{r}
f(q, resultExpected) f(q, resultExpected)
}) })
t.Run("clamp(time(), 1400, 1800)", func(t *testing.T) {
t.Parallel()
q := `clamp(time(), 1400, 1800)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1400, 1400, 1400, 1600, 1800, 1800},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run("clamp_max(time(), 1400)", func(t *testing.T) { t.Run("clamp_max(time(), 1400)", func(t *testing.T) {
t.Parallel() t.Parallel()
q := `clamp_max(time(), 1400)` q := `clamp_max(time(), 1400)`
@ -1716,6 +1727,17 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r1, r2} resultExpected := []netstorage.Result{r1, r2}
f(q, resultExpected) f(q, resultExpected)
}) })
t.Run(`sign(time()-1400)`, func(t *testing.T) {
t.Parallel()
q := `sign(time()-1400)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{-1, -1, 0, 1, 1, 1},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`round(time()/1e3)`, func(t *testing.T) { t.Run(`round(time()/1e3)`, func(t *testing.T) {
t.Parallel() t.Parallel()
q := `round(time()/1e3)` q := `round(time()/1e3)`
@ -2757,6 +2779,26 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{} resultExpected := []netstorage.Result{}
f(q, resultExpected) f(q, resultExpected)
}) })
t.Run(`histogram_quantile(single-value-inf-le)`, func(t *testing.T) {
t.Parallel()
q := `histogram_quantile(0.6, label_set(100, "le", "+Inf"))`
resultExpected := []netstorage.Result{}
f(q, resultExpected)
})
t.Run(`histogram_quantile(zero-value-inf-le)`, func(t *testing.T) {
t.Parallel()
q := `histogram_quantile(0.6, (
label_set(100, "le", "+Inf"),
label_set(0, "le", "42"),
))`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{42, 42, 42, 42, 42, 42},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`histogram_quantile(single-value-valid-le)`, func(t *testing.T) { t.Run(`histogram_quantile(single-value-valid-le)`, func(t *testing.T) {
t.Parallel() t.Parallel()
q := `histogram_quantile(0.6, label_set(100, "le", "200"))` q := `histogram_quantile(0.6, label_set(100, "le", "200"))`
@ -3295,14 +3337,14 @@ func TestExecSuccess(t *testing.T) {
)))` )))`
r1 := netstorage.Result{ r1 := netstorage.Result{
MetricName: metricNameExpected, MetricName: metricNameExpected,
Values: []float64{52, 52, 52, 52, 52, 52}, Values: []float64{9, 9, 9, 9, 9, 9},
Timestamps: timestampsExpected, Timestamps: timestampsExpected,
} }
r1.MetricName.MetricGroup = []byte("metric") r1.MetricName.MetricGroup = []byte("metric")
r1.MetricName.Tags = []storage.Tag{ r1.MetricName.Tags = []storage.Tag{
{ {
Key: []byte("le"), Key: []byte("le"),
Value: []byte("200"), Value: []byte("10"),
}, },
{ {
Key: []byte("x"), Key: []byte("x"),
@ -3311,11 +3353,27 @@ func TestExecSuccess(t *testing.T) {
} }
r2 := netstorage.Result{ r2 := netstorage.Result{
MetricName: metricNameExpected, MetricName: metricNameExpected,
Values: []float64{100, 100, 100, 100, 100, 100}, Values: []float64{98, 98, 98, 98, 98, 98},
Timestamps: timestampsExpected, Timestamps: timestampsExpected,
} }
r2.MetricName.MetricGroup = []byte("metric") r2.MetricName.MetricGroup = []byte("metric")
r2.MetricName.Tags = []storage.Tag{ r2.MetricName.Tags = []storage.Tag{
{
Key: []byte("le"),
Value: []byte("300"),
},
{
Key: []byte("x"),
Value: []byte("y"),
},
}
r3 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{100, 100, 100, 100, 100, 100},
Timestamps: timestampsExpected,
}
r3.MetricName.MetricGroup = []byte("metric")
r3.MetricName.Tags = []storage.Tag{
{ {
Key: []byte("le"), Key: []byte("le"),
Value: []byte("inf"), Value: []byte("inf"),
@ -3325,7 +3383,7 @@ func TestExecSuccess(t *testing.T) {
Value: []byte("y"), Value: []byte("y"),
}, },
} }
resultExpected := []netstorage.Result{r1, r2} resultExpected := []netstorage.Result{r1, r2, r3}
f(q, resultExpected) f(q, resultExpected)
}) })
t.Run(`prometheus_buckets(missing-vmrange)`, func(t *testing.T) { t.Run(`prometheus_buckets(missing-vmrange)`, func(t *testing.T) {
@ -4133,11 +4191,11 @@ func TestExecSuccess(t *testing.T) {
}) })
t.Run(`sum(histogram_over_time) by (vmrange)`, func(t *testing.T) { t.Run(`sum(histogram_over_time) by (vmrange)`, func(t *testing.T) {
t.Parallel() t.Parallel()
q := `sort_desc( q := `sort_by_label(
buckets_limit( buckets_limit(
3, 3,
sum(histogram_over_time(alias(label_set(rand(0)*1.3+1.1, "foo", "bar"), "xxx")[200s:5s])) by (vmrange) sum(histogram_over_time(alias(label_set(rand(0)*1.3+1.1, "foo", "bar"), "xxx")[200s:5s])) by (vmrange)
) ), "le"
)` )`
r1 := netstorage.Result{ r1 := netstorage.Result{
MetricName: metricNameExpected, MetricName: metricNameExpected,
@ -4152,24 +4210,24 @@ func TestExecSuccess(t *testing.T) {
} }
r2 := netstorage.Result{ r2 := netstorage.Result{
MetricName: metricNameExpected, MetricName: metricNameExpected,
Values: []float64{24, 22, 26, 25, 24, 24}, Values: []float64{0, 0, 0, 0, 0, 0},
Timestamps: timestampsExpected, Timestamps: timestampsExpected,
} }
r2.MetricName.Tags = []storage.Tag{ r2.MetricName.Tags = []storage.Tag{
{ {
Key: []byte("le"), Key: []byte("le"),
Value: []byte("1.896e+00"), Value: []byte("1.000e+00"),
}, },
} }
r3 := netstorage.Result{ r3 := netstorage.Result{
MetricName: metricNameExpected, MetricName: metricNameExpected,
Values: []float64{11, 12, 11, 7, 11, 13}, Values: []float64{40, 40, 40, 40, 40, 40},
Timestamps: timestampsExpected, Timestamps: timestampsExpected,
} }
r3.MetricName.Tags = []storage.Tag{ r3.MetricName.Tags = []storage.Tag{
{ {
Key: []byte("le"), Key: []byte("le"),
Value: []byte("1.468e+00"), Value: []byte("2.448e+00"),
}, },
} }
resultExpected := []netstorage.Result{r1, r2, r3} resultExpected := []netstorage.Result{r1, r2, r3}
@ -5252,6 +5310,17 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r} resultExpected := []netstorage.Result{r}
f(q, resultExpected) f(q, resultExpected)
}) })
t.Run(`increase_pure(time())`, func(t *testing.T) {
t.Parallel()
q := `increase_pure(time())`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{200, 200, 200, 200, 200, 200},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`increase(time())`, func(t *testing.T) { t.Run(`increase(time())`, func(t *testing.T) {
t.Parallel() t.Parallel()
q := `increase(time())` q := `increase(time())`
@ -6276,6 +6345,7 @@ func TestExecError(t *testing.T) {
f(`abs()`) f(`abs()`)
f(`abs(1,2)`) f(`abs(1,2)`)
f(`absent(1, 2)`) f(`absent(1, 2)`)
f(`clamp()`)
f(`clamp_max()`) f(`clamp_max()`)
f(`clamp_min(1,2,3)`) f(`clamp_min(1,2,3)`)
f(`hour(1,2)`) f(`hour(1,2)`)
@ -6292,6 +6362,7 @@ func TestExecError(t *testing.T) {
f(`label_mismatch()`) f(`label_mismatch()`)
f(`round()`) f(`round()`)
f(`round(1,2,3)`) f(`round(1,2,3)`)
f(`sign()`)
f(`scalar()`) f(`scalar()`)
f(`sort(1,2)`) f(`sort(1,2)`)
f(`sort_desc()`) f(`sort_desc()`)

View file

@ -53,6 +53,7 @@ var rollupFuncs = map[string]newRollupFunc{
"distinct_over_time": newRollupFuncOneArg(rollupDistinct), "distinct_over_time": newRollupFuncOneArg(rollupDistinct),
"increases_over_time": newRollupFuncOneArg(rollupIncreases), "increases_over_time": newRollupFuncOneArg(rollupIncreases),
"decreases_over_time": newRollupFuncOneArg(rollupDecreases), "decreases_over_time": newRollupFuncOneArg(rollupDecreases),
"increase_pure": newRollupFuncOneArg(rollupIncreasePure), // + rollupFuncsRemoveCounterResets
"integrate": newRollupFuncOneArg(rollupIntegrate), "integrate": newRollupFuncOneArg(rollupIntegrate),
"ideriv": newRollupFuncOneArg(rollupIderiv), "ideriv": newRollupFuncOneArg(rollupIderiv),
"lifetime": newRollupFuncOneArg(rollupLifetime), "lifetime": newRollupFuncOneArg(rollupLifetime),
@ -123,6 +124,7 @@ var rollupAggrFuncs = map[string]rollupFunc{
"distinct_over_time": rollupDistinct, "distinct_over_time": rollupDistinct,
"increases_over_time": rollupIncreases, "increases_over_time": rollupIncreases,
"decreases_over_time": rollupDecreases, "decreases_over_time": rollupDecreases,
"increase_pure": rollupIncreasePure,
"integrate": rollupIntegrate, "integrate": rollupIntegrate,
"ideriv": rollupIderiv, "ideriv": rollupIderiv,
"lifetime": rollupLifetime, "lifetime": rollupLifetime,
@ -160,6 +162,7 @@ var rollupFuncsCannotAdjustWindow = map[string]bool{
"distinct_over_time": true, "distinct_over_time": true,
"increases_over_time": true, "increases_over_time": true,
"decreases_over_time": true, "decreases_over_time": true,
"increase_pure": true,
"integrate": true, "integrate": true,
"ascent_over_time": true, "ascent_over_time": true,
"descent_over_time": true, "descent_over_time": true,
@ -172,6 +175,7 @@ var rollupFuncsRemoveCounterResets = map[string]bool{
"rate": true, "rate": true,
"rollup_rate": true, "rollup_rate": true,
"rollup_increase": true, "rollup_increase": true,
"increase_pure": true,
} }
var rollupFuncsKeepMetricGroup = map[string]bool{ var rollupFuncsKeepMetricGroup = map[string]bool{
@ -1323,6 +1327,25 @@ func rollupStdvar(rfa *rollupFuncArg) float64 {
return q / count return q / count
} }
func rollupIncreasePure(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
prevValue := rfa.prevValue
if math.IsNaN(prevValue) {
if len(values) == 0 {
return nan
}
// Assume the counter starts from 0.
prevValue = 0
}
if len(values) == 0 {
// Assume the counter didsn't change since prevValue.
return 0
}
return values[len(values)-1] - prevValue
}
func rollupDelta(rfa *rollupFuncArg) float64 { func rollupDelta(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleaned up // There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs. // before calling rollup funcs.

View file

@ -476,6 +476,7 @@ func TestRollupNewRollupFuncSuccess(t *testing.T) {
f("ideriv", 0) f("ideriv", 0)
f("decreases_over_time", 5) f("decreases_over_time", 5)
f("increases_over_time", 5) f("increases_over_time", 5)
f("increase_pure", 398)
f("ascent_over_time", 142) f("ascent_over_time", 142)
f("descent_over_time", 231) f("descent_over_time", 231)
f("zscore_over_time", -0.4254336383156416) f("zscore_over_time", -0.4254336383156416)

View file

@ -19,6 +19,7 @@ import (
var transformFuncsKeepMetricGroup = map[string]bool{ var transformFuncsKeepMetricGroup = map[string]bool{
"ceil": true, "ceil": true,
"clamp": true,
"clamp_max": true, "clamp_max": true,
"clamp_min": true, "clamp_min": true,
"floor": true, "floor": true,
@ -44,6 +45,7 @@ var transformFuncs = map[string]transformFunc{
"abs": newTransformFuncOneArg(transformAbs), "abs": newTransformFuncOneArg(transformAbs),
"absent": transformAbsent, "absent": transformAbsent,
"ceil": newTransformFuncOneArg(transformCeil), "ceil": newTransformFuncOneArg(transformCeil),
"clamp": transformClamp,
"clamp_max": transformClampMax, "clamp_max": transformClampMax,
"clamp_min": transformClampMin, "clamp_min": transformClampMin,
"day_of_month": newTransformFuncDateTime(transformDayOfMonth), "day_of_month": newTransformFuncDateTime(transformDayOfMonth),
@ -61,6 +63,7 @@ var transformFuncs = map[string]transformFunc{
"minute": newTransformFuncDateTime(transformMinute), "minute": newTransformFuncDateTime(transformMinute),
"month": newTransformFuncDateTime(transformMonth), "month": newTransformFuncDateTime(transformMonth),
"round": transformRound, "round": transformRound,
"sign": transformSign,
"scalar": transformScalar, "scalar": transformScalar,
"sort": newTransformFuncSort(false), "sort": newTransformFuncSort(false),
"sort_desc": newTransformFuncSort(true), "sort_desc": newTransformFuncSort(true),
@ -215,6 +218,31 @@ func transformCeil(v float64) float64 {
return math.Ceil(v) return math.Ceil(v)
} }
func transformClamp(tfa *transformFuncArg) ([]*timeseries, error) {
args := tfa.args
if err := expectTransformArgsNum(args, 3); err != nil {
return nil, err
}
mins, err := getScalar(args[1], 1)
if err != nil {
return nil, err
}
maxs, err := getScalar(args[2], 2)
if err != nil {
return nil, err
}
tf := func(values []float64) {
for i, v := range values {
if v > maxs[i] {
values[i] = maxs[i]
} else if v < mins[i] {
values[i] = mins[i]
}
}
}
return doTransformValues(args[0], tf, tfa.fe)
}
func transformClampMax(tfa *transformFuncArg) ([]*timeseries, error) { func transformClampMax(tfa *transformFuncArg) ([]*timeseries, error) {
args := tfa.args args := tfa.args
if err := expectTransformArgsNum(args, 2); err != nil { if err := expectTransformArgsNum(args, 2); err != nil {
@ -315,6 +343,11 @@ func transformBucketsLimit(tfa *transformFuncArg) ([]*timeseries, error) {
if limit <= 0 { if limit <= 0 {
return nil, nil return nil, nil
} }
if limit < 3 {
// Preserve the first and the last bucket for better accuracy,
// since these buckets are usually `[0...leMin]` and `(leMax ... +Inf]`
limit = 3
}
tss := vmrangeBucketsToLE(args[1]) tss := vmrangeBucketsToLE(args[1])
if len(tss) == 0 { if len(tss) == 0 {
return nil, nil return nil, nil
@ -376,15 +409,18 @@ func transformBucketsLimit(tfa *transformFuncArg) ([]*timeseries, error) {
} }
} }
for len(leGroup) > limit { for len(leGroup) > limit {
// Preserve the first and the last bucket for better accuracy,
// since these buckets are usually `[0...leMin]` and `(leMax ... +Inf]`
xxMinIdx := 0 xxMinIdx := 0
for i, xx := range leGroup { for i, xx := range leGroup[1 : len(leGroup)-1] {
if xx.hits < leGroup[xxMinIdx].hits { if xx.hits < leGroup[xxMinIdx].hits {
xxMinIdx = i xxMinIdx = i
} }
} }
xxMinIdx++
// Merge the leGroup[xxMinIdx] bucket with the smallest adjacent bucket in order to preserve // Merge the leGroup[xxMinIdx] bucket with the smallest adjacent bucket in order to preserve
// the maximum accuracy. // the maximum accuracy.
if xxMinIdx+1 == len(leGroup) || (xxMinIdx > 0 && leGroup[xxMinIdx-1].hits < leGroup[xxMinIdx+1].hits) { if xxMinIdx > 1 && leGroup[xxMinIdx-1].hits < leGroup[xxMinIdx+1].hits {
xxMinIdx-- xxMinIdx--
} }
leGroup[xxMinIdx+1].hits += leGroup[xxMinIdx].hits leGroup[xxMinIdx+1].hits += leGroup[xxMinIdx].hits
@ -550,7 +586,6 @@ func transformHistogramShare(tfa *transformFuncArg) ([]*timeseries, error) {
m := groupLeTimeseries(tss) m := groupLeTimeseries(tss)
// Calculate share for les // Calculate share for les
share := func(i int, les []float64, xss []leTimeseries) (q, lower, upper float64) { share := func(i int, les []float64, xss []leTimeseries) (q, lower, upper float64) {
leReq := les[i] leReq := les[i]
if math.IsNaN(leReq) || len(xss) == 0 { if math.IsNaN(leReq) || len(xss) == 0 {
@ -649,14 +684,9 @@ func transformHistogramQuantile(tfa *transformFuncArg) ([]*timeseries, error) {
m := groupLeTimeseries(tss) m := groupLeTimeseries(tss)
// Calculate quantile for each group in m // Calculate quantile for each group in m
lastNonInf := func(i int, xss []leTimeseries) float64 { lastNonInf := func(i int, xss []leTimeseries) float64 {
for len(xss) > 0 { for len(xss) > 0 {
xsLast := xss[len(xss)-1] xsLast := xss[len(xss)-1]
v := xsLast.ts.Values[i]
if v == 0 {
return nan
}
if !math.IsInf(xsLast.le, 0) { if !math.IsInf(xsLast.le, 0) {
return xsLast.le return xsLast.le
} }
@ -700,8 +730,7 @@ func transformHistogramQuantile(tfa *transformFuncArg) ([]*timeseries, error) {
continue continue
} }
if math.IsInf(le, 0) { if math.IsInf(le, 0) {
vv := lastNonInf(i, xss) break
return vv, vv, inf
} }
if v == vPrev { if v == vPrev {
return lePrev, lePrev, v return lePrev, lePrev, v
@ -1575,6 +1604,25 @@ func transformRound(tfa *transformFuncArg) ([]*timeseries, error) {
return doTransformValues(args[0], tf, tfa.fe) return doTransformValues(args[0], tf, tfa.fe)
} }
func transformSign(tfa *transformFuncArg) ([]*timeseries, error) {
args := tfa.args
if err := expectTransformArgsNum(args, 1); err != nil {
return nil, err
}
tf := func(values []float64) {
for i, v := range values {
sign := float64(0)
if v < 0 {
sign = -1
} else if v > 0 {
sign = 1
}
values[i] = sign
}
}
return doTransformValues(args[0], tf, tfa.fe)
}
func transformScalar(tfa *transformFuncArg) ([]*timeseries, error) { func transformScalar(tfa *transformFuncArg) ([]*timeseries, error) {
args := tfa.args args := tfa.args
if err := expectTransformArgsNum(args, 1); err != nil { if err := expectTransformArgsNum(args, 1); err != nil {

View file

@ -2,6 +2,27 @@
# tip # tip
* FEATURE: add `sign(q)` and `clamp(q, min, max)` functions, which are planned to be added in [the upcoming Prometheus release](https://twitter.com/roidelapluie/status/1363428376162295811) . The `last_over_time(m[d])` function is already supported in [MetricsQL](https://victoriametrics.github.io/MetricsQL.html).
* FEATURE: vmagent: add `scrape_align_interval` config option, which can be used for aligning scrapes to the beginning of the configured interval. See [these docs](https://victoriametrics.github.io/vmagent.html#troubleshooting) for details.
* FEATURE: expose io-related metrics at `/metrics` page for every VictoriaMetrics component:
* `process_io_read_bytes_total` - the number of bytes read via io syscalls such as read and pread
* `process_io_written_bytes_total` - the number of bytes written via io syscalls such as write and pwrite
* `process_io_read_syscalls_total` - the number of read syscalls such as read and pread
* `process_io_write_syscalls_total` - the number of write syscalls such as write and pwrite
* `process_io_storage_read_bytes_total` - the number of bytes read from storage layer
* `process_io_storage_written_bytes_total` - the number of bytes written to storage layer
* FEATURE: vmagent: use watch API for Kuberntes service discovery. This should reduce load on Kuberntes API server when it tracks big number of objects (for example, 10K pods). This should also reduce the time needed for k8s targets discovery.
* FEATURE: vmagent: export `vm_promscrape_target_relabel_duration_seconds` metric, which can be used for monitoring the time spend on relabeling for discovered targets.
* FEATURE: vmagent: optimize [relabeling](https://victoriametrics.github.io/vmagent.html#relabeling) performance for common cases.
* FEATURE: add `increase_pure(m[d])` function to MetricsQL. It works the same as `increase(m[d])` except of various edge cases. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/962) for details.
* FEATURE: increase accuracy for `buckets_limit(limit, buckets)` results for small `limit` values. See [MetricsQL docs](https://victoriametrics.github.io/MetricsQL.html) for details.
* BUGFIX: vmagent: properly perform graceful shutdown on `SIGINT` and `SIGTERM` signals. The graceful shutdown has been broken in `v1.54.0`. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1065
* BUGFIX: reduce the probability of `duplicate time series` errors when querying Kubernetes metrics.
* BUGFIX: properly calculate `histogram_quantile()` over time series with only a single non-zero bucket with `{le="+Inf"}`. Previously `NaN` was returned, now the value for the last bucket before `{le="+Inf"}` is returned like Prometheus does.
* BUGFIX: vmselect: do not cache partial query results on timeout when receiving data from `vmstorage` nodes. See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1085
# [v1.54.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.54.1) # [v1.54.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.54.1)

View file

@ -1,9 +1,9 @@
# Case studies and talks # Case studies and talks
Below are approved public case studies and talks from VictoriaMetrics users. Join our [community Slack channel](http://slack.victoriametrics.com/) Below please find public case studies and talks from VictoriaMetrics users. You can also join our [community Slack channel](http://slack.victoriametrics.com/)
and feel free asking for references, reviews and additional case studies from real VictoriaMetrics users there. where you can chat with VictoriaMetrics users to get additional references, reviews and case studies.
See also [articles about VictoriaMetrics from our users](https://victoriametrics.github.io/Articles.html#third-party-articles-and-slides). You can also read [articles about VictoriaMetrics from our users](https://victoriametrics.github.io/Articles.html#third-party-articles-and-slides).
Alphabetically sorted links to case studies: Alphabetically sorted links to case studies:
@ -23,201 +23,141 @@ Alphabetically sorted links to case studies:
* [zhihu](#zhihu) * [zhihu](#zhihu)
## zhihu
[zhihu](https://www.zhihu.com) is the largest chinese question-and-answer website. We use VictoriaMetrics to store and use Graphite metrics, and we shared the [promate](https://github.com/zhihu/promate) solution in our [单机 20 亿指标,知乎 Graphite 极致优化!](https://qcon.infoq.cn/2020/shenzhen/presentation/2881)([slides](https://static001.geekbang.org/con/76/pdf/828698018/file/%E5%8D%95%E6%9C%BA%2020%20%E4%BA%BF%E6%8C%87%E6%A0%87%EF%BC%8C%E7%9F%A5%E4%B9%8E%20Graphite%20%E6%9E%81%E8%87%B4%E4%BC%98%E5%8C%96%EF%BC%81-%E7%86%8A%E8%B1%B9.pdf)) talk at [QCon 2020](https://qcon.infoq.cn/2020/shenzhen/).
Numbers:
- Active time series: ~2500 Million
- Datapoints: ~20 Trillion
- Ingestion rate: ~1800k/s
- Disk usage: ~20 TiB
- Index size: ~600 GiB
- The average query rate is ~3k per second (mostly alert queries).
- Query duration: median is ~40ms, 99th percentile is ~100ms.
## adidas ## adidas
See [slides](https://promcon.io/2019-munich/slides/remote-write-storage-wars.pdf) and [video](https://youtu.be/OsH6gPdxR4s) See our [slides](https://promcon.io/2019-munich/slides/remote-write-storage-wars.pdf) and [video](https://youtu.be/OsH6gPdxR4s)
from [Remote Write Storage Wars](https://promcon.io/2019-munich/talks/remote-write-storage-wars/) talk at [PromCon 2019](https://promcon.io/2019-munich/). from [Remote Write Storage Wars](https://promcon.io/2019-munich/talks/remote-write-storage-wars/) talk at [PromCon 2019](https://promcon.io/2019-munich/).
VictoriaMetrics is compared to Thanos, Corex and M3DB in the talk. VictoriaMetrics is compared to Thanos, Corex and M3DB in the talk.
## Adsterra
## CERN [Adsterra Network](https://adsterra.com) is a leading digital advertising agency that offers
performance-based solutions for advertisers and media partners worldwide.
The European Organization for Nuclear Research known as [CERN](https://home.cern/) uses VictoriaMetrics for real-time monitoring We used to collect and store our metrics with Prometheus. Over time, the data volume on our servers
of the [CMS](https://home.cern/science/experiments/cms) detector system. and metrics increased to the point that we were forced to gradually reduce what we were retaining. When our retention got as low as 7 days
According to [published talk](https://indico.cern.ch/event/877333/contributions/3696707/attachments/1972189/3281133/CMS_mon_RD_for_opInt.pdf) we looked for alternative solutions. We chose between Thanos, VictoriaMetrics and Prometheus federation.
VictoriaMetrics is used for the following purposes as a part of "CMS Monitoring cluster":
* As long-term storage for messages consumed from the [NATS messaging system](https://nats.io/). Consumed messages are pushed directly to VictoriaMetrics via HTTP protocol We ended up with the following configuration:
* As long-term storage for Prometheus monitoring system (30 days retention policy, there are plans to increase it up to ½ year)
* As a data source for visualizing metrics in Grafana.
R&D topic: Evaluate VictoraMetrics vs InfluxDB for large cardinality data. - Local instances of Prometheus with VictoriaMetrics as the remote storage on our backend servers.
- A single Prometheus on our monitoring server scrapes metrics from other servers and writes to VictoriaMetrics.
- A separate Prometheus that federates from other instances of Prometheus and processes alerts.
See also [The CMS monitoring infrastructure and applications](https://arxiv.org/pdf/2007.03630.pdf) publication from CERN with details about VictoriaMetrics usage. We learned that remote write protocol generated too much traffic and connections so after 8 months we started looking for alternatives.
Around the same time, VictoriaMetrics released [vmagent](https://victoriametrics.github.io/vmagent.html).
We tried to scrape all the metrics via a single instance of vmagent but it that didn't work because vmgent wasn't able to catch up with writes
into VictoriaMetrics. We tested different options and end up with the following scheme:
- We removed Prometheus from our setup.
- VictoriaMetrics [can scrape targets](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#how-to-scrape-prometheus-exporters-such-as-node-exporter) as well
so we removed vmagent. Now, VictoriaMetrics scrapes all the metrics from 110 jobs and 5531 targets.
- We use [Promxy](https://github.com/jacksontj/promxy) for alerting.
Such a scheme has generated the following benefits compared with Prometheus:
- We can store more metrics.
- We need less RAM and CPU for the same workload.
Cons are the following:
- VictoriaMetrics didn't support replication (it [supports replication now](https://victoriametrics.github.io/Cluster-VictoriaMetrics.html#replication-and-data-safety)) - we run an extra instance of VictoriaMetrics and Promxy in front of a VictoriaMetrics pair for high availability.
- VictoriaMetrics stores 1 extra month for defined retention (if retention is set to N months, then VM stores N+1 months of data), but this is still better than other solutions.
Here are some numbers from our single-node VictoriaMetrics setup:
- active time series: 10M
- ingestion rate: 800K samples/sec
- total number of datapoints: more than 2 trillion
- total number of entries in inverted index: more than 1 billion
- daily time series churn rate: 2.6M
- data size on disk: 1.5 TB
- index size on disk: 27 GB
- average datapoint size on disk: 0.75 bytes
- range query rate: 16 rps
- instant query rate: 25 rps
- range query duration: max: 0.5s; median: 0.05s; 97th percentile: 0.29s
- instant query duration: max: 2.1s; median: 0.04s; 97th percentile: 0.15s
VictoriaMetrics consumes about 50GB of RAM.
Setup:
We have 2 single-node instances of VictoriaMetrics. The first instance collects and stores high-resolution metrics (10s scrape interval) for a month.
The second instance collects and stores low-resolution metrics (300s scrape interval) for a month.
We use Promxy + Alertmanager for global view and alerts evaluation.
## COLOPL ## ARNES
[COLOPL](http://www.colopl.co.jp/en/) is Japaneese Game Development company. It started using VictoriaMetrics [The Academic and Research Network of Slovenia](https://www.arnes.si/en/) (ARNES) is a public institute that provides network services to research,
after evaulating the following remote storage solutions for Prometheus: educational and cultural organizations enabling connections and cooperation with each other and with related organizations worldwide.
* Cortex After using Cacti, Graphite and StatsD for years, we wanted to upgrade our monitoring stack to something that:
* Thanos
* M3DB
* VictoriaMetrics
See [slides](https://speakerdeck.com/inletorder/monitoring-platform-with-victoria-metrics) and [video](https://www.youtube.com/watch?v=hUpHIluxw80) - has native alerting support
from `Large-scale, super-load system monitoring platform built with VictoriaMetrics` talk at [Prometheus Meetup Tokyo #3](https://prometheus.connpass.com/event/157721/). - can be run on-prem
- has multi-dimensional metrics
- has lower hardware requirements
- is scalable
- has a simple client that allows for provisioning and discovery with Puppet
We hed been running Prometheus for about a year in a test environment and it was working well but there was a need/wish for a few more years of retention than the old system provided. We tested Thanos which was a bit resource hungry but worked great for about half a year.
Then we discovered VictoriaMetrics. Our scale isn't that big so we don't have on-prem S3 and no Kubernetes. VM's single node instance provided
the same result with far less maintenance overhead and lower hardware requirements.
## Zerodha After testing it a few months and with great support from the maintainers on [Slack](http://slack.victoriametrics.com/),
we decided to go with it. VM's support for the ingestion of InfluxDB metrics was an additional bonus as our hardware team uses
[Zerodha](https://zerodha.com/) is India's largest stock broker. Monitoring team at Zerodha faced with the following requirements: SNMPCollector to collect metrics from network devices and switching from InfluxDB to VictoriaMetrics required just a simple change in the config file.
* Multiple K8s clusters to monitor
* Consistent monitoring infra for each cluster across the fleet
* Ability to handle billions of timeseries events at any point of time
* Easier to operate and cost effective
Thanos, Cortex and VictoriaMetrics were evaluated as a long-term storage for Prometheus. VictoriaMetrics has been selected due to the following reasons:
* Blazing fast benchmarks for a single node setup.
* Single binary mode. Easy to scale vertically, very less operational headache.
* Considerable [improvements on creating Histograms](https://medium.com/@valyala/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350).
* [MetricsQL](https://victoriametrics.github.io/MetricsQL.html) gives us the ability to extend PromQL with more aggregation operators.
* API is compatible with Prometheus, almost all standard PromQL queries just work out of the box.
* Handles storage well, with periodic compaction. Makes it easy to take snapshots.
See [Monitoring K8S with VictoriaMetrics](https://docs.google.com/presentation/d/1g7yUyVEaAp4tPuRy-MZbPXKqJ1z78_5VKuV841aQfsg/edit) slides,
[video](https://youtu.be/ZJQYW-cFOms) and [Infrastructure monitoring with Prometheus at Zerodha](https://zerodha.tech/blog/infra-monitoring-at-zerodha/) blog post for more details.
## Wix.com
[Wix.com](https://en.wikipedia.org/wiki/Wix.com) is the leading web development platform.
> We needed to redesign metric infrastructure from the ground up after the move to Kubernethes. A few approaches/designs have been tried before the one that works great has been chosen: Prometheus instance in every datacenter with 2 hours retention for local storage and remote write into [HA pair of single-node VictoriaMetrics instances](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#high-availability).
Numbers: Numbers:
* The number of active time series per VictoriaMetrics instance is 50 millions. - 2 single node instances per DC (one for Prometheus and one for InfluxDB metrics)
* The total number of time series per VictoriaMetrics instance is 5000 millions. - Active time series per VictoriaMetrics instance: ~500k (Prometheus) + ~320k (InfluxDB)
* Ingestion rate per VictoriaMetrics instance is 1.1 millions data points per second. - Ingestion rate per VictoriaMetrics instance: 45k/s (Prometheus) / 30k/s (InfluxDB)
* The total number of datapoints per VictoriaMetrics instance is 8.5 trillions. - Query duration: median ~5ms, 99th percentile ~45ms
* The average churn rate is 150 millions new time series per day. - Total number of datapoints per instance: 390B (Prometheus), 110B (InfluxDB)
* The average query rate is ~150 per second (mostly alert queries). - Average datapoint size on drive: 0.4 bytes
* Query duration: median is ~1ms, 99th percentile is ~1sec. - Disk usage per VictoriaMetrics instance: 125GB (Prometheus), 185GB (InfluxDB)
* Retention: 3 months. - Index size per VictoriaMetrics instance: 1.6GB (Prometheus), 1.2GB (InfluxDB)
> Alternatives that weve played with before choosing VictoriaMetrics are: federated Prometheus, Cortex, IronDB and Thanos. We are running 1 Prometheus, 1 VictoriaMetrics and 1 Grafana server in each datacenter on baremetal servers, scraping 350+ targets
> Points that were critical to us when we were choosing a central tsdb, in order of importance: (and 3k+ devices collected via SNMPCollector sending metrics directly to VM). Each Prometheus is scraping all targets
so we have all metrics in both VictoriaMetrics instances. We are using [Promxy](https://github.com/jacksontj/promxy) to deduplicate metrics from both instances.
* At least 3 month worth of history. Grafana has an LB infront so if one DC has problems we can still view all metrics from both DCs on the other Grafana instance.
* Raw data, no aggregation, no sampling.
* High query speed.
* Clean fail state for HA (multi-node clusters may return partial data resulting in false alerts).
* Enough head room/scaling capacity for future growth, up to 100M active time series.
* Ability to split DB replicas per workload. Alert queries go to one replica, user queries go to another (speed for users, effective cache).
> Optimizing for those points and our specific workload VictoriaMetrics proved to be the best option. As an icing on a cake weve got [PromQL extensions](https://victoriametrics.github.io/MetricsQL.html) - `default 0` and `histogram` are my favorite ones, for example. What we specially like is having a lot of tsdb params easily available via config options, that makes tsdb easy to tune for specific use case. Also worth noting is a great community in [Slack channel](http://slack.victoriametrics.com/) and of course maintainer support.
Alex Ulstein, Head of Monitoring, Wix.com
## Wedos.com
> [Wedos](https://www.wedos.com/) is the Biggest Czech Hosting. We have our own private data center, that holds only our servers and technologies. The second data center, where the servers will be cooled in an oil bath, is being built. We started using [cluster VictoriaMetrics](https://victoriametrics.github.io/Cluster-VictoriaMetrics.html) to store Prometheus metrics from all our infrastructure after receiving positive references from our friends who successfully use VictoriaMetrics.
Numbers:
* The number of acitve time series: 5M.
* Ingestion rate: 170K data points per second.
* Query duration: median is ~2ms, 99th percentile is ~50ms.
> We like configuration simplicity and zero maintenance for VictoriaMetrics - once installed and forgot about it. It works out of the box without any issues.
## Synthesio
[Synthesio](https://www.synthesio.com/) is the leading social intelligence tool for social media monitoring & social analytics.
> We fully migrated from [Metrictank](https://grafana.com/oss/metrictank/) to Victoria Metrics
Numbers:
- Single node
- Active time series - 5 Million
- Datapoints: 1.25 Trillion
- Ingestion rate - 550k datapoints per second
- Disk usage - 150gb
- Index size - 3gb
- Query duration 99th percentile - 147ms
- Churn rate - 100 new time series per hour
## MHI Vestas Offshore Wind
The mission of [MHI Vestas Offshore Wind](http://www.mhivestasoffshore.com) is to co-develop offshore wind as an economically viable and sustainable energy resource to benefit future generations.
MHI Vestas Offshore Wind is using VictoriaMetrics to ingest and visualize sensor data from offshore wind turbines. The very efficient storage and ability to backfill was key in chosing VictoriaMetrics. MHI Vestas Offshore Wind is running the cluster version of VictoriaMetrics on Kubernetes using the Helm charts for deployment to be able to scale up capacity as the solution will be rolled out.
Numbers with current limited roll out:
- Active time series: 270K
- Ingestion rate: 70K/sec
- Total number of datapoints: 850 billions
- Data size on disk: 800 GiB
- Retention time: 3 years
## Dreamteam
[Dreamteam](https://dreamteam.gg/) successfully uses single-node VictoriaMetrics in multiple environments.
Numbers:
* Active time series: from 350K to 725K.
* Total number of time series: from 100M to 320M.
* Total number of datapoints: from 120 billions to 155 billions.
* Retention: 3 months.
VictoriaMetrics in production environment runs on 2 M5 EC2 instances in "HA" mode, managed by Terraform and Ansible TF module.
2 Prometheus instances are writing to both VMs, with 2 [Promxy](https://github.com/jacksontj/promxy) replicas
as load balancer for reads.
We are still in the process of migration, but we are really happy with the whole stack. It has proven to be an essential tool
for gathering insights into our services during COVID-19 and has enabled us to provide better service and identify problems faster.
## Brandwatch ## Brandwatch
[Brandwatch](https://www.brandwatch.com/) is the world's pioneering digital consumer intelligence suite, [Brandwatch](https://www.brandwatch.com/) is the world's pioneering digital consumer intelligence suite,
helping over 2,000 of the world's most admired brands and agencies to make insightful, data-driven business decisions. helping over 2,000 of the world's most admired brands and agencies to make insightful, data-driven business decisions.
The engineering department at Brandwatch has been using InfluxDB for storing application metrics for many years The engineering department at Brandwatch has been using InfluxDB to store application metrics for many years
and when End-of-Life of InfluxDB version 1.x was announced we decided to re-evaluate our whole metrics collection and storage stack. but when End-of-Life of InfluxDB version 1.x was announced we decided to re-evaluate our entire metrics collection and storage stack.
Main goals for the new metrics stack were: The main goals for the new metrics stack were:
- improved performance - improved performance
- lower maintenance - lower maintenance
- support for native clustering in open source version - support for native clustering in open source version
- the less metrics shipment had to change, the better - the less metrics shipment had to change, the better
- achieving longer data retention would be great but not critical - longer data retention time period would be great but not critical
We initially looked at CrateDB and TimescaleDB which both turned out to have limitations or requirements in the open source versions We initially tested CrateDB and TimescaleDB wand found that both had limitations or requirements in their open source versions
that made them unfit for our use case. Prometheus was also considered but push vs. pull metrics was a big change we did not want that made them unfit for our use case. Prometheus was also considered but it's push vs. pull metrics was a big change we did not want
to include in the already significant change. to include in the already significant change.
Once we found VictoriaMetrics it solved the following problems: Once we found VictoriaMetrics it solved the following problems:
- it is very light weight and we can now run virtual machines instead of dedicated hardware machines for metrics storage - it is very lightweight and we can now run virtual machines instead of dedicated hardware machines for metrics storage
- very short startup time and any possible gaps in data can easily be filled in by using Promxy - very short startup time and any possible gaps in data can easily be filled in using Promxy
- we could continue using Telegraf as our metrics agent and ship identical metrics to both InfluxDB and VictoriaMetrics during a migration period (migration just about to start) - we could continue using Telegraf as our metrics agent and ship identical metrics to both InfluxDB and VictoriaMetrics during the migration period (migration just about to start)
- compression is really good so we can store more metrics and we can spin up new VictoriaMetrics instances - compression im VM is really good. We can store more metrics and we can easily spin up new VictoriaMetrics instances
for new data and keep read-only nodes with older data if we need to extend our retention period further for new data and keep read-only nodes with older data if we need to extend our retention period further
than single virtual machine disks allow and we can aggregate all the data from VictoriaMetrics with Promxy than single virtual machine disks allow and we can aggregate all the data from VictoriaMetrics with Promxy
High availability is done the same way we did with InfluxDB, by running parallel single nodes of VictoriaMetrics. High availability is done the same way we did with InfluxDB by running parallel single nodes of VictoriaMetrics.
Numbers: Numbers:
@ -234,116 +174,56 @@ Query rates are insignificant as we have concentrated on data ingestion so far.
Anders Bomberg, Monitoring and Infrastructure Team Lead, brandwatch.com Anders Bomberg, Monitoring and Infrastructure Team Lead, brandwatch.com
## CERN
## Adsterra The European Organization for Nuclear Research better known as [CERN](https://home.cern/) uses VictoriaMetrics for real-time monitoring
of the [CMS](https://home.cern/science/experiments/cms) detector system.
According to [published talk](https://indico.cern.ch/event/877333/contributions/3696707/attachments/1972189/3281133/CMS_mon_RD_for_opInt.pdf)
VictoriaMetrics is used for the following purposes as a part of the "CMS Monitoring cluster":
[Adsterra Network](https://adsterra.com) is a leading digital advertising company that offers * As a long-term storage for messages ingested from the [NATS messaging system](https://nats.io/). Ingested messages are pushed directly to VictoriaMetrics via HTTP protocol
performance-based solutions for advertisers and media partners worldwide. * As a long-term storage for Prometheus monitoring system (30 days retention policy. There are plans to increase it up to ½ year)
* As a data source for visualizing metrics in Grafana.
We used to collect and store our metrics via Prometheus. Over time the amount of our servers R&D topic: Evaluate VictoraMetrics vs InfluxDB for large cardinality data.
and metrics increased so we were gradually reducing the retention. When retention became 7 days
we started to look for alternative solutions. We were choosing among Thanos, VictoriaMetrics and Prometheus federation.
We end up with the following configuration: Please also see [The CMS monitoring infrastructure and applications](https://arxiv.org/pdf/2007.03630.pdf) publication from CERN with details about their VictoriaMetrics usage.
- Local Prometheus'es with VictoriaMetrics as remote storage on our backend servers.
- A single Prometheus on our monitoring server scrapes metrics from other servers and writes to VictoriaMetrics.
- A separate Prometheus that federates from other Prometheus'es and processes alerts.
Turns out that remote write protocol generates too much traffic and connections. So after 8 months we started to look for alternatives.
Around the same time VictoriaMetrics released [vmagent](https://victoriametrics.github.io/vmagent.html).
We tried to scrape all the metrics via a single insance of vmagent. But that didn't work - vmgent wasn't able to catch up with writes
into VictoriaMetrics. We tested different options and end up with the following scheme:
- We removed Prometheus from our setup.
- VictoriaMetrics [can scrape targets](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#how-to-scrape-prometheus-exporters-such-as-node-exporter) as well,
so we removed vmagent. Now VictoriaMetrics scrapes all the metrics from 110 jobs and 5531 targets.
- We use [Promxy](https://github.com/jacksontj/promxy) for alerting.
Such a scheme has the following benefits comparing to Prometheus:
- We can store more metrics.
- We need less RAM and CPU for the same workload.
Cons are the following:
- VictoriaMetrics didn't support replication (it [supports replication now](https://victoriametrics.github.io/Cluster-VictoriaMetrics.html#replication-and-data-safety)) - we run extra instance of VictoriaMetrics and Promxy in front of VictoriaMetrics pair for high availability.
- VictoriaMetrics stores 1 extra month for defined retention (if retention is set to N months, then VM stores N+1 months of data), but this is still better than other solutions.
Some numbers from our single-node VictoriaMetrics setup:
- active time series: 10M
- ingestion rate: 800K samples/sec
- total number of datapoints: more than 2 trillion
- total number of entries in inverted index: more than 1 billion
- daily time series churn rate: 2.6M
- data size on disk: 1.5 TB
- index size on disk: 27 GB
- average datapoint size on disk: 0.75 bytes
- range query rate: 16 rps
- instant query rate: 25 rps
- range query duration: max: 0.5s; median: 0.05s; 97th percentile: 0.29s
- instant query duration: max: 2.1s; median: 0.04s; 97th percentile: 0.15s
VictoriaMetrics consumes about 50GiB of RAM.
Setup:
We have 2 single-node instances of VictoriaMetircs. The first instance collects and stores high-resolution metrics (10s scrape interval) for a month.
The second instance collects and stores low-resolution metrics (300s scrape interval) for a month.
We use Promxy + Alertmanager for global view and alerts evaluation.
## ARNES ## COLOPL
[The Academic and Research Network of Slovenia](https://www.arnes.si/en/) (ARNES) is a public institute that provides network services to research, [COLOPL](http://www.colopl.co.jp/en/) is Japanese game development company. It started using VictoriaMetrics
educational and cultural organizations, and enables them to establish connections and cooperation with each other and with related organizations abroad. after evaulating the following remote storage solutions for Prometheus:
After using Cacti, Graphite and StatsD for years, we wanted to upgrade our monitoring stack to something that: * Cortex
* Thanos
* M3DB
* VictoriaMetrics
- has native alerting support See [slides](https://speakerdeck.com/inletorder/monitoring-platform-with-victoria-metrics) and [video](https://www.youtube.com/watch?v=hUpHIluxw80)
- can run on-prem from `Large-scale, super-load system monitoring platform built with VictoriaMetrics` talk at [Prometheus Meetup Tokyo #3](https://prometheus.connpass.com/event/157721/).
- has multi-dimension metrics
- lower hardware requirements
- is scalable
- simple client provisioning and discovery with Puppet
We were running Prometheus for about a year in a test environment and it worked great. But there was a need/wish for a few years of retention time, ## Dreamteam
like the old systems provided. We tested Thanos, which was a bit resource hungry back then, but it worked great for about half a year
until we discovered VictoriaMetrics. As our scale is not that big, we don't have on-prem S3 and no Kubernetes, VM's single node instance provided
the same result with less maintenance overhead and lower hardware requirements.
After testing it a few months and having great support from the maintainers on [Slack](http://slack.victoriametrics.com/), [Dreamteam](https://dreamteam.gg/) successfully uses single-node VictoriaMetrics in multiple environments.
we decided to go with it. VM's support for ingesting InfluxDB metrics was an additional bonus, since our hardware team uses
SNMPCollector to collect metrics from network devices and switching from InfluxDB to VictoriaMetrics was a simple change in the config file for them.
Numbers: Numbers:
- 2 single node instances per DC (one for prometheus and one for influxdb metrics) * Active time series: from 350K to 725K.
- Active time series per VictoriaMetrics instance: ~500k (prometheus) + ~320k (influxdb) * Total number of time series: from 100M to 320M.
- Ingestion rate per VictoriaMetrics instance: 45k/s (prometheus) / 30k/s (influxdb) * Total number of datapoints: from 120 billion to 155 billion.
- Query duration: median is ~5ms, 99th percentile is ~45ms * Retention period: 3 months.
- Total number of datapoints per instance: 390B (prometheus), 110B (influxdb)
- Average datapoint size on drive: 0.4 bytes
- Disk usage per VictoriaMetrics instance: 125GB (prometheus), 185GB (influxdb)
- Index size per VictoriaMetrics instance: 1.6GB (prometheus), 1.2GB (influcdb)
We are running 1 Prometheus, 1 VictoriaMetrics and 1 Grafana server in each datacenter on baremetal servers, scraping 350+ targets
(and 3k+ devices collected via SNMPCollector sending metrics directly to VM). Each Prometheus is scraping all targets,
so we have all metrics in both VictoriaMetrics instances. We are using [Promxy](https://github.com/jacksontj/promxy) to deduplicate metrics from both instances.
Grafana has a LB infront, so if one DC has problems, we can still view all metrics from both DCs on the other Grafana instance.
We are still in the process of migration, but we are really happy with the whole stack. It has proven as an essential piece
for insight into our services during COVID-19 and has enabled us to provide better service and spot problems faster.
VictoriaMetrics in production environment runs on 2 M5 EC2 instances in "HA" mode, managed by Terraform and Ansible TF module.
2 Prometheus instances are writing to both VMs, with 2 [Promxy](https://github.com/jacksontj/promxy) replicas
as the load balancer for reads.
## Idealo.de ## Idealo.de
[idealo.de](https://www.idealo.de/) is the leading price comparison website in Germany. We use Prometheus for metrics on our container platform. [idealo.de](https://www.idealo.de/) is the leading price comparison website in Germany. We use Prometheus for metrics on our container platform.
When we introduced Prometheus at idealo we started with m3db as a longterm storage. In our setup m3db was quite unstable and consumed a lot of resources. When we introduced Prometheus at idealo we started with m3db as our longterm storage. In our setup, m3db was quite unstable and consumed a lot of resources.
VictoriaMetrics runs very stable for us and uses only a fraction of the resources. Although we also increased our retention time from 1 month to 13 months. VictoriaMetrics in poroduction is very stable for us and uses only a fraction of the resources even though we also increased our retention period from 1 month to 13 months.
Numbers: Numbers:
@ -354,3 +234,114 @@ Numbers:
- The average query rate is ~20 per second. Response time for 99th quantile is 120ms. - The average query rate is ~20 per second. Response time for 99th quantile is 120ms.
- Retention: 13 months. - Retention: 13 months.
- Size of all datapoints: 3.5 TB - Size of all datapoints: 3.5 TB
## MHI Vestas Offshore Wind
The mission of [MHI Vestas Offshore Wind](http://www.mhivestasoffshore.com) is to co-develop offshore wind as an economically viable and sustainable energy resource to benefit future generations.
MHI Vestas Offshore Wind is using VictoriaMetrics to ingest and visualize sensor data from offshore wind turbines. The very efficient storage and ability to backfill was key in choosing VictoriaMetrics. MHI Vestas Offshore Wind is running the cluster version of VictoriaMetrics on Kubernetes using the Helm charts for deployment to be able to scale up capacity as the solution is rolled out.
Numbers with current, limited roll out:
- Active time series: 270K
- Ingestion rate: 70K/sec
- Total number of datapoints: 850 billion
- Data size on disk: 800 GiB
- Retention period: 3 years
## Synthesio
[Synthesio](https://www.synthesio.com/) is the leading social intelligence tool for social media monitoring and analytics.
> We fully migrated from [Metrictank](https://grafana.com/oss/metrictank/) to VictoriaMetrics
Numbers:
- Single node
- Active time series - 5 Million
- Datapoints: 1.25 Trillion
- Ingestion rate - 550k datapoints per second
- Disk usage - 150gb
- Index size - 3gb
- Query duration 99th percentile - 147ms
- Churn rate - 100 new time series per hour
## Wedos.com
> [Wedos](https://www.wedos.com/) is the biggest hosting provider in the Czech Republic. We have our own private data center that holds our servers and technologies. We are in the process of building a second, stae of the art data center where the servers will be cooled in an oil bath. We started using [cluster VictoriaMetrics](https://victoriametrics.github.io/Cluster-VictoriaMetrics.html) to store Prometheus metrics from all our infrastructure after receiving positive references from people who had successfully used VictoriaMetrics.
Numbers:
* The number of acitve time series: 5M.
* Ingestion rate: 170K data points per second.
* Query duration: median is ~2ms, 99th percentile is ~50ms.
> We like that VictoriaMetrics is simple to configuree and requires zero maintenance. It works right out of the box and once it's set up you can just forget about it.
## Wix.com
[Wix.com](https://en.wikipedia.org/wiki/Wix.com) is the leading web development platform.
> We needed to redesign our metrics infrastructure from the ground up after the move to Kubernetes. We had tried out a few different options before landing on this solution which is working great. We have a Prometheus instance in every datacenter with 2 hours retention for local storage and remote write into [HA pair of single-node VictoriaMetrics instances](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#high-availability).
Numbers:
* The number of active time series per VictoriaMetrics instance is 50 millios.
* The total number of time series per VictoriaMetrics instance is 5000 million.
* Ingestion rate per VictoriaMetrics instance is 1.1 millions data points per second.
* The total number of datapoints per VictoriaMetrics instance is 8.5 trillion.
* The average churn rate is 150 millions new time series per day.
* The average query rate is ~150 per second (mostly alert queries).
* Query duration: median is ~1ms, 99th percentile is ~1sec.
* Retention period: 3 months.
> The alternatives that we tested prior to choosing VictoriaMetrics were: Prometheus federated, Cortex, IronDB and Thanos.
> The items that were critical to us central tsdb, in order of importance were as follows:
* At least 3 month worth of retention.
* Raw data, no aggregation, no sampling.
* High query speed.
* Clean fail state for HA (multi-node clusters may return partial data resulting in false alerts).
* Enough headroom/scaling capacity for future growth which is planned to be up to 100M active time series.
* Ability to split DB replicas per workload. Alert queries go to one replica and user queries go to another (speed for users, effective cache).
> Optimizing for those points and our specific workload, VictoriaMetrics proved to be the best option. As icing on the cake weve got [PromQL extensions](https://victoriametrics.github.io/MetricsQL.html) - `default 0` and `histogram` are my favorite ones. We really like having a lot of tsdb params easily available via config options which makes tsdb easy to tune for each specific use case. We've also found a great community in [Slack channel](http://slack.victoriametrics.com/) and responsive and helpful maintainer support.
Alex Ulstein, Head of Monitoring, Wix.com
## Zerodha
[Zerodha](https://zerodha.com/) is India's largest stock broker. The monitoring team at Zerodha had the following requirements:
* Multiple K8s clusters to monitor
* Consistent monitoring infra for each cluster across the fleet
* The ability to handle billions of timeseries events at any point of time
* Easy to operate and cost effective
Thanos, Cortex and VictoriaMetrics were evaluated as a long-term storage for Prometheus. VictoriaMetrics has been selected for the following reasons:
* Blazingly fast benchmarks for a single node setup.
* Single binary mode. Easy to scale vertically with far fewer operational headaches.
* Considerable [improvements on creating Histograms](https://medium.com/@valyala/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350).
* [MetricsQL](https://victoriametrics.github.io/MetricsQL.html) gives us the ability to extend PromQL with more aggregation operators.
* The API is compatible with Prometheus and nearly all standard PromQL queries work well out of the box.
* Handles storage well, with periodic compaction which makes it easy to take snapshots.
Please see [Monitoring K8S with VictoriaMetrics](https://docs.google.com/presentation/d/1g7yUyVEaAp4tPuRy-MZbPXKqJ1z78_5VKuV841aQfsg/edit) slides,
[video](https://youtu.be/ZJQYW-cFOms) and [Infrastructure monitoring with Prometheus at Zerodha](https://zerodha.tech/blog/infra-monitoring-at-zerodha/) blog post for more details.
## zhihu
[zhihu](https://www.zhihu.com) is the largest Chinese question-and-answer website. We use VictoriaMetrics to store and use Graphite metrics. We shared the [promate](https://github.com/zhihu/promate) solution in our [单机 20 亿指标,知乎 Graphite 极致优化!](https://qcon.infoq.cn/2020/shenzhen/presentation/2881)([slides](https://static001.geekbang.org/con/76/pdf/828698018/file/%E5%8D%95%E6%9C%BA%2020%20%E4%BA%BF%E6%8C%87%E6%A0%87%EF%BC%8C%E7%9F%A5%E4%B9%8E%20Graphite%20%E6%9E%81%E8%87%B4%E4%BC%98%E5%8C%96%EF%BC%81-%E7%86%8A%E8%B1%B9.pdf)) talk at [QCon 2020](https://qcon.infoq.cn/2020/shenzhen/).
Numbers:
- Active time series: ~2500 Million
- Datapoints: ~20 Trillion
- Ingestion rate: ~1800k/s
- Disk usage: ~20 TB
- Index size: ~600 GB
- The average query rate is ~3k per second (mostly alert queries).
- Query duration: median is ~40ms, 99th percentile is ~100ms.

View file

@ -72,6 +72,7 @@ This functionality can be tried at [an editable Grafana dashboard](http://play-g
- `start()` and `end()` functions for returning the start and end timestamps of the `[start ... end]` range used in the query. - `start()` and `end()` functions for returning the start and end timestamps of the `[start ... end]` range used in the query.
- `integrate(m[d])` for returning integral over the given duration `d` for the given metric `m`. - `integrate(m[d])` for returning integral over the given duration `d` for the given metric `m`.
- `ideriv(m[d])` - for calculating `instant` derivative for the metric `m` over the duration `d`. - `ideriv(m[d])` - for calculating `instant` derivative for the metric `m` over the duration `d`.
- `increase_pure(m[d])` - for calculating increase of `m` over `d` without edge-case handling compared to `increase(m[d])`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/962) for details.
- `deriv_fast(m[d])` - for calculating `fast` derivative for `m` based on the first and the last points from duration `d`. - `deriv_fast(m[d])` - for calculating `fast` derivative for `m` based on the first and the last points from duration `d`.
- `running_` functions - `running_sum`, `running_min`, `running_max`, `running_avg` - for calculating [running values](https://en.wikipedia.org/wiki/Running_total) on the selected time range. - `running_` functions - `running_sum`, `running_min`, `running_max`, `running_avg` - for calculating [running values](https://en.wikipedia.org/wiki/Running_total) on the selected time range.
- `range_` functions - `range_sum`, `range_min`, `range_max`, `range_avg`, `range_first`, `range_last`, `range_median`, `range_quantile` - for calculating global value over the selected time range. Note that global value is based on calculated datapoints for the inner query. The calculated datapoints can differ from raw datapoints stored in the database. See [these docs](https://prometheus.io/docs/prometheus/latest/querying/basics/#staleness) for details. - `range_` functions - `range_sum`, `range_min`, `range_max`, `range_avg`, `range_first`, `range_last`, `range_median`, `range_quantile` - for calculating global value over the selected time range. Note that global value is based on calculated datapoints for the inner query. The calculated datapoints can differ from raw datapoints stored in the database. See [these docs](https://prometheus.io/docs/prometheus/latest/querying/basics/#staleness) for details.

View file

@ -1,31 +1,31 @@
# Quick Start # Quick Start
1. If you run Ubuntu, then just run `snap install victoriametrics` command in order to install and start VictoriaMetrics, then read [these docs](https://snapcraft.io/victoriametrics). 1. If you run Ubuntu please run the `snap install victoriametrics` command to install and start VictoriaMetrics. Then read [these docs](https://snapcraft.io/victoriametrics).
Otherwise download the latest VictoriaMetrics release from [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases), Otherwise you can download the latest VictoriaMetrics release from [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases),
from [Docker hub](https://hub.docker.com/r/victoriametrics/victoria-metrics/) or [Docker hub](https://hub.docker.com/r/victoriametrics/victoria-metrics/)
or [build it from sources](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#how-to-build-from-sources). or [build it from sources](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#how-to-build-from-sources).
2. This step isn't needed if you run VictoriaMetrics via `snap install victoriametrics` as described above. 2. This step isn't needed if you run VictoriaMetrics via `snap install victoriametrics` as described above.
Otherwise run the binary or Docker image with the desired command-line flags. Pass `-help` in order to see description for all the available flags Otherwise, please run the binary or Docker image with your desired command-line flags. You can look at `-help` to see descriptions of all available flags
and their default values. Default flag values should fit the majoirty of cases. The minimum required flags to configure are: and their default values. The default flag values should fit the majority of cases. The minimum required flags that must be configured are:
* `-storageDataPath` - path to directory where VictoriaMetrics stores all the data. * `-storageDataPath` - the path to directory where VictoriaMetrics stores your data.
* `-retentionPeriod` - data retention. * `-retentionPeriod` - data retention.
For instance: For example:
`./victoria-metrics-prod -storageDataPath=/var/lib/victoria-metrics-data -retentionPeriod=3` `./victoria-metrics-prod -storageDataPath=/var/lib/victoria-metrics-data -retentionPeriod=3`
See [these instructions](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/43) in order to configure VictoriaMetrics as OS service. Check [these instructions](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/43) to configure VictoriaMetrics as an OS service.
It is recommended setting up [VictoriaMetrics monitoring](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#monitoring). We recommended setting up [VictoriaMetrics monitoring](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#monitoring).
3. Configure [vmagent](https://victoriametrics.github.io/vmagent.html) or Prometheus to write data to VictoriaMetrics. 3. Configure either [vmagent](https://victoriametrics.github.io/vmagent.html) or Prometheus to write data to VictoriaMetrics.
It is recommended to use `vmagent` instead of Prometheus, since it is more resource efficient. If you still prefer Prometheus, then We recommended using `vmagent` instead of Prometheus because it is more resource efficient. If you still prefer Prometheus
see [these instructions](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#prometheus-setup) see [these instructions](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#prometheus-setup)
for details on how to configure Prometheus. for details on how it may be properly configured.
4. Configure Grafana to query VictoriaMetrics instead of Prometheus. 4. To configure Grafana to query VictoriaMetrics instead of Prometheus
See [these instructions](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#grafana-setup). please see [these instructions](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#grafana-setup).
There is also [cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster) and [SaaS playground](https://play.victoriametrics.com/signIn). There is also [cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster) and [SaaS playground](https://play.victoriametrics.com/signIn).

View file

@ -605,7 +605,8 @@ and it is easier to use when migrating from Graphite to VictoriaMetrics.
### Graphite Render API usage ### Graphite Render API usage
[VictoriaMetrics Enterprise](https://victoriametrics.com/enterprise.html) supports [Graphite Render API](https://graphite.readthedocs.io/en/stable/render_api.html) subset [VictoriaMetrics Enterprise](https://victoriametrics.com/enterprise.html) supports [Graphite Render API](https://graphite.readthedocs.io/en/stable/render_api.html) subset
at `/render` endpoint. This subset is required for [Graphite datasource in Grafana](https://grafana.com/docs/grafana/latest/datasources/graphite/). at `/render` endpoint, which is used by [Graphite datasource in Grafana](https://grafana.com/docs/grafana/latest/datasources/graphite/).
It supports `Storage-Step` http request header, which must be set to a step between data points stored in VictoriaMetrics when configuring Graphite datasource in Grafana.
### Graphite Metrics API usage ### Graphite Metrics API usage

View file

@ -298,6 +298,16 @@ It may be useful for performing `vmagent` rolling update without scrape loss.
the url may contain sensitive information such as auth tokens or passwords. the url may contain sensitive information such as auth tokens or passwords.
Pass `-remoteWrite.showURL` command-line flag when starting `vmagent` in order to see all the valid urls. Pass `-remoteWrite.showURL` command-line flag when starting `vmagent` in order to see all the valid urls.
* If scrapes must be aligned in time (for instance, if they must be performed at the beginning of every hour), then set `scrape_align_interval` option
in the corresponding scrape config. For example, the following config aligns hourly scrapes to the nearest 10 minutes:
```yml
scrape_configs:
- job_name: foo
scrape_interval: 1h
scrape_align_interval: 10m
```
* If you see `skipping duplicate scrape target with identical labels` errors when scraping Kubernetes pods, then it is likely these pods listen multiple ports * If you see `skipping duplicate scrape target with identical labels` errors when scraping Kubernetes pods, then it is likely these pods listen multiple ports
or they use init container. These errors can be either fixed or suppressed with `-promscrape.suppressDuplicateScrapeTargetErrors` command-line flag. or they use init container. These errors can be either fixed or suppressed with `-promscrape.suppressDuplicateScrapeTargetErrors` command-line flag.
See available options below if you prefer fixing the root cause of the error: See available options below if you prefer fixing the root cause of the error:

4
go.mod
View file

@ -8,8 +8,8 @@ require (
// Do not use the original github.com/valyala/fasthttp because of issues // Do not use the original github.com/valyala/fasthttp because of issues
// like https://github.com/valyala/fasthttp/commit/996610f021ff45fdc98c2ce7884d5fa4e7f9199b // like https://github.com/valyala/fasthttp/commit/996610f021ff45fdc98c2ce7884d5fa4e7f9199b
github.com/VictoriaMetrics/fasthttp v1.0.12 github.com/VictoriaMetrics/fasthttp v1.0.12
github.com/VictoriaMetrics/metrics v1.14.0 github.com/VictoriaMetrics/metrics v1.15.0
github.com/VictoriaMetrics/metricsql v0.10.1 github.com/VictoriaMetrics/metricsql v0.12.0
github.com/aws/aws-sdk-go v1.37.12 github.com/aws/aws-sdk-go v1.37.12
github.com/cespare/xxhash/v2 v2.1.1 github.com/cespare/xxhash/v2 v2.1.1
github.com/cheggaaa/pb/v3 v3.0.6 github.com/cheggaaa/pb/v3 v3.0.6

8
go.sum
View file

@ -85,10 +85,10 @@ github.com/VictoriaMetrics/fastcache v1.5.7/go.mod h1:ptDBkNMQI4RtmVo8VS/XwRY6Ro
github.com/VictoriaMetrics/fasthttp v1.0.12 h1:Ag0E119yrH4BTxVyjKD9TeiSImtG9bUcg/stItLJhSE= github.com/VictoriaMetrics/fasthttp v1.0.12 h1:Ag0E119yrH4BTxVyjKD9TeiSImtG9bUcg/stItLJhSE=
github.com/VictoriaMetrics/fasthttp v1.0.12/go.mod h1:3SeUL4zwB/p/a9aEeRc6gdlbrtNHXBJR6N376EgiSHU= github.com/VictoriaMetrics/fasthttp v1.0.12/go.mod h1:3SeUL4zwB/p/a9aEeRc6gdlbrtNHXBJR6N376EgiSHU=
github.com/VictoriaMetrics/metrics v1.12.2/go.mod h1:Z1tSfPfngDn12bTfZSCqArT3OPY3u88J12hSoOhuiRE= github.com/VictoriaMetrics/metrics v1.12.2/go.mod h1:Z1tSfPfngDn12bTfZSCqArT3OPY3u88J12hSoOhuiRE=
github.com/VictoriaMetrics/metrics v1.14.0 h1:yvyEVo7cPN2Hv+Hrm1zPTA1f/squmEZTq6xtPH/8F64= github.com/VictoriaMetrics/metrics v1.15.0 h1:HGmGaILioC4vNk6UhkcwLIaDlg5y4MVganq1verl5js=
github.com/VictoriaMetrics/metrics v1.14.0/go.mod h1:Z1tSfPfngDn12bTfZSCqArT3OPY3u88J12hSoOhuiRE= github.com/VictoriaMetrics/metrics v1.15.0/go.mod h1:Z1tSfPfngDn12bTfZSCqArT3OPY3u88J12hSoOhuiRE=
github.com/VictoriaMetrics/metricsql v0.10.1 h1:wLl/YbMmBGFPyLKMfqNLC333iygibosSM5iSvlH2B4A= github.com/VictoriaMetrics/metricsql v0.12.0 h1:NMIu0MPBmGP34g4RUjI1U0xW5XYp7IVNXe9KtZI3PFQ=
github.com/VictoriaMetrics/metricsql v0.10.1/go.mod h1:ylO7YITho/Iw6P71oEaGyHbO94bGoGtzWfLGqFhMIg8= github.com/VictoriaMetrics/metricsql v0.12.0/go.mod h1:ylO7YITho/Iw6P71oEaGyHbO94bGoGtzWfLGqFhMIg8=
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=

View file

@ -3,6 +3,7 @@ package mergeset
import ( import (
"fmt" "fmt"
"sort" "sort"
"unsafe"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding" "github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger" "github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
@ -34,6 +35,10 @@ type blockHeader struct {
lensBlockSize uint32 lensBlockSize uint32
} }
func (bh *blockHeader) SizeBytes() int {
return int(unsafe.Sizeof(*bh)) + cap(bh.commonPrefix) + cap(bh.firstItem)
}
func (bh *blockHeader) Reset() { func (bh *blockHeader) Reset() {
bh.commonPrefix = bh.commonPrefix[:0] bh.commonPrefix = bh.commonPrefix[:0]
bh.firstItem = bh.firstItem[:0] bh.firstItem = bh.firstItem[:0]

View file

@ -195,7 +195,8 @@ func (bsr *blockStreamReader) Next() bool {
if err := bsr.readNextBHS(); err != nil { if err := bsr.readNextBHS(); err != nil {
if err == io.EOF { if err == io.EOF {
// Check the last item. // Check the last item.
lastItem := bsr.Block.items[len(bsr.Block.items)-1] b := &bsr.Block
lastItem := b.items[len(b.items)-1].Bytes(b.data)
if string(bsr.ph.lastItem) != string(lastItem) { if string(bsr.ph.lastItem) != string(lastItem) {
err = fmt.Errorf("unexpected last item; got %X; want %X", lastItem, bsr.ph.lastItem) err = fmt.Errorf("unexpected last item; got %X; want %X", lastItem, bsr.ph.lastItem)
} }
@ -240,12 +241,13 @@ func (bsr *blockStreamReader) Next() bool {
} }
if !bsr.firstItemChecked { if !bsr.firstItemChecked {
bsr.firstItemChecked = true bsr.firstItemChecked = true
if string(bsr.ph.firstItem) != string(bsr.Block.items[0]) { b := &bsr.Block
bsr.err = fmt.Errorf("unexpected first item; got %X; want %X", bsr.Block.items[0], bsr.ph.firstItem) firstItem := b.items[0].Bytes(b.data)
if string(bsr.ph.firstItem) != string(firstItem) {
bsr.err = fmt.Errorf("unexpected first item; got %X; want %X", firstItem, bsr.ph.firstItem)
return false return false
} }
} }
return true return true
} }

View file

@ -44,8 +44,10 @@ func testBlockStreamReaderRead(ip *inmemoryPart, items []string) error {
bsr := newTestBlockStreamReader(ip) bsr := newTestBlockStreamReader(ip)
i := 0 i := 0
for bsr.Next() { for bsr.Next() {
for _, item := range bsr.Block.items { data := bsr.Block.data
if string(item) != items[i] { for _, it := range bsr.Block.items {
item := it.String(data)
if item != items[i] {
return fmt.Errorf("unexpected item[%d]; got %q; want %q", i, item, items[i]) return fmt.Errorf("unexpected item[%d]; got %q; want %q", i, item, items[i])
} }
i++ i++

View file

@ -3,6 +3,7 @@ package mergeset
import ( import (
"fmt" "fmt"
"os" "os"
"reflect"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@ -13,35 +14,62 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger" "github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
) )
type byteSliceSorter [][]byte // Item represents a single item for storing in a mergeset.
type Item struct {
// Start is start offset for the item in data.
Start uint32
func (s byteSliceSorter) Len() int { return len(s) } // End is end offset for the item in data.
func (s byteSliceSorter) Less(i, j int) bool { End uint32
return string(s[i]) < string(s[j])
} }
func (s byteSliceSorter) Swap(i, j int) {
s[i], s[j] = s[j], s[i] // Bytes returns bytes representation of it obtained from data.
//
// The returned bytes representation belongs to data.
func (it Item) Bytes(data []byte) []byte {
sh := (*reflect.SliceHeader)(unsafe.Pointer(&data))
sh.Cap = int(it.End - it.Start)
sh.Len = int(it.End - it.Start)
sh.Data += uintptr(it.Start)
return data
}
// String returns string represetnation of it obtained from data.
//
// The returned string representation belongs to data.
func (it Item) String(data []byte) string {
sh := (*reflect.SliceHeader)(unsafe.Pointer(&data))
sh.Data += uintptr(it.Start)
sh.Len = int(it.End - it.Start)
return *(*string)(unsafe.Pointer(sh))
}
func (ib *inmemoryBlock) Len() int { return len(ib.items) }
func (ib *inmemoryBlock) Less(i, j int) bool {
data := ib.data
items := ib.items
return string(items[i].Bytes(data)) < string(items[j].Bytes(data))
}
func (ib *inmemoryBlock) Swap(i, j int) {
items := ib.items
items[i], items[j] = items[j], items[i]
} }
type inmemoryBlock struct { type inmemoryBlock struct {
commonPrefix []byte commonPrefix []byte
data []byte data []byte
items byteSliceSorter items []Item
} }
func (ib *inmemoryBlock) SizeBytes() int { func (ib *inmemoryBlock) SizeBytes() int {
return int(unsafe.Sizeof(*ib)) + cap(ib.commonPrefix) + cap(ib.data) + cap(ib.items)*int(unsafe.Sizeof([]byte{})) return int(unsafe.Sizeof(*ib)) + cap(ib.commonPrefix) + cap(ib.data) + cap(ib.items)*int(unsafe.Sizeof(Item{}))
} }
func (ib *inmemoryBlock) Reset() { func (ib *inmemoryBlock) Reset() {
ib.commonPrefix = ib.commonPrefix[:0] ib.commonPrefix = ib.commonPrefix[:0]
ib.data = ib.data[:0] ib.data = ib.data[:0]
items := ib.items
for i := range items {
// Remove reference to by slice, so GC could free the byte slice.
items[i] = nil
}
ib.items = ib.items[:0] ib.items = ib.items[:0]
} }
@ -50,12 +78,14 @@ func (ib *inmemoryBlock) updateCommonPrefix() {
if len(ib.items) == 0 { if len(ib.items) == 0 {
return return
} }
cp := ib.items[0] items := ib.items
data := ib.data
cp := items[0].Bytes(data)
if len(cp) == 0 { if len(cp) == 0 {
return return
} }
for _, item := range ib.items[1:] { for _, it := range items[1:] {
cpLen := commonPrefixLen(cp, item) cpLen := commonPrefixLen(cp, it.Bytes(data))
if cpLen == 0 { if cpLen == 0 {
return return
} }
@ -82,15 +112,21 @@ func commonPrefixLen(a, b []byte) int {
// //
// false is returned if x isn't added to ib due to block size contraints. // false is returned if x isn't added to ib due to block size contraints.
func (ib *inmemoryBlock) Add(x []byte) bool { func (ib *inmemoryBlock) Add(x []byte) bool {
if len(x)+len(ib.data) > maxInmemoryBlockSize { data := ib.data
if len(x)+len(data) > maxInmemoryBlockSize {
return false return false
} }
if cap(ib.data) < maxInmemoryBlockSize { if cap(data) < maxInmemoryBlockSize {
dataLen := len(ib.data) dataLen := len(data)
ib.data = bytesutil.Resize(ib.data, maxInmemoryBlockSize)[:dataLen] data = bytesutil.Resize(data, maxInmemoryBlockSize)[:dataLen]
} }
ib.data = append(ib.data, x...) dataLen := len(data)
ib.items = append(ib.items, ib.data[len(ib.data)-len(x):]) data = append(data, x...)
ib.items = append(ib.items, Item{
Start: uint32(dataLen),
End: uint32(len(data)),
})
ib.data = data
return true return true
} }
@ -100,16 +136,21 @@ func (ib *inmemoryBlock) Add(x []byte) bool {
const maxInmemoryBlockSize = 64 * 1024 const maxInmemoryBlockSize = 64 * 1024
func (ib *inmemoryBlock) sort() { func (ib *inmemoryBlock) sort() {
// Use sort.Sort instead of sort.Slice in order to eliminate memory allocation. sort.Sort(ib)
sort.Sort(&ib.items) data := ib.data
items := ib.items
bb := bbPool.Get() bb := bbPool.Get()
b := bytesutil.Resize(bb.B, len(ib.data)) b := bytesutil.Resize(bb.B, len(data))
b = b[:0] b = b[:0]
for i, item := range ib.items { for i, it := range items {
b = append(b, item...) bLen := len(b)
ib.items[i] = b[len(b)-len(item):] b = append(b, it.String(data)...)
items[i] = Item{
Start: uint32(bLen),
End: uint32(len(b)),
}
} }
bb.B, ib.data = ib.data, b bb.B, ib.data = data, b
bbPool.Put(bb) bbPool.Put(bb)
} }
@ -140,7 +181,7 @@ func checkMarshalType(mt marshalType) error {
func (ib *inmemoryBlock) isSorted() bool { func (ib *inmemoryBlock) isSorted() bool {
// Use sort.IsSorted instead of sort.SliceIsSorted in order to eliminate memory allocation. // Use sort.IsSorted instead of sort.SliceIsSorted in order to eliminate memory allocation.
return sort.IsSorted(&ib.items) return sort.IsSorted(ib)
} }
// MarshalUnsortedData marshals unsorted items from ib to sb. // MarshalUnsortedData marshals unsorted items from ib to sb.
@ -179,9 +220,11 @@ func (ib *inmemoryBlock) MarshalSortedData(sb *storageBlock, firstItemDst, commo
func (ib *inmemoryBlock) debugItemsString() string { func (ib *inmemoryBlock) debugItemsString() string {
var sb strings.Builder var sb strings.Builder
var prevItem []byte var prevItem string
for i, item := range ib.items { data := ib.data
if string(item) < string(prevItem) { for i, it := range ib.items {
item := it.String(data)
if item < prevItem {
fmt.Fprintf(&sb, "!!! the next item is smaller than the previous item !!!\n") fmt.Fprintf(&sb, "!!! the next item is smaller than the previous item !!!\n")
} }
fmt.Fprintf(&sb, "%05d %X\n", i, item) fmt.Fprintf(&sb, "%05d %X\n", i, item)
@ -201,7 +244,9 @@ func (ib *inmemoryBlock) marshalData(sb *storageBlock, firstItemDst, commonPrefi
logger.Panicf("BUG: the number of items in the block must be smaller than %d; got %d items", uint64(1<<32), len(ib.items)) logger.Panicf("BUG: the number of items in the block must be smaller than %d; got %d items", uint64(1<<32), len(ib.items))
} }
firstItemDst = append(firstItemDst, ib.items[0]...) data := ib.data
firstItem := ib.items[0].Bytes(data)
firstItemDst = append(firstItemDst, firstItem...)
commonPrefixDst = append(commonPrefixDst, ib.commonPrefix...) commonPrefixDst = append(commonPrefixDst, ib.commonPrefix...)
if len(ib.data)-len(ib.commonPrefix)*len(ib.items) < 64 || len(ib.items) < 2 { if len(ib.data)-len(ib.commonPrefix)*len(ib.items) < 64 || len(ib.items) < 2 {
@ -221,10 +266,11 @@ func (ib *inmemoryBlock) marshalData(sb *storageBlock, firstItemDst, commonPrefi
defer encoding.PutUint64s(xs) defer encoding.PutUint64s(xs)
cpLen := len(ib.commonPrefix) cpLen := len(ib.commonPrefix)
prevItem := ib.items[0][cpLen:] prevItem := firstItem[cpLen:]
prevPrefixLen := uint64(0) prevPrefixLen := uint64(0)
for i, item := range ib.items[1:] { for i, it := range ib.items[1:] {
item := item[cpLen:] it.Start += uint32(cpLen)
item := it.Bytes(data)
prefixLen := uint64(commonPrefixLen(prevItem, item)) prefixLen := uint64(commonPrefixLen(prevItem, item))
bItems = append(bItems, item[prefixLen:]...) bItems = append(bItems, item[prefixLen:]...)
xLen := prefixLen ^ prevPrefixLen xLen := prefixLen ^ prevPrefixLen
@ -240,9 +286,9 @@ func (ib *inmemoryBlock) marshalData(sb *storageBlock, firstItemDst, commonPrefi
bbPool.Put(bbItems) bbPool.Put(bbItems)
// Marshal lens data. // Marshal lens data.
prevItemLen := uint64(len(ib.items[0]) - cpLen) prevItemLen := uint64(len(firstItem) - cpLen)
for i, item := range ib.items[1:] { for i, it := range ib.items[1:] {
itemLen := uint64(len(item) - cpLen) itemLen := uint64(int(it.End-it.Start) - cpLen)
xLen := itemLen ^ prevItemLen xLen := itemLen ^ prevItemLen
prevItemLen = itemLen prevItemLen = itemLen
@ -346,11 +392,15 @@ func (ib *inmemoryBlock) UnmarshalData(sb *storageBlock, firstItem, commonPrefix
} }
data := bytesutil.Resize(ib.data, maxInmemoryBlockSize) data := bytesutil.Resize(ib.data, maxInmemoryBlockSize)
if n := int(itemsCount) - cap(ib.items); n > 0 { if n := int(itemsCount) - cap(ib.items); n > 0 {
ib.items = append(ib.items[:cap(ib.items)], make([][]byte, n)...) ib.items = append(ib.items[:cap(ib.items)], make([]Item, n)...)
} }
ib.items = ib.items[:itemsCount] ib.items = ib.items[:itemsCount]
data = append(data[:0], firstItem...) data = append(data[:0], firstItem...)
ib.items[0] = data items := ib.items
items[0] = Item{
Start: 0,
End: uint32(len(data)),
}
prevItem := data[len(commonPrefix):] prevItem := data[len(commonPrefix):]
b := bb.B b := bb.B
for i := 1; i < int(itemsCount); i++ { for i := 1; i < int(itemsCount); i++ {
@ -363,17 +413,19 @@ func (ib *inmemoryBlock) UnmarshalData(sb *storageBlock, firstItem, commonPrefix
if uint64(len(b)) < suffixLen { if uint64(len(b)) < suffixLen {
return fmt.Errorf("not enough data for decoding item from itemsData; want %d bytes; remained %d bytes", suffixLen, len(b)) return fmt.Errorf("not enough data for decoding item from itemsData; want %d bytes; remained %d bytes", suffixLen, len(b))
} }
data = append(data, commonPrefix...)
if prefixLen > uint64(len(prevItem)) { if prefixLen > uint64(len(prevItem)) {
return fmt.Errorf("prefixLen cannot exceed %d; got %d", len(prevItem), prefixLen) return fmt.Errorf("prefixLen cannot exceed %d; got %d", len(prevItem), prefixLen)
} }
dataLen := len(data)
data = append(data, commonPrefix...)
data = append(data, prevItem[:prefixLen]...) data = append(data, prevItem[:prefixLen]...)
data = append(data, b[:suffixLen]...) data = append(data, b[:suffixLen]...)
item := data[len(data)-int(itemLen)-len(commonPrefix):] items[i] = Item{
ib.items[i] = item Start: uint32(dataLen),
End: uint32(len(data)),
}
b = b[suffixLen:] b = b[suffixLen:]
prevItem = item[len(commonPrefix):] prevItem = data[len(data)-int(itemLen):]
} }
if len(b) > 0 { if len(b) > 0 {
return fmt.Errorf("unexpected tail left after itemsData with len %d: %q", len(b), b) return fmt.Errorf("unexpected tail left after itemsData with len %d: %q", len(b), b)
@ -381,30 +433,33 @@ func (ib *inmemoryBlock) UnmarshalData(sb *storageBlock, firstItem, commonPrefix
if uint64(len(data)) != dataLen { if uint64(len(data)) != dataLen {
return fmt.Errorf("unexpected data len; got %d; want %d", len(data), dataLen) return fmt.Errorf("unexpected data len; got %d; want %d", len(data), dataLen)
} }
ib.data = data
if !ib.isSorted() { if !ib.isSorted() {
return fmt.Errorf("decoded data block contains unsorted items; items:\n%s", ib.debugItemsString()) return fmt.Errorf("decoded data block contains unsorted items; items:\n%s", ib.debugItemsString())
} }
ib.data = data
return nil return nil
} }
var bbPool bytesutil.ByteBufferPool var bbPool bytesutil.ByteBufferPool
func (ib *inmemoryBlock) marshalDataPlain(sb *storageBlock) { func (ib *inmemoryBlock) marshalDataPlain(sb *storageBlock) {
data := ib.data
// Marshal items data. // Marshal items data.
// There is no need in marshaling the first item, since it is returned // There is no need in marshaling the first item, since it is returned
// to the caller in marshalData. // to the caller in marshalData.
cpLen := len(ib.commonPrefix) cpLen := len(ib.commonPrefix)
b := sb.itemsData[:0] b := sb.itemsData[:0]
for _, item := range ib.items[1:] { for _, it := range ib.items[1:] {
b = append(b, item[cpLen:]...) it.Start += uint32(cpLen)
b = append(b, it.String(data)...)
} }
sb.itemsData = b sb.itemsData = b
// Marshal length data. // Marshal length data.
b = sb.lensData[:0] b = sb.lensData[:0]
for _, item := range ib.items[1:] { for _, it := range ib.items[1:] {
b = encoding.MarshalUint64(b, uint64(len(item)-cpLen)) b = encoding.MarshalUint64(b, uint64(int(it.End-it.Start)-cpLen))
} }
sb.lensData = b sb.lensData = b
} }
@ -431,26 +486,34 @@ func (ib *inmemoryBlock) unmarshalDataPlain(sb *storageBlock, firstItem []byte,
} }
// Unmarshal items data. // Unmarshal items data.
ib.data = bytesutil.Resize(ib.data, len(firstItem)+len(sb.itemsData)+len(commonPrefix)*int(itemsCount)) data := ib.data
ib.data = append(ib.data[:0], firstItem...) items := ib.items
ib.items = append(ib.items[:0], ib.data) data = bytesutil.Resize(data, len(firstItem)+len(sb.itemsData)+len(commonPrefix)*int(itemsCount))
data = append(data[:0], firstItem...)
items = append(items[:0], Item{
Start: 0,
End: uint32(len(data)),
})
b = sb.itemsData b = sb.itemsData
for i := 1; i < int(itemsCount); i++ { for i := 1; i < int(itemsCount); i++ {
itemLen := lb.lens[i] itemLen := lb.lens[i]
if uint64(len(b)) < itemLen { if uint64(len(b)) < itemLen {
return fmt.Errorf("not enough data for decoding item from itemsData; want %d bytes; remained %d bytes", itemLen, len(b)) return fmt.Errorf("not enough data for decoding item from itemsData; want %d bytes; remained %d bytes", itemLen, len(b))
} }
ib.data = append(ib.data, commonPrefix...) dataLen := len(data)
ib.data = append(ib.data, b[:itemLen]...) data = append(data, commonPrefix...)
item := ib.data[len(ib.data)-int(itemLen)-len(commonPrefix):] data = append(data, b[:itemLen]...)
ib.items = append(ib.items, item) items = append(items, Item{
Start: uint32(dataLen),
End: uint32(len(data)),
})
b = b[itemLen:] b = b[itemLen:]
} }
ib.data = data
ib.items = items
if len(b) > 0 { if len(b) > 0 {
return fmt.Errorf("unexpected tail left after itemsData with len %d: %q", len(b), b) return fmt.Errorf("unexpected tail left after itemsData with len %d: %q", len(b), b)
} }
return nil return nil
} }

View file

@ -37,8 +37,10 @@ func TestInmemoryBlockAdd(t *testing.T) {
if len(ib.data) != totalLen { if len(ib.data) != totalLen {
t.Fatalf("unexpected ib.data len; got %d; want %d", len(ib.data), totalLen) t.Fatalf("unexpected ib.data len; got %d; want %d", len(ib.data), totalLen)
} }
for j, item := range ib.items { data := ib.data
if items[j] != string(item) { for j, it := range ib.items {
item := it.String(data)
if items[j] != item {
t.Fatalf("unexpected item at index %d out of %d, loop %d\ngot\n%X\nwant\n%X", j, len(items), i, item, items[j]) t.Fatalf("unexpected item at index %d out of %d, loop %d\ngot\n%X\nwant\n%X", j, len(items), i, item, items[j])
} }
} }
@ -75,8 +77,10 @@ func TestInmemoryBlockSort(t *testing.T) {
if len(ib.data) != totalLen { if len(ib.data) != totalLen {
t.Fatalf("unexpected ib.data len; got %d; want %d", len(ib.data), totalLen) t.Fatalf("unexpected ib.data len; got %d; want %d", len(ib.data), totalLen)
} }
for j, item := range ib.items { data := ib.data
if items[j] != string(item) { for j, it := range ib.items {
item := it.String(data)
if items[j] != item {
t.Fatalf("unexpected item at index %d out of %d, loop %d\ngot\n%X\nwant\n%X", j, len(items), i, item, items[j]) t.Fatalf("unexpected item at index %d out of %d, loop %d\ngot\n%X\nwant\n%X", j, len(items), i, item, items[j])
} }
} }
@ -122,8 +126,9 @@ func TestInmemoryBlockMarshalUnmarshal(t *testing.T) {
if int(itemsLen) != len(ib.items) { if int(itemsLen) != len(ib.items) {
t.Fatalf("unexpected number of items marshaled; got %d; want %d", itemsLen, len(ib.items)) t.Fatalf("unexpected number of items marshaled; got %d; want %d", itemsLen, len(ib.items))
} }
if string(firstItem) != string(ib.items[0]) { firstItemExpected := ib.items[0].String(ib.data)
t.Fatalf("unexpected the first item\ngot\n%q\nwant\n%q", firstItem, ib.items[0]) if string(firstItem) != firstItemExpected {
t.Fatalf("unexpected the first item\ngot\n%q\nwant\n%q", firstItem, firstItemExpected)
} }
if err := checkMarshalType(mt); err != nil { if err := checkMarshalType(mt); err != nil {
t.Fatalf("invalid mt: %s", err) t.Fatalf("invalid mt: %s", err)
@ -143,12 +148,15 @@ func TestInmemoryBlockMarshalUnmarshal(t *testing.T) {
t.Fatalf("unexpected ib.data len; got %d; want %d", len(ib2.data), totalLen) t.Fatalf("unexpected ib.data len; got %d; want %d", len(ib2.data), totalLen)
} }
for j := range items { for j := range items {
if len(items[j]) != len(ib2.items[j]) { it2 := ib2.items[j]
item2 := it2.String(ib2.data)
if len(items[j]) != len(item2) {
t.Fatalf("items length mismatch at index %d out of %d, loop %d\ngot\n(len=%d) %X\nwant\n(len=%d) %X", t.Fatalf("items length mismatch at index %d out of %d, loop %d\ngot\n(len=%d) %X\nwant\n(len=%d) %X",
j, len(items), i, len(ib2.items[j]), ib2.items[j], len(items[j]), items[j]) j, len(items), i, len(item2), item2, len(items[j]), items[j])
} }
} }
for j, item := range ib2.items { for j, it := range ib2.items {
item := it.String(ib2.data)
if items[j] != string(item) { if items[j] != string(item) {
t.Fatalf("unexpected item at index %d out of %d, loop %d\ngot\n(len=%d) %X\nwant\n(len=%d) %X", t.Fatalf("unexpected item at index %d out of %d, loop %d\ngot\n(len=%d) %X\nwant\n(len=%d) %X",
j, len(items), i, len(item), item, len(items[j]), items[j]) j, len(items), i, len(item), item, len(items[j]), items[j])

View file

@ -56,8 +56,8 @@ func (ip *inmemoryPart) Init(ib *inmemoryBlock) {
ip.ph.itemsCount = uint64(len(ib.items)) ip.ph.itemsCount = uint64(len(ib.items))
ip.ph.blocksCount = 1 ip.ph.blocksCount = 1
ip.ph.firstItem = append(ip.ph.firstItem[:0], ib.items[0]...) ip.ph.firstItem = append(ip.ph.firstItem[:0], ib.items[0].String(ib.data)...)
ip.ph.lastItem = append(ip.ph.lastItem[:0], ib.items[len(ib.items)-1]...) ip.ph.lastItem = append(ip.ph.lastItem[:0], ib.items[len(ib.items)-1].String(ib.data)...)
fs.MustWriteData(&ip.itemsData, ip.sb.itemsData) fs.MustWriteData(&ip.itemsData, ip.sb.itemsData)
ip.bh.itemsBlockOffset = 0 ip.bh.itemsBlockOffset = 0

View file

@ -16,7 +16,7 @@ import (
// //
// The callback must return sorted items. The first and the last item must be unchanged. // The callback must return sorted items. The first and the last item must be unchanged.
// The callback can re-use data and items for storing the result. // The callback can re-use data and items for storing the result.
type PrepareBlockCallback func(data []byte, items [][]byte) ([]byte, [][]byte) type PrepareBlockCallback func(data []byte, items []Item) ([]byte, []Item)
// mergeBlockStreams merges bsrs and writes result to bsw. // mergeBlockStreams merges bsrs and writes result to bsw.
// //
@ -122,8 +122,10 @@ again:
nextItem = bsm.bsrHeap[0].bh.firstItem nextItem = bsm.bsrHeap[0].bh.firstItem
hasNextItem = true hasNextItem = true
} }
items := bsr.Block.items
data := bsr.Block.data
for bsr.blockItemIdx < len(bsr.Block.items) { for bsr.blockItemIdx < len(bsr.Block.items) {
item := bsr.Block.items[bsr.blockItemIdx] item := items[bsr.blockItemIdx].Bytes(data)
if hasNextItem && string(item) > string(nextItem) { if hasNextItem && string(item) > string(nextItem) {
break break
} }
@ -148,32 +150,36 @@ again:
// The next item in the bsr.Block exceeds nextItem. // The next item in the bsr.Block exceeds nextItem.
// Adjust bsr.bh.firstItem and return bsr to heap. // Adjust bsr.bh.firstItem and return bsr to heap.
bsr.bh.firstItem = append(bsr.bh.firstItem[:0], bsr.Block.items[bsr.blockItemIdx]...) bsr.bh.firstItem = append(bsr.bh.firstItem[:0], bsr.Block.items[bsr.blockItemIdx].String(bsr.Block.data)...)
heap.Push(&bsm.bsrHeap, bsr) heap.Push(&bsm.bsrHeap, bsr)
goto again goto again
} }
func (bsm *blockStreamMerger) flushIB(bsw *blockStreamWriter, ph *partHeader, itemsMerged *uint64) { func (bsm *blockStreamMerger) flushIB(bsw *blockStreamWriter, ph *partHeader, itemsMerged *uint64) {
if len(bsm.ib.items) == 0 { items := bsm.ib.items
data := bsm.ib.data
if len(items) == 0 {
// Nothing to flush. // Nothing to flush.
return return
} }
atomic.AddUint64(itemsMerged, uint64(len(bsm.ib.items))) atomic.AddUint64(itemsMerged, uint64(len(items)))
if bsm.prepareBlock != nil { if bsm.prepareBlock != nil {
bsm.firstItem = append(bsm.firstItem[:0], bsm.ib.items[0]...) bsm.firstItem = append(bsm.firstItem[:0], items[0].String(data)...)
bsm.lastItem = append(bsm.lastItem[:0], bsm.ib.items[len(bsm.ib.items)-1]...) bsm.lastItem = append(bsm.lastItem[:0], items[len(items)-1].String(data)...)
bsm.ib.data, bsm.ib.items = bsm.prepareBlock(bsm.ib.data, bsm.ib.items) data, items = bsm.prepareBlock(data, items)
if len(bsm.ib.items) == 0 { bsm.ib.data = data
bsm.ib.items = items
if len(items) == 0 {
// Nothing to flush // Nothing to flush
return return
} }
// Consistency checks after prepareBlock call. // Consistency checks after prepareBlock call.
firstItem := bsm.ib.items[0] firstItem := items[0].String(data)
if string(firstItem) != string(bsm.firstItem) { if firstItem != string(bsm.firstItem) {
logger.Panicf("BUG: prepareBlock must return first item equal to the original first item;\ngot\n%X\nwant\n%X", firstItem, bsm.firstItem) logger.Panicf("BUG: prepareBlock must return first item equal to the original first item;\ngot\n%X\nwant\n%X", firstItem, bsm.firstItem)
} }
lastItem := bsm.ib.items[len(bsm.ib.items)-1] lastItem := items[len(items)-1].String(data)
if string(lastItem) != string(bsm.lastItem) { if lastItem != string(bsm.lastItem) {
logger.Panicf("BUG: prepareBlock must return last item equal to the original last item;\ngot\n%X\nwant\n%X", lastItem, bsm.lastItem) logger.Panicf("BUG: prepareBlock must return last item equal to the original last item;\ngot\n%X\nwant\n%X", lastItem, bsm.lastItem)
} }
// Verify whether the bsm.ib.items are sorted only in tests, since this // Verify whether the bsm.ib.items are sorted only in tests, since this
@ -182,12 +188,12 @@ func (bsm *blockStreamMerger) flushIB(bsw *blockStreamWriter, ph *partHeader, it
logger.Panicf("BUG: prepareBlock must return sorted items;\ngot\n%s", bsm.ib.debugItemsString()) logger.Panicf("BUG: prepareBlock must return sorted items;\ngot\n%s", bsm.ib.debugItemsString())
} }
} }
ph.itemsCount += uint64(len(bsm.ib.items)) ph.itemsCount += uint64(len(items))
if !bsm.phFirstItemCaught { if !bsm.phFirstItemCaught {
ph.firstItem = append(ph.firstItem[:0], bsm.ib.items[0]...) ph.firstItem = append(ph.firstItem[:0], items[0].String(data)...)
bsm.phFirstItemCaught = true bsm.phFirstItemCaught = true
} }
ph.lastItem = append(ph.lastItem[:0], bsm.ib.items[len(bsm.ib.items)-1]...) ph.lastItem = append(ph.lastItem[:0], items[len(items)-1].String(data)...)
bsw.WriteBlock(&bsm.ib) bsw.WriteBlock(&bsm.ib)
bsm.ib.Reset() bsm.ib.Reset()
ph.blocksCount++ ph.blocksCount++

View file

@ -157,10 +157,12 @@ func testCheckItems(dstIP *inmemoryPart, items []string) error {
if bh.itemsCount <= 0 { if bh.itemsCount <= 0 {
return fmt.Errorf("unexpected empty block") return fmt.Errorf("unexpected empty block")
} }
if string(bh.firstItem) != string(dstBsr.Block.items[0]) { item := dstBsr.Block.items[0].Bytes(dstBsr.Block.data)
return fmt.Errorf("unexpected blockHeader.firstItem; got %q; want %q", bh.firstItem, dstBsr.Block.items[0]) if string(bh.firstItem) != string(item) {
return fmt.Errorf("unexpected blockHeader.firstItem; got %q; want %q", bh.firstItem, item)
} }
for _, item := range dstBsr.Block.items { for _, it := range dstBsr.Block.items {
item := it.Bytes(dstBsr.Block.data)
dstItems = append(dstItems, string(item)) dstItems = append(dstItems, string(item))
} }
} }

View file

@ -138,24 +138,14 @@ type indexBlock struct {
} }
func (idxb *indexBlock) SizeBytes() int { func (idxb *indexBlock) SizeBytes() int {
return cap(idxb.bhs) * int(unsafe.Sizeof(blockHeader{})) bhs := idxb.bhs[:cap(idxb.bhs)]
} n := int(unsafe.Sizeof(*idxb))
for i := range bhs {
func getIndexBlock() *indexBlock { n += bhs[i].SizeBytes()
v := indexBlockPool.Get()
if v == nil {
return &indexBlock{}
} }
return v.(*indexBlock) return n
} }
func putIndexBlock(idxb *indexBlock) {
idxb.bhs = idxb.bhs[:0]
indexBlockPool.Put(idxb)
}
var indexBlockPool sync.Pool
type indexBlockCache struct { type indexBlockCache struct {
// Atomically updated counters must go first in the struct, so they are properly // Atomically updated counters must go first in the struct, so they are properly
// aligned to 8 bytes on 32-bit architectures. // aligned to 8 bytes on 32-bit architectures.
@ -194,12 +184,6 @@ func newIndexBlockCache() *indexBlockCache {
func (idxbc *indexBlockCache) MustClose() { func (idxbc *indexBlockCache) MustClose() {
close(idxbc.cleanerStopCh) close(idxbc.cleanerStopCh)
idxbc.cleanerWG.Wait() idxbc.cleanerWG.Wait()
// It is safe returning idxbc.m to pool, since the MustClose can be called
// when the idxbc entries are no longer accessed by concurrent goroutines.
for _, idxbe := range idxbc.m {
putIndexBlock(idxbe.idxb)
}
idxbc.m = nil idxbc.m = nil
} }
@ -223,8 +207,6 @@ func (idxbc *indexBlockCache) cleanByTimeout() {
for k, idxbe := range idxbc.m { for k, idxbe := range idxbc.m {
// Delete items accessed more than two minutes ago. // Delete items accessed more than two minutes ago.
if currentTime-atomic.LoadUint64(&idxbe.lastAccessTime) > 2*60 { if currentTime-atomic.LoadUint64(&idxbe.lastAccessTime) > 2*60 {
// do not call putIndexBlock(ibxbc.m[k]), since it
// may be used by concurrent goroutines.
delete(idxbc.m, k) delete(idxbc.m, k)
} }
} }
@ -257,8 +239,6 @@ func (idxbc *indexBlockCache) Put(k uint64, idxb *indexBlock) {
// Remove 10% of items from the cache. // Remove 10% of items from the cache.
overflow = int(float64(len(idxbc.m)) * 0.1) overflow = int(float64(len(idxbc.m)) * 0.1)
for k := range idxbc.m { for k := range idxbc.m {
// do not call putIndexBlock(ibxbc.m[k]), since it
// may be used by concurrent goroutines.
delete(idxbc.m, k) delete(idxbc.m, k)
overflow-- overflow--
if overflow == 0 { if overflow == 0 {
@ -348,12 +328,6 @@ func newInmemoryBlockCache() *inmemoryBlockCache {
func (ibc *inmemoryBlockCache) MustClose() { func (ibc *inmemoryBlockCache) MustClose() {
close(ibc.cleanerStopCh) close(ibc.cleanerStopCh)
ibc.cleanerWG.Wait() ibc.cleanerWG.Wait()
// It is safe returning ibc.m entries to pool, since the MustClose can be called
// only if no other goroutines access ibc entries.
for _, ibe := range ibc.m {
putInmemoryBlock(ibe.ib)
}
ibc.m = nil ibc.m = nil
} }
@ -377,8 +351,6 @@ func (ibc *inmemoryBlockCache) cleanByTimeout() {
for k, ibe := range ibc.m { for k, ibe := range ibc.m {
// Delete items accessed more than a two minutes ago. // Delete items accessed more than a two minutes ago.
if currentTime-atomic.LoadUint64(&ibe.lastAccessTime) > 2*60 { if currentTime-atomic.LoadUint64(&ibe.lastAccessTime) > 2*60 {
// do not call putInmemoryBlock(ibc.m[k]), since it
// may be used by concurrent goroutines.
delete(ibc.m, k) delete(ibc.m, k)
} }
} }
@ -412,8 +384,6 @@ func (ibc *inmemoryBlockCache) Put(k inmemoryBlockCacheKey, ib *inmemoryBlock) {
// Remove 10% of items from the cache. // Remove 10% of items from the cache.
overflow = int(float64(len(ibc.m)) * 0.1) overflow = int(float64(len(ibc.m)) * 0.1)
for k := range ibc.m { for k := range ibc.m {
// do not call putInmemoryBlock(ib), since the ib
// may be used by concurrent goroutines.
delete(ibc.m, k) delete(ibc.m, k)
overflow-- overflow--
if overflow == 0 { if overflow == 0 {

View file

@ -142,14 +142,17 @@ func (ps *partSearch) Seek(k []byte) {
// Locate the first item to scan in the block. // Locate the first item to scan in the block.
items := ps.ib.items items := ps.ib.items
data := ps.ib.data
cpLen := commonPrefixLen(ps.ib.commonPrefix, k) cpLen := commonPrefixLen(ps.ib.commonPrefix, k)
if cpLen > 0 { if cpLen > 0 {
keySuffix := k[cpLen:] keySuffix := k[cpLen:]
ps.ibItemIdx = sort.Search(len(items), func(i int) bool { ps.ibItemIdx = sort.Search(len(items), func(i int) bool {
return string(keySuffix) <= string(items[i][cpLen:]) it := items[i]
it.Start += uint32(cpLen)
return string(keySuffix) <= it.String(data)
}) })
} else { } else {
ps.ibItemIdx = binarySearchKey(items, k) ps.ibItemIdx = binarySearchKey(data, items, k)
} }
if ps.ibItemIdx < len(items) { if ps.ibItemIdx < len(items) {
// The item has been found. // The item has been found.
@ -168,13 +171,14 @@ func (ps *partSearch) tryFastSeek(k []byte) bool {
if ps.ib == nil { if ps.ib == nil {
return false return false
} }
data := ps.ib.data
items := ps.ib.items items := ps.ib.items
idx := ps.ibItemIdx idx := ps.ibItemIdx
if idx >= len(items) { if idx >= len(items) {
// The ib is exhausted. // The ib is exhausted.
return false return false
} }
if string(k) > string(items[len(items)-1]) { if string(k) > items[len(items)-1].String(data) {
// The item is located in next blocks. // The item is located in next blocks.
return false return false
} }
@ -183,8 +187,8 @@ func (ps *partSearch) tryFastSeek(k []byte) bool {
if idx > 0 { if idx > 0 {
idx-- idx--
} }
if string(k) < string(items[idx]) { if string(k) < items[idx].String(data) {
if string(k) < string(items[0]) { if string(k) < items[0].String(data) {
// The item is located in previous blocks. // The item is located in previous blocks.
return false return false
} }
@ -192,7 +196,7 @@ func (ps *partSearch) tryFastSeek(k []byte) bool {
} }
// The item is located in the current block // The item is located in the current block
ps.ibItemIdx = idx + binarySearchKey(items[idx:], k) ps.ibItemIdx = idx + binarySearchKey(data, items[idx:], k)
return true return true
} }
@ -204,10 +208,11 @@ func (ps *partSearch) NextItem() bool {
return false return false
} }
if ps.ibItemIdx < len(ps.ib.items) { items := ps.ib.items
if ps.ibItemIdx < len(items) {
// Fast path - the current block contains more items. // Fast path - the current block contains more items.
// Proceed to the next item. // Proceed to the next item.
ps.Item = ps.ib.items[ps.ibItemIdx] ps.Item = items[ps.ibItemIdx].Bytes(ps.ib.data)
ps.ibItemIdx++ ps.ibItemIdx++
return true return true
} }
@ -219,7 +224,7 @@ func (ps *partSearch) NextItem() bool {
} }
// Invariant: len(ps.ib.items) > 0 after nextBlock. // Invariant: len(ps.ib.items) > 0 after nextBlock.
ps.Item = ps.ib.items[0] ps.Item = ps.ib.items[0].Bytes(ps.ib.data)
ps.ibItemIdx++ ps.ibItemIdx++
return true return true
} }
@ -279,7 +284,7 @@ func (ps *partSearch) readIndexBlock(mr *metaindexRow) (*indexBlock, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot decompress index block: %w", err) return nil, fmt.Errorf("cannot decompress index block: %w", err)
} }
idxb := getIndexBlock() idxb := &indexBlock{}
idxb.bhs, err = unmarshalBlockHeaders(idxb.bhs[:0], ps.indexBuf, int(mr.blockHeadersCount)) idxb.bhs, err = unmarshalBlockHeaders(idxb.bhs[:0], ps.indexBuf, int(mr.blockHeadersCount))
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot unmarshal block headers from index block (offset=%d, size=%d): %w", mr.indexBlockOffset, mr.indexBlockSize, err) return nil, fmt.Errorf("cannot unmarshal block headers from index block (offset=%d, size=%d): %w", mr.indexBlockOffset, mr.indexBlockSize, err)
@ -319,11 +324,11 @@ func (ps *partSearch) readInmemoryBlock(bh *blockHeader) (*inmemoryBlock, error)
return ib, nil return ib, nil
} }
func binarySearchKey(items [][]byte, key []byte) int { func binarySearchKey(data []byte, items []Item, key []byte) int {
if len(items) == 0 { if len(items) == 0 {
return 0 return 0
} }
if string(key) <= string(items[0]) { if string(key) <= items[0].String(data) {
// Fast path - the item is the first. // Fast path - the item is the first.
return 0 return 0
} }
@ -335,7 +340,7 @@ func binarySearchKey(items [][]byte, key []byte) int {
i, j := uint(0), n i, j := uint(0), n
for i < j { for i < j {
h := uint(i+j) >> 1 h := uint(i+j) >> 1
if h >= 0 && h < uint(len(items)) && string(key) > string(items[h]) { if h >= 0 && h < uint(len(items)) && string(key) > items[h].String(data) {
i = h + 1 i = h + 1
} else { } else {
j = h j = h

View file

@ -1333,12 +1333,6 @@ func appendPartsToMerge(dst, src []*partWrapper, maxPartsToMerge int, maxItems u
for _, pw := range a { for _, pw := range a {
itemsSum += pw.p.ph.itemsCount itemsSum += pw.p.ph.itemsCount
} }
if itemsSum < 1e6 && len(a) < maxPartsToMerge {
// Do not merge parts with too small number of items if the number of source parts
// isn't equal to maxPartsToMerge. This should reduce CPU usage and disk IO usage
// for small parts merge.
continue
}
if itemsSum > maxItems { if itemsSum > maxItems {
// There is no sense in checking the remaining bigger parts. // There is no sense in checking the remaining bigger parts.
break break

View file

@ -46,7 +46,7 @@ func benchmarkTableSearch(b *testing.B, itemsCount int) {
b.Run("sequential-keys-exact", func(b *testing.B) { b.Run("sequential-keys-exact", func(b *testing.B) {
benchmarkTableSearchKeys(b, tb, keys, 0) benchmarkTableSearchKeys(b, tb, keys, 0)
}) })
b.Run("sequential-keys-without-siffux", func(b *testing.B) { b.Run("sequential-keys-without-suffix", func(b *testing.B) {
benchmarkTableSearchKeys(b, tb, keys, 4) benchmarkTableSearchKeys(b, tb, keys, 4)
}) })
@ -57,7 +57,7 @@ func benchmarkTableSearch(b *testing.B, itemsCount int) {
b.Run("random-keys-exact", func(b *testing.B) { b.Run("random-keys-exact", func(b *testing.B) {
benchmarkTableSearchKeys(b, tb, randKeys, 0) benchmarkTableSearchKeys(b, tb, randKeys, 0)
}) })
b.Run("random-keys-without-siffux", func(b *testing.B) { b.Run("random-keys-without-suffix", func(b *testing.B) {
benchmarkTableSearchKeys(b, tb, randKeys, 4) benchmarkTableSearchKeys(b, tb, randKeys, 4)
}) })
} }

View file

@ -218,7 +218,7 @@ func TestTableAddItemsConcurrent(t *testing.T) {
atomic.AddUint64(&flushes, 1) atomic.AddUint64(&flushes, 1)
} }
var itemsMerged uint64 var itemsMerged uint64
prepareBlock := func(data []byte, items [][]byte) ([]byte, [][]byte) { prepareBlock := func(data []byte, items []Item) ([]byte, []Item) {
atomic.AddUint64(&itemsMerged, uint64(len(items))) atomic.AddUint64(&itemsMerged, uint64(len(items)))
return data, items return data, items
} }

View file

@ -52,16 +52,24 @@ func MustOpenFastQueue(path, name string, maxInmemoryBlocks, maxPendingBytes int
return fq return fq
} }
// MustClose unblocks all the readers. // UnblockAllReaders unblocks all the readers.
// func (fq *FastQueue) UnblockAllReaders() {
// It is expected no new writers during and after the call.
func (fq *FastQueue) MustClose() {
fq.mu.Lock() fq.mu.Lock()
defer fq.mu.Unlock() defer fq.mu.Unlock()
// Unblock blocked readers // Unblock blocked readers
fq.mustStop = true fq.mustStop = true
fq.cond.Broadcast() fq.cond.Broadcast()
}
// MustClose unblocks all the readers.
//
// It is expected no new writers during and after the call.
func (fq *FastQueue) MustClose() {
fq.UnblockAllReaders()
fq.mu.Lock()
defer fq.mu.Unlock()
// flush blocks from fq.ch to fq.pq, so they can be persisted // flush blocks from fq.ch to fq.pq, so they can be persisted
fq.flushInmemoryBlocksToFileLocked() fq.flushInmemoryBlocksToFileLocked()

View file

@ -23,38 +23,78 @@ type RelabelConfig struct {
Action string `yaml:"action,omitempty"` Action string `yaml:"action,omitempty"`
} }
// ParsedConfigs represents parsed relabel configs.
type ParsedConfigs struct {
prcs []*parsedRelabelConfig
}
// Len returns the number of relabel configs in pcs.
func (pcs *ParsedConfigs) Len() int {
if pcs == nil {
return 0
}
return len(pcs.prcs)
}
// String returns human-readabale representation for pcs.
func (pcs *ParsedConfigs) String() string {
if pcs == nil {
return ""
}
var sb strings.Builder
for _, prc := range pcs.prcs {
fmt.Fprintf(&sb, "%s", prc.String())
}
return sb.String()
}
// LoadRelabelConfigs loads relabel configs from the given path. // LoadRelabelConfigs loads relabel configs from the given path.
func LoadRelabelConfigs(path string) ([]ParsedRelabelConfig, error) { func LoadRelabelConfigs(path string) (*ParsedConfigs, error) {
data, err := ioutil.ReadFile(path) data, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot read `relabel_configs` from %q: %w", path, err) return nil, fmt.Errorf("cannot read `relabel_configs` from %q: %w", path, err)
} }
data = envtemplate.Replace(data) data = envtemplate.Replace(data)
var rcs []RelabelConfig pcs, err := ParseRelabelConfigsData(data)
if err := yaml.UnmarshalStrict(data, &rcs); err != nil { if err != nil {
return nil, fmt.Errorf("cannot unmarshal `relabel_configs` from %q: %w", path, err) return nil, fmt.Errorf("cannot unmarshal `relabel_configs` from %q: %w", path, err)
} }
return ParseRelabelConfigs(nil, rcs) return pcs, nil
}
// ParseRelabelConfigsData parses relabel configs from the given data.
func ParseRelabelConfigsData(data []byte) (*ParsedConfigs, error) {
var rcs []RelabelConfig
if err := yaml.UnmarshalStrict(data, &rcs); err != nil {
return nil, err
}
return ParseRelabelConfigs(rcs)
} }
// ParseRelabelConfigs parses rcs to dst. // ParseRelabelConfigs parses rcs to dst.
func ParseRelabelConfigs(dst []ParsedRelabelConfig, rcs []RelabelConfig) ([]ParsedRelabelConfig, error) { func ParseRelabelConfigs(rcs []RelabelConfig) (*ParsedConfigs, error) {
if len(rcs) == 0 { if len(rcs) == 0 {
return dst, nil return nil, nil
} }
prcs := make([]*parsedRelabelConfig, len(rcs))
for i := range rcs { for i := range rcs {
var err error prc, err := parseRelabelConfig(&rcs[i])
dst, err = parseRelabelConfig(dst, &rcs[i])
if err != nil { if err != nil {
return dst, fmt.Errorf("error when parsing `relabel_config` #%d: %w", i+1, err) return nil, fmt.Errorf("error when parsing `relabel_config` #%d: %w", i+1, err)
} }
prcs[i] = prc
} }
return dst, nil return &ParsedConfigs{
prcs: prcs,
}, nil
} }
var defaultRegexForRelabelConfig = regexp.MustCompile("^(.*)$") var (
defaultOriginalRegexForRelabelConfig = regexp.MustCompile(".*")
defaultRegexForRelabelConfig = regexp.MustCompile("^(.*)$")
)
func parseRelabelConfig(dst []ParsedRelabelConfig, rc *RelabelConfig) ([]ParsedRelabelConfig, error) { func parseRelabelConfig(rc *RelabelConfig) (*parsedRelabelConfig, error) {
sourceLabels := rc.SourceLabels sourceLabels := rc.SourceLabels
separator := ";" separator := ";"
if rc.Separator != nil { if rc.Separator != nil {
@ -62,6 +102,7 @@ func parseRelabelConfig(dst []ParsedRelabelConfig, rc *RelabelConfig) ([]ParsedR
} }
targetLabel := rc.TargetLabel targetLabel := rc.TargetLabel
regexCompiled := defaultRegexForRelabelConfig regexCompiled := defaultRegexForRelabelConfig
regexOriginalCompiled := defaultOriginalRegexForRelabelConfig
if rc.Regex != nil { if rc.Regex != nil {
regex := *rc.Regex regex := *rc.Regex
if rc.Action != "replace_all" && rc.Action != "labelmap_all" { if rc.Action != "replace_all" && rc.Action != "labelmap_all" {
@ -69,9 +110,14 @@ func parseRelabelConfig(dst []ParsedRelabelConfig, rc *RelabelConfig) ([]ParsedR
} }
re, err := regexp.Compile(regex) re, err := regexp.Compile(regex)
if err != nil { if err != nil {
return dst, fmt.Errorf("cannot parse `regex` %q: %w", regex, err) return nil, fmt.Errorf("cannot parse `regex` %q: %w", regex, err)
} }
regexCompiled = re regexCompiled = re
reOriginal, err := regexp.Compile(*rc.Regex)
if err != nil {
return nil, fmt.Errorf("cannot parse `regex` %q: %w", *rc.Regex, err)
}
regexOriginalCompiled = reOriginal
} }
modulus := rc.Modulus modulus := rc.Modulus
replacement := "$1" replacement := "$1"
@ -85,49 +131,49 @@ func parseRelabelConfig(dst []ParsedRelabelConfig, rc *RelabelConfig) ([]ParsedR
switch action { switch action {
case "replace": case "replace":
if targetLabel == "" { if targetLabel == "" {
return dst, fmt.Errorf("missing `target_label` for `action=replace`") return nil, fmt.Errorf("missing `target_label` for `action=replace`")
} }
case "replace_all": case "replace_all":
if len(sourceLabels) == 0 { if len(sourceLabels) == 0 {
return dst, fmt.Errorf("missing `source_labels` for `action=replace_all`") return nil, fmt.Errorf("missing `source_labels` for `action=replace_all`")
} }
if targetLabel == "" { if targetLabel == "" {
return dst, fmt.Errorf("missing `target_label` for `action=replace_all`") return nil, fmt.Errorf("missing `target_label` for `action=replace_all`")
} }
case "keep_if_equal": case "keep_if_equal":
if len(sourceLabels) < 2 { if len(sourceLabels) < 2 {
return dst, fmt.Errorf("`source_labels` must contain at least two entries for `action=keep_if_equal`; got %q", sourceLabels) return nil, fmt.Errorf("`source_labels` must contain at least two entries for `action=keep_if_equal`; got %q", sourceLabels)
} }
case "drop_if_equal": case "drop_if_equal":
if len(sourceLabels) < 2 { if len(sourceLabels) < 2 {
return dst, fmt.Errorf("`source_labels` must contain at least two entries for `action=drop_if_equal`; got %q", sourceLabels) return nil, fmt.Errorf("`source_labels` must contain at least two entries for `action=drop_if_equal`; got %q", sourceLabels)
} }
case "keep": case "keep":
if len(sourceLabels) == 0 { if len(sourceLabels) == 0 {
return dst, fmt.Errorf("missing `source_labels` for `action=keep`") return nil, fmt.Errorf("missing `source_labels` for `action=keep`")
} }
case "drop": case "drop":
if len(sourceLabels) == 0 { if len(sourceLabels) == 0 {
return dst, fmt.Errorf("missing `source_labels` for `action=drop`") return nil, fmt.Errorf("missing `source_labels` for `action=drop`")
} }
case "hashmod": case "hashmod":
if len(sourceLabels) == 0 { if len(sourceLabels) == 0 {
return dst, fmt.Errorf("missing `source_labels` for `action=hashmod`") return nil, fmt.Errorf("missing `source_labels` for `action=hashmod`")
} }
if targetLabel == "" { if targetLabel == "" {
return dst, fmt.Errorf("missing `target_label` for `action=hashmod`") return nil, fmt.Errorf("missing `target_label` for `action=hashmod`")
} }
if modulus < 1 { if modulus < 1 {
return dst, fmt.Errorf("unexpected `modulus` for `action=hashmod`: %d; must be greater than 0", modulus) return nil, fmt.Errorf("unexpected `modulus` for `action=hashmod`: %d; must be greater than 0", modulus)
} }
case "labelmap": case "labelmap":
case "labelmap_all": case "labelmap_all":
case "labeldrop": case "labeldrop":
case "labelkeep": case "labelkeep":
default: default:
return dst, fmt.Errorf("unknown `action` %q", action) return nil, fmt.Errorf("unknown `action` %q", action)
} }
dst = append(dst, ParsedRelabelConfig{ return &parsedRelabelConfig{
SourceLabels: sourceLabels, SourceLabels: sourceLabels,
Separator: separator, Separator: separator,
TargetLabel: targetLabel, TargetLabel: targetLabel,
@ -136,8 +182,8 @@ func parseRelabelConfig(dst []ParsedRelabelConfig, rc *RelabelConfig) ([]ParsedR
Replacement: replacement, Replacement: replacement,
Action: action, Action: action,
regexOriginal: regexOriginalCompiled,
hasCaptureGroupInTargetLabel: strings.Contains(targetLabel, "$"), hasCaptureGroupInTargetLabel: strings.Contains(targetLabel, "$"),
hasCaptureGroupInReplacement: strings.Contains(replacement, "$"), hasCaptureGroupInReplacement: strings.Contains(replacement, "$"),
}) }, nil
return dst, nil
} }

View file

@ -7,12 +7,12 @@ import (
func TestLoadRelabelConfigsSuccess(t *testing.T) { func TestLoadRelabelConfigsSuccess(t *testing.T) {
path := "testdata/relabel_configs_valid.yml" path := "testdata/relabel_configs_valid.yml"
prcs, err := LoadRelabelConfigs(path) pcs, err := LoadRelabelConfigs(path)
if err != nil { if err != nil {
t.Fatalf("cannot load relabel configs from %q: %s", path, err) t.Fatalf("cannot load relabel configs from %q: %s", path, err)
} }
if len(prcs) != 9 { if n := pcs.Len(); n != 9 {
t.Fatalf("unexpected number of relabel configs loaded from %q; got %d; want %d", path, len(prcs), 9) t.Fatalf("unexpected number of relabel configs loaded from %q; got %d; want %d", path, n, 9)
} }
} }
@ -23,7 +23,7 @@ func TestLoadRelabelConfigsFailure(t *testing.T) {
if err == nil { if err == nil {
t.Fatalf("expecting non-nil error") t.Fatalf("expecting non-nil error")
} }
if len(rcs) != 0 { if rcs.Len() != 0 {
t.Fatalf("unexpected non-empty rcs: %#v", rcs) t.Fatalf("unexpected non-empty rcs: %#v", rcs)
} }
} }
@ -36,14 +36,14 @@ func TestLoadRelabelConfigsFailure(t *testing.T) {
} }
func TestParseRelabelConfigsSuccess(t *testing.T) { func TestParseRelabelConfigsSuccess(t *testing.T) {
f := func(rcs []RelabelConfig, prcsExpected []ParsedRelabelConfig) { f := func(rcs []RelabelConfig, pcsExpected *ParsedConfigs) {
t.Helper() t.Helper()
prcs, err := ParseRelabelConfigs(nil, rcs) pcs, err := ParseRelabelConfigs(rcs)
if err != nil { if err != nil {
t.Fatalf("unexected error: %s", err) t.Fatalf("unexected error: %s", err)
} }
if !reflect.DeepEqual(prcs, prcsExpected) { if !reflect.DeepEqual(pcs, pcsExpected) {
t.Fatalf("unexpected prcs; got\n%#v\nwant\n%#v", prcs, prcsExpected) t.Fatalf("unexpected pcs; got\n%#v\nwant\n%#v", pcs, pcsExpected)
} }
} }
f(nil, nil) f(nil, nil)
@ -52,16 +52,19 @@ func TestParseRelabelConfigsSuccess(t *testing.T) {
SourceLabels: []string{"foo", "bar"}, SourceLabels: []string{"foo", "bar"},
TargetLabel: "xxx", TargetLabel: "xxx",
}, },
}, []ParsedRelabelConfig{ }, &ParsedConfigs{
{ prcs: []*parsedRelabelConfig{
SourceLabels: []string{"foo", "bar"}, {
Separator: ";", SourceLabels: []string{"foo", "bar"},
TargetLabel: "xxx", Separator: ";",
Regex: defaultRegexForRelabelConfig, TargetLabel: "xxx",
Replacement: "$1", Regex: defaultRegexForRelabelConfig,
Action: "replace", Replacement: "$1",
Action: "replace",
hasCaptureGroupInReplacement: true, regexOriginal: defaultOriginalRegexForRelabelConfig,
hasCaptureGroupInReplacement: true,
},
}, },
}) })
} }
@ -69,12 +72,12 @@ func TestParseRelabelConfigsSuccess(t *testing.T) {
func TestParseRelabelConfigsFailure(t *testing.T) { func TestParseRelabelConfigsFailure(t *testing.T) {
f := func(rcs []RelabelConfig) { f := func(rcs []RelabelConfig) {
t.Helper() t.Helper()
prcs, err := ParseRelabelConfigs(nil, rcs) pcs, err := ParseRelabelConfigs(rcs)
if err == nil { if err == nil {
t.Fatalf("expecting non-nil error") t.Fatalf("expecting non-nil error")
} }
if len(prcs) > 0 { if pcs.Len() > 0 {
t.Fatalf("unexpected non-empty prcs: %#v", prcs) t.Fatalf("unexpected non-empty pcs: %#v", pcs)
} }
} }
t.Run("invalid-regex", func(t *testing.T) { t.Run("invalid-regex", func(t *testing.T) {

View file

@ -12,10 +12,10 @@ import (
xxhash "github.com/cespare/xxhash/v2" xxhash "github.com/cespare/xxhash/v2"
) )
// ParsedRelabelConfig contains parsed `relabel_config`. // parsedRelabelConfig contains parsed `relabel_config`.
// //
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config
type ParsedRelabelConfig struct { type parsedRelabelConfig struct {
SourceLabels []string SourceLabels []string
Separator string Separator string
TargetLabel string TargetLabel string
@ -24,29 +24,32 @@ type ParsedRelabelConfig struct {
Replacement string Replacement string
Action string Action string
regexOriginal *regexp.Regexp
hasCaptureGroupInTargetLabel bool hasCaptureGroupInTargetLabel bool
hasCaptureGroupInReplacement bool hasCaptureGroupInReplacement bool
} }
// String returns human-readable representation for prc. // String returns human-readable representation for prc.
func (prc *ParsedRelabelConfig) String() string { func (prc *parsedRelabelConfig) String() string {
return fmt.Sprintf("SourceLabels=%s, Separator=%s, TargetLabel=%s, Regex=%s, Modulus=%d, Replacement=%s, Action=%s", return fmt.Sprintf("SourceLabels=%s, Separator=%s, TargetLabel=%s, Regex=%s, Modulus=%d, Replacement=%s, Action=%s",
prc.SourceLabels, prc.Separator, prc.TargetLabel, prc.Regex.String(), prc.Modulus, prc.Replacement, prc.Action) prc.SourceLabels, prc.Separator, prc.TargetLabel, prc.Regex.String(), prc.Modulus, prc.Replacement, prc.Action)
} }
// ApplyRelabelConfigs applies prcs to labels starting from the labelsOffset. // Apply applies pcs to labels starting from the labelsOffset.
// //
// If isFinalize is set, then FinalizeLabels is called on the labels[labelsOffset:]. // If isFinalize is set, then FinalizeLabels is called on the labels[labelsOffset:].
// //
// The returned labels at labels[labelsOffset:] are sorted. // The returned labels at labels[labelsOffset:] are sorted.
func ApplyRelabelConfigs(labels []prompbmarshal.Label, labelsOffset int, prcs []ParsedRelabelConfig, isFinalize bool) []prompbmarshal.Label { func (pcs *ParsedConfigs) Apply(labels []prompbmarshal.Label, labelsOffset int, isFinalize bool) []prompbmarshal.Label {
for i := range prcs { if pcs != nil {
tmp := applyRelabelConfig(labels, labelsOffset, &prcs[i]) for _, prc := range pcs.prcs {
if len(tmp) == labelsOffset { tmp := prc.apply(labels, labelsOffset)
// All the labels have been removed. if len(tmp) == labelsOffset {
return tmp // All the labels have been removed.
return tmp
}
labels = tmp
} }
labels = tmp
} }
labels = removeEmptyLabels(labels, labelsOffset) labels = removeEmptyLabels(labels, labelsOffset)
if isFinalize { if isFinalize {
@ -106,21 +109,32 @@ func FinalizeLabels(dst, src []prompbmarshal.Label) []prompbmarshal.Label {
return dst return dst
} }
// applyRelabelConfig applies relabeling according to prc. // apply applies relabeling according to prc.
// //
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config
func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, prc *ParsedRelabelConfig) []prompbmarshal.Label { func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset int) []prompbmarshal.Label {
src := labels[labelsOffset:] src := labels[labelsOffset:]
switch prc.Action { switch prc.Action {
case "replace": case "replace":
bb := relabelBufPool.Get() bb := relabelBufPool.Get()
bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator) bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator)
if len(bb.B) == 0 && prc.Regex == defaultRegexForRelabelConfig && !prc.hasCaptureGroupInReplacement && !prc.hasCaptureGroupInTargetLabel { if prc.Regex == defaultRegexForRelabelConfig && !prc.hasCaptureGroupInTargetLabel {
// Fast path for the following rule that just sets label value: if prc.Replacement == "$1" {
// - target_label: foobar // Fast path for the rule that copies source label values to destination:
// replacement: something-here // - source_labels: [...]
relabelBufPool.Put(bb) // target_label: foobar
return setLabelValue(labels, labelsOffset, prc.TargetLabel, prc.Replacement) valueStr := string(bb.B)
relabelBufPool.Put(bb)
return setLabelValue(labels, labelsOffset, prc.TargetLabel, valueStr)
}
if !prc.hasCaptureGroupInReplacement {
// Fast path for the rule that sets label value:
// - target_label: foobar
// replacement: something-here
relabelBufPool.Put(bb)
labels = setLabelValue(labels, labelsOffset, prc.TargetLabel, prc.Replacement)
return labels
}
} }
match := prc.Regex.FindSubmatchIndex(bb.B) match := prc.Regex.FindSubmatchIndex(bb.B)
if match == nil { if match == nil {
@ -139,15 +153,13 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, prc *Par
case "replace_all": case "replace_all":
bb := relabelBufPool.Get() bb := relabelBufPool.Get()
bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator) bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator)
if !prc.Regex.Match(bb.B) { sourceStr := string(bb.B)
// Fast path - nothing to replace.
relabelBufPool.Put(bb)
return labels
}
sourceStr := string(bb.B) // Make a copy of bb, since it can be returned from ReplaceAllString
relabelBufPool.Put(bb) relabelBufPool.Put(bb)
valueStr := prc.Regex.ReplaceAllString(sourceStr, prc.Replacement) valueStr, ok := prc.replaceStringSubmatches(sourceStr, prc.Replacement, prc.hasCaptureGroupInReplacement)
return setLabelValue(labels, labelsOffset, prc.TargetLabel, valueStr) if ok {
labels = setLabelValue(labels, labelsOffset, prc.TargetLabel, valueStr)
}
return labels
case "keep_if_equal": case "keep_if_equal":
// Keep the entry if all the label values in source_labels are equal. // Keep the entry if all the label values in source_labels are equal.
// For example: // For example:
@ -175,7 +187,7 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, prc *Par
case "keep": case "keep":
bb := relabelBufPool.Get() bb := relabelBufPool.Get()
bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator) bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator)
keep := prc.Regex.Match(bb.B) keep := prc.matchString(bytesutil.ToUnsafeString(bb.B))
relabelBufPool.Put(bb) relabelBufPool.Put(bb)
if !keep { if !keep {
return labels[:labelsOffset] return labels[:labelsOffset]
@ -184,7 +196,7 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, prc *Par
case "drop": case "drop":
bb := relabelBufPool.Get() bb := relabelBufPool.Get()
bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator) bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator)
drop := prc.Regex.Match(bb.B) drop := prc.matchString(bytesutil.ToUnsafeString(bb.B))
relabelBufPool.Put(bb) relabelBufPool.Put(bb)
if drop { if drop {
return labels[:labelsOffset] return labels[:labelsOffset]
@ -200,30 +212,23 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, prc *Par
case "labelmap": case "labelmap":
for i := range src { for i := range src {
label := &src[i] label := &src[i]
match := prc.Regex.FindStringSubmatchIndex(label.Name) labelName, ok := prc.replaceFullString(label.Name, prc.Replacement, prc.hasCaptureGroupInReplacement)
if match == nil { if ok {
continue labels = setLabelValue(labels, labelsOffset, labelName, label.Value)
} }
value := relabelBufPool.Get()
value.B = prc.Regex.ExpandString(value.B[:0], prc.Replacement, label.Name, match)
labelName := string(value.B)
relabelBufPool.Put(value)
labels = setLabelValue(labels, labelsOffset, labelName, label.Value)
} }
return labels return labels
case "labelmap_all": case "labelmap_all":
for i := range src { for i := range src {
label := &src[i] label := &src[i]
if !prc.Regex.MatchString(label.Name) { label.Name, _ = prc.replaceStringSubmatches(label.Name, prc.Replacement, prc.hasCaptureGroupInReplacement)
continue
}
label.Name = prc.Regex.ReplaceAllString(label.Name, prc.Replacement)
} }
return labels return labels
case "labeldrop": case "labeldrop":
keepSrc := true keepSrc := true
for i := range src { for i := range src {
if prc.Regex.MatchString(src[i].Name) { label := &src[i]
if prc.matchString(label.Name) {
keepSrc = false keepSrc = false
break break
} }
@ -234,7 +239,7 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, prc *Par
dst := labels[:labelsOffset] dst := labels[:labelsOffset]
for i := range src { for i := range src {
label := &src[i] label := &src[i]
if !prc.Regex.MatchString(label.Name) { if !prc.matchString(label.Name) {
dst = append(dst, *label) dst = append(dst, *label)
} }
} }
@ -242,7 +247,8 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, prc *Par
case "labelkeep": case "labelkeep":
keepSrc := true keepSrc := true
for i := range src { for i := range src {
if !prc.Regex.MatchString(src[i].Name) { label := &src[i]
if !prc.matchString(label.Name) {
keepSrc = false keepSrc = false
break break
} }
@ -253,7 +259,7 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, prc *Par
dst := labels[:labelsOffset] dst := labels[:labelsOffset]
for i := range src { for i := range src {
label := &src[i] label := &src[i]
if prc.Regex.MatchString(label.Name) { if prc.matchString(label.Name) {
dst = append(dst, *label) dst = append(dst, *label)
} }
} }
@ -264,7 +270,88 @@ func applyRelabelConfig(labels []prompbmarshal.Label, labelsOffset int, prc *Par
} }
} }
func (prc *ParsedRelabelConfig) expandCaptureGroups(template, source string, match []int) string { func (prc *parsedRelabelConfig) replaceFullString(s, replacement string, hasCaptureGroupInReplacement bool) (string, bool) {
prefix, complete := prc.regexOriginal.LiteralPrefix()
if complete && !hasCaptureGroupInReplacement {
if s == prefix {
return replacement, true
}
return s, false
}
if !strings.HasPrefix(s, prefix) {
return s, false
}
if replacement == "$1" {
// Fast path for commonly used rule for deleting label prefixes such as:
//
// - action: labelmap
// regex: __meta_kubernetes_node_label_(.+)
//
reStr := prc.regexOriginal.String()
if strings.HasPrefix(reStr, prefix) {
suffix := s[len(prefix):]
reSuffix := reStr[len(prefix):]
switch reSuffix {
case "(.*)":
return suffix, true
case "(.+)":
if len(suffix) > 0 {
return suffix, true
}
return s, false
}
}
}
// Slow path - regexp processing
match := prc.Regex.FindStringSubmatchIndex(s)
if match == nil {
return s, false
}
bb := relabelBufPool.Get()
bb.B = prc.Regex.ExpandString(bb.B[:0], replacement, s, match)
result := string(bb.B)
relabelBufPool.Put(bb)
return result, true
}
func (prc *parsedRelabelConfig) replaceStringSubmatches(s, replacement string, hasCaptureGroupInReplacement bool) (string, bool) {
re := prc.regexOriginal
prefix, complete := re.LiteralPrefix()
if complete && !hasCaptureGroupInReplacement {
if !strings.Contains(s, prefix) {
return s, false
}
return strings.ReplaceAll(s, prefix, replacement), true
}
if !re.MatchString(s) {
return s, false
}
return re.ReplaceAllString(s, replacement), true
}
func (prc *parsedRelabelConfig) matchString(s string) bool {
prefix, complete := prc.regexOriginal.LiteralPrefix()
if complete {
return prefix == s
}
if !strings.HasPrefix(s, prefix) {
return false
}
reStr := prc.regexOriginal.String()
if strings.HasPrefix(reStr, prefix) {
// Fast path for `foo.*` and `bar.+` regexps
reSuffix := reStr[len(prefix):]
switch reSuffix {
case ".+", "(.+)":
return len(s) > len(prefix)
case ".*", "(.*)":
return true
}
}
return prc.Regex.MatchString(s)
}
func (prc *parsedRelabelConfig) expandCaptureGroups(template, source string, match []int) string {
bb := relabelBufPool.Get() bb := relabelBufPool.Get()
bb.B = prc.Regex.ExpandString(bb.B[:0], template, source, match) bb.B = prc.Regex.ExpandString(bb.B[:0], template, source, match)
s := string(bb.B) s := string(bb.B)

View file

@ -2,24 +2,27 @@ package promrelabel
import ( import (
"reflect" "reflect"
"regexp"
"testing" "testing"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal" "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
) )
func TestApplyRelabelConfigs(t *testing.T) { func TestApplyRelabelConfigs(t *testing.T) {
f := func(prcs []ParsedRelabelConfig, labels []prompbmarshal.Label, isFinalize bool, resultExpected []prompbmarshal.Label) { f := func(config string, labels []prompbmarshal.Label, isFinalize bool, resultExpected []prompbmarshal.Label) {
t.Helper() t.Helper()
result := ApplyRelabelConfigs(labels, 0, prcs, isFinalize) pcs, err := ParseRelabelConfigsData([]byte(config))
if err != nil {
t.Fatalf("cannot parse %q: %s", config, err)
}
result := pcs.Apply(labels, 0, isFinalize)
if !reflect.DeepEqual(result, resultExpected) { if !reflect.DeepEqual(result, resultExpected) {
t.Fatalf("unexpected result; got\n%v\nwant\n%v", result, resultExpected) t.Fatalf("unexpected result; got\n%v\nwant\n%v", result, resultExpected)
} }
} }
t.Run("empty_relabel_configs", func(t *testing.T) { t.Run("empty_relabel_configs", func(t *testing.T) {
f(nil, nil, false, nil) f("", nil, false, nil)
f(nil, nil, true, nil) f("", nil, true, nil)
f(nil, []prompbmarshal.Label{ f("", []prompbmarshal.Label{
{ {
Name: "foo", Name: "foo",
Value: "bar", Value: "bar",
@ -30,7 +33,7 @@ func TestApplyRelabelConfigs(t *testing.T) {
Value: "bar", Value: "bar",
}, },
}) })
f(nil, []prompbmarshal.Label{ f("", []prompbmarshal.Label{
{ {
Name: "foo", Name: "foo",
Value: "bar", Value: "bar",
@ -55,35 +58,20 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("replace-miss", func(t *testing.T) { t.Run("replace-miss", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: replace
Action: "replace", target_label: bar
TargetLabel: "bar", `, nil, false, []prompbmarshal.Label{})
Regex: defaultRegexForRelabelConfig, f(`
Replacement: "$1", - action: replace
hasCaptureGroupInReplacement: true, source_labels: ["foo"]
}, target_label: bar
}, nil, false, []prompbmarshal.Label{}) `, nil, false, []prompbmarshal.Label{})
f([]ParsedRelabelConfig{ f(`
{ - action: replace
Action: "replace", source_labels: ["foo"]
SourceLabels: []string{"foo"}, target_label: "bar"
TargetLabel: "bar", `, []prompbmarshal.Label{
Regex: defaultRegexForRelabelConfig,
Replacement: "$1",
hasCaptureGroupInReplacement: true,
},
}, nil, false, []prompbmarshal.Label{})
f([]ParsedRelabelConfig{
{
Action: "replace",
SourceLabels: []string{"foo"},
TargetLabel: "bar",
Regex: defaultRegexForRelabelConfig,
Replacement: "$1",
hasCaptureGroupInReplacement: true,
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -94,16 +82,12 @@ func TestApplyRelabelConfigs(t *testing.T) {
Value: "yyy", Value: "yyy",
}, },
}) })
f([]ParsedRelabelConfig{ f(`
{ - action: replace
Action: "replace", source_labels: ["foo"]
SourceLabels: []string{"foo"}, target_label: "bar"
TargetLabel: "bar", regex: ".+"
Regex: regexp.MustCompile(".+"), `, []prompbmarshal.Label{
Replacement: "$1",
hasCaptureGroupInReplacement: true,
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -116,17 +100,12 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("replace-hit", func(t *testing.T) { t.Run("replace-hit", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: replace
Action: "replace", source_labels: ["xxx", "foo"]
SourceLabels: []string{"xxx", "foo"}, target_label: "bar"
Separator: ";", replacement: "a-$1-b"
TargetLabel: "bar", `, []prompbmarshal.Label{
Regex: defaultRegexForRelabelConfig,
Replacement: "a-$1-b",
hasCaptureGroupInReplacement: true,
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -143,18 +122,12 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("replace-hit-target-label-with-capture-group", func(t *testing.T) { t.Run("replace-hit-target-label-with-capture-group", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: replace
Action: "replace", source_labels: ["xxx", "foo"]
SourceLabels: []string{"xxx", "foo"}, target_label: "bar-$1"
Separator: ";", replacement: "a-$1-b"
TargetLabel: "bar-$1", `, []prompbmarshal.Label{
Regex: defaultRegexForRelabelConfig,
Replacement: "a-$1-b",
hasCaptureGroupInTargetLabel: true,
hasCaptureGroupInReplacement: true,
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -171,35 +144,21 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("replace_all-miss", func(t *testing.T) { t.Run("replace_all-miss", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: replace_all
Action: "replace_all", source_labels: [foo]
TargetLabel: "bar", target_label: "bar"
Regex: defaultRegexForRelabelConfig, `, nil, false, []prompbmarshal.Label{})
Replacement: "$1", f(`
hasCaptureGroupInReplacement: true, - action: replace_all
}, source_labels: ["foo"]
}, nil, false, []prompbmarshal.Label{}) target_label: "bar"
f([]ParsedRelabelConfig{ `, nil, false, []prompbmarshal.Label{})
{ f(`
Action: "replace_all", - action: replace_all
SourceLabels: []string{"foo"}, source_labels: ["foo"]
TargetLabel: "bar", target_label: "bar"
Regex: defaultRegexForRelabelConfig, `, []prompbmarshal.Label{
Replacement: "$1",
hasCaptureGroupInReplacement: true,
},
}, nil, false, []prompbmarshal.Label{})
f([]ParsedRelabelConfig{
{
Action: "replace_all",
SourceLabels: []string{"foo"},
TargetLabel: "bar",
Regex: defaultRegexForRelabelConfig,
Replacement: "$1",
hasCaptureGroupInReplacement: true,
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -210,16 +169,12 @@ func TestApplyRelabelConfigs(t *testing.T) {
Value: "yyy", Value: "yyy",
}, },
}) })
f([]ParsedRelabelConfig{ f(`
{ - action: replace_all
Action: "replace_all", source_labels: ["foo"]
SourceLabels: []string{"foo"}, target_label: "bar"
TargetLabel: "bar", regex: ".+"
Regex: regexp.MustCompile(".+"), `, []prompbmarshal.Label{
Replacement: "$1",
hasCaptureGroupInReplacement: true,
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -232,17 +187,32 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("replace_all-hit", func(t *testing.T) { t.Run("replace_all-hit", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
- action: replace_all
source_labels: ["xxx"]
target_label: "xxx"
regex: "-"
replacement: "."
`, []prompbmarshal.Label{
{ {
Action: "replace_all", Name: "xxx",
SourceLabels: []string{"xxx", "foo"}, Value: "a-b-c",
Separator: ";",
TargetLabel: "xxx",
Regex: regexp.MustCompile("(;)"),
Replacement: "-$1-",
hasCaptureGroupInReplacement: true,
}, },
}, []prompbmarshal.Label{ }, false, []prompbmarshal.Label{
{
Name: "xxx",
Value: "a.b.c",
},
})
})
t.Run("replace_all-regex-hit", func(t *testing.T) {
f(`
- action: replace_all
source_labels: ["xxx", "foo"]
target_label: "xxx"
regex: "(;)"
replacement: "-$1-"
`, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "y;y", Value: "y;y",
@ -255,23 +225,16 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("replace-add-multi-labels", func(t *testing.T) { t.Run("replace-add-multi-labels", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: replace
Action: "replace", source_labels: ["xxx"]
SourceLabels: []string{"xxx"}, target_label: "bar"
TargetLabel: "bar", replacement: "a-$1"
Regex: defaultRegexForRelabelConfig, - action: replace
Replacement: "a-$1", source_labels: ["bar"]
hasCaptureGroupInReplacement: true, target_label: "zar"
}, replacement: "b-$1"
{ `, []prompbmarshal.Label{
Action: "replace",
SourceLabels: []string{"bar"},
TargetLabel: "zar",
Regex: defaultRegexForRelabelConfig,
Replacement: "b-$1",
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -300,16 +263,12 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("replace-self", func(t *testing.T) { t.Run("replace-self", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: replace
Action: "replace", source_labels: ["foo"]
SourceLabels: []string{"foo"}, target_label: "foo"
TargetLabel: "foo", replacement: "a-$1"
Regex: defaultRegexForRelabelConfig, `, []prompbmarshal.Label{
Replacement: "a-$1",
hasCaptureGroupInReplacement: true,
},
}, []prompbmarshal.Label{
{ {
Name: "foo", Name: "foo",
Value: "aaxx", Value: "aaxx",
@ -322,14 +281,11 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("replace-missing-source", func(t *testing.T) { t.Run("replace-missing-source", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: replace
Action: "replace", target_label: foo
TargetLabel: "foo", replacement: "foobar"
Regex: defaultRegexForRelabelConfig, `, []prompbmarshal.Label{}, true, []prompbmarshal.Label{
Replacement: "foobar",
},
}, []prompbmarshal.Label{}, true, []prompbmarshal.Label{
{ {
Name: "foo", Name: "foo",
Value: "foobar", Value: "foobar",
@ -337,18 +293,14 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("keep_if_equal-miss", func(t *testing.T) { t.Run("keep_if_equal-miss", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: keep_if_equal
Action: "keep_if_equal", source_labels: ["foo", "bar"]
SourceLabels: []string{"foo", "bar"}, `, nil, true, nil)
}, f(`
}, nil, true, nil) - action: keep_if_equal
f([]ParsedRelabelConfig{ source_labels: ["xxx", "bar"]
{ `, []prompbmarshal.Label{
Action: "keep_if_equal",
SourceLabels: []string{"xxx", "bar"},
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -356,12 +308,10 @@ func TestApplyRelabelConfigs(t *testing.T) {
}, true, []prompbmarshal.Label{}) }, true, []prompbmarshal.Label{})
}) })
t.Run("keep_if_equal-hit", func(t *testing.T) { t.Run("keep_if_equal-hit", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: keep_if_equal
Action: "keep_if_equal", source_labels: ["xxx", "bar"]
SourceLabels: []string{"xxx", "bar"}, `, []prompbmarshal.Label{
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -382,18 +332,14 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("drop_if_equal-miss", func(t *testing.T) { t.Run("drop_if_equal-miss", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: drop_if_equal
Action: "drop_if_equal", source_labels: ["foo", "bar"]
SourceLabels: []string{"foo", "bar"}, `, nil, true, nil)
}, f(`
}, nil, true, nil) - action: drop_if_equal
f([]ParsedRelabelConfig{ source_labels: ["xxx", "bar"]
{ `, []prompbmarshal.Label{
Action: "drop_if_equal",
SourceLabels: []string{"xxx", "bar"},
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -406,12 +352,10 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("drop_if_equal-hit", func(t *testing.T) { t.Run("drop_if_equal-hit", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: drop_if_equal
Action: "drop_if_equal", source_labels: [xxx, bar]
SourceLabels: []string{"xxx", "bar"}, `, []prompbmarshal.Label{
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -423,20 +367,16 @@ func TestApplyRelabelConfigs(t *testing.T) {
}, true, []prompbmarshal.Label{}) }, true, []prompbmarshal.Label{})
}) })
t.Run("keep-miss", func(t *testing.T) { t.Run("keep-miss", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: keep
Action: "keep", source_labels: [foo]
SourceLabels: []string{"foo"}, regex: ".+"
Regex: regexp.MustCompile(".+"), `, nil, true, nil)
}, f(`
}, nil, true, nil) - action: keep
f([]ParsedRelabelConfig{ source_labels: [foo]
{ regex: ".+"
Action: "keep", `, []prompbmarshal.Label{
SourceLabels: []string{"foo"},
Regex: regexp.MustCompile(".+"),
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -444,13 +384,28 @@ func TestApplyRelabelConfigs(t *testing.T) {
}, true, []prompbmarshal.Label{}) }, true, []prompbmarshal.Label{})
}) })
t.Run("keep-hit", func(t *testing.T) { t.Run("keep-hit", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
- action: keep
source_labels: [foo]
regex: "yyy"
`, []prompbmarshal.Label{
{ {
Action: "keep", Name: "foo",
SourceLabels: []string{"foo"}, Value: "yyy",
Regex: regexp.MustCompile(".+"),
}, },
}, []prompbmarshal.Label{ }, false, []prompbmarshal.Label{
{
Name: "foo",
Value: "yyy",
},
})
})
t.Run("keep-hit-regexp", func(t *testing.T) {
f(`
- action: keep
source_labels: ["foo"]
regex: ".+"
`, []prompbmarshal.Label{
{ {
Name: "foo", Name: "foo",
Value: "yyy", Value: "yyy",
@ -463,20 +418,16 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("drop-miss", func(t *testing.T) { t.Run("drop-miss", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: drop
Action: "drop", source_labels: [foo]
SourceLabels: []string{"foo"}, regex: ".+"
Regex: regexp.MustCompile(".+"), `, nil, false, nil)
}, f(`
}, nil, false, nil) - action: drop
f([]ParsedRelabelConfig{ source_labels: [foo]
{ regex: ".+"
Action: "drop", `, []prompbmarshal.Label{
SourceLabels: []string{"foo"},
Regex: regexp.MustCompile(".+"),
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -489,13 +440,23 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("drop-hit", func(t *testing.T) { t.Run("drop-hit", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
- action: drop
source_labels: [foo]
regex: yyy
`, []prompbmarshal.Label{
{ {
Action: "drop", Name: "foo",
SourceLabels: []string{"foo"}, Value: "yyy",
Regex: regexp.MustCompile(".+"),
}, },
}, []prompbmarshal.Label{ }, true, []prompbmarshal.Label{})
})
t.Run("drop-hit-regexp", func(t *testing.T) {
f(`
- action: drop
source_labels: [foo]
regex: ".+"
`, []prompbmarshal.Label{
{ {
Name: "foo", Name: "foo",
Value: "yyy", Value: "yyy",
@ -503,14 +464,12 @@ func TestApplyRelabelConfigs(t *testing.T) {
}, true, []prompbmarshal.Label{}) }, true, []prompbmarshal.Label{})
}) })
t.Run("hashmod-miss", func(t *testing.T) { t.Run("hashmod-miss", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: hashmod
Action: "hashmod", source_labels: [foo]
SourceLabels: []string{"foo"}, target_label: aaa
TargetLabel: "aaa", modulus: 123
Modulus: 123, `, []prompbmarshal.Label{
},
}, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -527,14 +486,12 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("hashmod-hit", func(t *testing.T) { t.Run("hashmod-hit", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: hashmod
Action: "hashmod", source_labels: [foo]
SourceLabels: []string{"foo"}, target_label: aaa
TargetLabel: "aaa", modulus: 123
Modulus: 123, `, []prompbmarshal.Label{
},
}, []prompbmarshal.Label{
{ {
Name: "foo", Name: "foo",
Value: "yyy", Value: "yyy",
@ -550,14 +507,97 @@ func TestApplyRelabelConfigs(t *testing.T) {
}, },
}) })
}) })
t.Run("labelmap", func(t *testing.T) { t.Run("labelmap-copy-label", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
- action: labelmap
regex: "foo"
replacement: "bar"
`, []prompbmarshal.Label{
{ {
Action: "labelmap", Name: "foo",
Regex: regexp.MustCompile("foo(.+)"), Value: "yyy",
Replacement: "$1-x",
}, },
}, []prompbmarshal.Label{ {
Name: "foobar",
Value: "aaa",
},
}, true, []prompbmarshal.Label{
{
Name: "bar",
Value: "yyy",
},
{
Name: "foo",
Value: "yyy",
},
{
Name: "foobar",
Value: "aaa",
},
})
})
t.Run("labelmap-remove-prefix-dot-star", func(t *testing.T) {
f(`
- action: labelmap
regex: "foo(.*)"
`, []prompbmarshal.Label{
{
Name: "xoo",
Value: "yyy",
},
{
Name: "foobar",
Value: "aaa",
},
}, true, []prompbmarshal.Label{
{
Name: "bar",
Value: "aaa",
},
{
Name: "foobar",
Value: "aaa",
},
{
Name: "xoo",
Value: "yyy",
},
})
})
t.Run("labelmap-remove-prefix-dot-plus", func(t *testing.T) {
f(`
- action: labelmap
regex: "foo(.+)"
`, []prompbmarshal.Label{
{
Name: "foo",
Value: "yyy",
},
{
Name: "foobar",
Value: "aaa",
},
}, true, []prompbmarshal.Label{
{
Name: "bar",
Value: "aaa",
},
{
Name: "foo",
Value: "yyy",
},
{
Name: "foobar",
Value: "aaa",
},
})
})
t.Run("labelmap-regex", func(t *testing.T) {
f(`
- action: labelmap
regex: "foo(.+)"
replacement: "$1-x"
`, []prompbmarshal.Label{
{ {
Name: "foo", Name: "foo",
Value: "yyy", Value: "yyy",
@ -582,13 +622,11 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("labelmap_all", func(t *testing.T) { t.Run("labelmap_all", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: labelmap_all
Action: "labelmap_all", regex: "\\."
Regex: regexp.MustCompile(`\.`), replacement: "-"
Replacement: "-", `, []prompbmarshal.Label{
},
}, []prompbmarshal.Label{
{ {
Name: "foo.bar.baz", Name: "foo.bar.baz",
Value: "yyy", Value: "yyy",
@ -608,13 +646,36 @@ func TestApplyRelabelConfigs(t *testing.T) {
}, },
}) })
}) })
t.Run("labeldrop", func(t *testing.T) { t.Run("labelmap_all-regexp", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
- action: labelmap_all
regex: "ba(.)"
replacement: "${1}ss"
`, []prompbmarshal.Label{
{ {
Action: "labeldrop", Name: "foo.bar.baz",
Regex: regexp.MustCompile("dropme.*"), Value: "yyy",
}, },
}, []prompbmarshal.Label{ {
Name: "foozar",
Value: "aaa",
},
}, true, []prompbmarshal.Label{
{
Name: "foo.rss.zss",
Value: "yyy",
},
{
Name: "foozar",
Value: "aaa",
},
})
})
t.Run("labeldrop", func(t *testing.T) {
f(`
- action: labeldrop
regex: dropme
`, []prompbmarshal.Label{
{ {
Name: "aaa", Name: "aaa",
Value: "bbb", Value: "bbb",
@ -625,12 +686,94 @@ func TestApplyRelabelConfigs(t *testing.T) {
Value: "bbb", Value: "bbb",
}, },
}) })
f([]ParsedRelabelConfig{ f(`
- action: labeldrop
regex: dropme
`, []prompbmarshal.Label{
{ {
Action: "labeldrop", Name: "xxx",
Regex: regexp.MustCompile("dropme.*"), Value: "yyy",
}, },
}, []prompbmarshal.Label{ {
Name: "dropme",
Value: "aaa",
},
{
Name: "foo",
Value: "bar",
},
}, false, []prompbmarshal.Label{
{
Name: "foo",
Value: "bar",
},
{
Name: "xxx",
Value: "yyy",
},
})
})
t.Run("labeldrop-prefix", func(t *testing.T) {
f(`
- action: labeldrop
regex: "dropme.*"
`, []prompbmarshal.Label{
{
Name: "aaa",
Value: "bbb",
},
}, true, []prompbmarshal.Label{
{
Name: "aaa",
Value: "bbb",
},
})
f(`
- action: labeldrop
regex: "dropme(.+)"
`, []prompbmarshal.Label{
{
Name: "xxx",
Value: "yyy",
},
{
Name: "dropme-please",
Value: "aaa",
},
{
Name: "foo",
Value: "bar",
},
}, false, []prompbmarshal.Label{
{
Name: "foo",
Value: "bar",
},
{
Name: "xxx",
Value: "yyy",
},
})
})
t.Run("labeldrop-regexp", func(t *testing.T) {
f(`
- action: labeldrop
regex: ".*dropme.*"
`, []prompbmarshal.Label{
{
Name: "aaa",
Value: "bbb",
},
}, true, []prompbmarshal.Label{
{
Name: "aaa",
Value: "bbb",
},
})
f(`
- action: labeldrop
regex: ".*dropme.*"
`, []prompbmarshal.Label{
{ {
Name: "xxx", Name: "xxx",
Value: "yyy", Value: "yyy",
@ -655,12 +798,10 @@ func TestApplyRelabelConfigs(t *testing.T) {
}) })
}) })
t.Run("labelkeep", func(t *testing.T) { t.Run("labelkeep", func(t *testing.T) {
f([]ParsedRelabelConfig{ f(`
{ - action: labelkeep
Action: "labelkeep", regex: "keepme"
Regex: regexp.MustCompile("keepme.*"), `, []prompbmarshal.Label{
},
}, []prompbmarshal.Label{
{ {
Name: "keepme", Name: "keepme",
Value: "aaa", Value: "aaa",
@ -671,12 +812,48 @@ func TestApplyRelabelConfigs(t *testing.T) {
Value: "aaa", Value: "aaa",
}, },
}) })
f([]ParsedRelabelConfig{ f(`
- action: labelkeep
regex: keepme
`, []prompbmarshal.Label{
{ {
Action: "labelkeep", Name: "keepme",
Regex: regexp.MustCompile("keepme.*"), Value: "aaa",
}, },
}, []prompbmarshal.Label{ {
Name: "aaaa",
Value: "awef",
},
{
Name: "keepme-aaa",
Value: "234",
},
}, false, []prompbmarshal.Label{
{
Name: "keepme",
Value: "aaa",
},
})
})
t.Run("labelkeep-regexp", func(t *testing.T) {
f(`
- action: labelkeep
regex: "keepme.*"
`, []prompbmarshal.Label{
{
Name: "keepme",
Value: "aaa",
},
}, true, []prompbmarshal.Label{
{
Name: "keepme",
Value: "aaa",
},
})
f(`
- action: labelkeep
regex: "keepme.*"
`, []prompbmarshal.Label{
{ {
Name: "keepme", Name: "keepme",
Value: "aaa", Value: "aaa",

View file

@ -2,7 +2,6 @@ package promrelabel
import ( import (
"fmt" "fmt"
"regexp"
"testing" "testing"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal" "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
@ -10,15 +9,11 @@ import (
func BenchmarkApplyRelabelConfigs(b *testing.B) { func BenchmarkApplyRelabelConfigs(b *testing.B) {
b.Run("replace-label-copy", func(b *testing.B) { b.Run("replace-label-copy", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
{ - action: replace
Action: "replace", source_labels: [id]
SourceLabels: []string{"id"}, target_label: __name__
TargetLabel: "__name__", `)
Regex: defaultRegexForRelabelConfig,
Replacement: "$1",
},
}
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -35,7 +30,7 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != len(labelsOrig) { if len(labels) != len(labelsOrig) {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels))
} }
@ -55,14 +50,11 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
}) })
}) })
b.Run("replace-set-label", func(b *testing.B) { b.Run("replace-set-label", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
{ - action: replace
Action: "replace", target_label: __name__
TargetLabel: "__name__", replacement: foobar
Regex: defaultRegexForRelabelConfig, `)
Replacement: "foobar",
},
}
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -79,7 +71,7 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != len(labelsOrig) { if len(labels) != len(labelsOrig) {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels))
} }
@ -99,14 +91,11 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
}) })
}) })
b.Run("replace-add-label", func(b *testing.B) { b.Run("replace-add-label", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
{ - action: replace
Action: "replace", target_label: aaa
TargetLabel: "aaa", replacement: foobar
Regex: defaultRegexForRelabelConfig, `)
Replacement: "foobar",
},
}
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -119,7 +108,7 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != 2 { if len(labels) != 2 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 2, labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 2, labels))
} }
@ -139,15 +128,12 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
}) })
}) })
b.Run("replace-mismatch", func(b *testing.B) { b.Run("replace-mismatch", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
{ - action: replace
Action: "replace", source_labels: ["non-existing-label"]
SourceLabels: []string{"non-existing-label"}, target_label: id
TargetLabel: "id", regex: "(foobar)-.*"
Regex: regexp.MustCompile("(foobar)-.*"), `)
Replacement: "$1",
},
}
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -164,7 +150,7 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != len(labelsOrig) { if len(labels) != len(labelsOrig) {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels))
} }
@ -183,16 +169,13 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
} }
}) })
}) })
b.Run("replace-match", func(b *testing.B) { b.Run("replace-match-regex", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
{ - action: replace
Action: "replace", source_labels: [id]
SourceLabels: []string{"id"}, target_label: id
TargetLabel: "id", regex: "(foobar)-.*"
Regex: regexp.MustCompile("(foobar)-.*"), `)
Replacement: "$1",
},
}
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -209,7 +192,7 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != len(labelsOrig) { if len(labels) != len(labelsOrig) {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels))
} }
@ -229,13 +212,11 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
}) })
}) })
b.Run("drop-mismatch", func(b *testing.B) { b.Run("drop-mismatch", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
{ - action: drop
Action: "drop", source_labels: ["non-existing-label"]
SourceLabels: []string{"non-existing-label"}, regex: "(foobar)-.*"
Regex: regexp.MustCompile("(foobar)-.*"), `)
},
}
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -252,7 +233,7 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != len(labelsOrig) { if len(labels) != len(labelsOrig) {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels))
} }
@ -272,13 +253,40 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
}) })
}) })
b.Run("drop-match", func(b *testing.B) { b.Run("drop-match", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
- action: drop
source_labels: [id]
regex: yes
`)
labelsOrig := []prompbmarshal.Label{
{ {
Action: "drop", Name: "__name__",
SourceLabels: []string{"id"}, Value: "metric",
Regex: regexp.MustCompile("(foobar)-.*"), },
{
Name: "id",
Value: "yes",
}, },
} }
b.ReportAllocs()
b.SetBytes(1)
b.RunParallel(func(pb *testing.PB) {
var labels []prompbmarshal.Label
for pb.Next() {
labels = append(labels[:0], labelsOrig...)
labels = pcs.Apply(labels, 0, true)
if len(labels) != 0 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 0, labels))
}
}
})
})
b.Run("drop-match-regexp", func(b *testing.B) {
pcs := mustParseRelabelConfigs(`
- action: drop
source_labels: [id]
regex: "(foobar)-.*"
`)
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -295,7 +303,7 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != 0 { if len(labels) != 0 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 0, labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 0, labels))
} }
@ -303,13 +311,11 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
}) })
}) })
b.Run("keep-mismatch", func(b *testing.B) { b.Run("keep-mismatch", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
{ - action: keep
Action: "keep", source_labels: ["non-existing-label"]
SourceLabels: []string{"non-existing-label"}, regex: "(foobar)-.*"
Regex: regexp.MustCompile("(foobar)-.*"), `)
},
}
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -326,7 +332,7 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != 0 { if len(labels) != 0 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 0, labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 0, labels))
} }
@ -334,13 +340,52 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
}) })
}) })
b.Run("keep-match", func(b *testing.B) { b.Run("keep-match", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
- action: keep
source_labels: [id]
regex: yes
`)
labelsOrig := []prompbmarshal.Label{
{ {
Action: "keep", Name: "__name__",
SourceLabels: []string{"id"}, Value: "metric",
Regex: regexp.MustCompile("(foobar)-.*"), },
{
Name: "id",
Value: "yes",
}, },
} }
b.ReportAllocs()
b.SetBytes(1)
b.RunParallel(func(pb *testing.PB) {
var labels []prompbmarshal.Label
for pb.Next() {
labels = append(labels[:0], labelsOrig...)
labels = pcs.Apply(labels, 0, true)
if len(labels) != len(labelsOrig) {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels))
}
if labels[0].Name != "__name__" {
panic(fmt.Errorf("unexpected label name; got %q; want %q", labels[0].Name, "__name__"))
}
if labels[0].Value != "metric" {
panic(fmt.Errorf("unexpected label value; got %q; want %q", labels[0].Value, "metric"))
}
if labels[1].Name != "id" {
panic(fmt.Errorf("unexpected label name; got %q; want %q", labels[1].Name, "id"))
}
if labels[1].Value != "yes" {
panic(fmt.Errorf("unexpected label value; got %q; want %q", labels[1].Value, "yes"))
}
}
})
})
b.Run("keep-match-regexp", func(b *testing.B) {
pcs := mustParseRelabelConfigs(`
- action: keep
source_labels: [id]
regex: "(foobar)-.*"
`)
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -357,7 +402,7 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != len(labelsOrig) { if len(labels) != len(labelsOrig) {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels))
} }
@ -377,12 +422,10 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
}) })
}) })
b.Run("labeldrop-mismatch", func(b *testing.B) { b.Run("labeldrop-mismatch", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
{ - action: labeldrop
Action: "labeldrop", regex: "non-existing-label"
Regex: regexp.MustCompile("non-existing-label"), `)
},
}
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -399,7 +442,7 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != len(labelsOrig) { if len(labels) != len(labelsOrig) {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels))
} }
@ -419,12 +462,10 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
}) })
}) })
b.Run("labeldrop-match", func(b *testing.B) { b.Run("labeldrop-match", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
{ - action: labeldrop
Action: "labeldrop", regex: id
Regex: regexp.MustCompile("id"), `)
},
}
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -441,7 +482,75 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != 1 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 1, labels))
}
if labels[0].Name != "__name__" {
panic(fmt.Errorf("unexpected label name; got %q; want %q", labels[0].Name, "__name__"))
}
if labels[0].Value != "metric" {
panic(fmt.Errorf("unexpected label value; got %q; want %q", labels[0].Value, "metric"))
}
}
})
})
b.Run("labeldrop-match-prefix", func(b *testing.B) {
pcs := mustParseRelabelConfigs(`
- action: labeldrop
regex: "id.*"
`)
labelsOrig := []prompbmarshal.Label{
{
Name: "__name__",
Value: "metric",
},
{
Name: "id",
Value: "foobar-random-string-here",
},
}
b.ReportAllocs()
b.SetBytes(1)
b.RunParallel(func(pb *testing.PB) {
var labels []prompbmarshal.Label
for pb.Next() {
labels = append(labels[:0], labelsOrig...)
labels = pcs.Apply(labels, 0, true)
if len(labels) != 1 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 1, labels))
}
if labels[0].Name != "__name__" {
panic(fmt.Errorf("unexpected label name; got %q; want %q", labels[0].Name, "__name__"))
}
if labels[0].Value != "metric" {
panic(fmt.Errorf("unexpected label value; got %q; want %q", labels[0].Value, "metric"))
}
}
})
})
b.Run("labeldrop-match-regexp", func(b *testing.B) {
pcs := mustParseRelabelConfigs(`
- action: labeldrop
regex: ".*id.*"
`)
labelsOrig := []prompbmarshal.Label{
{
Name: "__name__",
Value: "metric",
},
{
Name: "id",
Value: "foobar-random-string-here",
},
}
b.ReportAllocs()
b.SetBytes(1)
b.RunParallel(func(pb *testing.PB) {
var labels []prompbmarshal.Label
for pb.Next() {
labels = append(labels[:0], labelsOrig...)
labels = pcs.Apply(labels, 0, true)
if len(labels) != 1 { if len(labels) != 1 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 1, labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 1, labels))
} }
@ -455,12 +564,10 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
}) })
}) })
b.Run("labelkeep-mismatch", func(b *testing.B) { b.Run("labelkeep-mismatch", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
{ - action: labelkeep
Action: "labelkeep", regex: "non-existing-label"
Regex: regexp.MustCompile("non-existing-label"), `)
},
}
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -477,7 +584,7 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != 0 { if len(labels) != 0 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 0, labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 0, labels))
} }
@ -485,12 +592,10 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
}) })
}) })
b.Run("labelkeep-match", func(b *testing.B) { b.Run("labelkeep-match", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
{ - action: labelkeep
Action: "labelkeep", regex: id
Regex: regexp.MustCompile("id"), `)
},
}
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -507,7 +612,7 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != 1 { if len(labels) != 1 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 1, labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 1, labels))
} }
@ -520,15 +625,11 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
} }
}) })
}) })
b.Run("hashmod", func(b *testing.B) { b.Run("labelkeep-match-prefix", func(b *testing.B) {
prcs := []ParsedRelabelConfig{ pcs := mustParseRelabelConfigs(`
{ - action: labelkeep
Action: "hashmod", regex: "id.*"
SourceLabels: []string{"id"}, `)
TargetLabel: "id",
Modulus: 23,
},
}
labelsOrig := []prompbmarshal.Label{ labelsOrig := []prompbmarshal.Label{
{ {
Name: "__name__", Name: "__name__",
@ -545,7 +646,179 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
var labels []prompbmarshal.Label var labels []prompbmarshal.Label
for pb.Next() { for pb.Next() {
labels = append(labels[:0], labelsOrig...) labels = append(labels[:0], labelsOrig...)
labels = ApplyRelabelConfigs(labels, 0, prcs, true) labels = pcs.Apply(labels, 0, true)
if len(labels) != 1 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 1, labels))
}
if labels[0].Name != "id" {
panic(fmt.Errorf("unexpected label name; got %q; want %q", labels[0].Name, "id"))
}
if labels[0].Value != "foobar-random-string-here" {
panic(fmt.Errorf("unexpected label value; got %q; want %q", labels[0].Value, "foobar-random-string-here"))
}
}
})
})
b.Run("labelkeep-match-regexp", func(b *testing.B) {
pcs := mustParseRelabelConfigs(`
- action: labelkeep
regex: ".*id.*"
`)
labelsOrig := []prompbmarshal.Label{
{
Name: "__name__",
Value: "metric",
},
{
Name: "id",
Value: "foobar-random-string-here",
},
}
b.ReportAllocs()
b.SetBytes(1)
b.RunParallel(func(pb *testing.PB) {
var labels []prompbmarshal.Label
for pb.Next() {
labels = append(labels[:0], labelsOrig...)
labels = pcs.Apply(labels, 0, true)
if len(labels) != 1 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 1, labels))
}
if labels[0].Name != "id" {
panic(fmt.Errorf("unexpected label name; got %q; want %q", labels[0].Name, "id"))
}
if labels[0].Value != "foobar-random-string-here" {
panic(fmt.Errorf("unexpected label value; got %q; want %q", labels[0].Value, "foobar-random-string-here"))
}
}
})
})
b.Run("labelmap-mismatch", func(b *testing.B) {
pcs := mustParseRelabelConfigs(`
- action: labelmap
regex: "a(.*)"
`)
labelsOrig := []prompbmarshal.Label{
{
Name: "foo",
Value: "bar",
},
}
b.ReportAllocs()
b.SetBytes(1)
b.RunParallel(func(pb *testing.PB) {
var labels []prompbmarshal.Label
for pb.Next() {
labels = append(labels[:0], labelsOrig...)
labels = pcs.Apply(labels, 0, true)
if len(labels) != 1 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 3, labels))
}
if labels[0].Name != "foo" {
panic(fmt.Errorf("unexpected label name; got %q; want %q", labels[0].Name, "foo"))
}
if labels[0].Value != "bar" {
panic(fmt.Errorf("unexpected label value; got %q; want %q", labels[0].Value, "bar"))
}
}
})
})
b.Run("labelmap-match-remove-prefix", func(b *testing.B) {
pcs := mustParseRelabelConfigs(`
- action: labelmap
regex: "a(.*)"
`)
labelsOrig := []prompbmarshal.Label{
{
Name: "aabc",
Value: "foobar-random-string-here",
},
}
b.ReportAllocs()
b.SetBytes(1)
b.RunParallel(func(pb *testing.PB) {
var labels []prompbmarshal.Label
for pb.Next() {
labels = append(labels[:0], labelsOrig...)
labels = pcs.Apply(labels, 0, true)
if len(labels) != 2 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 3, labels))
}
if labels[0].Name != "aabc" {
panic(fmt.Errorf("unexpected label name; got %q; want %q", labels[0].Name, "aabc"))
}
if labels[0].Value != "foobar-random-string-here" {
panic(fmt.Errorf("unexpected label value; got %q; want %q", labels[0].Value, "foobar-random-string-here"))
}
if labels[1].Name != "abc" {
panic(fmt.Errorf("unexpected label name; got %q; want %q", labels[1].Name, "abc"))
}
if labels[1].Value != "foobar-random-string-here" {
panic(fmt.Errorf("unexpected label value; got %q; want %q", labels[1].Value, "foobar-random-string-here"))
}
}
})
})
b.Run("labelmap-match-regexp", func(b *testing.B) {
pcs := mustParseRelabelConfigs(`
- action: labelmap
regex: "(.*)bc"
`)
labelsOrig := []prompbmarshal.Label{
{
Name: "aabc",
Value: "foobar-random-string-here",
},
}
b.ReportAllocs()
b.SetBytes(1)
b.RunParallel(func(pb *testing.PB) {
var labels []prompbmarshal.Label
for pb.Next() {
labels = append(labels[:0], labelsOrig...)
labels = pcs.Apply(labels, 0, true)
if len(labels) != 2 {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), 3, labels))
}
if labels[0].Name != "aa" {
panic(fmt.Errorf("unexpected label name; got %q; want %q", labels[0].Name, "aa"))
}
if labels[0].Value != "foobar-random-string-here" {
panic(fmt.Errorf("unexpected label value; got %q; want %q", labels[0].Value, "foobar-random-string-here"))
}
if labels[1].Name != "aabc" {
panic(fmt.Errorf("unexpected label name; got %q; want %q", labels[1].Name, "aabc"))
}
if labels[1].Value != "foobar-random-string-here" {
panic(fmt.Errorf("unexpected label value; got %q; want %q", labels[1].Value, "foobar-random-string-here"))
}
}
})
})
b.Run("hashmod", func(b *testing.B) {
pcs := mustParseRelabelConfigs(`
- action: hashmod
source_labels: [id]
target_label: id
modulus: 23
`)
labelsOrig := []prompbmarshal.Label{
{
Name: "__name__",
Value: "metric",
},
{
Name: "id",
Value: "foobar-random-string-here",
},
}
b.ReportAllocs()
b.SetBytes(1)
b.RunParallel(func(pb *testing.PB) {
var labels []prompbmarshal.Label
for pb.Next() {
labels = append(labels[:0], labelsOrig...)
labels = pcs.Apply(labels, 0, true)
if len(labels) != len(labelsOrig) { if len(labels) != len(labelsOrig) {
panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels)) panic(fmt.Errorf("unexpected number of labels; got %d; want %d; labels:\n%#v", len(labels), len(labelsOrig), labels))
} }
@ -565,3 +838,11 @@ func BenchmarkApplyRelabelConfigs(b *testing.B) {
}) })
}) })
} }
func mustParseRelabelConfigs(config string) *ParsedConfigs {
pcs, err := ParseRelabelConfigsData([]byte(config))
if err != nil {
panic(fmt.Errorf("unexpected error: %w", err))
}
return pcs
}

View file

@ -6,11 +6,15 @@ import (
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"path/filepath" "path/filepath"
"sort"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envtemplate" "github.com/VictoriaMetrics/VictoriaMetrics/lib/envtemplate"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger" "github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal" "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
@ -24,6 +28,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/kubernetes" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/kubernetes"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/openstack" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/openstack"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/proxy" "github.com/VictoriaMetrics/VictoriaMetrics/lib/proxy"
"github.com/VictoriaMetrics/metrics"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -89,9 +94,10 @@ type ScrapeConfig struct {
SampleLimit int `yaml:"sample_limit,omitempty"` SampleLimit int `yaml:"sample_limit,omitempty"`
// These options are supported only by lib/promscrape. // These options are supported only by lib/promscrape.
DisableCompression bool `yaml:"disable_compression,omitempty"` DisableCompression bool `yaml:"disable_compression,omitempty"`
DisableKeepAlive bool `yaml:"disable_keepalive,omitempty"` DisableKeepAlive bool `yaml:"disable_keepalive,omitempty"`
StreamParse bool `yaml:"stream_parse,omitempty"` StreamParse bool `yaml:"stream_parse,omitempty"`
ScrapeAlignInterval time.Duration `yaml:"scrape_align_interval,omitempty"`
// This is set in loadConfig // This is set in loadConfig
swc *scrapeWorkConfig swc *scrapeWorkConfig
@ -194,7 +200,7 @@ func (cfg *Config) getKubernetesSDScrapeWork(prev []*ScrapeWork) []*ScrapeWork {
for j := range sc.KubernetesSDConfigs { for j := range sc.KubernetesSDConfigs {
sdc := &sc.KubernetesSDConfigs[j] sdc := &sc.KubernetesSDConfigs[j]
var okLocal bool var okLocal bool
dst, okLocal = appendKubernetesScrapeWork(dst, sdc, cfg.baseDir, sc.swc) dst, okLocal = appendSDScrapeWork(dst, sdc, cfg.baseDir, sc.swc, "kubernetes_sd_config")
if ok { if ok {
ok = okLocal ok = okLocal
} }
@ -222,7 +228,7 @@ func (cfg *Config) getOpenStackSDScrapeWork(prev []*ScrapeWork) []*ScrapeWork {
for j := range sc.OpenStackSDConfigs { for j := range sc.OpenStackSDConfigs {
sdc := &sc.OpenStackSDConfigs[j] sdc := &sc.OpenStackSDConfigs[j]
var okLocal bool var okLocal bool
dst, okLocal = appendOpenstackScrapeWork(dst, sdc, cfg.baseDir, sc.swc) dst, okLocal = appendSDScrapeWork(dst, sdc, cfg.baseDir, sc.swc, "openstack_sd_config")
if ok { if ok {
ok = okLocal ok = okLocal
} }
@ -250,7 +256,7 @@ func (cfg *Config) getDockerSwarmSDScrapeWork(prev []*ScrapeWork) []*ScrapeWork
for j := range sc.DockerSwarmConfigs { for j := range sc.DockerSwarmConfigs {
sdc := &sc.DockerSwarmConfigs[j] sdc := &sc.DockerSwarmConfigs[j]
var okLocal bool var okLocal bool
dst, okLocal = appendDockerSwarmScrapeWork(dst, sdc, cfg.baseDir, sc.swc) dst, okLocal = appendSDScrapeWork(dst, sdc, cfg.baseDir, sc.swc, "dockerswarm_sd_config")
if ok { if ok {
ok = okLocal ok = okLocal
} }
@ -278,7 +284,7 @@ func (cfg *Config) getConsulSDScrapeWork(prev []*ScrapeWork) []*ScrapeWork {
for j := range sc.ConsulSDConfigs { for j := range sc.ConsulSDConfigs {
sdc := &sc.ConsulSDConfigs[j] sdc := &sc.ConsulSDConfigs[j]
var okLocal bool var okLocal bool
dst, okLocal = appendConsulScrapeWork(dst, sdc, cfg.baseDir, sc.swc) dst, okLocal = appendSDScrapeWork(dst, sdc, cfg.baseDir, sc.swc, "consul_sd_config")
if ok { if ok {
ok = okLocal ok = okLocal
} }
@ -306,7 +312,7 @@ func (cfg *Config) getEurekaSDScrapeWork(prev []*ScrapeWork) []*ScrapeWork {
for j := range sc.EurekaSDConfigs { for j := range sc.EurekaSDConfigs {
sdc := &sc.EurekaSDConfigs[j] sdc := &sc.EurekaSDConfigs[j]
var okLocal bool var okLocal bool
dst, okLocal = appendEurekaScrapeWork(dst, sdc, cfg.baseDir, sc.swc) dst, okLocal = appendSDScrapeWork(dst, sdc, cfg.baseDir, sc.swc, "eureka_sd_config")
if ok { if ok {
ok = okLocal ok = okLocal
} }
@ -334,7 +340,7 @@ func (cfg *Config) getDNSSDScrapeWork(prev []*ScrapeWork) []*ScrapeWork {
for j := range sc.DNSSDConfigs { for j := range sc.DNSSDConfigs {
sdc := &sc.DNSSDConfigs[j] sdc := &sc.DNSSDConfigs[j]
var okLocal bool var okLocal bool
dst, okLocal = appendDNSScrapeWork(dst, sdc, sc.swc) dst, okLocal = appendSDScrapeWork(dst, sdc, cfg.baseDir, sc.swc, "dns_sd_config")
if ok { if ok {
ok = okLocal ok = okLocal
} }
@ -362,7 +368,7 @@ func (cfg *Config) getEC2SDScrapeWork(prev []*ScrapeWork) []*ScrapeWork {
for j := range sc.EC2SDConfigs { for j := range sc.EC2SDConfigs {
sdc := &sc.EC2SDConfigs[j] sdc := &sc.EC2SDConfigs[j]
var okLocal bool var okLocal bool
dst, okLocal = appendEC2ScrapeWork(dst, sdc, sc.swc) dst, okLocal = appendSDScrapeWork(dst, sdc, cfg.baseDir, sc.swc, "ec2_sd_config")
if ok { if ok {
ok = okLocal ok = okLocal
} }
@ -390,7 +396,7 @@ func (cfg *Config) getGCESDScrapeWork(prev []*ScrapeWork) []*ScrapeWork {
for j := range sc.GCESDConfigs { for j := range sc.GCESDConfigs {
sdc := &sc.GCESDConfigs[j] sdc := &sc.GCESDConfigs[j]
var okLocal bool var okLocal bool
dst, okLocal = appendGCEScrapeWork(dst, sdc, sc.swc) dst, okLocal = appendSDScrapeWork(dst, sdc, cfg.baseDir, sc.swc, "gce_sd_config")
if ok { if ok {
ok = okLocal ok = okLocal
} }
@ -480,13 +486,11 @@ func getScrapeWorkConfig(sc *ScrapeConfig, baseDir string, globalCfg *GlobalConf
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot parse auth config for `job_name` %q: %w", jobName, err) return nil, fmt.Errorf("cannot parse auth config for `job_name` %q: %w", jobName, err)
} }
var relabelConfigs []promrelabel.ParsedRelabelConfig relabelConfigs, err := promrelabel.ParseRelabelConfigs(sc.RelabelConfigs)
relabelConfigs, err = promrelabel.ParseRelabelConfigs(relabelConfigs[:0], sc.RelabelConfigs)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot parse `relabel_configs` for `job_name` %q: %w", jobName, err) return nil, fmt.Errorf("cannot parse `relabel_configs` for `job_name` %q: %w", jobName, err)
} }
var metricRelabelConfigs []promrelabel.ParsedRelabelConfig metricRelabelConfigs, err := promrelabel.ParseRelabelConfigs(sc.MetricRelabelConfigs)
metricRelabelConfigs, err = promrelabel.ParseRelabelConfigs(metricRelabelConfigs[:0], sc.MetricRelabelConfigs)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot parse `metric_relabel_configs` for `job_name` %q: %w", jobName, err) return nil, fmt.Errorf("cannot parse `metric_relabel_configs` for `job_name` %q: %w", jobName, err)
} }
@ -508,6 +512,9 @@ func getScrapeWorkConfig(sc *ScrapeConfig, baseDir string, globalCfg *GlobalConf
disableCompression: sc.DisableCompression, disableCompression: sc.DisableCompression,
disableKeepAlive: sc.DisableKeepAlive, disableKeepAlive: sc.DisableKeepAlive,
streamParse: sc.StreamParse, streamParse: sc.StreamParse,
scrapeAlignInterval: sc.ScrapeAlignInterval,
cache: newScrapeWorkCache(),
} }
return swc, nil return swc, nil
} }
@ -524,96 +531,114 @@ type scrapeWorkConfig struct {
honorLabels bool honorLabels bool
honorTimestamps bool honorTimestamps bool
externalLabels map[string]string externalLabels map[string]string
relabelConfigs []promrelabel.ParsedRelabelConfig relabelConfigs *promrelabel.ParsedConfigs
metricRelabelConfigs []promrelabel.ParsedRelabelConfig metricRelabelConfigs *promrelabel.ParsedConfigs
sampleLimit int sampleLimit int
disableCompression bool disableCompression bool
disableKeepAlive bool disableKeepAlive bool
streamParse bool streamParse bool
scrapeAlignInterval time.Duration
cache *scrapeWorkCache
} }
func appendKubernetesScrapeWork(dst []*ScrapeWork, sdc *kubernetes.SDConfig, baseDir string, swc *scrapeWorkConfig) ([]*ScrapeWork, bool) { type scrapeWorkCache struct {
targetLabels, err := kubernetes.GetLabels(sdc, baseDir) mu sync.Mutex
m map[string]*scrapeWorkEntry
lastCleanupTime uint64
}
type scrapeWorkEntry struct {
sw *ScrapeWork
lastAccessTime uint64
}
func newScrapeWorkCache() *scrapeWorkCache {
return &scrapeWorkCache{
m: make(map[string]*scrapeWorkEntry),
}
}
func (swc *scrapeWorkCache) Get(key string) *ScrapeWork {
currentTime := fasttime.UnixTimestamp()
swc.mu.Lock()
swe := swc.m[key]
swe.lastAccessTime = currentTime
swc.mu.Unlock()
return swe.sw
}
func (swc *scrapeWorkCache) Set(key string, sw *ScrapeWork) {
currentTime := fasttime.UnixTimestamp()
swc.mu.Lock()
swc.m[key] = &scrapeWorkEntry{
sw: sw,
lastAccessTime: currentTime,
}
if currentTime > swc.lastCleanupTime+10*60 {
for k, swe := range swc.m {
if currentTime > swe.lastAccessTime+2*60 {
delete(swc.m, k)
}
}
swc.lastCleanupTime = currentTime
}
swc.mu.Unlock()
}
type targetLabelsGetter interface {
GetLabels(baseDir string) ([]map[string]string, error)
}
func appendSDScrapeWork(dst []*ScrapeWork, sdc targetLabelsGetter, baseDir string, swc *scrapeWorkConfig, discoveryType string) ([]*ScrapeWork, bool) {
targetLabels, err := sdc.GetLabels(baseDir)
if err != nil { if err != nil {
logger.Errorf("error when discovering kubernetes targets for `job_name` %q: %s; skipping it", swc.jobName, err) logger.Errorf("skipping %s targets for job_name %q because of error: %s", discoveryType, swc.jobName, err)
return dst, false return dst, false
} }
return appendScrapeWorkForTargetLabels(dst, swc, targetLabels, "kubernetes_sd_config"), true return appendScrapeWorkForTargetLabels(dst, swc, targetLabels, discoveryType), true
} }
func appendOpenstackScrapeWork(dst []*ScrapeWork, sdc *openstack.SDConfig, baseDir string, swc *scrapeWorkConfig) ([]*ScrapeWork, bool) { func appendScrapeWorkForTargetLabels(dst []*ScrapeWork, swc *scrapeWorkConfig, targetLabels []map[string]string, discoveryType string) []*ScrapeWork {
targetLabels, err := openstack.GetLabels(sdc, baseDir) startTime := time.Now()
if err != nil { // Process targetLabels in parallel in order to reduce processing time for big number of targetLabels.
logger.Errorf("error when discovering openstack targets for `job_name` %q: %s; skipping it", swc.jobName, err) type result struct {
return dst, false sw *ScrapeWork
err error
} }
return appendScrapeWorkForTargetLabels(dst, swc, targetLabels, "openstack_sd_config"), true resultCh := make(chan result)
} workCh := make(chan map[string]string)
goroutines := cgroup.AvailableCPUs()
func appendDockerSwarmScrapeWork(dst []*ScrapeWork, sdc *dockerswarm.SDConfig, baseDir string, swc *scrapeWorkConfig) ([]*ScrapeWork, bool) { for i := 0; i < goroutines; i++ {
targetLabels, err := dockerswarm.GetLabels(sdc, baseDir) go func() {
if err != nil { for metaLabels := range workCh {
logger.Errorf("error when discovering dockerswarm targets for `job_name` %q: %s; skipping it", swc.jobName, err) target := metaLabels["__address__"]
return dst, false sw, err := swc.getScrapeWork(target, nil, metaLabels)
if err != nil {
err = fmt.Errorf("skipping %s target %q for job_name %q because of error: %w", discoveryType, target, swc.jobName, err)
}
resultCh <- result{
sw: sw,
err: err,
}
}
}()
} }
return appendScrapeWorkForTargetLabels(dst, swc, targetLabels, "dockerswarm_sd_config"), true
}
func appendConsulScrapeWork(dst []*ScrapeWork, sdc *consul.SDConfig, baseDir string, swc *scrapeWorkConfig) ([]*ScrapeWork, bool) {
targetLabels, err := consul.GetLabels(sdc, baseDir)
if err != nil {
logger.Errorf("error when discovering consul targets for `job_name` %q: %s; skipping it", swc.jobName, err)
return dst, false
}
return appendScrapeWorkForTargetLabels(dst, swc, targetLabels, "consul_sd_config"), true
}
func appendEurekaScrapeWork(dst []*ScrapeWork, sdc *eureka.SDConfig, baseDir string, swc *scrapeWorkConfig) ([]*ScrapeWork, bool) {
targetLabels, err := eureka.GetLabels(sdc, baseDir)
if err != nil {
logger.Errorf("error when discovering eureka targets for `job_name` %q: %s; skipping it", swc.jobName, err)
return dst, false
}
return appendScrapeWorkForTargetLabels(dst, swc, targetLabels, "eureka_sd_config"), true
}
func appendDNSScrapeWork(dst []*ScrapeWork, sdc *dns.SDConfig, swc *scrapeWorkConfig) ([]*ScrapeWork, bool) {
targetLabels, err := dns.GetLabels(sdc)
if err != nil {
logger.Errorf("error when discovering dns targets for `job_name` %q: %s; skipping it", swc.jobName, err)
return dst, false
}
return appendScrapeWorkForTargetLabels(dst, swc, targetLabels, "dns_sd_config"), true
}
func appendEC2ScrapeWork(dst []*ScrapeWork, sdc *ec2.SDConfig, swc *scrapeWorkConfig) ([]*ScrapeWork, bool) {
targetLabels, err := ec2.GetLabels(sdc)
if err != nil {
logger.Errorf("error when discovering ec2 targets for `job_name` %q: %s; skipping it", swc.jobName, err)
return dst, false
}
return appendScrapeWorkForTargetLabels(dst, swc, targetLabels, "ec2_sd_config"), true
}
func appendGCEScrapeWork(dst []*ScrapeWork, sdc *gce.SDConfig, swc *scrapeWorkConfig) ([]*ScrapeWork, bool) {
targetLabels, err := gce.GetLabels(sdc)
if err != nil {
logger.Errorf("error when discovering gce targets for `job_name` %q: %s; skippint it", swc.jobName, err)
return dst, false
}
return appendScrapeWorkForTargetLabels(dst, swc, targetLabels, "gce_sd_config"), true
}
func appendScrapeWorkForTargetLabels(dst []*ScrapeWork, swc *scrapeWorkConfig, targetLabels []map[string]string, sectionName string) []*ScrapeWork {
for _, metaLabels := range targetLabels { for _, metaLabels := range targetLabels {
target := metaLabels["__address__"] workCh <- metaLabels
var err error }
dst, err = appendScrapeWork(dst, swc, target, nil, metaLabels) close(workCh)
if err != nil { for range targetLabels {
logger.Errorf("error when parsing `%s` target %q for `job_name` %q: %s; skipping it", sectionName, target, swc.jobName, err) r := <-resultCh
if r.err != nil {
logger.Errorf("%s", r.err)
continue continue
} }
if r.sw != nil {
dst = append(dst, r.sw)
}
} }
metrics.GetOrCreateHistogram(fmt.Sprintf("vm_promscrape_target_relabel_duration_seconds{type=%q}", discoveryType)).UpdateDuration(startTime)
return dst return dst
} }
@ -669,18 +694,55 @@ func (stc *StaticConfig) appendScrapeWork(dst []*ScrapeWork, swc *scrapeWorkConf
logger.Errorf("`static_configs` target for `job_name` %q cannot be empty; skipping it", swc.jobName) logger.Errorf("`static_configs` target for `job_name` %q cannot be empty; skipping it", swc.jobName)
continue continue
} }
var err error sw, err := swc.getScrapeWork(target, stc.Labels, metaLabels)
dst, err = appendScrapeWork(dst, swc, target, stc.Labels, metaLabels)
if err != nil { if err != nil {
// Do not return this error, since other targets may be valid // Do not return this error, since other targets may be valid
logger.Errorf("error when parsing `static_configs` target %q for `job_name` %q: %s; skipping it", target, swc.jobName, err) logger.Errorf("error when parsing `static_configs` target %q for `job_name` %q: %s; skipping it", target, swc.jobName, err)
continue continue
} }
if sw != nil {
dst = append(dst, sw)
}
} }
return dst return dst
} }
func appendScrapeWork(dst []*ScrapeWork, swc *scrapeWorkConfig, target string, extraLabels, metaLabels map[string]string) ([]*ScrapeWork, error) { func (swc *scrapeWorkConfig) getScrapeWork(target string, extraLabels, metaLabels map[string]string) (*ScrapeWork, error) {
key := getScrapeWorkKey(extraLabels, metaLabels)
if sw := swc.cache.Get(key); sw != nil {
return sw, nil
}
sw, err := swc.getScrapeWorkReal(target, extraLabels, metaLabels)
if err != nil {
swc.cache.Set(key, sw)
}
return sw, err
}
func getScrapeWorkKey(extraLabels, metaLabels map[string]string) string {
var b []byte
b = appendSortedKeyValuePairs(b, extraLabels)
b = appendSortedKeyValuePairs(b, metaLabels)
return string(b)
}
func appendSortedKeyValuePairs(dst []byte, m map[string]string) []byte {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
dst = strconv.AppendQuote(dst, k)
dst = append(dst, ':')
dst = strconv.AppendQuote(dst, m[k])
dst = append(dst, ',')
}
dst = append(dst, '\n')
return dst
}
func (swc *scrapeWorkConfig) getScrapeWorkReal(target string, extraLabels, metaLabels map[string]string) (*ScrapeWork, error) {
labels := mergeLabels(swc.jobName, swc.scheme, target, swc.metricsPath, extraLabels, swc.externalLabels, metaLabels, swc.params) labels := mergeLabels(swc.jobName, swc.scheme, target, swc.metricsPath, extraLabels, swc.externalLabels, metaLabels, swc.params)
var originalLabels []prompbmarshal.Label var originalLabels []prompbmarshal.Label
if !*dropOriginalLabels { if !*dropOriginalLabels {
@ -689,7 +751,7 @@ func appendScrapeWork(dst []*ScrapeWork, swc *scrapeWorkConfig, target string, e
// Reduce memory usage by interning all the strings in originalLabels. // Reduce memory usage by interning all the strings in originalLabels.
internLabelStrings(originalLabels) internLabelStrings(originalLabels)
} }
labels = promrelabel.ApplyRelabelConfigs(labels, 0, swc.relabelConfigs, false) labels = swc.relabelConfigs.Apply(labels, 0, false)
labels = promrelabel.RemoveMetaLabels(labels[:0], labels) labels = promrelabel.RemoveMetaLabels(labels[:0], labels)
// Remove references to already deleted labels, so GC could clean strings for label name and label value past len(labels). // Remove references to already deleted labels, so GC could clean strings for label name and label value past len(labels).
// This should reduce memory usage when relabeling creates big number of temporary labels with long names and/or values. // This should reduce memory usage when relabeling creates big number of temporary labels with long names and/or values.
@ -699,7 +761,7 @@ func appendScrapeWork(dst []*ScrapeWork, swc *scrapeWorkConfig, target string, e
if len(labels) == 0 { if len(labels) == 0 {
// Drop target without labels. // Drop target without labels.
droppedTargetsMap.Register(originalLabels) droppedTargetsMap.Register(originalLabels)
return dst, nil return nil, nil
} }
// See https://www.robustperception.io/life-of-a-label // See https://www.robustperception.io/life-of-a-label
schemeRelabeled := promrelabel.GetLabelValueByName(labels, "__scheme__") schemeRelabeled := promrelabel.GetLabelValueByName(labels, "__scheme__")
@ -710,12 +772,12 @@ func appendScrapeWork(dst []*ScrapeWork, swc *scrapeWorkConfig, target string, e
if len(addressRelabeled) == 0 { if len(addressRelabeled) == 0 {
// Drop target without scrape address. // Drop target without scrape address.
droppedTargetsMap.Register(originalLabels) droppedTargetsMap.Register(originalLabels)
return dst, nil return nil, nil
} }
if strings.Contains(addressRelabeled, "/") { if strings.Contains(addressRelabeled, "/") {
// Drop target with '/' // Drop target with '/'
droppedTargetsMap.Register(originalLabels) droppedTargetsMap.Register(originalLabels)
return dst, nil return nil, nil
} }
addressRelabeled = addMissingPort(schemeRelabeled, addressRelabeled) addressRelabeled = addMissingPort(schemeRelabeled, addressRelabeled)
metricsPathRelabeled := promrelabel.GetLabelValueByName(labels, "__metrics_path__") metricsPathRelabeled := promrelabel.GetLabelValueByName(labels, "__metrics_path__")
@ -733,7 +795,7 @@ func appendScrapeWork(dst []*ScrapeWork, swc *scrapeWorkConfig, target string, e
paramsStr := url.Values(paramsRelabeled).Encode() paramsStr := url.Values(paramsRelabeled).Encode()
scrapeURL := fmt.Sprintf("%s://%s%s%s%s", schemeRelabeled, addressRelabeled, metricsPathRelabeled, optionalQuestion, paramsStr) scrapeURL := fmt.Sprintf("%s://%s%s%s%s", schemeRelabeled, addressRelabeled, metricsPathRelabeled, optionalQuestion, paramsStr)
if _, err := url.Parse(scrapeURL); err != nil { if _, err := url.Parse(scrapeURL); err != nil {
return dst, fmt.Errorf("invalid url %q for scheme=%q (%q), target=%q (%q), metrics_path=%q (%q) for `job_name` %q: %w", return nil, fmt.Errorf("invalid url %q for scheme=%q (%q), target=%q (%q), metrics_path=%q (%q) for `job_name` %q: %w",
scrapeURL, swc.scheme, schemeRelabeled, target, addressRelabeled, swc.metricsPath, metricsPathRelabeled, swc.jobName, err) scrapeURL, swc.scheme, schemeRelabeled, target, addressRelabeled, swc.metricsPath, metricsPathRelabeled, swc.jobName, err)
} }
// Set missing "instance" label according to https://www.robustperception.io/life-of-a-label // Set missing "instance" label according to https://www.robustperception.io/life-of-a-label
@ -746,7 +808,7 @@ func appendScrapeWork(dst []*ScrapeWork, swc *scrapeWorkConfig, target string, e
} }
// Reduce memory usage by interning all the strings in labels. // Reduce memory usage by interning all the strings in labels.
internLabelStrings(labels) internLabelStrings(labels)
dst = append(dst, &ScrapeWork{ sw := &ScrapeWork{
ScrapeURL: scrapeURL, ScrapeURL: scrapeURL,
ScrapeInterval: swc.scrapeInterval, ScrapeInterval: swc.scrapeInterval,
ScrapeTimeout: swc.scrapeTimeout, ScrapeTimeout: swc.scrapeTimeout,
@ -761,10 +823,11 @@ func appendScrapeWork(dst []*ScrapeWork, swc *scrapeWorkConfig, target string, e
DisableCompression: swc.disableCompression, DisableCompression: swc.disableCompression,
DisableKeepAlive: swc.disableKeepAlive, DisableKeepAlive: swc.disableKeepAlive,
StreamParse: swc.streamParse, StreamParse: swc.streamParse,
ScrapeAlignInterval: swc.scrapeAlignInterval,
jobNameOriginal: swc.jobName, jobNameOriginal: swc.jobName,
}) }
return dst, nil return sw, nil
} }
func internLabelStrings(labels []prompbmarshal.Label) { func internLabelStrings(labels []prompbmarshal.Label) {

View file

@ -4,13 +4,11 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"reflect" "reflect"
"regexp"
"testing" "testing"
"time" "time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal" "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
) )
func TestLoadStaticConfigs(t *testing.T) { func TestLoadStaticConfigs(t *testing.T) {
@ -1034,13 +1032,6 @@ scrape_configs:
}, },
}) })
prcs, err := promrelabel.ParseRelabelConfigs(nil, []promrelabel.RelabelConfig{{
SourceLabels: []string{"foo"},
TargetLabel: "abc",
}})
if err != nil {
t.Fatalf("unexpected error when parsing relabel configs: %s", err)
}
f(` f(`
scrape_configs: scrape_configs:
- job_name: foo - job_name: foo
@ -1076,9 +1067,12 @@ scrape_configs:
Value: "foo", Value: "foo",
}, },
}, },
AuthConfig: &promauth.Config{}, AuthConfig: &promauth.Config{},
MetricRelabelConfigs: prcs, MetricRelabelConfigs: mustParseRelabelConfigs(`
jobNameOriginal: "foo", - source_labels: [foo]
target_label: abc
`),
jobNameOriginal: "foo",
}, },
}) })
f(` f(`
@ -1275,6 +1269,7 @@ scrape_configs:
disable_keepalive: true disable_keepalive: true
disable_compression: true disable_compression: true
stream_parse: true stream_parse: true
scrape_align_interval: 1s
static_configs: static_configs:
- targets: - targets:
- 192.168.1.2 # SNMP device. - 192.168.1.2 # SNMP device.
@ -1323,12 +1318,13 @@ scrape_configs:
Value: "snmp", Value: "snmp",
}, },
}, },
AuthConfig: &promauth.Config{}, AuthConfig: &promauth.Config{},
SampleLimit: 100, SampleLimit: 100,
DisableKeepAlive: true, DisableKeepAlive: true,
DisableCompression: true, DisableCompression: true,
StreamParse: true, StreamParse: true,
jobNameOriginal: "snmp", ScrapeAlignInterval: time.Second,
jobNameOriginal: "snmp",
}, },
}) })
f(` f(`
@ -1372,8 +1368,6 @@ scrape_configs:
}) })
} }
var defaultRegexForRelabelConfig = regexp.MustCompile("^(.*)$")
func equalStaticConfigForScrapeWorks(a, b []*ScrapeWork) bool { func equalStaticConfigForScrapeWorks(a, b []*ScrapeWork) bool {
if len(a) != len(b) { if len(a) != len(b) {
return false return false

View file

@ -29,7 +29,7 @@ type SDConfig struct {
} }
// GetLabels returns Consul labels according to sdc. // GetLabels returns Consul labels according to sdc.
func GetLabels(sdc *SDConfig, baseDir string) ([]map[string]string, error) { func (sdc *SDConfig) GetLabels(baseDir string) ([]map[string]string, error) {
cfg, err := getAPIConfig(sdc, baseDir) cfg, err := getAPIConfig(sdc, baseDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get API config: %w", err) return nil, fmt.Errorf("cannot get API config: %w", err)

View file

@ -24,7 +24,7 @@ type SDConfig struct {
} }
// GetLabels returns DNS labels according to sdc. // GetLabels returns DNS labels according to sdc.
func GetLabels(sdc *SDConfig) ([]map[string]string, error) { func (sdc *SDConfig) GetLabels(baseDir string) ([]map[string]string, error) {
if len(sdc.Names) == 0 { if len(sdc.Names) == 0 {
return nil, fmt.Errorf("`names` cannot be empty in `dns_sd_config`") return nil, fmt.Errorf("`names` cannot be empty in `dns_sd_config`")
} }

View file

@ -31,7 +31,7 @@ type Filter struct {
} }
// GetLabels returns dockerswarm labels according to sdc. // GetLabels returns dockerswarm labels according to sdc.
func GetLabels(sdc *SDConfig, baseDir string) ([]map[string]string, error) { func (sdc *SDConfig) GetLabels(baseDir string) ([]map[string]string, error) {
cfg, err := getAPIConfig(sdc, baseDir) cfg, err := getAPIConfig(sdc, baseDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get API config: %w", err) return nil, fmt.Errorf("cannot get API config: %w", err)

View file

@ -31,7 +31,7 @@ type Filter struct {
} }
// GetLabels returns ec2 labels according to sdc. // GetLabels returns ec2 labels according to sdc.
func GetLabels(sdc *SDConfig) ([]map[string]string, error) { func (sdc *SDConfig) GetLabels(baseDir string) ([]map[string]string, error) {
cfg, err := getAPIConfig(sdc) cfg, err := getAPIConfig(sdc)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get API config: %w", err) return nil, fmt.Errorf("cannot get API config: %w", err)

View file

@ -82,7 +82,7 @@ type DataCenterInfo struct {
} }
// GetLabels returns Eureka labels according to sdc. // GetLabels returns Eureka labels according to sdc.
func GetLabels(sdc *SDConfig, baseDir string) ([]map[string]string, error) { func (sdc *SDConfig) GetLabels(baseDir string) ([]map[string]string, error) {
cfg, err := getAPIConfig(sdc, baseDir) cfg, err := getAPIConfig(sdc, baseDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get API config: %w", err) return nil, fmt.Errorf("cannot get API config: %w", err)

View file

@ -48,7 +48,7 @@ func (z *ZoneYAML) UnmarshalYAML(unmarshal func(interface{}) error) error {
} }
// GetLabels returns gce labels according to sdc. // GetLabels returns gce labels according to sdc.
func GetLabels(sdc *SDConfig) ([]map[string]string, error) { func (sdc *SDConfig) GetLabels(baseDir string) ([]map[string]string, error) {
cfg, err := getAPIConfig(sdc) cfg, err := getAPIConfig(sdc)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get API config: %w", err) return nil, fmt.Errorf("cannot get API config: %w", err)

View file

@ -1,19 +1,32 @@
package kubernetes package kubernetes
import ( import (
"encoding/json"
"errors"
"flag"
"fmt" "fmt"
"io"
"io/ioutil"
"net" "net"
"net/http"
"net/url"
"os" "os"
"strconv"
"strings"
"sync"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
) )
var apiServerTimeout = flag.Duration("promscrape.kubernetes.apiServerTimeout", 2*time.Minute, "Timeout for requests to Kuberntes API server")
// apiConfig contains config for API server // apiConfig contains config for API server
type apiConfig struct { type apiConfig struct {
client *discoveryutils.Client aw *apiWatcher
namespaces []string
selectors []Selector
} }
var configMap = discoveryutils.NewConfigMap() var configMap = discoveryutils.NewConfigMap()
@ -56,22 +69,437 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
} }
ac = acNew ac = acNew
} }
client, err := discoveryutils.NewClient(apiServer, ac, sdc.ProxyURL) if !strings.Contains(apiServer, "://") {
if err != nil { proto := "http"
return nil, fmt.Errorf("cannot create HTTP client for %q: %w", apiServer, err) if sdc.TLSConfig != nil {
proto = "https"
}
apiServer = proto + "://" + apiServer
} }
for strings.HasSuffix(apiServer, "/") {
apiServer = apiServer[:len(apiServer)-1]
}
var proxy func(*http.Request) (*url.URL, error)
if proxyURL := sdc.ProxyURL.URL(); proxyURL != nil {
proxy = http.ProxyURL(proxyURL)
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: ac.NewTLSConfig(),
Proxy: proxy,
TLSHandshakeTimeout: 10 * time.Second,
IdleConnTimeout: *apiServerTimeout,
},
Timeout: *apiServerTimeout,
}
aw := newAPIWatcher(client, apiServer, ac.Authorization, sdc.Namespaces.Names, sdc.Selectors)
cfg := &apiConfig{ cfg := &apiConfig{
client: client, aw: aw,
namespaces: sdc.Namespaces.Names,
selectors: sdc.Selectors,
} }
return cfg, nil return cfg, nil
} }
func getAPIResponse(cfg *apiConfig, role, path string) ([]byte, error) { // WatchEvent is a watch event returned from API server endpoints if `watch=1` query arg is set.
query := joinSelectors(role, cfg.namespaces, cfg.selectors) //
if len(query) > 0 { // See https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes
path += "?" + query type WatchEvent struct {
} Type string
return cfg.client.GetAPIResponse(path) Object json.RawMessage
}
// object is any Kubernetes object.
type object interface {
key() string
getTargetLabels(aw *apiWatcher) []map[string]string
}
// parseObjectFunc must parse object from the given data.
type parseObjectFunc func(data []byte) (object, error)
// parseObjectListFunc must parse objectList from the given data.
type parseObjectListFunc func(data []byte) (map[string]object, ListMeta, error)
// apiWatcher is used for watching for Kuberntes object changes and caching their latest states.
type apiWatcher struct {
// The client used for watching for object changes
client *http.Client
// Kubenetes API server address in the form http://api-server
apiServer string
// The contents for `Authorization` HTTP request header
authorization string
// Namespaces to watch
namespaces []string
// Selectors to apply during watch
selectors []Selector
// mu protects watchersByURL and lastAccessTime
mu sync.Mutex
// a map of watchers keyed by request paths
watchersByURL map[string]*urlWatcher
// The last time the apiWatcher was queried for cached objects.
// It is used for stopping unused watchers.
lastAccessTime uint64
}
func newAPIWatcher(client *http.Client, apiServer, authorization string, namespaces []string, selectors []Selector) *apiWatcher {
return &apiWatcher{
apiServer: apiServer,
authorization: authorization,
client: client,
namespaces: namespaces,
selectors: selectors,
watchersByURL: make(map[string]*urlWatcher),
lastAccessTime: fasttime.UnixTimestamp(),
}
}
// getLabelsForRole returns all the sets of labels for the given role.
func (aw *apiWatcher) getLabelsForRole(role string) []map[string]string {
var ms []map[string]string
aw.mu.Lock()
for _, uw := range aw.watchersByURL {
if uw.role != role {
continue
}
uw.mu.Lock()
for _, labels := range uw.labelsByKey {
ms = append(ms, labels...)
}
uw.mu.Unlock()
}
aw.lastAccessTime = fasttime.UnixTimestamp()
aw.mu.Unlock()
return ms
}
// getObjectByRole returns an object with the given (namespace, name) key and the given role.
func (aw *apiWatcher) getObjectByRole(role, namespace, name string) object {
if aw == nil {
return nil
}
key := namespace + "/" + name
aw.startWatchersForRole(role)
var o object
aw.mu.Lock()
for _, uw := range aw.watchersByURL {
if uw.role != role {
continue
}
uw.mu.Lock()
o = uw.objectsByKey[key]
uw.mu.Unlock()
if o != nil {
break
}
}
aw.lastAccessTime = fasttime.UnixTimestamp()
aw.mu.Unlock()
return o
}
func (aw *apiWatcher) startWatchersForRole(role string) {
parseObject, parseObjectList := getObjectParsersForRole(role)
paths := getAPIPaths(role, aw.namespaces, aw.selectors)
for _, path := range paths {
apiURL := aw.apiServer + path
aw.startWatcherForURL(role, apiURL, parseObject, parseObjectList)
}
}
func (aw *apiWatcher) startWatcherForURL(role, apiURL string, parseObject parseObjectFunc, parseObjectList parseObjectListFunc) {
aw.mu.Lock()
defer aw.mu.Unlock()
if aw.watchersByURL[apiURL] != nil {
// Watcher for the given path already exists.
return
}
uw := aw.newURLWatcher(role, apiURL, parseObject, parseObjectList)
resourceVersion := uw.reloadObjects()
aw.watchersByURL[apiURL] = uw
go func() {
uw.watchForUpdates(resourceVersion)
aw.mu.Lock()
delete(aw.watchersByURL, apiURL)
aw.mu.Unlock()
}()
}
// needStop returns true if aw wasn't used for long time.
func (aw *apiWatcher) needStop() bool {
aw.mu.Lock()
defer aw.mu.Unlock()
return fasttime.UnixTimestamp() > aw.lastAccessTime+5*60
}
// doRequest performs http request to the given requestURL.
func (aw *apiWatcher) doRequest(requestURL string) (*http.Response, error) {
req, err := http.NewRequest("GET", requestURL, nil)
if err != nil {
logger.Fatalf("cannot create a request for %q: %s", requestURL, err)
}
if aw.authorization != "" {
req.Header.Set("Authorization", aw.authorization)
}
return aw.client.Do(req)
}
// urlWatcher watches for an apiURL and updates object states in objectsByKey.
type urlWatcher struct {
role string
apiURL string
parseObject parseObjectFunc
parseObjectList parseObjectListFunc
// mu protects objectsByKey and labelsByKey
mu sync.Mutex
// objectsByKey contains the latest state for objects obtained from apiURL
objectsByKey map[string]object
labelsByKey map[string][]map[string]string
// the parent apiWatcher
aw *apiWatcher
}
func (aw *apiWatcher) newURLWatcher(role, apiURL string, parseObject parseObjectFunc, parseObjectList parseObjectListFunc) *urlWatcher {
return &urlWatcher{
role: role,
apiURL: apiURL,
parseObject: parseObject,
parseObjectList: parseObjectList,
objectsByKey: make(map[string]object),
labelsByKey: make(map[string][]map[string]string),
aw: aw,
}
}
// reloadObjects reloads objects to the latest state and returns resourceVersion for the latest state.
func (uw *urlWatcher) reloadObjects() string {
requestURL := uw.apiURL
resp, err := uw.aw.doRequest(requestURL)
if err != nil {
logger.Errorf("error when performing a request to %q: %s", requestURL, err)
return ""
}
body, _ := ioutil.ReadAll(resp.Body)
_ = resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Errorf("unexpected status code for request to %q: %d; want %d; response: %q", requestURL, resp.StatusCode, http.StatusOK, body)
return ""
}
objectsByKey, metadata, err := uw.parseObjectList(body)
if err != nil {
logger.Errorf("cannot parse response from %q: %s", requestURL, err)
return ""
}
uw.mu.Lock()
uw.objectsByKey = objectsByKey
uw.mu.Unlock()
return metadata.ResourceVersion
}
// watchForUpdates watches for object updates starting from resourceVersion and updates the corresponding objects to the latest state.
//
// See https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes
func (uw *urlWatcher) watchForUpdates(resourceVersion string) {
aw := uw.aw
backoffDelay := time.Second
maxBackoffDelay := 30 * time.Second
backoffSleep := func() {
time.Sleep(backoffDelay)
backoffDelay *= 2
if backoffDelay > maxBackoffDelay {
backoffDelay = maxBackoffDelay
}
}
apiURL := uw.apiURL
delimiter := "?"
if strings.Contains(apiURL, "?") {
delimiter = "&"
}
timeoutSeconds := time.Duration(0.9 * float64(aw.client.Timeout)).Seconds()
apiURL += delimiter + "watch=1&timeoutSeconds=" + strconv.Itoa(int(timeoutSeconds))
logger.Infof("started watcher for %q", apiURL)
for {
if aw.needStop() {
logger.Infof("stopped unused watcher for %q", apiURL)
return
}
requestURL := apiURL
if resourceVersion != "" {
requestURL += "&resourceVersion=" + url.QueryEscape(resourceVersion) + "&resourceVersionMatch=NotOlderThan"
}
resp, err := aw.doRequest(requestURL)
if err != nil {
logger.Errorf("error when performing a request to %q: %s", requestURL, err)
backoffSleep()
// There is no sense in reloading resources on non-http errors.
continue
}
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
_ = resp.Body.Close()
logger.Errorf("unexpected status code for request to %q: %d; want %d; response: %q", requestURL, resp.StatusCode, http.StatusOK, body)
if resp.StatusCode == 410 {
// Update stale resourceVersion. See https://kubernetes.io/docs/reference/using-api/api-concepts/#410-gone-responses
resourceVersion = uw.reloadObjects()
backoffDelay = time.Second
} else {
backoffSleep()
// There is no sense in reloading resources on non-410 status codes.
}
continue
}
backoffDelay = time.Second
err = uw.readObjectUpdateStream(resp.Body)
_ = resp.Body.Close()
if err != nil {
if errors.Is(err, io.EOF) {
// The stream has been closed (probably due to timeout)
backoffSleep()
continue
}
logger.Errorf("error when reading WatchEvent stream from %q: %s", requestURL, err)
backoffSleep()
// There is no sense in reloading resources on non-http errors.
continue
}
}
}
// readObjectUpdateStream reads Kuberntes watch events from r and updates locally cached objects according to the received events.
func (uw *urlWatcher) readObjectUpdateStream(r io.Reader) error {
d := json.NewDecoder(r)
var we WatchEvent
for {
if err := d.Decode(&we); err != nil {
return err
}
o, err := uw.parseObject(we.Object)
if err != nil {
return err
}
key := o.key()
switch we.Type {
case "ADDED", "MODIFIED":
uw.mu.Lock()
uw.objectsByKey[key] = o
uw.mu.Unlock()
labels := o.getTargetLabels(uw.aw)
uw.mu.Lock()
uw.labelsByKey[key] = labels
uw.mu.Unlock()
case "DELETED":
uw.mu.Lock()
delete(uw.objectsByKey, key)
delete(uw.labelsByKey, key)
uw.mu.Unlock()
default:
return fmt.Errorf("unexpected WatchEvent type %q for role %q", we.Type, uw.role)
}
}
}
func getAPIPaths(role string, namespaces []string, selectors []Selector) []string {
objectName := getObjectNameByRole(role)
if objectName == "nodes" || len(namespaces) == 0 {
query := joinSelectors(role, selectors)
path := getAPIPath(objectName, "", query)
return []string{path}
}
query := joinSelectors(role, selectors)
paths := make([]string, len(namespaces))
for i, namespace := range namespaces {
paths[i] = getAPIPath(objectName, namespace, query)
}
return paths
}
func getAPIPath(objectName, namespace, query string) string {
suffix := objectName
if namespace != "" {
suffix = "namespaces/" + namespace + "/" + objectName
}
if len(query) > 0 {
suffix += "?" + query
}
if objectName == "endpointslices" {
return "/apis/discovery.k8s.io/v1beta1/" + suffix
}
return "/api/v1/" + suffix
}
func joinSelectors(role string, selectors []Selector) string {
var labelSelectors, fieldSelectors []string
for _, s := range selectors {
if s.Role != role {
continue
}
if s.Label != "" {
labelSelectors = append(labelSelectors, s.Label)
}
if s.Field != "" {
fieldSelectors = append(fieldSelectors, s.Field)
}
}
var args []string
if len(labelSelectors) > 0 {
args = append(args, "labelSelector="+url.QueryEscape(strings.Join(labelSelectors, ",")))
}
if len(fieldSelectors) > 0 {
args = append(args, "fieldSelector="+url.QueryEscape(strings.Join(fieldSelectors, ",")))
}
return strings.Join(args, "&")
}
func getObjectNameByRole(role string) string {
switch role {
case "node":
return "nodes"
case "pod":
return "pods"
case "service":
return "services"
case "endpoints":
return "endpoints"
case "endpointslices":
return "endpointslices"
case "ingress":
return "ingresses"
default:
logger.Panicf("BUG: unknonw role=%q", role)
return ""
}
}
func getObjectParsersForRole(role string) (parseObjectFunc, parseObjectListFunc) {
switch role {
case "node":
return parseNode, parseNodeList
case "pod":
return parsePod, parsePodList
case "service":
return parseService, parseServiceList
case "endpoints":
return parseEndpoints, parseEndpointsList
case "endpointslices":
return parseEndpointSlice, parseEndpointSliceList
case "ingress":
return parseIngress, parseIngressList
default:
logger.Panicf("BUG: unsupported role=%q", role)
return nil, nil
}
} }

View file

@ -0,0 +1,162 @@
package kubernetes
import (
"reflect"
"testing"
)
func TestGetAPIPaths(t *testing.T) {
f := func(role string, namespaces []string, selectors []Selector, expectedPaths []string) {
t.Helper()
paths := getAPIPaths(role, namespaces, selectors)
if !reflect.DeepEqual(paths, expectedPaths) {
t.Fatalf("unexpected paths; got\n%q\nwant\n%q", paths, expectedPaths)
}
}
// role=node
f("node", nil, nil, []string{"/api/v1/nodes"})
f("node", []string{"foo", "bar"}, nil, []string{"/api/v1/nodes"})
f("node", nil, []Selector{
{
Role: "pod",
Label: "foo",
Field: "bar",
},
}, []string{"/api/v1/nodes"})
f("node", nil, []Selector{
{
Role: "node",
Label: "foo",
Field: "bar",
},
}, []string{"/api/v1/nodes?labelSelector=foo&fieldSelector=bar"})
f("node", []string{"x", "y"}, []Selector{
{
Role: "node",
Label: "foo",
Field: "bar",
},
}, []string{"/api/v1/nodes?labelSelector=foo&fieldSelector=bar"})
// role=pod
f("pod", nil, nil, []string{"/api/v1/pods"})
f("pod", []string{"foo", "bar"}, nil, []string{
"/api/v1/namespaces/foo/pods",
"/api/v1/namespaces/bar/pods",
})
f("pod", nil, []Selector{
{
Role: "node",
Label: "foo",
},
}, []string{"/api/v1/pods"})
f("pod", nil, []Selector{
{
Role: "pod",
Label: "foo",
},
{
Role: "pod",
Label: "x",
Field: "y",
},
}, []string{"/api/v1/pods?labelSelector=foo%2Cx&fieldSelector=y"})
f("pod", []string{"x", "y"}, []Selector{
{
Role: "pod",
Label: "foo",
},
{
Role: "pod",
Label: "x",
Field: "y",
},
}, []string{
"/api/v1/namespaces/x/pods?labelSelector=foo%2Cx&fieldSelector=y",
"/api/v1/namespaces/y/pods?labelSelector=foo%2Cx&fieldSelector=y",
})
// role=service
f("service", nil, nil, []string{"/api/v1/services"})
f("service", []string{"x", "y"}, nil, []string{
"/api/v1/namespaces/x/services",
"/api/v1/namespaces/y/services",
})
f("service", nil, []Selector{
{
Role: "node",
Label: "foo",
},
{
Role: "service",
Field: "bar",
},
}, []string{"/api/v1/services?fieldSelector=bar"})
f("service", []string{"x", "y"}, []Selector{
{
Role: "service",
Label: "abc=de",
},
}, []string{
"/api/v1/namespaces/x/services?labelSelector=abc%3Dde",
"/api/v1/namespaces/y/services?labelSelector=abc%3Dde",
})
// role=endpoints
f("endpoints", nil, nil, []string{"/api/v1/endpoints"})
f("endpoints", []string{"x", "y"}, nil, []string{
"/api/v1/namespaces/x/endpoints",
"/api/v1/namespaces/y/endpoints",
})
f("endpoints", []string{"x", "y"}, []Selector{
{
Role: "endpoints",
Label: "bbb",
},
{
Role: "node",
Label: "aa",
},
}, []string{
"/api/v1/namespaces/x/endpoints?labelSelector=bbb",
"/api/v1/namespaces/y/endpoints?labelSelector=bbb",
})
// role=endpointslices
f("endpointslices", nil, nil, []string{"/apis/discovery.k8s.io/v1beta1/endpointslices"})
f("endpointslices", []string{"x", "y"}, []Selector{
{
Role: "endpointslices",
Field: "field",
Label: "label",
},
}, []string{
"/apis/discovery.k8s.io/v1beta1/namespaces/x/endpointslices?labelSelector=label&fieldSelector=field",
"/apis/discovery.k8s.io/v1beta1/namespaces/y/endpointslices?labelSelector=label&fieldSelector=field",
})
// role=ingress
f("ingress", nil, nil, []string{"/api/v1/ingresses"})
f("ingress", []string{"x", "y"}, []Selector{
{
Role: "node",
Field: "xyay",
},
{
Role: "ingress",
Field: "abc",
},
{
Role: "ingress",
Label: "cde",
},
{
Role: "ingress",
Label: "baaa",
},
}, []string{
"/api/v1/namespaces/x/ingresses?labelSelector=cde%2Cbaaa&fieldSelector=abc",
"/api/v1/namespaces/y/ingresses?labelSelector=cde%2Cbaaa&fieldSelector=abc",
})
}

View file

@ -1,9 +1,6 @@
package kubernetes package kubernetes
import ( import (
"net/url"
"strings"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
) )
@ -19,6 +16,16 @@ type ObjectMeta struct {
OwnerReferences []OwnerReference OwnerReferences []OwnerReference
} }
func (om *ObjectMeta) key() string {
return om.Namespace + "/" + om.Name
}
// ListMeta is a Kubernetes list metadata
// https://v1-17.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#listmeta-v1-meta
type ListMeta struct {
ResourceVersion string
}
func (om *ObjectMeta) registerLabelsAndAnnotations(prefix string, m map[string]string) { func (om *ObjectMeta) registerLabelsAndAnnotations(prefix string, m map[string]string) {
for _, lb := range om.Labels { for _, lb := range om.Labels {
ln := discoveryutils.SanitizeLabelName(lb.Name) ln := discoveryutils.SanitizeLabelName(lb.Name)
@ -47,29 +54,3 @@ type OwnerReference struct {
type DaemonEndpoint struct { type DaemonEndpoint struct {
Port int Port int
} }
func joinSelectors(role string, namespaces []string, selectors []Selector) string {
var labelSelectors, fieldSelectors []string
for _, ns := range namespaces {
fieldSelectors = append(fieldSelectors, "metadata.namespace="+ns)
}
for _, s := range selectors {
if s.Role != role {
continue
}
if s.Label != "" {
labelSelectors = append(labelSelectors, s.Label)
}
if s.Field != "" {
fieldSelectors = append(fieldSelectors, s.Field)
}
}
var args []string
if len(labelSelectors) > 0 {
args = append(args, "labelSelector="+url.QueryEscape(strings.Join(labelSelectors, ",")))
}
if len(fieldSelectors) > 0 {
args = append(args, "fieldSelector="+url.QueryEscape(strings.Join(fieldSelectors, ",")))
}
return strings.Join(args, "&")
}

View file

@ -7,66 +7,36 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
) )
// getEndpointsLabels returns labels for k8s endpoints obtained from the given cfg. func (eps *Endpoints) key() string {
func getEndpointsLabels(cfg *apiConfig) ([]map[string]string, error) { return eps.Metadata.key()
eps, err := getEndpoints(cfg)
if err != nil {
return nil, err
}
pods, err := getPods(cfg)
if err != nil {
return nil, err
}
svcs, err := getServices(cfg)
if err != nil {
return nil, err
}
var ms []map[string]string
for _, ep := range eps {
ms = ep.appendTargetLabels(ms, pods, svcs)
}
return ms, nil
} }
func getEndpoints(cfg *apiConfig) ([]Endpoints, error) { func parseEndpointsList(data []byte) (map[string]object, ListMeta, error) {
if len(cfg.namespaces) == 0 { var epsl EndpointsList
return getEndpointsByPath(cfg, "/api/v1/endpoints") if err := json.Unmarshal(data, &epsl); err != nil {
return nil, epsl.Metadata, fmt.Errorf("cannot unmarshal EndpointsList from %q: %w", data, err)
} }
// Query /api/v1/namespaces/* for each namespace. objectsByKey := make(map[string]object)
// This fixes authorization issue at https://github.com/VictoriaMetrics/VictoriaMetrics/issues/432 for _, eps := range epsl.Items {
cfgCopy := *cfg objectsByKey[eps.key()] = eps
namespaces := cfgCopy.namespaces
cfgCopy.namespaces = nil
cfg = &cfgCopy
var result []Endpoints
for _, ns := range namespaces {
path := fmt.Sprintf("/api/v1/namespaces/%s/endpoints", ns)
eps, err := getEndpointsByPath(cfg, path)
if err != nil {
return nil, err
}
result = append(result, eps...)
} }
return result, nil return objectsByKey, epsl.Metadata, nil
} }
func getEndpointsByPath(cfg *apiConfig, path string) ([]Endpoints, error) { func parseEndpoints(data []byte) (object, error) {
data, err := getAPIResponse(cfg, "endpoints", path) var eps Endpoints
if err != nil { if err := json.Unmarshal(data, &eps); err != nil {
return nil, fmt.Errorf("cannot obtain endpoints data from API server: %w", err) return nil, err
} }
epl, err := parseEndpointsList(data) return &eps, nil
if err != nil {
return nil, fmt.Errorf("cannot parse endpoints response from API server: %w", err)
}
return epl.Items, nil
} }
// EndpointsList implements k8s endpoints list. // EndpointsList implements k8s endpoints list.
// //
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#endpointslist-v1-core // See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#endpointslist-v1-core
type EndpointsList struct { type EndpointsList struct {
Items []Endpoints Metadata ListMeta
Items []*Endpoints
} }
// Endpoints implements k8s endpoints. // Endpoints implements k8s endpoints.
@ -115,25 +85,20 @@ type EndpointPort struct {
Protocol string Protocol string
} }
// parseEndpointsList parses EndpointsList from data. // getTargetLabels returns labels for each endpoint in eps.
func parseEndpointsList(data []byte) (*EndpointsList, error) {
var esl EndpointsList
if err := json.Unmarshal(data, &esl); err != nil {
return nil, fmt.Errorf("cannot unmarshal EndpointsList from %q: %w", data, err)
}
return &esl, nil
}
// appendTargetLabels appends labels for each endpoint in eps to ms and returns the result.
// //
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#endpoints // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#endpoints
func (eps *Endpoints) appendTargetLabels(ms []map[string]string, pods []Pod, svcs []Service) []map[string]string { func (eps *Endpoints) getTargetLabels(aw *apiWatcher) []map[string]string {
svc := getService(svcs, eps.Metadata.Namespace, eps.Metadata.Name) var svc *Service
if o := aw.getObjectByRole("service", eps.Metadata.Namespace, eps.Metadata.Name); o != nil {
svc = o.(*Service)
}
podPortsSeen := make(map[*Pod][]int) podPortsSeen := make(map[*Pod][]int)
var ms []map[string]string
for _, ess := range eps.Subsets { for _, ess := range eps.Subsets {
for _, epp := range ess.Ports { for _, epp := range ess.Ports {
ms = appendEndpointLabelsForAddresses(ms, podPortsSeen, eps, ess.Addresses, epp, pods, svc, "true") ms = appendEndpointLabelsForAddresses(ms, aw, podPortsSeen, eps, ess.Addresses, epp, svc, "true")
ms = appendEndpointLabelsForAddresses(ms, podPortsSeen, eps, ess.NotReadyAddresses, epp, pods, svc, "false") ms = appendEndpointLabelsForAddresses(ms, aw, podPortsSeen, eps, ess.NotReadyAddresses, epp, svc, "false")
} }
} }
@ -168,10 +133,13 @@ func (eps *Endpoints) appendTargetLabels(ms []map[string]string, pods []Pod, svc
return ms return ms
} }
func appendEndpointLabelsForAddresses(ms []map[string]string, podPortsSeen map[*Pod][]int, eps *Endpoints, eas []EndpointAddress, epp EndpointPort, func appendEndpointLabelsForAddresses(ms []map[string]string, aw *apiWatcher, podPortsSeen map[*Pod][]int, eps *Endpoints,
pods []Pod, svc *Service, ready string) []map[string]string { eas []EndpointAddress, epp EndpointPort, svc *Service, ready string) []map[string]string {
for _, ea := range eas { for _, ea := range eas {
p := getPod(pods, ea.TargetRef.Namespace, ea.TargetRef.Name) var p *Pod
if o := aw.getObjectByRole("pod", ea.TargetRef.Namespace, ea.TargetRef.Name); o != nil {
p = o.(*Pod)
}
m := getEndpointLabelsForAddressAndPort(podPortsSeen, eps, ea, epp, p, svc, ready) m := getEndpointLabelsForAddressAndPort(podPortsSeen, eps, ea, epp, p, svc, ready)
ms = append(ms, m) ms = append(ms, m)
} }

View file

@ -11,12 +11,12 @@ import (
func TestParseEndpointsListFailure(t *testing.T) { func TestParseEndpointsListFailure(t *testing.T) {
f := func(s string) { f := func(s string) {
t.Helper() t.Helper()
els, err := parseEndpointsList([]byte(s)) objectsByKey, _, err := parseEndpointsList([]byte(s))
if err == nil { if err == nil {
t.Fatalf("expecting non-nil error") t.Fatalf("expecting non-nil error")
} }
if els != nil { if len(objectsByKey) != 0 {
t.Fatalf("unexpected non-nil EnpointsList: %v", els) t.Fatalf("unexpected non-empty objectsByKey: %v", objectsByKey)
} }
} }
f(``) f(``)
@ -79,21 +79,16 @@ func TestParseEndpointsListSuccess(t *testing.T) {
] ]
} }
` `
els, err := parseEndpointsList([]byte(data)) objectsByKey, meta, err := parseEndpointsList([]byte(data))
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
if len(els.Items) != 1 { expectedResourceVersion := "128055"
t.Fatalf("unexpected length of EndpointsList.Items; got %d; want %d", len(els.Items), 1) if meta.ResourceVersion != expectedResourceVersion {
t.Fatalf("unexpected resource version; got %s; want %s", meta.ResourceVersion, expectedResourceVersion)
} }
endpoint := els.Items[0]
// Check endpoint.appendTargetLabels() sortedLabelss := getSortedLabelss(objectsByKey)
labelss := endpoint.appendTargetLabels(nil, nil, nil)
var sortedLabelss [][]prompbmarshal.Label
for _, labels := range labelss {
sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels))
}
expectedLabelss := [][]prompbmarshal.Label{ expectedLabelss := [][]prompbmarshal.Label{
discoveryutils.GetSortedLabels(map[string]string{ discoveryutils.GetSortedLabels(map[string]string{
"__address__": "172.17.0.2:8443", "__address__": "172.17.0.2:8443",

View file

@ -8,85 +8,48 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
) )
// getEndpointSlicesLabels returns labels for k8s endpointSlices obtained from the given cfg. func (eps *EndpointSlice) key() string {
func getEndpointSlicesLabels(cfg *apiConfig) ([]map[string]string, error) { return eps.Metadata.key()
eps, err := getEndpointSlices(cfg) }
if err != nil {
func parseEndpointSliceList(data []byte) (map[string]object, ListMeta, error) {
var epsl EndpointSliceList
if err := json.Unmarshal(data, &epsl); err != nil {
return nil, epsl.Metadata, fmt.Errorf("cannot unmarshal EndpointSliceList from %q: %w", data, err)
}
objectsByKey := make(map[string]object)
for _, eps := range epsl.Items {
objectsByKey[eps.key()] = eps
}
return objectsByKey, epsl.Metadata, nil
}
func parseEndpointSlice(data []byte) (object, error) {
var eps EndpointSlice
if err := json.Unmarshal(data, &eps); err != nil {
return nil, err return nil, err
} }
pods, err := getPods(cfg) return &eps, nil
if err != nil {
return nil, err
}
svcs, err := getServices(cfg)
if err != nil {
return nil, err
}
var ms []map[string]string
for _, ep := range eps {
ms = ep.appendTargetLabels(ms, pods, svcs)
}
return ms, nil
} }
// getEndpointSlices retrieves endpointSlice with given apiConfig // getTargetLabels returns labels for eps.
func getEndpointSlices(cfg *apiConfig) ([]EndpointSlice, error) { //
if len(cfg.namespaces) == 0 { // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#endpointslices
return getEndpointSlicesByPath(cfg, "/apis/discovery.k8s.io/v1beta1/endpointslices") func (eps *EndpointSlice) getTargetLabels(aw *apiWatcher) []map[string]string {
var svc *Service
if o := aw.getObjectByRole("service", eps.Metadata.Namespace, eps.Metadata.Name); o != nil {
svc = o.(*Service)
} }
// Query /api/v1/namespaces/* for each namespace.
// This fixes authorization issue at https://github.com/VictoriaMetrics/VictoriaMetrics/issues/432
cfgCopy := *cfg
namespaces := cfgCopy.namespaces
cfgCopy.namespaces = nil
cfg = &cfgCopy
var result []EndpointSlice
for _, ns := range namespaces {
path := fmt.Sprintf("/apis/discovery.k8s.io/v1beta1/namespaces/%s/endpointslices", ns)
eps, err := getEndpointSlicesByPath(cfg, path)
if err != nil {
return nil, err
}
result = append(result, eps...)
}
return result, nil
}
// getEndpointSlicesByPath retrieves endpointSlices from k8s api by given path
func getEndpointSlicesByPath(cfg *apiConfig, path string) ([]EndpointSlice, error) {
data, err := getAPIResponse(cfg, "endpointslices", path)
if err != nil {
return nil, fmt.Errorf("cannot obtain endpointslices data from API server: %w", err)
}
epl, err := parseEndpointSlicesList(data)
if err != nil {
return nil, fmt.Errorf("cannot parse endpointslices response from API server: %w", err)
}
return epl.Items, nil
}
// parseEndpointsList parses EndpointSliceList from data.
func parseEndpointSlicesList(data []byte) (*EndpointSliceList, error) {
var esl EndpointSliceList
if err := json.Unmarshal(data, &esl); err != nil {
return nil, fmt.Errorf("cannot unmarshal EndpointSliceList from %q: %w", data, err)
}
return &esl, nil
}
// appendTargetLabels injects labels for endPointSlice to slice map
// follows TargetRef for enrich labels with pod and service metadata
func (eps *EndpointSlice) appendTargetLabels(ms []map[string]string, pods []Pod, svcs []Service) []map[string]string {
svc := getService(svcs, eps.Metadata.Namespace, eps.Metadata.Name)
podPortsSeen := make(map[*Pod][]int) podPortsSeen := make(map[*Pod][]int)
var ms []map[string]string
for _, ess := range eps.Endpoints { for _, ess := range eps.Endpoints {
pod := getPod(pods, ess.TargetRef.Namespace, ess.TargetRef.Name) var p *Pod
if o := aw.getObjectByRole("pod", ess.TargetRef.Namespace, ess.TargetRef.Name); o != nil {
p = o.(*Pod)
}
for _, epp := range eps.Ports { for _, epp := range eps.Ports {
for _, addr := range ess.Addresses { for _, addr := range ess.Addresses {
ms = append(ms, getEndpointSliceLabelsForAddressAndPort(podPortsSeen, addr, eps, ess, epp, pod, svc)) ms = append(ms, getEndpointSliceLabelsForAddressAndPort(podPortsSeen, addr, eps, ess, epp, p, svc))
} }
} }
@ -121,13 +84,12 @@ func (eps *EndpointSlice) appendTargetLabels(ms []map[string]string, pods []Pod,
} }
} }
return ms return ms
} }
// getEndpointSliceLabelsForAddressAndPort gets labels for endpointSlice // getEndpointSliceLabelsForAddressAndPort gets labels for endpointSlice
// from address, Endpoint and EndpointPort // from address, Endpoint and EndpointPort
// enriches labels with TargetRef // enriches labels with TargetRef
// pod appended to seen Ports // p appended to seen Ports
// if TargetRef matches // if TargetRef matches
func getEndpointSliceLabelsForAddressAndPort(podPortsSeen map[*Pod][]int, addr string, eps *EndpointSlice, ea Endpoint, epp EndpointPort, p *Pod, svc *Service) map[string]string { func getEndpointSliceLabelsForAddressAndPort(podPortsSeen map[*Pod][]int, addr string, eps *EndpointSlice, ea Endpoint, epp EndpointPort, p *Pod, svc *Service) map[string]string {
m := getEndpointSliceLabels(eps, addr, ea, epp) m := getEndpointSliceLabels(eps, addr, ea, epp)
@ -186,7 +148,8 @@ func getEndpointSliceLabels(eps *EndpointSlice, addr string, ea Endpoint, epp En
// that groups service endpoints slices. // that groups service endpoints slices.
// https://v1-17.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#endpointslice-v1beta1-discovery-k8s-io // https://v1-17.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#endpointslice-v1beta1-discovery-k8s-io
type EndpointSliceList struct { type EndpointSliceList struct {
Items []EndpointSlice Metadata ListMeta
Items []*EndpointSlice
} }
// EndpointSlice - implements kubernetes endpoint slice. // EndpointSlice - implements kubernetes endpoint slice.

View file

@ -8,14 +8,14 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
) )
func Test_parseEndpointSlicesListFail(t *testing.T) { func TestParseEndpointSliceListFail(t *testing.T) {
f := func(data string) { f := func(data string) {
eslList, err := parseEndpointSlicesList([]byte(data)) objectsByKey, _, err := parseEndpointSliceList([]byte(data))
if err == nil { if err == nil {
t.Errorf("unexpected result, test must fail! data: %s", data) t.Errorf("unexpected result, test must fail! data: %s", data)
} }
if eslList != nil { if len(objectsByKey) != 0 {
t.Errorf("endpointSliceList must be nil, got: %v", eslList) t.Errorf("EndpointSliceList must be emptry, got: %v", objectsByKey)
} }
} }
@ -28,7 +28,7 @@ func Test_parseEndpointSlicesListFail(t *testing.T) {
} }
func Test_parseEndpointSlicesListSuccess(t *testing.T) { func TestParseEndpointSliceListSuccess(t *testing.T) {
data := `{ data := `{
"kind": "EndpointSliceList", "kind": "EndpointSliceList",
"apiVersion": "discovery.k8s.io/v1beta1", "apiVersion": "discovery.k8s.io/v1beta1",
@ -176,22 +176,17 @@ func Test_parseEndpointSlicesListSuccess(t *testing.T) {
} }
] ]
}` }`
esl, err := parseEndpointSlicesList([]byte(data)) objectsByKey, meta, err := parseEndpointSliceList([]byte(data))
if err != nil { if err != nil {
t.Errorf("cannot parse data for EndpointSliceList: %v", err) t.Errorf("cannot parse data for EndpointSliceList: %v", err)
return return
} }
if len(esl.Items) != 2 { expectedResourceVersion := "1177"
t.Fatalf("expected 2 items at endpointSliceList, got: %d", len(esl.Items)) if meta.ResourceVersion != expectedResourceVersion {
t.Fatalf("unexpected resource version; got %s; want %s", meta.ResourceVersion, expectedResourceVersion)
} }
sortedLabelss := getSortedLabelss(objectsByKey)
firstEsl := esl.Items[0] expectedLabelss := [][]prompbmarshal.Label{
got := firstEsl.appendTargetLabels(nil, nil, nil)
sortedLables := [][]prompbmarshal.Label{}
for _, labels := range got {
sortedLables = append(sortedLables, discoveryutils.GetSortedLabels(labels))
}
expectedLabels := [][]prompbmarshal.Label{
discoveryutils.GetSortedLabels(map[string]string{ discoveryutils.GetSortedLabels(map[string]string{
"__address__": "172.18.0.2:6443", "__address__": "172.18.0.2:6443",
"__meta_kubernetes_endpointslice_address_type": "IPv4", "__meta_kubernetes_endpointslice_address_type": "IPv4",
@ -201,253 +196,94 @@ func Test_parseEndpointSlicesListSuccess(t *testing.T) {
"__meta_kubernetes_endpointslice_port_name": "https", "__meta_kubernetes_endpointslice_port_name": "https",
"__meta_kubernetes_endpointslice_port_protocol": "TCP", "__meta_kubernetes_endpointslice_port_protocol": "TCP",
"__meta_kubernetes_namespace": "default", "__meta_kubernetes_namespace": "default",
})} }),
if !reflect.DeepEqual(sortedLables, expectedLabels) { discoveryutils.GetSortedLabels(map[string]string{
t.Fatalf("unexpected labels,\ngot:\n%v,\nwant:\n%v", sortedLables, expectedLabels) "__address__": "10.244.0.3:53",
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
"__meta_kubernetes_endpointslice_address_target_name": "coredns-66bff467f8-z8czk",
"__meta_kubernetes_endpointslice_address_type": "IPv4",
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_io_hostname": "kind-control-plane",
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_io_hostname": "true",
"__meta_kubernetes_endpointslice_name": "kube-dns-22mvb",
"__meta_kubernetes_endpointslice_port": "53",
"__meta_kubernetes_endpointslice_port_name": "dns-tcp",
"__meta_kubernetes_endpointslice_port_protocol": "TCP",
"__meta_kubernetes_namespace": "kube-system",
}),
discoveryutils.GetSortedLabels(map[string]string{
"__address__": "10.244.0.3:9153",
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
"__meta_kubernetes_endpointslice_address_target_name": "coredns-66bff467f8-z8czk",
"__meta_kubernetes_endpointslice_address_type": "IPv4",
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_io_hostname": "kind-control-plane",
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_io_hostname": "true",
"__meta_kubernetes_endpointslice_name": "kube-dns-22mvb",
"__meta_kubernetes_endpointslice_port": "9153",
"__meta_kubernetes_endpointslice_port_name": "metrics",
"__meta_kubernetes_endpointslice_port_protocol": "TCP",
"__meta_kubernetes_namespace": "kube-system",
}),
discoveryutils.GetSortedLabels(map[string]string{
"__address__": "10.244.0.3:53",
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
"__meta_kubernetes_endpointslice_address_target_name": "coredns-66bff467f8-z8czk",
"__meta_kubernetes_endpointslice_address_type": "IPv4",
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_io_hostname": "kind-control-plane",
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_io_hostname": "true",
"__meta_kubernetes_endpointslice_name": "kube-dns-22mvb",
"__meta_kubernetes_endpointslice_port": "53",
"__meta_kubernetes_endpointslice_port_name": "dns",
"__meta_kubernetes_endpointslice_port_protocol": "UDP",
"__meta_kubernetes_namespace": "kube-system",
}),
discoveryutils.GetSortedLabels(map[string]string{
"__address__": "10.244.0.4:53",
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
"__meta_kubernetes_endpointslice_address_target_name": "coredns-66bff467f8-kpbhk",
"__meta_kubernetes_endpointslice_address_type": "IPv4",
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_io_hostname": "kind-control-plane",
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_io_hostname": "true",
"__meta_kubernetes_endpointslice_name": "kube-dns-22mvb",
"__meta_kubernetes_endpointslice_port": "53",
"__meta_kubernetes_endpointslice_port_name": "dns-tcp",
"__meta_kubernetes_endpointslice_port_protocol": "TCP",
"__meta_kubernetes_namespace": "kube-system",
}),
discoveryutils.GetSortedLabels(map[string]string{
"__address__": "10.244.0.4:9153",
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
"__meta_kubernetes_endpointslice_address_target_name": "coredns-66bff467f8-kpbhk",
"__meta_kubernetes_endpointslice_address_type": "IPv4",
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_io_hostname": "kind-control-plane",
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_io_hostname": "true",
"__meta_kubernetes_endpointslice_name": "kube-dns-22mvb",
"__meta_kubernetes_endpointslice_port": "9153",
"__meta_kubernetes_endpointslice_port_name": "metrics",
"__meta_kubernetes_endpointslice_port_protocol": "TCP",
"__meta_kubernetes_namespace": "kube-system",
}),
discoveryutils.GetSortedLabels(map[string]string{
"__address__": "10.244.0.4:53",
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
"__meta_kubernetes_endpointslice_address_target_name": "coredns-66bff467f8-kpbhk",
"__meta_kubernetes_endpointslice_address_type": "IPv4",
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_io_hostname": "kind-control-plane",
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_io_hostname": "true",
"__meta_kubernetes_endpointslice_name": "kube-dns-22mvb",
"__meta_kubernetes_endpointslice_port": "53",
"__meta_kubernetes_endpointslice_port_name": "dns",
"__meta_kubernetes_endpointslice_port_protocol": "UDP",
"__meta_kubernetes_namespace": "kube-system",
}),
}
if !reflect.DeepEqual(sortedLabelss, expectedLabelss) {
t.Fatalf("unexpected labels,\ngot:\n%v,\nwant:\n%v", sortedLabelss, expectedLabelss)
} }
} }
func TestEndpointSlice_appendTargetLabels(t *testing.T) {
type fields struct {
Metadata ObjectMeta
Endpoints []Endpoint
AddressType string
Ports []EndpointPort
}
type args struct {
ms []map[string]string
pods []Pod
svcs []Service
}
tests := []struct {
name string
fields fields
args args
want [][]prompbmarshal.Label
}{
{
name: "simple eps",
args: args{},
fields: fields{
Metadata: ObjectMeta{
Name: "fake-esl",
Namespace: "default",
},
AddressType: "ipv4",
Endpoints: []Endpoint{
{Addresses: []string{"127.0.0.1"},
Hostname: "node-1",
Topology: map[string]string{"kubernetes.topoligy.io/zone": "gce-1"},
Conditions: EndpointConditions{Ready: true},
TargetRef: ObjectReference{
Kind: "Pod",
Namespace: "default",
Name: "main-pod",
},
},
},
Ports: []EndpointPort{
{
Name: "http",
Port: 8085,
AppProtocol: "http",
Protocol: "tcp",
},
},
},
want: [][]prompbmarshal.Label{
discoveryutils.GetSortedLabels(map[string]string{
"__address__": "127.0.0.1:8085",
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
"__meta_kubernetes_endpointslice_address_target_name": "main-pod",
"__meta_kubernetes_endpointslice_address_type": "ipv4",
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_topoligy_io_zone": "gce-1",
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_topoligy_io_zone": "true",
"__meta_kubernetes_endpointslice_endpoint_hostname": "node-1",
"__meta_kubernetes_endpointslice_name": "fake-esl",
"__meta_kubernetes_endpointslice_port": "8085",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "http",
"__meta_kubernetes_endpointslice_port_protocol": "tcp",
"__meta_kubernetes_namespace": "default",
}),
},
},
{
name: "eps with pods and services",
args: args{
pods: []Pod{
{
Metadata: ObjectMeta{
UID: "some-pod-uuid",
Namespace: "monitoring",
Name: "main-pod",
Labels: discoveryutils.GetSortedLabels(map[string]string{
"pod-label-1": "pod-value-1",
"pod-label-2": "pod-value-2",
}),
Annotations: discoveryutils.GetSortedLabels(map[string]string{
"pod-annotations-1": "annotation-value-1",
}),
},
Status: PodStatus{PodIP: "192.168.11.5", HostIP: "172.15.1.1"},
Spec: PodSpec{NodeName: "node-2", Containers: []Container{
{
Name: "container-1",
Ports: []ContainerPort{
{
ContainerPort: 8085,
Protocol: "tcp",
Name: "http",
},
{
ContainerPort: 8011,
Protocol: "udp",
Name: "dns",
},
},
},
}},
},
},
svcs: []Service{
{
Spec: ServiceSpec{Type: "ClusterIP", Ports: []ServicePort{
{
Name: "http",
Protocol: "tcp",
Port: 8085,
},
}},
Metadata: ObjectMeta{
Name: "custom-esl",
Namespace: "monitoring",
Labels: discoveryutils.GetSortedLabels(map[string]string{
"service-label-1": "value-1",
"service-label-2": "value-2",
}),
},
},
},
},
fields: fields{
Metadata: ObjectMeta{
Name: "custom-esl",
Namespace: "monitoring",
},
AddressType: "ipv4",
Endpoints: []Endpoint{
{Addresses: []string{"127.0.0.1"},
Hostname: "node-1",
Topology: map[string]string{"kubernetes.topoligy.io/zone": "gce-1"},
Conditions: EndpointConditions{Ready: true},
TargetRef: ObjectReference{
Kind: "Pod",
Namespace: "monitoring",
Name: "main-pod",
},
},
},
Ports: []EndpointPort{
{
Name: "http",
Port: 8085,
AppProtocol: "http",
Protocol: "tcp",
},
},
},
want: [][]prompbmarshal.Label{
discoveryutils.GetSortedLabels(map[string]string{
"__address__": "127.0.0.1:8085",
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
"__meta_kubernetes_endpointslice_address_target_name": "main-pod",
"__meta_kubernetes_endpointslice_address_type": "ipv4",
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_topoligy_io_zone": "gce-1",
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_topoligy_io_zone": "true",
"__meta_kubernetes_endpointslice_endpoint_hostname": "node-1",
"__meta_kubernetes_endpointslice_name": "custom-esl",
"__meta_kubernetes_endpointslice_port": "8085",
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
"__meta_kubernetes_endpointslice_port_name": "http",
"__meta_kubernetes_endpointslice_port_protocol": "tcp",
"__meta_kubernetes_namespace": "monitoring",
"__meta_kubernetes_pod_annotation_pod_annotations_1": "annotation-value-1",
"__meta_kubernetes_pod_annotationpresent_pod_annotations_1": "true",
"__meta_kubernetes_pod_container_name": "container-1",
"__meta_kubernetes_pod_container_port_name": "http",
"__meta_kubernetes_pod_container_port_number": "8085",
"__meta_kubernetes_pod_container_port_protocol": "tcp",
"__meta_kubernetes_pod_host_ip": "172.15.1.1",
"__meta_kubernetes_pod_ip": "192.168.11.5",
"__meta_kubernetes_pod_label_pod_label_1": "pod-value-1",
"__meta_kubernetes_pod_label_pod_label_2": "pod-value-2",
"__meta_kubernetes_pod_labelpresent_pod_label_1": "true",
"__meta_kubernetes_pod_labelpresent_pod_label_2": "true",
"__meta_kubernetes_pod_name": "main-pod",
"__meta_kubernetes_pod_node_name": "node-2",
"__meta_kubernetes_pod_phase": "",
"__meta_kubernetes_pod_ready": "unknown",
"__meta_kubernetes_pod_uid": "some-pod-uuid",
"__meta_kubernetes_service_cluster_ip": "",
"__meta_kubernetes_service_label_service_label_1": "value-1",
"__meta_kubernetes_service_label_service_label_2": "value-2",
"__meta_kubernetes_service_labelpresent_service_label_1": "true",
"__meta_kubernetes_service_labelpresent_service_label_2": "true",
"__meta_kubernetes_service_name": "custom-esl",
"__meta_kubernetes_service_type": "ClusterIP",
}),
discoveryutils.GetSortedLabels(map[string]string{
"__address__": "192.168.11.5:8011",
"__meta_kubernetes_namespace": "monitoring",
"__meta_kubernetes_pod_annotation_pod_annotations_1": "annotation-value-1",
"__meta_kubernetes_pod_annotationpresent_pod_annotations_1": "true",
"__meta_kubernetes_pod_container_name": "container-1",
"__meta_kubernetes_pod_container_port_name": "dns",
"__meta_kubernetes_pod_container_port_number": "8011",
"__meta_kubernetes_pod_container_port_protocol": "udp",
"__meta_kubernetes_pod_host_ip": "172.15.1.1",
"__meta_kubernetes_pod_ip": "192.168.11.5",
"__meta_kubernetes_pod_label_pod_label_1": "pod-value-1",
"__meta_kubernetes_pod_label_pod_label_2": "pod-value-2",
"__meta_kubernetes_pod_labelpresent_pod_label_1": "true",
"__meta_kubernetes_pod_labelpresent_pod_label_2": "true",
"__meta_kubernetes_pod_name": "main-pod",
"__meta_kubernetes_pod_node_name": "node-2",
"__meta_kubernetes_pod_phase": "",
"__meta_kubernetes_pod_ready": "unknown",
"__meta_kubernetes_pod_uid": "some-pod-uuid",
"__meta_kubernetes_service_cluster_ip": "",
"__meta_kubernetes_service_label_service_label_1": "value-1",
"__meta_kubernetes_service_label_service_label_2": "value-2",
"__meta_kubernetes_service_labelpresent_service_label_1": "true",
"__meta_kubernetes_service_labelpresent_service_label_2": "true",
"__meta_kubernetes_service_name": "custom-esl",
"__meta_kubernetes_service_type": "ClusterIP",
}),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
eps := &EndpointSlice{
Metadata: tt.fields.Metadata,
Endpoints: tt.fields.Endpoints,
AddressType: tt.fields.AddressType,
Ports: tt.fields.Ports,
}
got := eps.appendTargetLabels(tt.args.ms, tt.args.pods, tt.args.svcs)
var sortedLabelss [][]prompbmarshal.Label
for _, labels := range got {
sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels))
}
if !reflect.DeepEqual(sortedLabelss, tt.want) {
t.Errorf("got unxpected labels: \ngot:\n %v, \nexpect:\n %v", sortedLabelss, tt.want)
}
})
}
}

View file

@ -5,58 +5,36 @@ import (
"fmt" "fmt"
) )
// getIngressesLabels returns labels for k8s ingresses obtained from the given cfg. func (ig *Ingress) key() string {
func getIngressesLabels(cfg *apiConfig) ([]map[string]string, error) { return ig.Metadata.key()
igs, err := getIngresses(cfg) }
if err != nil {
func parseIngressList(data []byte) (map[string]object, ListMeta, error) {
var igl IngressList
if err := json.Unmarshal(data, &igl); err != nil {
return nil, igl.Metadata, fmt.Errorf("cannot unmarshal IngressList from %q: %w", data, err)
}
objectsByKey := make(map[string]object)
for _, ig := range igl.Items {
objectsByKey[ig.key()] = ig
}
return objectsByKey, igl.Metadata, nil
}
func parseIngress(data []byte) (object, error) {
var ig Ingress
if err := json.Unmarshal(data, &ig); err != nil {
return nil, err return nil, err
} }
var ms []map[string]string return &ig, nil
for _, ig := range igs {
ms = ig.appendTargetLabels(ms)
}
return ms, nil
}
func getIngresses(cfg *apiConfig) ([]Ingress, error) {
if len(cfg.namespaces) == 0 {
return getIngressesByPath(cfg, "/apis/extensions/v1beta1/ingresses")
}
// Query /api/v1/namespaces/* for each namespace.
// This fixes authorization issue at https://github.com/VictoriaMetrics/VictoriaMetrics/issues/432
cfgCopy := *cfg
namespaces := cfgCopy.namespaces
cfgCopy.namespaces = nil
cfg = &cfgCopy
var result []Ingress
for _, ns := range namespaces {
path := fmt.Sprintf("/apis/extensions/v1beta1/namespaces/%s/ingresses", ns)
igs, err := getIngressesByPath(cfg, path)
if err != nil {
return nil, err
}
result = append(result, igs...)
}
return result, nil
}
func getIngressesByPath(cfg *apiConfig, path string) ([]Ingress, error) {
data, err := getAPIResponse(cfg, "ingress", path)
if err != nil {
return nil, fmt.Errorf("cannot obtain ingresses data from API server: %w", err)
}
igl, err := parseIngressList(data)
if err != nil {
return nil, fmt.Errorf("cannot parse ingresses response from API server: %w", err)
}
return igl.Items, nil
} }
// IngressList represents ingress list in k8s. // IngressList represents ingress list in k8s.
// //
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#ingresslist-v1beta1-extensions // See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#ingresslist-v1beta1-extensions
type IngressList struct { type IngressList struct {
Items []Ingress Metadata ListMeta
Items []*Ingress
} }
// Ingress represents ingress in k8s. // Ingress represents ingress in k8s.
@ -104,25 +82,17 @@ type HTTPIngressPath struct {
Path string Path string
} }
// parseIngressList parses IngressList from data. // getTargetLabels returns labels for ig.
func parseIngressList(data []byte) (*IngressList, error) {
var il IngressList
if err := json.Unmarshal(data, &il); err != nil {
return nil, fmt.Errorf("cannot unmarshal IngressList from %q: %w", data, err)
}
return &il, nil
}
// appendTargetLabels appends labels for Ingress ig to ms and returns the result.
// //
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#ingress // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#ingress
func (ig *Ingress) appendTargetLabels(ms []map[string]string) []map[string]string { func (ig *Ingress) getTargetLabels(aw *apiWatcher) []map[string]string {
tlsHosts := make(map[string]bool) tlsHosts := make(map[string]bool)
for _, tls := range ig.Spec.TLS { for _, tls := range ig.Spec.TLS {
for _, host := range tls.Hosts { for _, host := range tls.Hosts {
tlsHosts[host] = true tlsHosts[host] = true
} }
} }
var ms []map[string]string
for _, r := range ig.Spec.Rules { for _, r := range ig.Spec.Rules {
paths := getIngressRulePaths(r.HTTP.Paths) paths := getIngressRulePaths(r.HTTP.Paths)
scheme := "http" scheme := "http"

View file

@ -11,12 +11,12 @@ import (
func TestParseIngressListFailure(t *testing.T) { func TestParseIngressListFailure(t *testing.T) {
f := func(s string) { f := func(s string) {
t.Helper() t.Helper()
nls, err := parseIngressList([]byte(s)) objectsByKey, _, err := parseIngressList([]byte(s))
if err == nil { if err == nil {
t.Fatalf("expecting non-nil error") t.Fatalf("expecting non-nil error")
} }
if nls != nil { if len(objectsByKey) != 0 {
t.Fatalf("unexpected non-nil IngressList: %v", nls) t.Fatalf("unexpected non-empty IngressList: %v", objectsByKey)
} }
} }
f(``) f(``)
@ -71,21 +71,15 @@ func TestParseIngressListSuccess(t *testing.T) {
} }
] ]
}` }`
igs, err := parseIngressList([]byte(data)) objectsByKey, meta, err := parseIngressList([]byte(data))
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
if len(igs.Items) != 1 { expectedResourceVersion := "351452"
t.Fatalf("unexpected length of IngressList.Items; got %d; want %d", len(igs.Items), 1) if meta.ResourceVersion != expectedResourceVersion {
} t.Fatalf("unexpected resource version; got %s; want %s", meta.ResourceVersion, expectedResourceVersion)
ig := igs.Items[0]
// Check ig.appendTargetLabels()
labelss := ig.appendTargetLabels(nil)
var sortedLabelss [][]prompbmarshal.Label
for _, labels := range labelss {
sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels))
} }
sortedLabelss := getSortedLabelss(objectsByKey)
expectedLabelss := [][]prompbmarshal.Label{ expectedLabelss := [][]prompbmarshal.Label{
discoveryutils.GetSortedLabels(map[string]string{ discoveryutils.GetSortedLabels(map[string]string{
"__address__": "foobar", "__address__": "foobar",

View file

@ -38,25 +38,15 @@ type Selector struct {
} }
// GetLabels returns labels for the given sdc and baseDir. // GetLabels returns labels for the given sdc and baseDir.
func GetLabels(sdc *SDConfig, baseDir string) ([]map[string]string, error) { func (sdc *SDConfig) GetLabels(baseDir string) ([]map[string]string, error) {
cfg, err := getAPIConfig(sdc, baseDir) cfg, err := getAPIConfig(sdc, baseDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot create API config: %w", err) return nil, fmt.Errorf("cannot create API config: %w", err)
} }
switch sdc.Role { switch sdc.Role {
case "node": case "node", "pod", "service", "endpoints", "endpointslices", "ingress":
return getNodesLabels(cfg) return cfg.aw.getLabelsForRole(sdc.Role), nil
case "service":
return getServicesLabels(cfg)
case "pod":
return getPodsLabels(cfg)
case "endpoints":
return getEndpointsLabels(cfg)
case "endpointslices":
return getEndpointSlicesLabels(cfg)
case "ingress":
return getIngressesLabels(cfg)
default: default:
return nil, fmt.Errorf("unexpected `role`: %q; must be one of `node`, `service`, `pod`, `endpoints` or `ingress`; skipping it", sdc.Role) return nil, fmt.Errorf("unexpected `role`: %q; must be one of `node`, `pod`, `service`, `endpoints`, `endpointslices` or `ingress`; skipping it", sdc.Role)
} }
} }

View file

@ -7,29 +7,37 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
) )
// getNodesLabels returns labels for k8s nodes obtained from the given cfg. // getNodesLabels returns labels for k8s nodes obtained from the given cfg
func getNodesLabels(cfg *apiConfig) ([]map[string]string, error) { func (n *Node) key() string {
data, err := getAPIResponse(cfg, "node", "/api/v1/nodes") return n.Metadata.key()
if err != nil { }
return nil, fmt.Errorf("cannot obtain nodes data from API server: %w", err)
func parseNodeList(data []byte) (map[string]object, ListMeta, error) {
var nl NodeList
if err := json.Unmarshal(data, &nl); err != nil {
return nil, nl.Metadata, fmt.Errorf("cannot unmarshal NodeList from %q: %w", data, err)
} }
nl, err := parseNodeList(data) objectsByKey := make(map[string]object)
if err != nil {
return nil, fmt.Errorf("cannot parse nodes response from API server: %w", err)
}
var ms []map[string]string
for _, n := range nl.Items { for _, n := range nl.Items {
// Do not apply namespaces, since they are missing in nodes. objectsByKey[n.key()] = n
ms = n.appendTargetLabels(ms)
} }
return ms, nil return objectsByKey, nl.Metadata, nil
}
func parseNode(data []byte) (object, error) {
var n Node
if err := json.Unmarshal(data, &n); err != nil {
return nil, err
}
return &n, nil
} }
// NodeList represents NodeList from k8s API. // NodeList represents NodeList from k8s API.
// //
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#nodelist-v1-core // See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#nodelist-v1-core
type NodeList struct { type NodeList struct {
Items []Node Metadata ListMeta
Items []*Node
} }
// Node represents Node from k8s API. // Node represents Node from k8s API.
@ -63,23 +71,14 @@ type NodeDaemonEndpoints struct {
KubeletEndpoint DaemonEndpoint KubeletEndpoint DaemonEndpoint
} }
// parseNodeList parses NodeList from data. // getTargetLabels returs labels for the given n.
func parseNodeList(data []byte) (*NodeList, error) {
var nl NodeList
if err := json.Unmarshal(data, &nl); err != nil {
return nil, fmt.Errorf("cannot unmarshal NodeList from %q: %w", data, err)
}
return &nl, nil
}
// appendTargetLabels appends labels for the given Node n to ms and returns the result.
// //
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#node // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#node
func (n *Node) appendTargetLabels(ms []map[string]string) []map[string]string { func (n *Node) getTargetLabels(aw *apiWatcher) []map[string]string {
addr := getNodeAddr(n.Status.Addresses) addr := getNodeAddr(n.Status.Addresses)
if len(addr) == 0 { if len(addr) == 0 {
// Skip node without address // Skip node without address
return ms return nil
} }
addr = discoveryutils.JoinHostPort(addr, n.Status.DaemonEndpoints.KubeletEndpoint.Port) addr = discoveryutils.JoinHostPort(addr, n.Status.DaemonEndpoints.KubeletEndpoint.Port)
m := map[string]string{ m := map[string]string{
@ -97,8 +96,7 @@ func (n *Node) appendTargetLabels(ms []map[string]string) []map[string]string {
ln := discoveryutils.SanitizeLabelName(a.Type) ln := discoveryutils.SanitizeLabelName(a.Type)
m["__meta_kubernetes_node_address_"+ln] = a.Address m["__meta_kubernetes_node_address_"+ln] = a.Address
} }
ms = append(ms, m) return []map[string]string{m}
return ms
} }
func getNodeAddr(nas []NodeAddress) string { func getNodeAddr(nas []NodeAddress) string {

View file

@ -4,18 +4,19 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
) )
func TestParseNodeListFailure(t *testing.T) { func TestParseNodeListFailure(t *testing.T) {
f := func(s string) { f := func(s string) {
t.Helper() t.Helper()
nls, err := parseNodeList([]byte(s)) objectsByKey, _, err := parseNodeList([]byte(s))
if err == nil { if err == nil {
t.Fatalf("expecting non-nil error") t.Fatalf("expecting non-nil error")
} }
if nls != nil { if len(objectsByKey) != 0 {
t.Fatalf("unexpected non-nil NodeList: %v", nls) t.Fatalf("unexpected non-empty objectsByKey: %v", objectsByKey)
} }
} }
f(``) f(``)
@ -226,97 +227,67 @@ func TestParseNodeListSuccess(t *testing.T) {
] ]
} }
` `
nls, err := parseNodeList([]byte(data)) objectsByKey, meta, err := parseNodeList([]byte(data))
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
if len(nls.Items) != 1 { expectedResourceVersion := "22627"
t.Fatalf("unexpected length of NodeList.Items; got %d; want %d", len(nls.Items), 1) if meta.ResourceVersion != expectedResourceVersion {
t.Fatalf("unexpected resource version; got %s; want %s", meta.ResourceVersion, expectedResourceVersion)
} }
node := nls.Items[0] sortedLabelss := getSortedLabelss(objectsByKey)
meta := node.Metadata expectedLabelss := [][]prompbmarshal.Label{
if meta.Name != "m01" { discoveryutils.GetSortedLabels(map[string]string{
t.Fatalf("unexpected ObjectMeta.Name; got %q; want %q", meta.Name, "m01") "instance": "m01",
"__address__": "172.17.0.2:10250",
"__meta_kubernetes_node_name": "m01",
"__meta_kubernetes_node_label_beta_kubernetes_io_arch": "amd64",
"__meta_kubernetes_node_label_beta_kubernetes_io_os": "linux",
"__meta_kubernetes_node_label_kubernetes_io_arch": "amd64",
"__meta_kubernetes_node_label_kubernetes_io_hostname": "m01",
"__meta_kubernetes_node_label_kubernetes_io_os": "linux",
"__meta_kubernetes_node_label_minikube_k8s_io_commit": "eb13446e786c9ef70cb0a9f85a633194e62396a1",
"__meta_kubernetes_node_label_minikube_k8s_io_name": "minikube",
"__meta_kubernetes_node_label_minikube_k8s_io_updated_at": "2020_03_16T22_44_27_0700",
"__meta_kubernetes_node_label_minikube_k8s_io_version": "v1.8.2",
"__meta_kubernetes_node_label_node_role_kubernetes_io_master": "",
"__meta_kubernetes_node_labelpresent_beta_kubernetes_io_arch": "true",
"__meta_kubernetes_node_labelpresent_beta_kubernetes_io_os": "true",
"__meta_kubernetes_node_labelpresent_kubernetes_io_arch": "true",
"__meta_kubernetes_node_labelpresent_kubernetes_io_hostname": "true",
"__meta_kubernetes_node_labelpresent_kubernetes_io_os": "true",
"__meta_kubernetes_node_labelpresent_minikube_k8s_io_commit": "true",
"__meta_kubernetes_node_labelpresent_minikube_k8s_io_name": "true",
"__meta_kubernetes_node_labelpresent_minikube_k8s_io_updated_at": "true",
"__meta_kubernetes_node_labelpresent_minikube_k8s_io_version": "true",
"__meta_kubernetes_node_labelpresent_node_role_kubernetes_io_master": "true",
"__meta_kubernetes_node_annotation_kubeadm_alpha_kubernetes_io_cri_socket": "/var/run/dockershim.sock",
"__meta_kubernetes_node_annotation_node_alpha_kubernetes_io_ttl": "0",
"__meta_kubernetes_node_annotation_volumes_kubernetes_io_controller_managed_attach_detach": "true",
"__meta_kubernetes_node_annotationpresent_kubeadm_alpha_kubernetes_io_cri_socket": "true",
"__meta_kubernetes_node_annotationpresent_node_alpha_kubernetes_io_ttl": "true",
"__meta_kubernetes_node_annotationpresent_volumes_kubernetes_io_controller_managed_attach_detach": "true",
"__meta_kubernetes_node_address_InternalIP": "172.17.0.2",
"__meta_kubernetes_node_address_Hostname": "m01",
}),
} }
expectedLabels := discoveryutils.GetSortedLabels(map[string]string{ if !reflect.DeepEqual(sortedLabelss, expectedLabelss) {
"beta.kubernetes.io/arch": "amd64", t.Fatalf("unexpected labels:\ngot\n%v\nwant\n%v", sortedLabelss, expectedLabelss)
"beta.kubernetes.io/os": "linux",
"kubernetes.io/arch": "amd64",
"kubernetes.io/hostname": "m01",
"kubernetes.io/os": "linux",
"minikube.k8s.io/commit": "eb13446e786c9ef70cb0a9f85a633194e62396a1",
"minikube.k8s.io/name": "minikube",
"minikube.k8s.io/updated_at": "2020_03_16T22_44_27_0700",
"minikube.k8s.io/version": "v1.8.2",
"node-role.kubernetes.io/master": "",
})
if !reflect.DeepEqual(meta.Labels, expectedLabels) {
t.Fatalf("unexpected ObjectMeta.Labels\ngot\n%v\nwant\n%v", meta.Labels, expectedLabels)
}
expectedAnnotations := discoveryutils.GetSortedLabels(map[string]string{
"kubeadm.alpha.kubernetes.io/cri-socket": "/var/run/dockershim.sock",
"node.alpha.kubernetes.io/ttl": "0",
"volumes.kubernetes.io/controller-managed-attach-detach": "true",
})
if !reflect.DeepEqual(meta.Annotations, expectedAnnotations) {
t.Fatalf("unexpected ObjectMeta.Annotations\ngot\n%v\nwant\n%v", meta.Annotations, expectedAnnotations)
}
status := node.Status
expectedAddresses := []NodeAddress{
{
Type: "InternalIP",
Address: "172.17.0.2",
},
{
Type: "Hostname",
Address: "m01",
},
}
if !reflect.DeepEqual(status.Addresses, expectedAddresses) {
t.Fatalf("unexpected addresses\ngot\n%v\nwant\n%v", status.Addresses, expectedAddresses)
}
// Check node.appendTargetLabels()
labels := discoveryutils.GetSortedLabels(node.appendTargetLabels(nil)[0])
expectedLabels = discoveryutils.GetSortedLabels(map[string]string{
"instance": "m01",
"__address__": "172.17.0.2:10250",
"__meta_kubernetes_node_name": "m01",
"__meta_kubernetes_node_label_beta_kubernetes_io_arch": "amd64",
"__meta_kubernetes_node_label_beta_kubernetes_io_os": "linux",
"__meta_kubernetes_node_label_kubernetes_io_arch": "amd64",
"__meta_kubernetes_node_label_kubernetes_io_hostname": "m01",
"__meta_kubernetes_node_label_kubernetes_io_os": "linux",
"__meta_kubernetes_node_label_minikube_k8s_io_commit": "eb13446e786c9ef70cb0a9f85a633194e62396a1",
"__meta_kubernetes_node_label_minikube_k8s_io_name": "minikube",
"__meta_kubernetes_node_label_minikube_k8s_io_updated_at": "2020_03_16T22_44_27_0700",
"__meta_kubernetes_node_label_minikube_k8s_io_version": "v1.8.2",
"__meta_kubernetes_node_label_node_role_kubernetes_io_master": "",
"__meta_kubernetes_node_labelpresent_beta_kubernetes_io_arch": "true",
"__meta_kubernetes_node_labelpresent_beta_kubernetes_io_os": "true",
"__meta_kubernetes_node_labelpresent_kubernetes_io_arch": "true",
"__meta_kubernetes_node_labelpresent_kubernetes_io_hostname": "true",
"__meta_kubernetes_node_labelpresent_kubernetes_io_os": "true",
"__meta_kubernetes_node_labelpresent_minikube_k8s_io_commit": "true",
"__meta_kubernetes_node_labelpresent_minikube_k8s_io_name": "true",
"__meta_kubernetes_node_labelpresent_minikube_k8s_io_updated_at": "true",
"__meta_kubernetes_node_labelpresent_minikube_k8s_io_version": "true",
"__meta_kubernetes_node_labelpresent_node_role_kubernetes_io_master": "true",
"__meta_kubernetes_node_annotation_kubeadm_alpha_kubernetes_io_cri_socket": "/var/run/dockershim.sock",
"__meta_kubernetes_node_annotation_node_alpha_kubernetes_io_ttl": "0",
"__meta_kubernetes_node_annotation_volumes_kubernetes_io_controller_managed_attach_detach": "true",
"__meta_kubernetes_node_annotationpresent_kubeadm_alpha_kubernetes_io_cri_socket": "true",
"__meta_kubernetes_node_annotationpresent_node_alpha_kubernetes_io_ttl": "true",
"__meta_kubernetes_node_annotationpresent_volumes_kubernetes_io_controller_managed_attach_detach": "true",
"__meta_kubernetes_node_address_InternalIP": "172.17.0.2",
"__meta_kubernetes_node_address_Hostname": "m01",
})
if !reflect.DeepEqual(labels, expectedLabels) {
t.Fatalf("unexpected labels:\ngot\n%v\nwant\n%v", labels, expectedLabels)
} }
} }
func getSortedLabelss(objectsByKey map[string]object) [][]prompbmarshal.Label {
var result [][]prompbmarshal.Label
for _, o := range objectsByKey {
labelss := o.getTargetLabels(nil)
for _, labels := range labelss {
result = append(result, discoveryutils.GetSortedLabels(labels))
}
}
return result
}

View file

@ -9,58 +9,36 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
) )
// getPodsLabels returns labels for k8s pods obtained from the given cfg func (p *Pod) key() string {
func getPodsLabels(cfg *apiConfig) ([]map[string]string, error) { return p.Metadata.key()
pods, err := getPods(cfg) }
if err != nil {
func parsePodList(data []byte) (map[string]object, ListMeta, error) {
var pl PodList
if err := json.Unmarshal(data, &pl); err != nil {
return nil, pl.Metadata, fmt.Errorf("cannot unmarshal PodList from %q: %w", data, err)
}
objectsByKey := make(map[string]object)
for _, p := range pl.Items {
objectsByKey[p.key()] = p
}
return objectsByKey, pl.Metadata, nil
}
func parsePod(data []byte) (object, error) {
var p Pod
if err := json.Unmarshal(data, &p); err != nil {
return nil, err return nil, err
} }
var ms []map[string]string return &p, nil
for _, p := range pods {
ms = p.appendTargetLabels(ms)
}
return ms, nil
}
func getPods(cfg *apiConfig) ([]Pod, error) {
if len(cfg.namespaces) == 0 {
return getPodsByPath(cfg, "/api/v1/pods")
}
// Query /api/v1/namespaces/* for each namespace.
// This fixes authorization issue at https://github.com/VictoriaMetrics/VictoriaMetrics/issues/432
cfgCopy := *cfg
namespaces := cfgCopy.namespaces
cfgCopy.namespaces = nil
cfg = &cfgCopy
var result []Pod
for _, ns := range namespaces {
path := fmt.Sprintf("/api/v1/namespaces/%s/pods", ns)
pods, err := getPodsByPath(cfg, path)
if err != nil {
return nil, err
}
result = append(result, pods...)
}
return result, nil
}
func getPodsByPath(cfg *apiConfig, path string) ([]Pod, error) {
data, err := getAPIResponse(cfg, "pod", path)
if err != nil {
return nil, fmt.Errorf("cannot obtain pods data from API server: %w", err)
}
pl, err := parsePodList(data)
if err != nil {
return nil, fmt.Errorf("cannot parse pods response from API server: %w", err)
}
return pl.Items, nil
} }
// PodList implements k8s pod list. // PodList implements k8s pod list.
// //
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#podlist-v1-core // See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#podlist-v1-core
type PodList struct { type PodList struct {
Items []Pod Metadata ListMeta
Items []*Pod
} }
// Pod implements k8s pod. // Pod implements k8s pod.
@ -114,23 +92,15 @@ type PodCondition struct {
Status string Status string
} }
// parsePodList parses PodList from data. // getTargetLabels returns labels for each port of the given p.
func parsePodList(data []byte) (*PodList, error) {
var pl PodList
if err := json.Unmarshal(data, &pl); err != nil {
return nil, fmt.Errorf("cannot unmarshal PodList from %q: %w", data, err)
}
return &pl, nil
}
// appendTargetLabels appends labels for each port of the given Pod p to ms and returns the result.
// //
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#pod // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#pod
func (p *Pod) appendTargetLabels(ms []map[string]string) []map[string]string { func (p *Pod) getTargetLabels(aw *apiWatcher) []map[string]string {
if len(p.Status.PodIP) == 0 { if len(p.Status.PodIP) == 0 {
// Skip pod without IP // Skip pod without IP
return ms return nil
} }
var ms []map[string]string
ms = appendPodLabels(ms, p, p.Spec.Containers, "false") ms = appendPodLabels(ms, p, p.Spec.Containers, "false")
ms = appendPodLabels(ms, p, p.Spec.InitContainers, "true") ms = appendPodLabels(ms, p, p.Spec.InitContainers, "true")
return ms return ms
@ -210,13 +180,3 @@ func getPodReadyStatus(conds []PodCondition) string {
} }
return "unknown" return "unknown"
} }
func getPod(pods []Pod, namespace, name string) *Pod {
for i := range pods {
pod := &pods[i]
if pod.Metadata.Name == name && pod.Metadata.Namespace == namespace {
return pod
}
}
return nil
}

View file

@ -11,12 +11,12 @@ import (
func TestParsePodListFailure(t *testing.T) { func TestParsePodListFailure(t *testing.T) {
f := func(s string) { f := func(s string) {
t.Helper() t.Helper()
nls, err := parsePodList([]byte(s)) objectsByKey, _, err := parsePodList([]byte(s))
if err == nil { if err == nil {
t.Fatalf("expecting non-nil error") t.Fatalf("expecting non-nil error")
} }
if nls != nil { if len(objectsByKey) != 0 {
t.Fatalf("unexpected non-nil PodList: %v", nls) t.Fatalf("unexpected non-empty objectsByKey: %v", objectsByKey)
} }
} }
f(``) f(``)
@ -228,22 +228,16 @@ func TestParsePodListSuccess(t *testing.T) {
] ]
} }
` `
pls, err := parsePodList([]byte(data)) objectsByKey, meta, err := parsePodList([]byte(data))
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
if len(pls.Items) != 1 { expectedResourceVersion := "72425"
t.Fatalf("unexpected length of PodList.Items; got %d; want %d", len(pls.Items), 1) if meta.ResourceVersion != expectedResourceVersion {
t.Fatalf("unexpected resource version; got %s; want %s", meta.ResourceVersion, expectedResourceVersion)
} }
pod := pls.Items[0] sortedLabelss := getSortedLabelss(objectsByKey)
expectedLabelss := [][]prompbmarshal.Label{
// Check pod.appendTargetLabels()
labelss := pod.appendTargetLabels(nil)
var sortedLabelss [][]prompbmarshal.Label
for _, labels := range labelss {
sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels))
}
expectedLabels := [][]prompbmarshal.Label{
discoveryutils.GetSortedLabels(map[string]string{ discoveryutils.GetSortedLabels(map[string]string{
"__address__": "172.17.0.2:1234", "__address__": "172.17.0.2:1234",
@ -280,7 +274,7 @@ func TestParsePodListSuccess(t *testing.T) {
"__meta_kubernetes_pod_annotationpresent_kubernetes_io_config_source": "true", "__meta_kubernetes_pod_annotationpresent_kubernetes_io_config_source": "true",
}), }),
} }
if !reflect.DeepEqual(sortedLabelss, expectedLabels) { if !reflect.DeepEqual(sortedLabelss, expectedLabelss) {
t.Fatalf("unexpected labels:\ngot\n%v\nwant\n%v", sortedLabelss, expectedLabels) t.Fatalf("unexpected labels:\ngot\n%v\nwant\n%v", sortedLabelss, expectedLabelss)
} }
} }

View file

@ -7,58 +7,36 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
) )
// getServicesLabels returns labels for k8s services obtained from the given cfg. func (s *Service) key() string {
func getServicesLabels(cfg *apiConfig) ([]map[string]string, error) { return s.Metadata.key()
svcs, err := getServices(cfg) }
if err != nil {
func parseServiceList(data []byte) (map[string]object, ListMeta, error) {
var sl ServiceList
if err := json.Unmarshal(data, &sl); err != nil {
return nil, sl.Metadata, fmt.Errorf("cannot unmarshal ServiceList from %q: %w", data, err)
}
objectsByKey := make(map[string]object)
for _, s := range sl.Items {
objectsByKey[s.key()] = s
}
return objectsByKey, sl.Metadata, nil
}
func parseService(data []byte) (object, error) {
var s Service
if err := json.Unmarshal(data, &s); err != nil {
return nil, err return nil, err
} }
var ms []map[string]string return &s, nil
for _, svc := range svcs {
ms = svc.appendTargetLabels(ms)
}
return ms, nil
}
func getServices(cfg *apiConfig) ([]Service, error) {
if len(cfg.namespaces) == 0 {
return getServicesByPath(cfg, "/api/v1/services")
}
// Query /api/v1/namespaces/* for each namespace.
// This fixes authorization issue at https://github.com/VictoriaMetrics/VictoriaMetrics/issues/432
cfgCopy := *cfg
namespaces := cfgCopy.namespaces
cfgCopy.namespaces = nil
cfg = &cfgCopy
var result []Service
for _, ns := range namespaces {
path := fmt.Sprintf("/api/v1/namespaces/%s/services", ns)
svcs, err := getServicesByPath(cfg, path)
if err != nil {
return nil, err
}
result = append(result, svcs...)
}
return result, nil
}
func getServicesByPath(cfg *apiConfig, path string) ([]Service, error) {
data, err := getAPIResponse(cfg, "service", path)
if err != nil {
return nil, fmt.Errorf("cannot obtain services data from API server: %w", err)
}
sl, err := parseServiceList(data)
if err != nil {
return nil, fmt.Errorf("cannot parse services response from API server: %w", err)
}
return sl.Items, nil
} }
// ServiceList is k8s service list. // ServiceList is k8s service list.
// //
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#servicelist-v1-core // See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#servicelist-v1-core
type ServiceList struct { type ServiceList struct {
Items []Service Metadata ListMeta
Items []*Service
} }
// Service is k8s service. // Service is k8s service.
@ -88,20 +66,12 @@ type ServicePort struct {
Port int Port int
} }
// parseServiceList parses ServiceList from data. // getTargetLabels returns labels for each port of the given s.
func parseServiceList(data []byte) (*ServiceList, error) {
var sl ServiceList
if err := json.Unmarshal(data, &sl); err != nil {
return nil, fmt.Errorf("cannot unmarshal ServiceList from %q: %w", data, err)
}
return &sl, nil
}
// appendTargetLabels appends labels for each port of the given Service s to ms and returns the result.
// //
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#service // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#service
func (s *Service) appendTargetLabels(ms []map[string]string) []map[string]string { func (s *Service) getTargetLabels(aw *apiWatcher) []map[string]string {
host := fmt.Sprintf("%s.%s.svc", s.Metadata.Name, s.Metadata.Namespace) host := fmt.Sprintf("%s.%s.svc", s.Metadata.Name, s.Metadata.Namespace)
var ms []map[string]string
for _, sp := range s.Spec.Ports { for _, sp := range s.Spec.Ports {
addr := discoveryutils.JoinHostPort(host, sp.Port) addr := discoveryutils.JoinHostPort(host, sp.Port)
m := map[string]string{ m := map[string]string{
@ -126,13 +96,3 @@ func (s *Service) appendCommonLabels(m map[string]string) {
} }
s.Metadata.registerLabelsAndAnnotations("__meta_kubernetes_service", m) s.Metadata.registerLabelsAndAnnotations("__meta_kubernetes_service", m)
} }
func getService(svcs []Service, namespace, name string) *Service {
for i := range svcs {
svc := &svcs[i]
if svc.Metadata.Name == name && svc.Metadata.Namespace == namespace {
return svc
}
}
return nil
}

View file

@ -11,12 +11,12 @@ import (
func TestParseServiceListFailure(t *testing.T) { func TestParseServiceListFailure(t *testing.T) {
f := func(s string) { f := func(s string) {
t.Helper() t.Helper()
nls, err := parseServiceList([]byte(s)) objectsByKey, _, err := parseServiceList([]byte(s))
if err == nil { if err == nil {
t.Fatalf("expecting non-nil error") t.Fatalf("expecting non-nil error")
} }
if nls != nil { if len(objectsByKey) != 0 {
t.Fatalf("unexpected non-nil ServiceList: %v", nls) t.Fatalf("unexpected non-empty objectsByKey: %v", objectsByKey)
} }
} }
f(``) f(``)
@ -89,68 +89,15 @@ func TestParseServiceListSuccess(t *testing.T) {
] ]
} }
` `
sls, err := parseServiceList([]byte(data)) objectsByKey, meta, err := parseServiceList([]byte(data))
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
if len(sls.Items) != 1 { expectedResourceVersion := "60485"
t.Fatalf("unexpected length of ServiceList.Items; got %d; want %d", len(sls.Items), 1) if meta.ResourceVersion != expectedResourceVersion {
} t.Fatalf("unexpected resource version; got %s; want %s", meta.ResourceVersion, expectedResourceVersion)
service := sls.Items[0]
meta := service.Metadata
if meta.Name != "kube-dns" {
t.Fatalf("unexpected ObjectMeta.Name; got %q; want %q", meta.Name, "kube-dns")
}
expectedLabels := discoveryutils.GetSortedLabels(map[string]string{
"k8s-app": "kube-dns",
"kubernetes.io/cluster-service": "true",
"kubernetes.io/name": "KubeDNS",
})
if !reflect.DeepEqual(meta.Labels, expectedLabels) {
t.Fatalf("unexpected ObjectMeta.Labels\ngot\n%v\nwant\n%v", meta.Labels, expectedLabels)
}
expectedAnnotations := discoveryutils.GetSortedLabels(map[string]string{
"prometheus.io/port": "9153",
"prometheus.io/scrape": "true",
})
if !reflect.DeepEqual(meta.Annotations, expectedAnnotations) {
t.Fatalf("unexpected ObjectMeta.Annotations\ngot\n%v\nwant\n%v", meta.Annotations, expectedAnnotations)
}
spec := service.Spec
expectedClusterIP := "10.96.0.10"
if spec.ClusterIP != expectedClusterIP {
t.Fatalf("unexpected clusterIP; got %q; want %q", spec.ClusterIP, expectedClusterIP)
}
if spec.Type != "ClusterIP" {
t.Fatalf("unexpected type; got %q; want %q", spec.Type, "ClusterIP")
}
expectedPorts := []ServicePort{
{
Name: "dns",
Protocol: "UDP",
Port: 53,
},
{
Name: "dns-tcp",
Protocol: "TCP",
Port: 53,
},
{
Name: "metrics",
Protocol: "TCP",
Port: 9153,
},
}
if !reflect.DeepEqual(spec.Ports, expectedPorts) {
t.Fatalf("unexpected ports\ngot\n%v\nwant\n%v", spec.Ports, expectedPorts)
}
// Check service.appendTargetLabels()
labelss := service.appendTargetLabels(nil)
var sortedLabelss [][]prompbmarshal.Label
for _, labels := range labelss {
sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels))
} }
sortedLabelss := getSortedLabelss(objectsByKey)
expectedLabelss := [][]prompbmarshal.Label{ expectedLabelss := [][]prompbmarshal.Label{
discoveryutils.GetSortedLabels(map[string]string{ discoveryutils.GetSortedLabels(map[string]string{
"__address__": "kube-dns.kube-system.svc:53", "__address__": "kube-dns.kube-system.svc:53",

View file

@ -31,8 +31,8 @@ type SDConfig struct {
Availability string `yaml:"availability,omitempty"` Availability string `yaml:"availability,omitempty"`
} }
// GetLabels returns gce labels according to sdc. // GetLabels returns OpenStack labels according to sdc.
func GetLabels(sdc *SDConfig, baseDir string) ([]map[string]string, error) { func (sdc *SDConfig) GetLabels(baseDir string) ([]map[string]string, error) {
cfg, err := getAPIConfig(sdc, baseDir) cfg, err := getAPIConfig(sdc, baseDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get API config: %w", err) return nil, fmt.Errorf("cannot get API config: %w", err)

View file

@ -6,7 +6,6 @@ import (
"math" "math"
"math/bits" "math/bits"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
@ -76,7 +75,7 @@ type ScrapeWork struct {
ProxyURL proxy.URL ProxyURL proxy.URL
// Optional `metric_relabel_configs`. // Optional `metric_relabel_configs`.
MetricRelabelConfigs []promrelabel.ParsedRelabelConfig MetricRelabelConfigs *promrelabel.ParsedConfigs
// The maximum number of metrics to scrape after relabeling. // The maximum number of metrics to scrape after relabeling.
SampleLimit int SampleLimit int
@ -90,6 +89,9 @@ type ScrapeWork struct {
// Whether to parse target responses in a streaming manner. // Whether to parse target responses in a streaming manner.
StreamParse bool StreamParse bool
// The interval for aligning the first scrape.
ScrapeAlignInterval time.Duration
// The original 'job_name' // The original 'job_name'
jobNameOriginal string jobNameOriginal string
} }
@ -100,20 +102,12 @@ type ScrapeWork struct {
func (sw *ScrapeWork) key() string { func (sw *ScrapeWork) key() string {
// Do not take into account OriginalLabels. // Do not take into account OriginalLabels.
key := fmt.Sprintf("ScrapeURL=%s, ScrapeInterval=%s, ScrapeTimeout=%s, HonorLabels=%v, HonorTimestamps=%v, Labels=%s, "+ key := fmt.Sprintf("ScrapeURL=%s, ScrapeInterval=%s, ScrapeTimeout=%s, HonorLabels=%v, HonorTimestamps=%v, Labels=%s, "+
"AuthConfig=%s, MetricRelabelConfigs=%s, SampleLimit=%d, DisableCompression=%v, DisableKeepAlive=%v, StreamParse=%v", "AuthConfig=%s, MetricRelabelConfigs=%s, SampleLimit=%d, DisableCompression=%v, DisableKeepAlive=%v, StreamParse=%v, ScrapeAlignInterval=%s",
sw.ScrapeURL, sw.ScrapeInterval, sw.ScrapeTimeout, sw.HonorLabels, sw.HonorTimestamps, sw.LabelsString(), sw.ScrapeURL, sw.ScrapeInterval, sw.ScrapeTimeout, sw.HonorLabels, sw.HonorTimestamps, sw.LabelsString(),
sw.AuthConfig.String(), sw.metricRelabelConfigsString(), sw.SampleLimit, sw.DisableCompression, sw.DisableKeepAlive, sw.StreamParse) sw.AuthConfig.String(), sw.MetricRelabelConfigs.String(), sw.SampleLimit, sw.DisableCompression, sw.DisableKeepAlive, sw.StreamParse, sw.ScrapeAlignInterval)
return key return key
} }
func (sw *ScrapeWork) metricRelabelConfigsString() string {
var sb strings.Builder
for _, prc := range sw.MetricRelabelConfigs {
fmt.Fprintf(&sb, "%s", prc.String())
}
return sb.String()
}
// Job returns job for the ScrapeWork // Job returns job for the ScrapeWork
func (sw *ScrapeWork) Job() string { func (sw *ScrapeWork) Job() string {
return promrelabel.GetLabelValueByName(sw.Labels, "job") return promrelabel.GetLabelValueByName(sw.Labels, "job")
@ -180,20 +174,27 @@ type scrapeWork struct {
} }
func (sw *scrapeWork) run(stopCh <-chan struct{}) { func (sw *scrapeWork) run(stopCh <-chan struct{}) {
// Calculate start time for the first scrape from ScrapeURL and labels.
// This should spread load when scraping many targets with different
// scrape urls and labels.
// This also makes consistent scrape times across restarts
// for a target with the same ScrapeURL and labels.
scrapeInterval := sw.Config.ScrapeInterval scrapeInterval := sw.Config.ScrapeInterval
key := fmt.Sprintf("ScrapeURL=%s, Labels=%s", sw.Config.ScrapeURL, sw.Config.LabelsString()) var randSleep uint64
h := uint32(xxhash.Sum64([]byte(key))) if sw.Config.ScrapeAlignInterval <= 0 {
randSleep := uint64(float64(scrapeInterval) * (float64(h) / (1 << 32))) // Calculate start time for the first scrape from ScrapeURL and labels.
sleepOffset := uint64(time.Now().UnixNano()) % uint64(scrapeInterval) // This should spread load when scraping many targets with different
if randSleep < sleepOffset { // scrape urls and labels.
randSleep += uint64(scrapeInterval) // This also makes consistent scrape times across restarts
// for a target with the same ScrapeURL and labels.
key := fmt.Sprintf("ScrapeURL=%s, Labels=%s", sw.Config.ScrapeURL, sw.Config.LabelsString())
h := uint32(xxhash.Sum64([]byte(key)))
randSleep = uint64(float64(scrapeInterval) * (float64(h) / (1 << 32)))
sleepOffset := uint64(time.Now().UnixNano()) % uint64(scrapeInterval)
if randSleep < sleepOffset {
randSleep += uint64(scrapeInterval)
}
randSleep -= sleepOffset
} else {
d := uint64(sw.Config.ScrapeAlignInterval)
randSleep = d - uint64(time.Now().UnixNano())%d
randSleep %= uint64(scrapeInterval)
} }
randSleep -= sleepOffset
timer := timerpool.Get(time.Duration(randSleep)) timer := timerpool.Get(time.Duration(randSleep))
var timestamp int64 var timestamp int64
var ticker *time.Ticker var ticker *time.Ticker
@ -493,7 +494,7 @@ func (sw *scrapeWork) addRowToTimeseries(wc *writeRequestCtx, r *parser.Row, tim
labelsLen := len(wc.labels) labelsLen := len(wc.labels)
wc.labels = appendLabels(wc.labels, r.Metric, r.Tags, sw.Config.Labels, sw.Config.HonorLabels) wc.labels = appendLabels(wc.labels, r.Metric, r.Tags, sw.Config.Labels, sw.Config.HonorLabels)
if needRelabel { if needRelabel {
wc.labels = promrelabel.ApplyRelabelConfigs(wc.labels, labelsLen, sw.Config.MetricRelabelConfigs, true) wc.labels = sw.Config.MetricRelabelConfigs.Apply(wc.labels, labelsLen, true)
} else { } else {
wc.labels = promrelabel.FinalizeLabels(wc.labels[:labelsLen], wc.labels[labelsLen:]) wc.labels = promrelabel.FinalizeLabels(wc.labels[:labelsLen], wc.labels[labelsLen:])
promrelabel.SortLabels(wc.labels[labelsLen:]) promrelabel.SortLabels(wc.labels[labelsLen:])

View file

@ -2,7 +2,6 @@ package promscrape
import ( import (
"fmt" "fmt"
"regexp"
"strings" "strings"
"testing" "testing"
@ -102,7 +101,8 @@ func TestScrapeWorkScrapeInternalSuccess(t *testing.T) {
sw.PushData = func(wr *prompbmarshal.WriteRequest) { sw.PushData = func(wr *prompbmarshal.WriteRequest) {
pushDataCalls++ pushDataCalls++
if len(wr.Timeseries) > len(timeseriesExpected) { if len(wr.Timeseries) > len(timeseriesExpected) {
pushDataErr = fmt.Errorf("too many time series obtained; got %d; want %d", len(wr.Timeseries), len(timeseriesExpected)) pushDataErr = fmt.Errorf("too many time series obtained; got %d; want %d\ngot\n%+v\nwant\n%+v",
len(wr.Timeseries), len(timeseriesExpected), wr.Timeseries, timeseriesExpected)
return return
} }
tsExpected := timeseriesExpected[:len(wr.Timeseries)] tsExpected := timeseriesExpected[:len(wr.Timeseries)]
@ -271,20 +271,14 @@ func TestScrapeWorkScrapeInternalSuccess(t *testing.T) {
Value: "foo.com", Value: "foo.com",
}, },
}, },
MetricRelabelConfigs: []promrelabel.ParsedRelabelConfig{ MetricRelabelConfigs: mustParseRelabelConfigs(`
{ - action: replace
SourceLabels: []string{"__address__", "job"}, source_labels: ["__address__", "job"]
Separator: "/", separator: "/"
TargetLabel: "instance", target_label: "instance"
Regex: defaultRegexForRelabelConfig, - action: labeldrop
Replacement: "$1", regex: c
Action: "replace", `),
},
{
Action: "labeldrop",
Regex: regexp.MustCompile("^c$"),
},
},
}, ` }, `
foo{bar="baz",job="xx",instance="foo.com/xx"} 34.44 123 foo{bar="baz",job="xx",instance="foo.com/xx"} 34.44 123
bar{a="b",job="xx",instance="foo.com/xx"} -3e4 123 bar{a="b",job="xx",instance="foo.com/xx"} -3e4 123
@ -311,18 +305,15 @@ func TestScrapeWorkScrapeInternalSuccess(t *testing.T) {
Value: "foo.com", Value: "foo.com",
}, },
}, },
MetricRelabelConfigs: []promrelabel.ParsedRelabelConfig{ MetricRelabelConfigs: mustParseRelabelConfigs(`
{ - action: drop
Action: "drop", separator: ""
SourceLabels: []string{"a", "c"}, source_labels: [a, c]
Regex: regexp.MustCompile("^bd$"), regex: "^bd$"
}, - action: drop
{ source_labels: [__name__]
Action: "drop", regex: "dropme|up"
SourceLabels: []string{"__name__"}, `),
Regex: regexp.MustCompile("^(dropme|up)$"),
},
},
}, ` }, `
foo{bar="baz",job="xx",instance="foo.com"} 34.44 123 foo{bar="baz",job="xx",instance="foo.com"} 34.44 123
up{job="xx",instance="foo.com"} 1 123 up{job="xx",instance="foo.com"} 1 123
@ -440,3 +431,11 @@ func timeseriesToString(ts *prompbmarshal.TimeSeries) string {
fmt.Fprintf(&sb, "%g %d", s.Value, s.Timestamp) fmt.Fprintf(&sb, "%g %d", s.Value, s.Timestamp)
return sb.String() return sb.String()
} }
func mustParseRelabelConfigs(config string) *promrelabel.ParsedConfigs {
pcs, err := promrelabel.ParseRelabelConfigsData([]byte(config))
if err != nil {
panic(fmt.Errorf("cannot parse %q: %w", config, err))
}
return pcs
}

View file

@ -76,6 +76,6 @@ func testBlockHeaderMarshalUnmarshal(t *testing.T, bh *blockHeader) {
t.Fatalf("unexpected tail after unmarshaling bh=%+v; got\n%x; want\n%x", bh, tail, suffix) t.Fatalf("unexpected tail after unmarshaling bh=%+v; got\n%x; want\n%x", bh, tail, suffix)
} }
if !reflect.DeepEqual(bh, &bh2) { if !reflect.DeepEqual(bh, &bh2) {
t.Fatalf("unexpected bh unmarshaled after adding siffux; got\n%+v; want\n%+v", &bh2, bh) t.Fatalf("unexpected bh unmarshaled after adding suffix; got\n%+v; want\n%+v", &bh2, bh)
} }
} }

View file

@ -104,9 +104,9 @@ type indexDB struct {
// matching low number of metrics. // matching low number of metrics.
uselessTagFiltersCache *workingsetcache.Cache uselessTagFiltersCache *workingsetcache.Cache
// Cache for (date, tagFilter) -> filterDuration, which is used for reducing // Cache for (date, tagFilter) -> loopsCount, which is used for reducing
// the amount of work when matching a set of filters. // the amount of work when matching a set of filters.
durationsPerDateTagFilterCache *workingsetcache.Cache loopsPerDateTagFilterCache *workingsetcache.Cache
indexSearchPool sync.Pool indexSearchPool sync.Pool
@ -150,12 +150,12 @@ func openIndexDB(path string, metricIDCache, metricNameCache, tsidCache *working
tb: tb, tb: tb,
name: name, name: name,
tagCache: workingsetcache.New(mem/32, time.Hour), tagCache: workingsetcache.New(mem/32, time.Hour),
metricIDCache: metricIDCache, metricIDCache: metricIDCache,
metricNameCache: metricNameCache, metricNameCache: metricNameCache,
tsidCache: tsidCache, tsidCache: tsidCache,
uselessTagFiltersCache: workingsetcache.New(mem/128, time.Hour), uselessTagFiltersCache: workingsetcache.New(mem/128, 3*time.Hour),
durationsPerDateTagFilterCache: workingsetcache.New(mem/128, time.Hour), loopsPerDateTagFilterCache: workingsetcache.New(mem/128, 3*time.Hour),
minTimestampForCompositeIndex: minTimestampForCompositeIndex, minTimestampForCompositeIndex: minTimestampForCompositeIndex,
} }
@ -320,14 +320,14 @@ func (db *indexDB) decRef() {
// Free space occupied by caches owned by db. // Free space occupied by caches owned by db.
db.tagCache.Stop() db.tagCache.Stop()
db.uselessTagFiltersCache.Stop() db.uselessTagFiltersCache.Stop()
db.durationsPerDateTagFilterCache.Stop() db.loopsPerDateTagFilterCache.Stop()
db.tagCache = nil db.tagCache = nil
db.metricIDCache = nil db.metricIDCache = nil
db.metricNameCache = nil db.metricNameCache = nil
db.tsidCache = nil db.tsidCache = nil
db.uselessTagFiltersCache = nil db.uselessTagFiltersCache = nil
db.durationsPerDateTagFilterCache = nil db.loopsPerDateTagFilterCache = nil
if atomic.LoadUint64(&db.mustDrop) == 0 { if atomic.LoadUint64(&db.mustDrop) == 0 {
return return
@ -2458,7 +2458,7 @@ func (is *indexSearch) getMetricIDsForTagFilterSlow(tf *tagFilter, filter *uint6
} }
// Slow path: need tf.matchSuffix call. // Slow path: need tf.matchSuffix call.
ok, err := tf.matchSuffix(suffix) ok, err := tf.matchSuffix(suffix)
loopsCount += reMatchCost loopsCount += tf.matchCost
if err != nil { if err != nil {
return loopsCount, fmt.Errorf("error when matching %s against suffix %q: %w", tf, suffix, err) return loopsCount, fmt.Errorf("error when matching %s against suffix %q: %w", tf, suffix, err)
} }
@ -2797,9 +2797,12 @@ func (is *indexSearch) getMetricIDsForDateAndFilters(date uint64, tfs *TagFilter
tf := &tfs.tfs[i] tf := &tfs.tfs[i]
loopsCount, lastQueryTimestamp := is.getLoopsCountAndTimestampForDateFilter(date, tf) loopsCount, lastQueryTimestamp := is.getLoopsCountAndTimestampForDateFilter(date, tf)
origLoopsCount := loopsCount origLoopsCount := loopsCount
if currentTime > lastQueryTimestamp+60*60 { if currentTime > lastQueryTimestamp+3*3600 {
// Reset loopsCount to 0 every hour for collecting updated stats for the tf. // Update stats once per 3 hours only for relatively fast tag filters.
loopsCount = 0 // There is no need in spending CPU resources on updating stats for slow tag filters.
if loopsCount <= 10e6 {
loopsCount = 0
}
} }
if loopsCount == 0 { if loopsCount == 0 {
// Prevent from possible thundering herd issue when heavy tf is executed from multiple concurrent queries // Prevent from possible thundering herd issue when heavy tf is executed from multiple concurrent queries
@ -3102,7 +3105,7 @@ func (is *indexSearch) getLoopsCountAndTimestampForDateFilter(date uint64, tf *t
is.kb.B = appendDateTagFilterCacheKey(is.kb.B[:0], date, tf) is.kb.B = appendDateTagFilterCacheKey(is.kb.B[:0], date, tf)
kb := kbPool.Get() kb := kbPool.Get()
defer kbPool.Put(kb) defer kbPool.Put(kb)
kb.B = is.db.durationsPerDateTagFilterCache.Get(kb.B[:0], is.kb.B) kb.B = is.db.loopsPerDateTagFilterCache.Get(kb.B[:0], is.kb.B)
if len(kb.B) != 16 { if len(kb.B) != 16 {
return 0, 0 return 0, 0
} }
@ -3122,7 +3125,7 @@ func (is *indexSearch) storeLoopsCountForDateFilter(date uint64, tf *tagFilter,
kb := kbPool.Get() kb := kbPool.Get()
kb.B = encoding.MarshalUint64(kb.B[:0], loopsCount) kb.B = encoding.MarshalUint64(kb.B[:0], loopsCount)
kb.B = encoding.MarshalUint64(kb.B, currentTimestamp) kb.B = encoding.MarshalUint64(kb.B, currentTimestamp)
is.db.durationsPerDateTagFilterCache.Set(is.kb.B, kb.B) is.db.loopsPerDateTagFilterCache.Set(is.kb.B, kb.B)
kbPool.Put(kb) kbPool.Put(kb)
} }
@ -3458,24 +3461,24 @@ func (mp *tagToMetricIDsRowParser) IsDeletedTag(dmis *uint64set.Set) bool {
return true return true
} }
func mergeTagToMetricIDsRows(data []byte, items [][]byte) ([]byte, [][]byte) { func mergeTagToMetricIDsRows(data []byte, items []mergeset.Item) ([]byte, []mergeset.Item) {
data, items = mergeTagToMetricIDsRowsInternal(data, items, nsPrefixTagToMetricIDs) data, items = mergeTagToMetricIDsRowsInternal(data, items, nsPrefixTagToMetricIDs)
data, items = mergeTagToMetricIDsRowsInternal(data, items, nsPrefixDateTagToMetricIDs) data, items = mergeTagToMetricIDsRowsInternal(data, items, nsPrefixDateTagToMetricIDs)
return data, items return data, items
} }
func mergeTagToMetricIDsRowsInternal(data []byte, items [][]byte, nsPrefix byte) ([]byte, [][]byte) { func mergeTagToMetricIDsRowsInternal(data []byte, items []mergeset.Item, nsPrefix byte) ([]byte, []mergeset.Item) {
// Perform quick checks whether items contain rows starting from nsPrefix // Perform quick checks whether items contain rows starting from nsPrefix
// based on the fact that items are sorted. // based on the fact that items are sorted.
if len(items) <= 2 { if len(items) <= 2 {
// The first and the last row must remain unchanged. // The first and the last row must remain unchanged.
return data, items return data, items
} }
firstItem := items[0] firstItem := items[0].Bytes(data)
if len(firstItem) > 0 && firstItem[0] > nsPrefix { if len(firstItem) > 0 && firstItem[0] > nsPrefix {
return data, items return data, items
} }
lastItem := items[len(items)-1] lastItem := items[len(items)-1].Bytes(data)
if len(lastItem) > 0 && lastItem[0] < nsPrefix { if len(lastItem) > 0 && lastItem[0] < nsPrefix {
return data, items return data, items
} }
@ -3488,14 +3491,18 @@ func mergeTagToMetricIDsRowsInternal(data []byte, items [][]byte, nsPrefix byte)
mpPrev := &tmm.mpPrev mpPrev := &tmm.mpPrev
dstData := data[:0] dstData := data[:0]
dstItems := items[:0] dstItems := items[:0]
for i, item := range items { for i, it := range items {
item := it.Bytes(data)
if len(item) == 0 || item[0] != nsPrefix || i == 0 || i == len(items)-1 { if len(item) == 0 || item[0] != nsPrefix || i == 0 || i == len(items)-1 {
// Write rows not starting with nsPrefix as-is. // Write rows not starting with nsPrefix as-is.
// Additionally write the first and the last row as-is in order to preserve // Additionally write the first and the last row as-is in order to preserve
// sort order for adjancent blocks. // sort order for adjancent blocks.
dstData, dstItems = tmm.flushPendingMetricIDs(dstData, dstItems, mpPrev) dstData, dstItems = tmm.flushPendingMetricIDs(dstData, dstItems, mpPrev)
dstData = append(dstData, item...) dstData = append(dstData, item...)
dstItems = append(dstItems, dstData[len(dstData)-len(item):]) dstItems = append(dstItems, mergeset.Item{
Start: uint32(len(dstData) - len(item)),
End: uint32(len(dstData)),
})
continue continue
} }
if err := mp.Init(item, nsPrefix); err != nil { if err := mp.Init(item, nsPrefix); err != nil {
@ -3504,7 +3511,10 @@ func mergeTagToMetricIDsRowsInternal(data []byte, items [][]byte, nsPrefix byte)
if mp.MetricIDsLen() >= maxMetricIDsPerRow { if mp.MetricIDsLen() >= maxMetricIDsPerRow {
dstData, dstItems = tmm.flushPendingMetricIDs(dstData, dstItems, mpPrev) dstData, dstItems = tmm.flushPendingMetricIDs(dstData, dstItems, mpPrev)
dstData = append(dstData, item...) dstData = append(dstData, item...)
dstItems = append(dstItems, dstData[len(dstData)-len(item):]) dstItems = append(dstItems, mergeset.Item{
Start: uint32(len(dstData) - len(item)),
End: uint32(len(dstData)),
})
continue continue
} }
if !mp.EqualPrefix(mpPrev) { if !mp.EqualPrefix(mpPrev) {
@ -3520,7 +3530,7 @@ func mergeTagToMetricIDsRowsInternal(data []byte, items [][]byte, nsPrefix byte)
if len(tmm.pendingMetricIDs) > 0 { if len(tmm.pendingMetricIDs) > 0 {
logger.Panicf("BUG: tmm.pendingMetricIDs must be empty at this point; got %d items: %d", len(tmm.pendingMetricIDs), tmm.pendingMetricIDs) logger.Panicf("BUG: tmm.pendingMetricIDs must be empty at this point; got %d items: %d", len(tmm.pendingMetricIDs), tmm.pendingMetricIDs)
} }
if !checkItemsSorted(dstItems) { if !checkItemsSorted(dstData, dstItems) {
// Items could become unsorted if initial items contain duplicate metricIDs: // Items could become unsorted if initial items contain duplicate metricIDs:
// //
// item1: 1, 1, 5 // item1: 1, 1, 5
@ -3538,15 +3548,8 @@ func mergeTagToMetricIDsRowsInternal(data []byte, items [][]byte, nsPrefix byte)
// into the same new time series from multiple concurrent goroutines. // into the same new time series from multiple concurrent goroutines.
atomic.AddUint64(&indexBlocksWithMetricIDsIncorrectOrder, 1) atomic.AddUint64(&indexBlocksWithMetricIDsIncorrectOrder, 1)
dstData = append(dstData[:0], tmm.dataCopy...) dstData = append(dstData[:0], tmm.dataCopy...)
dstItems = dstItems[:0] dstItems = append(dstItems[:0], tmm.itemsCopy...)
// tmm.itemsCopy can point to overwritten data, so it must be updated if !checkItemsSorted(dstData, dstItems) {
// to point to real data from tmm.dataCopy.
buf := dstData
for _, item := range tmm.itemsCopy {
dstItems = append(dstItems, buf[:len(item)])
buf = buf[len(item):]
}
if !checkItemsSorted(dstItems) {
logger.Panicf("BUG: the original items weren't sorted; items=%q", dstItems) logger.Panicf("BUG: the original items weren't sorted; items=%q", dstItems)
} }
} }
@ -3558,13 +3561,14 @@ func mergeTagToMetricIDsRowsInternal(data []byte, items [][]byte, nsPrefix byte)
var indexBlocksWithMetricIDsIncorrectOrder uint64 var indexBlocksWithMetricIDsIncorrectOrder uint64
var indexBlocksWithMetricIDsProcessed uint64 var indexBlocksWithMetricIDsProcessed uint64
func checkItemsSorted(items [][]byte) bool { func checkItemsSorted(data []byte, items []mergeset.Item) bool {
if len(items) == 0 { if len(items) == 0 {
return true return true
} }
prevItem := items[0] prevItem := items[0].String(data)
for _, currItem := range items[1:] { for _, it := range items[1:] {
if string(prevItem) > string(currItem) { currItem := it.String(data)
if prevItem > currItem {
return false return false
} }
prevItem = currItem prevItem = currItem
@ -3592,7 +3596,7 @@ type tagToMetricIDsRowsMerger struct {
mp tagToMetricIDsRowParser mp tagToMetricIDsRowParser
mpPrev tagToMetricIDsRowParser mpPrev tagToMetricIDsRowParser
itemsCopy [][]byte itemsCopy []mergeset.Item
dataCopy []byte dataCopy []byte
} }
@ -3605,7 +3609,7 @@ func (tmm *tagToMetricIDsRowsMerger) Reset() {
tmm.dataCopy = tmm.dataCopy[:0] tmm.dataCopy = tmm.dataCopy[:0]
} }
func (tmm *tagToMetricIDsRowsMerger) flushPendingMetricIDs(dstData []byte, dstItems [][]byte, mp *tagToMetricIDsRowParser) ([]byte, [][]byte) { func (tmm *tagToMetricIDsRowsMerger) flushPendingMetricIDs(dstData []byte, dstItems []mergeset.Item, mp *tagToMetricIDsRowParser) ([]byte, []mergeset.Item) {
if len(tmm.pendingMetricIDs) == 0 { if len(tmm.pendingMetricIDs) == 0 {
// Nothing to flush // Nothing to flush
return dstData, dstItems return dstData, dstItems
@ -3620,7 +3624,10 @@ func (tmm *tagToMetricIDsRowsMerger) flushPendingMetricIDs(dstData []byte, dstIt
for _, metricID := range tmm.pendingMetricIDs { for _, metricID := range tmm.pendingMetricIDs {
dstData = encoding.MarshalUint64(dstData, metricID) dstData = encoding.MarshalUint64(dstData, metricID)
} }
dstItems = append(dstItems, dstData[dstDataLen:]) dstItems = append(dstItems, mergeset.Item{
Start: uint32(dstDataLen),
End: uint32(len(dstData)),
})
tmm.pendingMetricIDs = tmm.pendingMetricIDs[:0] tmm.pendingMetricIDs = tmm.pendingMetricIDs[:0]
return dstData, dstItems return dstData, dstItems
} }

View file

@ -14,6 +14,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil" "github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding" "github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/mergeset"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/uint64set" "github.com/VictoriaMetrics/VictoriaMetrics/lib/uint64set"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/workingsetcache" "github.com/VictoriaMetrics/VictoriaMetrics/lib/workingsetcache"
) )
@ -36,33 +37,38 @@ func TestMergeTagToMetricIDsRows(t *testing.T) {
f := func(items []string, expectedItems []string) { f := func(items []string, expectedItems []string) {
t.Helper() t.Helper()
var data []byte var data []byte
var itemsB [][]byte var itemsB []mergeset.Item
for _, item := range items { for _, item := range items {
data = append(data, item...) data = append(data, item...)
itemsB = append(itemsB, data[len(data)-len(item):]) itemsB = append(itemsB, mergeset.Item{
Start: uint32(len(data) - len(item)),
End: uint32(len(data)),
})
} }
if !checkItemsSorted(itemsB) { if !checkItemsSorted(data, itemsB) {
t.Fatalf("source items aren't sorted; items:\n%q", itemsB) t.Fatalf("source items aren't sorted; items:\n%q", itemsB)
} }
resultData, resultItemsB := mergeTagToMetricIDsRows(data, itemsB) resultData, resultItemsB := mergeTagToMetricIDsRows(data, itemsB)
if len(resultItemsB) != len(expectedItems) { if len(resultItemsB) != len(expectedItems) {
t.Fatalf("unexpected len(resultItemsB); got %d; want %d", len(resultItemsB), len(expectedItems)) t.Fatalf("unexpected len(resultItemsB); got %d; want %d", len(resultItemsB), len(expectedItems))
} }
if !checkItemsSorted(resultItemsB) { if !checkItemsSorted(resultData, resultItemsB) {
t.Fatalf("result items aren't sorted; items:\n%q", resultItemsB) t.Fatalf("result items aren't sorted; items:\n%q", resultItemsB)
} }
for i, item := range resultItemsB { buf := resultData
if !bytes.HasPrefix(resultData, item) { for i, it := range resultItemsB {
t.Fatalf("unexpected prefix for resultData #%d;\ngot\n%X\nwant\n%X", i, resultData, item) item := it.Bytes(resultData)
if !bytes.HasPrefix(buf, item) {
t.Fatalf("unexpected prefix for resultData #%d;\ngot\n%X\nwant\n%X", i, buf, item)
} }
resultData = resultData[len(item):] buf = buf[len(item):]
} }
if len(resultData) != 0 { if len(buf) != 0 {
t.Fatalf("unexpected tail left in resultData: %X", resultData) t.Fatalf("unexpected tail left in resultData: %X", buf)
} }
var resultItems []string var resultItems []string
for _, item := range resultItemsB { for _, it := range resultItemsB {
resultItems = append(resultItems, string(item)) resultItems = append(resultItems, string(it.Bytes(resultData)))
} }
if !reflect.DeepEqual(expectedItems, resultItems) { if !reflect.DeepEqual(expectedItems, resultItems) {
t.Fatalf("unexpected items;\ngot\n%X\nwant\n%X", resultItems, expectedItems) t.Fatalf("unexpected items;\ngot\n%X\nwant\n%X", resultItems, expectedItems)

View file

@ -145,21 +145,6 @@ func (idxb *indexBlock) SizeBytes() int {
return cap(idxb.bhs) * int(unsafe.Sizeof(blockHeader{})) return cap(idxb.bhs) * int(unsafe.Sizeof(blockHeader{}))
} }
func getIndexBlock() *indexBlock {
v := indexBlockPool.Get()
if v == nil {
return &indexBlock{}
}
return v.(*indexBlock)
}
func putIndexBlock(ib *indexBlock) {
ib.bhs = ib.bhs[:0]
indexBlockPool.Put(ib)
}
var indexBlockPool sync.Pool
type indexBlockCache struct { type indexBlockCache struct {
// Put atomic counters to the top of struct in order to align them to 8 bytes on 32-bit architectures. // Put atomic counters to the top of struct in order to align them to 8 bytes on 32-bit architectures.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/212 // See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/212
@ -198,12 +183,6 @@ func newIndexBlockCache() *indexBlockCache {
func (ibc *indexBlockCache) MustClose(isBig bool) { func (ibc *indexBlockCache) MustClose(isBig bool) {
close(ibc.cleanerStopCh) close(ibc.cleanerStopCh)
ibc.cleanerWG.Wait() ibc.cleanerWG.Wait()
// It is safe returning ibc.m itemst to the pool, since Reset must
// be called only when no other goroutines access ibc entries.
for _, ibe := range ibc.m {
putIndexBlock(ibe.ib)
}
ibc.m = nil ibc.m = nil
} }
@ -259,7 +238,6 @@ func (ibc *indexBlockCache) Put(k uint64, ib *indexBlock) {
// Remove 10% of items from the cache. // Remove 10% of items from the cache.
overflow = int(float64(len(ibc.m)) * 0.1) overflow = int(float64(len(ibc.m)) * 0.1)
for k := range ibc.m { for k := range ibc.m {
// Do not call putIndexBlock on ibc.m entries, since they may be used by concurrent goroutines.
delete(ibc.m, k) delete(ibc.m, k)
overflow-- overflow--
if overflow == 0 { if overflow == 0 {

View file

@ -218,7 +218,7 @@ func (ps *partSearch) readIndexBlock(mr *metaindexRow) (*indexBlock, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot decompress index block: %w", err) return nil, fmt.Errorf("cannot decompress index block: %w", err)
} }
ib := getIndexBlock() ib := &indexBlock{}
ib.bhs, err = unmarshalBlockHeaders(ib.bhs[:0], ps.indexBuf, int(mr.BlockHeadersCount)) ib.bhs, err = unmarshalBlockHeaders(ib.bhs[:0], ps.indexBuf, int(mr.BlockHeadersCount))
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot unmarshal index block: %w", err) return nil, fmt.Errorf("cannot unmarshal index block: %w", err)

View file

@ -1469,12 +1469,6 @@ func appendPartsToMerge(dst, src []*partWrapper, maxPartsToMerge int, maxRows ui
continue continue
} }
rowsCount := getRowsCount(a) rowsCount := getRowsCount(a)
if rowsCount < 1e6 && len(a) < maxPartsToMerge {
// Do not merge parts with too small number of rows if the number of source parts
// isn't equal to maxPartsToMerge. This should reduce CPU usage and disk IO usage
// for small parts merge.
continue
}
if rowsCount > maxRows { if rowsCount > maxRows {
// There is no need in verifying remaining parts with higher number of rows // There is no need in verifying remaining parts with higher number of rows
needFreeSpace = true needFreeSpace = true

View file

@ -26,9 +26,11 @@ func TestAppendPartsToMerge(t *testing.T) {
testAppendPartsToMerge(t, 2, []uint64{4, 2, 4}, []uint64{4, 4}) testAppendPartsToMerge(t, 2, []uint64{4, 2, 4}, []uint64{4, 4})
testAppendPartsToMerge(t, 2, []uint64{1, 3, 7, 2}, nil) testAppendPartsToMerge(t, 2, []uint64{1, 3, 7, 2}, nil)
testAppendPartsToMerge(t, 3, []uint64{1, 3, 7, 2}, []uint64{1, 2, 3}) testAppendPartsToMerge(t, 3, []uint64{1, 3, 7, 2}, []uint64{1, 2, 3})
testAppendPartsToMerge(t, 4, []uint64{1, 3, 7, 2}, nil) testAppendPartsToMerge(t, 4, []uint64{1, 3, 7, 2}, []uint64{1, 2, 3})
testAppendPartsToMerge(t, 5, []uint64{1, 3, 7, 2}, nil)
testAppendPartsToMerge(t, 4, []uint64{1e6, 3e6, 7e6, 2e6}, []uint64{1e6, 2e6, 3e6}) testAppendPartsToMerge(t, 4, []uint64{1e6, 3e6, 7e6, 2e6}, []uint64{1e6, 2e6, 3e6})
testAppendPartsToMerge(t, 4, []uint64{2, 3, 7, 2}, []uint64{2, 2, 3, 7}) testAppendPartsToMerge(t, 4, []uint64{2, 3, 7, 2}, []uint64{2, 2, 3})
testAppendPartsToMerge(t, 5, []uint64{2, 3, 7, 2}, nil)
testAppendPartsToMerge(t, 3, []uint64{11, 1, 10, 100, 10}, []uint64{10, 10, 11}) testAppendPartsToMerge(t, 3, []uint64{11, 1, 10, 100, 10}, []uint64{10, 10, 11})
} }

View file

@ -93,7 +93,7 @@ exposed from your application.
#### How to implement [CounterVec](https://godoc.org/github.com/prometheus/client_golang/prometheus#CounterVec) in `metrics`? #### How to implement [CounterVec](https://godoc.org/github.com/prometheus/client_golang/prometheus#CounterVec) in `metrics`?
Just use [GetOrCreateCounter](http://godoc.org/github.com/VictoriaMetrics/metrics#GetOrCreateCounter) Just use [GetOrCreateCounter](http://godoc.org/github.com/VictoriaMetrics/metrics#GetOrCreateCounter)
instead of `CounterVec.With`. See [this example](https://godoc.org/github.com/VictoriaMetrics/metrics#example-Counter--Vec) for details. instead of `CounterVec.With`. See [this example](https://pkg.go.dev/github.com/VictoriaMetrics/metrics#example-Counter-Vec) for details.
#### Why [Histogram](http://godoc.org/github.com/VictoriaMetrics/metrics#Histogram) buckets contain `vmrange` labels instead of `le` labels like in Prometheus histograms? #### Why [Histogram](http://godoc.org/github.com/VictoriaMetrics/metrics#Histogram) buckets contain `vmrange` labels instead of `le` labels like in Prometheus histograms?

View file

@ -12,8 +12,6 @@ import (
"time" "time"
) )
const statFilepath = "/proc/self/stat"
// See https://github.com/prometheus/procfs/blob/a4ac0826abceb44c40fc71daed2b301db498b93e/proc_stat.go#L40 . // See https://github.com/prometheus/procfs/blob/a4ac0826abceb44c40fc71daed2b301db498b93e/proc_stat.go#L40 .
const userHZ = 100 const userHZ = 100
@ -44,6 +42,7 @@ type procStat struct {
} }
func writeProcessMetrics(w io.Writer) { func writeProcessMetrics(w io.Writer) {
statFilepath := "/proc/self/stat"
data, err := ioutil.ReadFile(statFilepath) data, err := ioutil.ReadFile(statFilepath)
if err != nil { if err != nil {
log.Printf("ERROR: cannot open %s: %s", statFilepath, err) log.Printf("ERROR: cannot open %s: %s", statFilepath, err)
@ -68,7 +67,8 @@ func writeProcessMetrics(w io.Writer) {
} }
// It is expensive obtaining `process_open_fds` when big number of file descriptors is opened, // It is expensive obtaining `process_open_fds` when big number of file descriptors is opened,
// don't do it here. // so don't do it here.
// See writeFDMetrics instead.
utime := float64(p.Utime) / userHZ utime := float64(p.Utime) / userHZ
stime := float64(p.Stime) / userHZ stime := float64(p.Stime) / userHZ
@ -81,6 +81,54 @@ func writeProcessMetrics(w io.Writer) {
fmt.Fprintf(w, "process_resident_memory_bytes %d\n", p.Rss*4096) fmt.Fprintf(w, "process_resident_memory_bytes %d\n", p.Rss*4096)
fmt.Fprintf(w, "process_start_time_seconds %d\n", startTimeSeconds) fmt.Fprintf(w, "process_start_time_seconds %d\n", startTimeSeconds)
fmt.Fprintf(w, "process_virtual_memory_bytes %d\n", p.Vsize) fmt.Fprintf(w, "process_virtual_memory_bytes %d\n", p.Vsize)
writeIOMetrics(w)
}
func writeIOMetrics(w io.Writer) {
ioFilepath := "/proc/self/io"
data, err := ioutil.ReadFile(ioFilepath)
if err != nil {
log.Printf("ERROR: cannot open %q: %s", ioFilepath, err)
}
getInt := func(s string) int64 {
n := strings.IndexByte(s, ' ')
if n < 0 {
log.Printf("ERROR: cannot find whitespace in %q at %q", s, ioFilepath)
return 0
}
v, err := strconv.ParseInt(s[n+1:], 10, 64)
if err != nil {
log.Printf("ERROR: cannot parse %q at %q: %s", s, ioFilepath, err)
return 0
}
return v
}
var rchar, wchar, syscr, syscw, readBytes, writeBytes int64
lines := strings.Split(string(data), "\n")
for _, s := range lines {
s = strings.TrimSpace(s)
switch {
case strings.HasPrefix(s, "rchar: "):
rchar = getInt(s)
case strings.HasPrefix(s, "wchar: "):
wchar = getInt(s)
case strings.HasPrefix(s, "syscr: "):
syscr = getInt(s)
case strings.HasPrefix(s, "syscw: "):
syscw = getInt(s)
case strings.HasPrefix(s, "read_bytes: "):
readBytes = getInt(s)
case strings.HasPrefix(s, "write_bytes: "):
writeBytes = getInt(s)
}
}
fmt.Fprintf(w, "process_io_read_bytes_total %d\n", rchar)
fmt.Fprintf(w, "process_io_written_bytes_total %d\n", wchar)
fmt.Fprintf(w, "process_io_read_syscalls_total %d\n", syscr)
fmt.Fprintf(w, "process_io_write_syscalls_total %d\n", syscw)
fmt.Fprintf(w, "process_io_storage_read_bytes_total %d\n", readBytes)
fmt.Fprintf(w, "process_io_storage_written_bytes_total %d\n", writeBytes)
} }
var startTimeSeconds = time.Now().Unix() var startTimeSeconds = time.Now().Unix()

View file

@ -38,6 +38,7 @@ var rollupFuncs = map[string]bool{
"distinct_over_time": true, "distinct_over_time": true,
"increases_over_time": true, "increases_over_time": true,
"decreases_over_time": true, "decreases_over_time": true,
"increase_pure": true,
"integrate": true, "integrate": true,
"ideriv": true, "ideriv": true,
"lifetime": true, "lifetime": true,

View file

@ -10,6 +10,7 @@ var transformFuncs = map[string]bool{
"abs": true, "abs": true,
"absent": true, "absent": true,
"ceil": true, "ceil": true,
"clamp": true,
"clamp_max": true, "clamp_max": true,
"clamp_min": true, "clamp_min": true,
"day_of_month": true, "day_of_month": true,
@ -28,6 +29,7 @@ var transformFuncs = map[string]bool{
"month": true, "month": true,
"round": true, "round": true,
"scalar": true, "scalar": true,
"sign": true,
"sort": true, "sort": true,
"sort_desc": true, "sort_desc": true,
"sqrt": true, "sqrt": true,

4
vendor/modules.txt vendored
View file

@ -14,9 +14,9 @@ github.com/VictoriaMetrics/fastcache
github.com/VictoriaMetrics/fasthttp github.com/VictoriaMetrics/fasthttp
github.com/VictoriaMetrics/fasthttp/fasthttputil github.com/VictoriaMetrics/fasthttp/fasthttputil
github.com/VictoriaMetrics/fasthttp/stackless github.com/VictoriaMetrics/fasthttp/stackless
# github.com/VictoriaMetrics/metrics v1.14.0 # github.com/VictoriaMetrics/metrics v1.15.0
github.com/VictoriaMetrics/metrics github.com/VictoriaMetrics/metrics
# github.com/VictoriaMetrics/metricsql v0.10.1 # github.com/VictoriaMetrics/metricsql v0.12.0
github.com/VictoriaMetrics/metricsql github.com/VictoriaMetrics/metricsql
github.com/VictoriaMetrics/metricsql/binaryop github.com/VictoriaMetrics/metricsql/binaryop
# github.com/VividCortex/ewma v1.1.1 # github.com/VividCortex/ewma v1.1.1