mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-11 14:53:49 +00:00
Merge branch 'public-single-node' into pmm-6401-read-prometheus-data-files
This commit is contained in:
commit
0ad887fd4d
76 changed files with 3150 additions and 2095 deletions
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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]) {
|
||||||
|
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
|
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) {
|
||||||
|
|
|
@ -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()`)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 we’ve 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 we’ve 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 lightweight 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 we’ve 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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
4
go.mod
|
@ -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
8
go.sum
|
@ -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=
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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++
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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++
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,23 +138,13 @@ 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 {
|
||||||
|
n += bhs[i].SizeBytes()
|
||||||
}
|
}
|
||||||
|
return n
|
||||||
func getIndexBlock() *indexBlock {
|
|
||||||
v := indexBlockPool.Get()
|
|
||||||
if v == nil {
|
|
||||||
return &indexBlock{}
|
|
||||||
}
|
}
|
||||||
return v.(*indexBlock)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,7 +52,8 @@ 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"},
|
SourceLabels: []string{"foo", "bar"},
|
||||||
Separator: ";",
|
Separator: ";",
|
||||||
|
@ -61,20 +62,22 @@ func TestParseRelabelConfigsSuccess(t *testing.T) {
|
||||||
Replacement: "$1",
|
Replacement: "$1",
|
||||||
Action: "replace",
|
Action: "replace",
|
||||||
|
|
||||||
|
regexOriginal: defaultOriginalRegexForRelabelConfig,
|
||||||
hasCaptureGroupInReplacement: true,
|
hasCaptureGroupInReplacement: true,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
@ -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,30 +24,33 @@ 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 {
|
||||||
|
tmp := prc.apply(labels, labelsOffset)
|
||||||
if len(tmp) == labelsOffset {
|
if len(tmp) == labelsOffset {
|
||||||
// All the labels have been removed.
|
// All the labels have been removed.
|
||||||
return tmp
|
return tmp
|
||||||
}
|
}
|
||||||
labels = tmp
|
labels = tmp
|
||||||
}
|
}
|
||||||
|
}
|
||||||
labels = removeEmptyLabels(labels, labelsOffset)
|
labels = removeEmptyLabels(labels, labelsOffset)
|
||||||
if isFinalize {
|
if isFinalize {
|
||||||
labels = FinalizeLabels(labels[:labelsOffset], labels[labelsOffset:])
|
labels = FinalizeLabels(labels[:labelsOffset], labels[labelsOffset:])
|
||||||
|
@ -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" {
|
||||||
|
// Fast path for the rule that copies source label values to destination:
|
||||||
|
// - source_labels: [...]
|
||||||
|
// target_label: foobar
|
||||||
|
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
|
// - target_label: foobar
|
||||||
// replacement: something-here
|
// replacement: something-here
|
||||||
relabelBufPool.Put(bb)
|
relabelBufPool.Put(bb)
|
||||||
return setLabelValue(labels, labelsOffset, prc.TargetLabel, prc.Replacement)
|
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)
|
relabelBufPool.Put(bb)
|
||||||
return labels
|
valueStr, ok := prc.replaceStringSubmatches(sourceStr, prc.Replacement, prc.hasCaptureGroupInReplacement)
|
||||||
|
if ok {
|
||||||
|
labels = setLabelValue(labels, labelsOffset, prc.TargetLabel, valueStr)
|
||||||
}
|
}
|
||||||
sourceStr := string(bb.B) // Make a copy of bb, since it can be returned from ReplaceAllString
|
return labels
|
||||||
relabelBufPool.Put(bb)
|
|
||||||
valueStr := prc.Regex.ReplaceAllString(sourceStr, prc.Replacement)
|
|
||||||
return setLabelValue(labels, labelsOffset, prc.TargetLabel, valueStr)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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)
|
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)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -92,6 +97,7 @@ type ScrapeConfig struct {
|
||||||
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)
|
|
||||||
return dst, false
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
target := metaLabels["__address__"]
|
target := metaLabels["__address__"]
|
||||||
var err error
|
sw, err := swc.getScrapeWork(target, nil, metaLabels)
|
||||||
dst, err = appendScrapeWork(dst, swc, target, nil, metaLabels)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("error when parsing `%s` target %q for `job_name` %q: %s; skipping it", sectionName, target, swc.jobName, err)
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
for _, metaLabels := range targetLabels {
|
||||||
|
workCh <- metaLabels
|
||||||
|
}
|
||||||
|
close(workCh)
|
||||||
|
for range targetLabels {
|
||||||
|
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) {
|
||||||
|
|
|
@ -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
|
||||||
|
@ -1077,7 +1068,10 @@ scrape_configs:
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AuthConfig: &promauth.Config{},
|
AuthConfig: &promauth.Config{},
|
||||||
MetricRelabelConfigs: prcs,
|
MetricRelabelConfigs: mustParseRelabelConfigs(`
|
||||||
|
- source_labels: [foo]
|
||||||
|
target_label: abc
|
||||||
|
`),
|
||||||
jobNameOriginal: "foo",
|
jobNameOriginal: "foo",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -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.
|
||||||
|
@ -1328,6 +1323,7 @@ scrape_configs:
|
||||||
DisableKeepAlive: true,
|
DisableKeepAlive: true,
|
||||||
DisableCompression: true,
|
DisableCompression: true,
|
||||||
StreamParse: true,
|
StreamParse: true,
|
||||||
|
ScrapeAlignInterval: time.Second,
|
||||||
jobNameOriginal: "snmp",
|
jobNameOriginal: "snmp",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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`")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
//
|
||||||
|
// See https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes
|
||||||
|
type WatchEvent struct {
|
||||||
|
Type string
|
||||||
|
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 {
|
if len(query) > 0 {
|
||||||
path += "?" + query
|
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
|
||||||
}
|
}
|
||||||
return cfg.client.GetAPIResponse(path)
|
|
||||||
}
|
}
|
||||||
|
|
162
lib/promscrape/discovery/kubernetes/api_test.go
Normal file
162
lib/promscrape/discovery/kubernetes/api_test.go
Normal 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",
|
||||||
|
})
|
||||||
|
}
|
|
@ -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, "&")
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 objectsByKey, epsl.Metadata, nil
|
||||||
}
|
|
||||||
return result, 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
// getTargetLabels returns labels for eps.
|
||||||
|
//
|
||||||
|
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEndpointSlices retrieves endpointSlice with given apiConfig
|
|
||||||
func getEndpointSlices(cfg *apiConfig) ([]EndpointSlice, error) {
|
|
||||||
if len(cfg.namespaces) == 0 {
|
|
||||||
return getEndpointSlicesByPath(cfg, "/apis/discovery.k8s.io/v1beta1/endpointslices")
|
|
||||||
}
|
|
||||||
// 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.
|
||||||
|
|
|
@ -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) {
|
|
||||||
t.Fatalf("unexpected labels,\ngot:\n%v,\nwant:\n%v", sortedLables, expectedLabels)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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{
|
discoveryutils.GetSortedLabels(map[string]string{
|
||||||
"__address__": "127.0.0.1:8085",
|
"__address__": "10.244.0.3:53",
|
||||||
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
|
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
|
||||||
"__meta_kubernetes_endpointslice_address_target_name": "main-pod",
|
"__meta_kubernetes_endpointslice_address_target_name": "coredns-66bff467f8-z8czk",
|
||||||
"__meta_kubernetes_endpointslice_address_type": "ipv4",
|
"__meta_kubernetes_endpointslice_address_type": "IPv4",
|
||||||
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
|
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
|
||||||
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_topoligy_io_zone": "gce-1",
|
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_io_hostname": "kind-control-plane",
|
||||||
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_topoligy_io_zone": "true",
|
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_io_hostname": "true",
|
||||||
"__meta_kubernetes_endpointslice_endpoint_hostname": "node-1",
|
"__meta_kubernetes_endpointslice_name": "kube-dns-22mvb",
|
||||||
"__meta_kubernetes_endpointslice_name": "fake-esl",
|
"__meta_kubernetes_endpointslice_port": "53",
|
||||||
"__meta_kubernetes_endpointslice_port": "8085",
|
"__meta_kubernetes_endpointslice_port_name": "dns-tcp",
|
||||||
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
|
"__meta_kubernetes_endpointslice_port_protocol": "TCP",
|
||||||
"__meta_kubernetes_endpointslice_port_name": "http",
|
"__meta_kubernetes_namespace": "kube-system",
|
||||||
"__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{
|
discoveryutils.GetSortedLabels(map[string]string{
|
||||||
"__address__": "127.0.0.1:8085",
|
"__address__": "10.244.0.3:9153",
|
||||||
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
|
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
|
||||||
"__meta_kubernetes_endpointslice_address_target_name": "main-pod",
|
"__meta_kubernetes_endpointslice_address_target_name": "coredns-66bff467f8-z8czk",
|
||||||
"__meta_kubernetes_endpointslice_address_type": "ipv4",
|
"__meta_kubernetes_endpointslice_address_type": "IPv4",
|
||||||
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
|
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
|
||||||
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_topoligy_io_zone": "gce-1",
|
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_io_hostname": "kind-control-plane",
|
||||||
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_topoligy_io_zone": "true",
|
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_io_hostname": "true",
|
||||||
"__meta_kubernetes_endpointslice_endpoint_hostname": "node-1",
|
"__meta_kubernetes_endpointslice_name": "kube-dns-22mvb",
|
||||||
"__meta_kubernetes_endpointslice_name": "custom-esl",
|
"__meta_kubernetes_endpointslice_port": "9153",
|
||||||
"__meta_kubernetes_endpointslice_port": "8085",
|
"__meta_kubernetes_endpointslice_port_name": "metrics",
|
||||||
"__meta_kubernetes_endpointslice_port_app_protocol": "http",
|
"__meta_kubernetes_endpointslice_port_protocol": "TCP",
|
||||||
"__meta_kubernetes_endpointslice_port_name": "http",
|
"__meta_kubernetes_namespace": "kube-system",
|
||||||
"__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{
|
discoveryutils.GetSortedLabels(map[string]string{
|
||||||
"__address__": "192.168.11.5:8011",
|
"__address__": "10.244.0.3:53",
|
||||||
"__meta_kubernetes_namespace": "monitoring",
|
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
|
||||||
"__meta_kubernetes_pod_annotation_pod_annotations_1": "annotation-value-1",
|
"__meta_kubernetes_endpointslice_address_target_name": "coredns-66bff467f8-z8czk",
|
||||||
"__meta_kubernetes_pod_annotationpresent_pod_annotations_1": "true",
|
"__meta_kubernetes_endpointslice_address_type": "IPv4",
|
||||||
"__meta_kubernetes_pod_container_name": "container-1",
|
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
|
||||||
"__meta_kubernetes_pod_container_port_name": "dns",
|
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_io_hostname": "kind-control-plane",
|
||||||
"__meta_kubernetes_pod_container_port_number": "8011",
|
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_io_hostname": "true",
|
||||||
"__meta_kubernetes_pod_container_port_protocol": "udp",
|
"__meta_kubernetes_endpointslice_name": "kube-dns-22mvb",
|
||||||
"__meta_kubernetes_pod_host_ip": "172.15.1.1",
|
"__meta_kubernetes_endpointslice_port": "53",
|
||||||
"__meta_kubernetes_pod_ip": "192.168.11.5",
|
"__meta_kubernetes_endpointslice_port_name": "dns",
|
||||||
"__meta_kubernetes_pod_label_pod_label_1": "pod-value-1",
|
"__meta_kubernetes_endpointslice_port_protocol": "UDP",
|
||||||
"__meta_kubernetes_pod_label_pod_label_2": "pod-value-2",
|
"__meta_kubernetes_namespace": "kube-system",
|
||||||
"__meta_kubernetes_pod_labelpresent_pod_label_1": "true",
|
}),
|
||||||
"__meta_kubernetes_pod_labelpresent_pod_label_2": "true",
|
discoveryutils.GetSortedLabels(map[string]string{
|
||||||
"__meta_kubernetes_pod_name": "main-pod",
|
"__address__": "10.244.0.4:53",
|
||||||
"__meta_kubernetes_pod_node_name": "node-2",
|
"__meta_kubernetes_endpointslice_address_target_kind": "Pod",
|
||||||
"__meta_kubernetes_pod_phase": "",
|
"__meta_kubernetes_endpointslice_address_target_name": "coredns-66bff467f8-kpbhk",
|
||||||
"__meta_kubernetes_pod_ready": "unknown",
|
"__meta_kubernetes_endpointslice_address_type": "IPv4",
|
||||||
"__meta_kubernetes_pod_uid": "some-pod-uuid",
|
"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
|
||||||
"__meta_kubernetes_service_cluster_ip": "",
|
"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_io_hostname": "kind-control-plane",
|
||||||
"__meta_kubernetes_service_label_service_label_1": "value-1",
|
"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_io_hostname": "true",
|
||||||
"__meta_kubernetes_service_label_service_label_2": "value-2",
|
"__meta_kubernetes_endpointslice_name": "kube-dns-22mvb",
|
||||||
"__meta_kubernetes_service_labelpresent_service_label_1": "true",
|
"__meta_kubernetes_endpointslice_port": "53",
|
||||||
"__meta_kubernetes_service_labelpresent_service_label_2": "true",
|
"__meta_kubernetes_endpointslice_port_name": "dns-tcp",
|
||||||
"__meta_kubernetes_service_name": "custom-esl",
|
"__meta_kubernetes_endpointslice_port_protocol": "TCP",
|
||||||
"__meta_kubernetes_service_type": "ClusterIP",
|
"__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",
|
||||||
}),
|
}),
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
if !reflect.DeepEqual(sortedLabelss, expectedLabelss) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Fatalf("unexpected labels,\ngot:\n%v,\nwant:\n%v", sortedLabelss, expectedLabelss)
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var ms []map[string]string
|
|
||||||
for _, ig := range igs {
|
|
||||||
ms = ig.appendTargetLabels(ms)
|
|
||||||
}
|
|
||||||
return ms, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIngresses(cfg *apiConfig) ([]Ingress, error) {
|
func parseIngressList(data []byte) (map[string]object, ListMeta, error) {
|
||||||
if len(cfg.namespaces) == 0 {
|
var igl IngressList
|
||||||
return getIngressesByPath(cfg, "/apis/extensions/v1beta1/ingresses")
|
if err := json.Unmarshal(data, &igl); err != nil {
|
||||||
|
return nil, igl.Metadata, fmt.Errorf("cannot unmarshal IngressList 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 _, ig := range igl.Items {
|
||||||
cfgCopy := *cfg
|
objectsByKey[ig.key()] = ig
|
||||||
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 objectsByKey, igl.Metadata, nil
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIngressesByPath(cfg *apiConfig, path string) ([]Ingress, error) {
|
func parseIngress(data []byte) (object, error) {
|
||||||
data, err := getAPIResponse(cfg, "ingress", path)
|
var ig Ingress
|
||||||
if err != nil {
|
if err := json.Unmarshal(data, &ig); err != nil {
|
||||||
return nil, fmt.Errorf("cannot obtain ingresses data from API server: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
igl, err := parseIngressList(data)
|
return &ig, nil
|
||||||
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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
nl, err := parseNodeList(data)
|
|
||||||
if err != nil {
|
func parseNodeList(data []byte) (map[string]object, ListMeta, error) {
|
||||||
return nil, fmt.Errorf("cannot parse nodes response from API server: %w", err)
|
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)
|
||||||
}
|
}
|
||||||
var ms []map[string]string
|
objectsByKey := make(map[string]object)
|
||||||
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 {
|
||||||
|
|
|
@ -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,59 +227,17 @@ 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")
|
|
||||||
}
|
|
||||||
expectedLabels := discoveryutils.GetSortedLabels(map[string]string{
|
|
||||||
"beta.kubernetes.io/arch": "amd64",
|
|
||||||
"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",
|
"instance": "m01",
|
||||||
"__address__": "172.17.0.2:10250",
|
"__address__": "172.17.0.2:10250",
|
||||||
"__meta_kubernetes_node_name": "m01",
|
"__meta_kubernetes_node_name": "m01",
|
||||||
|
@ -315,8 +274,20 @@ func TestParseNodeListSuccess(t *testing.T) {
|
||||||
|
|
||||||
"__meta_kubernetes_node_address_InternalIP": "172.17.0.2",
|
"__meta_kubernetes_node_address_InternalIP": "172.17.0.2",
|
||||||
"__meta_kubernetes_node_address_Hostname": "m01",
|
"__meta_kubernetes_node_address_Hostname": "m01",
|
||||||
})
|
}),
|
||||||
if !reflect.DeepEqual(labels, expectedLabels) {
|
}
|
||||||
t.Fatalf("unexpected labels:\ngot\n%v\nwant\n%v", labels, expectedLabels)
|
if !reflect.DeepEqual(sortedLabelss, expectedLabelss) {
|
||||||
|
t.Fatalf("unexpected labels:\ngot\n%v\nwant\n%v", sortedLabelss, expectedLabelss)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var ms []map[string]string
|
|
||||||
for _, p := range pods {
|
|
||||||
ms = p.appendTargetLabels(ms)
|
|
||||||
}
|
|
||||||
return ms, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPods(cfg *apiConfig) ([]Pod, error) {
|
func parsePodList(data []byte) (map[string]object, ListMeta, error) {
|
||||||
if len(cfg.namespaces) == 0 {
|
var pl PodList
|
||||||
return getPodsByPath(cfg, "/api/v1/pods")
|
if err := json.Unmarshal(data, &pl); err != nil {
|
||||||
|
return nil, pl.Metadata, fmt.Errorf("cannot unmarshal PodList 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 _, p := range pl.Items {
|
||||||
cfgCopy := *cfg
|
objectsByKey[p.key()] = p
|
||||||
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 objectsByKey, pl.Metadata, nil
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPodsByPath(cfg *apiConfig, path string) ([]Pod, error) {
|
func parsePod(data []byte) (object, error) {
|
||||||
data, err := getAPIResponse(cfg, "pod", path)
|
var p Pod
|
||||||
if err != nil {
|
if err := json.Unmarshal(data, &p); err != nil {
|
||||||
return nil, fmt.Errorf("cannot obtain pods data from API server: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
pl, err := parsePodList(data)
|
return &p, nil
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var ms []map[string]string
|
|
||||||
for _, svc := range svcs {
|
|
||||||
ms = svc.appendTargetLabels(ms)
|
|
||||||
}
|
|
||||||
return ms, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServices(cfg *apiConfig) ([]Service, error) {
|
func parseServiceList(data []byte) (map[string]object, ListMeta, error) {
|
||||||
if len(cfg.namespaces) == 0 {
|
var sl ServiceList
|
||||||
return getServicesByPath(cfg, "/api/v1/services")
|
if err := json.Unmarshal(data, &sl); err != nil {
|
||||||
|
return nil, sl.Metadata, fmt.Errorf("cannot unmarshal ServiceList 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 _, s := range sl.Items {
|
||||||
cfgCopy := *cfg
|
objectsByKey[s.key()] = s
|
||||||
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 objectsByKey, sl.Metadata, nil
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServicesByPath(cfg *apiConfig, path string) ([]Service, error) {
|
func parseService(data []byte) (object, error) {
|
||||||
data, err := getAPIResponse(cfg, "service", path)
|
var s Service
|
||||||
if err != nil {
|
if err := json.Unmarshal(data, &s); err != nil {
|
||||||
return nil, fmt.Errorf("cannot obtain services data from API server: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
sl, err := parseServiceList(data)
|
return &s, nil
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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{}) {
|
||||||
|
scrapeInterval := sw.Config.ScrapeInterval
|
||||||
|
var randSleep uint64
|
||||||
|
if sw.Config.ScrapeAlignInterval <= 0 {
|
||||||
// Calculate start time for the first scrape from ScrapeURL and labels.
|
// Calculate start time for the first scrape from ScrapeURL and labels.
|
||||||
// This should spread load when scraping many targets with different
|
// This should spread load when scraping many targets with different
|
||||||
// scrape urls and labels.
|
// scrape urls and labels.
|
||||||
// This also makes consistent scrape times across restarts
|
// This also makes consistent scrape times across restarts
|
||||||
// for a target with the same ScrapeURL and labels.
|
// for a target with the same ScrapeURL and labels.
|
||||||
scrapeInterval := sw.Config.ScrapeInterval
|
|
||||||
key := fmt.Sprintf("ScrapeURL=%s, Labels=%s", sw.Config.ScrapeURL, sw.Config.LabelsString())
|
key := fmt.Sprintf("ScrapeURL=%s, Labels=%s", sw.Config.ScrapeURL, sw.Config.LabelsString())
|
||||||
h := uint32(xxhash.Sum64([]byte(key)))
|
h := uint32(xxhash.Sum64([]byte(key)))
|
||||||
randSleep := uint64(float64(scrapeInterval) * (float64(h) / (1 << 32)))
|
randSleep = uint64(float64(scrapeInterval) * (float64(h) / (1 << 32)))
|
||||||
sleepOffset := uint64(time.Now().UnixNano()) % uint64(scrapeInterval)
|
sleepOffset := uint64(time.Now().UnixNano()) % uint64(scrapeInterval)
|
||||||
if randSleep < sleepOffset {
|
if randSleep < sleepOffset {
|
||||||
randSleep += uint64(scrapeInterval)
|
randSleep += uint64(scrapeInterval)
|
||||||
}
|
}
|
||||||
randSleep -= sleepOffset
|
randSleep -= sleepOffset
|
||||||
|
} else {
|
||||||
|
d := uint64(sw.Config.ScrapeAlignInterval)
|
||||||
|
randSleep = d - uint64(time.Now().UnixNano())%d
|
||||||
|
randSleep %= uint64(scrapeInterval)
|
||||||
|
}
|
||||||
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:])
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -154,8 +154,8 @@ func openIndexDB(path string, metricIDCache, metricNameCache, tsidCache *working
|
||||||
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,10 +2797,13 @@ 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.
|
||||||
|
// There is no need in spending CPU resources on updating stats for slow tag filters.
|
||||||
|
if loopsCount <= 10e6 {
|
||||||
loopsCount = 0
|
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
|
||||||
// by temporary persisting its position in the tag filters list.
|
// by temporary persisting its position in the tag filters list.
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
vendor/github.com/VictoriaMetrics/metrics/README.md
generated
vendored
2
vendor/github.com/VictoriaMetrics/metrics/README.md
generated
vendored
|
@ -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?
|
||||||
|
|
54
vendor/github.com/VictoriaMetrics/metrics/process_metrics_linux.go
generated
vendored
54
vendor/github.com/VictoriaMetrics/metrics/process_metrics_linux.go
generated
vendored
|
@ -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()
|
||||||
|
|
1
vendor/github.com/VictoriaMetrics/metricsql/rollup.go
generated
vendored
1
vendor/github.com/VictoriaMetrics/metricsql/rollup.go
generated
vendored
|
@ -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,
|
||||||
|
|
2
vendor/github.com/VictoriaMetrics/metricsql/transform.go
generated
vendored
2
vendor/github.com/VictoriaMetrics/metricsql/transform.go
generated
vendored
|
@ -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
4
vendor/modules.txt
vendored
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue