From a7168d86844b46e22109899424bab23861ee2795 Mon Sep 17 00:00:00 2001 From: Andrii Chubatiuk Date: Fri, 29 May 2026 09:12:36 +0300 Subject: [PATCH] vmcluster/vmsingle: support downsampling and retention filters --- api/operator/v1beta1/vmcluster_types.go | 28 +++ api/operator/v1beta1/vmcluster_types_test.go | 142 +++++++++++++ api/operator/v1beta1/vmextra_types.go | 200 ++++++++++++++++++ api/operator/v1beta1/vmsingle_types.go | 29 +++ api/operator/v1beta1/vmsingle_types_test.go | 160 ++++++++++++++ api/operator/v1beta1/zz_generated.deepcopy.go | 92 ++++++++ config/crd/overlay/crd.descriptionless.yaml | 76 +++++++ config/crd/overlay/crd.yaml | 158 ++++++++++++++ docs/CHANGELOG.md | 6 +- docs/api.md | 53 +++++ docs/resources/vmcluster.md | 60 +++--- docs/resources/vmsingle.md | 48 +++-- .../operator/factory/vmcluster/vmcluster.go | 38 +++- .../operator/factory/vmsingle/vmsingle.go | 19 ++ 14 files changed, 1064 insertions(+), 45 deletions(-) diff --git a/api/operator/v1beta1/vmcluster_types.go b/api/operator/v1beta1/vmcluster_types.go index 56916a423..b23f3fde7 100644 --- a/api/operator/v1beta1/vmcluster_types.go +++ b/api/operator/v1beta1/vmcluster_types.go @@ -56,6 +56,11 @@ type VMClusterSpec struct { // +optional License *License `json:"license,omitempty"` + // Downsampling defines downsampling rules applied to vmselect and vmstorage components. + // Requires enterprise license. See https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#downsampling + // +optional + Downsampling *DownsamplingConfig `json:"downsampling,omitempty"` + // +optional VMSelect *VMSelect `json:"vmselect,omitempty"` // +optional @@ -462,6 +467,10 @@ type VMStorage struct { // VMBackup configuration for backup // +optional VMBackup *VMBackup `json:"vmBackup,omitempty"` + // RetentionFilters defines per-series retention filters for vmstorage. + // Requires enterprise license. See https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#retention-filters + // +optional + RetentionFilters []RetentionFilter `json:"retentionFilters,omitempty"` // ServiceSpec that will be create additional service for vmstorage // +optional ServiceSpec *AdditionalServiceSpec `json:"serviceSpec,omitempty"` @@ -679,6 +688,9 @@ func (cr *VMCluster) Validate() error { } } } + if err := cr.Spec.Downsampling.validate(cr.Spec.License); err != nil { + return err + } if cr.Spec.VMStorage != nil { vms := cr.Spec.VMStorage name := cr.PrefixedName(ClusterComponentSelect) @@ -690,6 +702,22 @@ func (cr *VMCluster) Validate() error { return err } } + if len(vms.RetentionFilters) > 0 { + if !cr.Spec.License.IsProvided() { + return fmt.Errorf("it is required to provide license key for retentionFilters. See [here](https://docs.victoriametrics.com/victoriametrics/enterprise/)") + } + if err := cr.Spec.License.validate(); err != nil { + return err + } + for i := range vms.RetentionFilters { + if err := vms.RetentionFilters[i].validate(); err != nil { + return fmt.Errorf("vmstorage.retentionFilters[%d]: %w", i, err) + } + } + if err := validateRetentionFiltersAgainstPeriod(vms.RetentionFilters, cr.Spec.RetentionPeriod, "vmstorage.retentionFilters"); err != nil { + return err + } + } if vms.RollingUpdateStrategyBehavior != nil { if err := vms.RollingUpdateStrategyBehavior.Validate(); err != nil { return fmt.Errorf("vmstorage: %w", err) diff --git a/api/operator/v1beta1/vmcluster_types_test.go b/api/operator/v1beta1/vmcluster_types_test.go index 8cbace183..bb74d180c 100644 --- a/api/operator/v1beta1/vmcluster_types_test.go +++ b/api/operator/v1beta1/vmcluster_types_test.go @@ -121,3 +121,145 @@ func TestVMCluster_AvailableStorageNodeIDs(t *testing.T) { }, }, ClusterComponentSelect, []int32{0, 1, 2}) } + +func TestVMCluster_Validate(t *testing.T) { + f := func(spec VMClusterSpec, wantErr bool) { + t.Helper() + cr := &VMCluster{Spec: spec} + if wantErr { + assert.Error(t, cr.Validate()) + } else { + assert.NoError(t, cr.Validate()) + } + } + + // empty spec + f(VMClusterSpec{}, false) + + // downsampling without license + f(VMClusterSpec{ + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "10m"}}}}, + }, + }, true) + + // downsampling with valid config + f(VMClusterSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "10m"}}}}, + }, + }, false) + + // downsampling with filter and dedupInterval + f(VMClusterSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Filter: `{env="prod"}`, Periods: []DownsamplingPeriod{{Offset: "90d", Interval: "1h"}}}}, + DedupInterval: "1m", + }, + }, false) + + // downsampling - multiple periods per rule + f(VMClusterSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{ + {Offset: "30d", Interval: "10m"}, + {Offset: "180d", Interval: "1h"}, + }}}, + }, + }, false) + + // downsampling - duplicate filter + f(VMClusterSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{ + {Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "10m"}}}, + {Periods: []DownsamplingPeriod{{Offset: "180d", Interval: "1h"}}}, + }, + }, + }, true) + + // downsampling - offset not a multiple of interval + f(VMClusterSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{{Offset: "1d", Interval: "7m"}}}}, + }, + }, true) + + // downsampling - period interval not a multiple of dedupInterval + f(VMClusterSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "10m"}}}}, + DedupInterval: "7m", + }, + }, true) + + // downsampling - period interval is a multiple of dedupInterval + f(VMClusterSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "10m"}}}}, + DedupInterval: "5m", + }, + }, false) + + // retention filters without vmstorage section — no error (vmstorage is nil) + f(VMClusterSpec{ + License: testLicense, + }, false) + + // retention filters without license + f(VMClusterSpec{ + VMStorage: &VMStorage{ + RetentionFilters: []RetentionFilter{{Filter: `{env="dev"}`, Retention: "3d"}}, + }, + }, true) + + // retention filters with valid config + f(VMClusterSpec{ + License: testLicense, + RetentionPeriod: "30", + VMStorage: &VMStorage{ + RetentionFilters: []RetentionFilter{{Filter: `{env="dev"}`, Retention: "3d"}}, + }, + }, false) + + // retention filters - invalid filter + f(VMClusterSpec{ + License: testLicense, + VMStorage: &VMStorage{ + RetentionFilters: []RetentionFilter{{Filter: "not-a-filter", Retention: "3d"}}, + }, + }, true) + + // retention filters - invalid retention + f(VMClusterSpec{ + License: testLicense, + VMStorage: &VMStorage{ + RetentionFilters: []RetentionFilter{{Filter: `{env="dev"}`, Retention: "bad"}}, + }, + }, true) + + // retention filters - retention exceeds retentionPeriod + f(VMClusterSpec{ + License: testLicense, + RetentionPeriod: "30d", + VMStorage: &VMStorage{ + RetentionFilters: []RetentionFilter{{Filter: `{env="dev"}`, Retention: "1y"}}, + }, + }, true) + + // retention filters - retention equal to retentionPeriod is ok + f(VMClusterSpec{ + License: testLicense, + RetentionPeriod: "1y", + VMStorage: &VMStorage{ + RetentionFilters: []RetentionFilter{{Filter: `{env="dev"}`, Retention: "1y"}}, + }, + }, false) +} diff --git a/api/operator/v1beta1/vmextra_types.go b/api/operator/v1beta1/vmextra_types.go index fe9502b5f..1bb6f631e 100644 --- a/api/operator/v1beta1/vmextra_types.go +++ b/api/operator/v1beta1/vmextra_types.go @@ -11,6 +11,9 @@ import ( "strconv" "strings" + "github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil" + "github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel" + "github.com/VictoriaMetrics/metricsql" "gopkg.in/yaml.v2" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" @@ -673,6 +676,203 @@ func (config *StreamAggrConfig) HasAnyRule() bool { return false } +// DownsamplingConfig defines downsampling configuration for VMSingle +// Requires enterprise license. +// See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#downsampling +// +k8s:openapi-gen=true +type DownsamplingConfig struct { + // Rules defines downsampling rules. + // Each rule matches time series by an optional filter and applies downsampling after a given offset. + // +optional + Rules []DownsamplingRule `json:"rules,omitempty"` + // DedupInterval sets the deduplication interval used with downsampling. + // It corresponds to -dedup.minScrapeInterval flag. + // Must be a multiple of all configured downsampling intervals. + // +optional + DedupInterval string `json:"dedupInterval,omitempty"` +} + +// HasAnyRule returns true if at least one downsampling rule is configured +func (dc *DownsamplingConfig) HasAnyRule() bool { + return dc != nil && len(dc.Rules) > 0 +} + +// validate checks DownsamplingConfig for correctness. +// It requires a valid enterprise license and uses the same parsing primitives +// as the VictoriaMetrics storage layer. +func (dc *DownsamplingConfig) validate(l *License) error { + if dc == nil { + return nil + } + if !l.IsProvided() { + return fmt.Errorf("it is required to provide license key for downsampling. See [here](https://docs.victoriametrics.com/victoriametrics/enterprise/)") + } + if err := l.validate(); err != nil { + return err + } + seenFilters := make(map[string]struct{}) + for i, rule := range dc.Rules { + if _, dup := seenFilters[rule.Filter]; dup { + return fmt.Errorf("downsampling.rules[%d]: duplicate filter %q", i, rule.Filter) + } + seenFilters[rule.Filter] = struct{}{} + if err := rule.validate(); err != nil { + return fmt.Errorf("downsampling.rules[%d]: %w", i, err) + } + } + if dc.DedupInterval != "" { + dedupMs, err := metricsql.PositiveDurationValue(dc.DedupInterval, 0) + if err != nil { + return fmt.Errorf("downsampling.dedupInterval: invalid value %q: %w", dc.DedupInterval, err) + } + for i, rule := range dc.Rules { + for j, p := range rule.Periods { + intervalMs, err := metricsql.PositiveDurationValue(p.Interval, 0) + if err != nil { + continue + } + if dedupMs != 0 && intervalMs%dedupMs != 0 { + return fmt.Errorf("downsampling.rules[%d].periods[%d]: interval %q must be a multiple of dedupInterval %q", i, j, p.Interval, dc.DedupInterval) + } + } + } + } + return nil +} + +func (rule *DownsamplingRule) validate() error { + if rule.Filter != "" { + var ie promrelabel.IfExpression + if err := ie.Parse(rule.Filter); err != nil { + return fmt.Errorf("invalid filter %q: %w", rule.Filter, err) + } + } + if len(rule.Periods) == 0 { + return fmt.Errorf("periods must not be empty") + } + for i, p := range rule.Periods { + if err := p.validate(); err != nil { + return fmt.Errorf("periods[%d]: %w", i, err) + } + } + for i := 0; i < len(rule.Periods); i++ { + iMs, err := metricsql.PositiveDurationValue(rule.Periods[i].Interval, 0) + if err != nil || iMs == 0 { + continue + } + for j := i + 1; j < len(rule.Periods); j++ { + jMs, err := metricsql.PositiveDurationValue(rule.Periods[j].Interval, 0) + if err != nil || jMs == 0 { + continue + } + if iMs%jMs != 0 && jMs%iMs != 0 { + return fmt.Errorf("periods[%d] interval %q and periods[%d] interval %q must be multiples of each other", + i, rule.Periods[i].Interval, j, rule.Periods[j].Interval) + } + } + } + return nil +} + +func (p *DownsamplingPeriod) validate() error { + if p.Offset == "" { + return fmt.Errorf("offset must not be empty") + } + offsetMs, err := metricsql.PositiveDurationValue(p.Offset, 0) + if err != nil { + return fmt.Errorf("invalid offset %q: %w", p.Offset, err) + } + if p.Interval == "" { + return fmt.Errorf("interval must not be empty") + } + intervalMs, err := metricsql.PositiveDurationValue(p.Interval, 0) + if err != nil { + return fmt.Errorf("invalid interval %q: %w", p.Interval, err) + } + if intervalMs == 0 && offsetMs != 0 { + return fmt.Errorf("interval must be greater than 0 when offset is not 0") + } + if intervalMs != 0 && offsetMs%intervalMs != 0 { + return fmt.Errorf("offset %q must be a multiple of interval %q", p.Offset, p.Interval) + } + return nil +} + +// DownsamplingRule defines downsampling periods for an optional label filter. +// All period intervals must be pairwise multiples of each other. +// +k8s:openapi-gen=true +type DownsamplingRule struct { + // Filter is an optional MetricsQL label filter for matching time series. + // If not set, the rule is applied to all time series. + // Example: '{env="prod"}' + // +optional + Filter string `json:"filter,omitempty"` + // Periods defines the downsampling time horizons applied to matching series. + Periods []DownsamplingPeriod `json:"periods"` +} + +// DownsamplingPeriod defines a single downsampling time horizon. +// +k8s:openapi-gen=true +type DownsamplingPeriod struct { + // Offset is the minimum age of data before downsampling is applied. + // Example: "30d" + Offset string `json:"offset"` + // Interval is the target downsampling resolution; only the last sample per interval is kept. + // Example: "10m" + Interval string `json:"interval"` +} + +// RetentionFilter defines per-series retention based on a label filter. +// Requires enterprise license. +// See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention-filters +// +k8s:openapi-gen=true +type RetentionFilter struct { + // Filter is a MetricsQL label filter for matching time series. + // Example: '{env="dev"}' + Filter string `json:"filter"` + // Retention is the retention period for matching time series. + // Must not exceed the global retentionPeriod. + // Example: "3d", "1w" + Retention string `json:"retention"` +} + +// validate checks RetentionFilter content using the same parsing primitives +// as the VictoriaMetrics storage layer. +func (rf *RetentionFilter) validate() error { + var ie promrelabel.IfExpression + if err := ie.Parse(rf.Filter); err != nil { + return fmt.Errorf("invalid filter %q: %w", rf.Filter, err) + } + var rd flagutil.RetentionDuration + if err := rd.Set(rf.Retention); err != nil { + return fmt.Errorf("invalid retention %q: %w", rf.Retention, err) + } + return nil +} + +// validateRetentionFiltersAgainstPeriod checks that no filter retention exceeds the global retentionPeriod. +// prefix is used in error messages (e.g. "retentionFilters" or "vmstorage.retentionFilters"). +// If retentionPeriod is empty the check is skipped. +func validateRetentionFiltersAgainstPeriod(filters []RetentionFilter, retentionPeriod, prefix string) error { + if retentionPeriod == "" || len(filters) == 0 { + return nil + } + var rpDur flagutil.RetentionDuration + if err := rpDur.Set(retentionPeriod); err != nil { + return fmt.Errorf("invalid retentionPeriod %q: %w", retentionPeriod, err) + } + for i, rf := range filters { + var rfDur flagutil.RetentionDuration + if err := rfDur.Set(rf.Retention); err != nil { + return fmt.Errorf("%s[%d]: invalid retention %q: %w", prefix, i, rf.Retention, err) + } + if rfDur.Milliseconds() > rpDur.Milliseconds() { + return fmt.Errorf("%s[%d]: retention %q must not exceed retentionPeriod %q", prefix, i, rf.Retention, retentionPeriod) + } + } + return nil +} + // CommonRelabelParams defines params for relabelling type CommonRelabelParams struct { // RelabelConfig ConfigMap with global relabel config -remoteWrite.relabelConfig diff --git a/api/operator/v1beta1/vmsingle_types.go b/api/operator/v1beta1/vmsingle_types.go index 254e4788e..6635cea93 100644 --- a/api/operator/v1beta1/vmsingle_types.go +++ b/api/operator/v1beta1/vmsingle_types.go @@ -77,6 +77,14 @@ type VMSingleSpec struct { ServiceScrapeSpec *VMServiceScrapeSpec `json:"serviceScrapeSpec,omitempty"` // StreamAggrConfig defines stream aggregation configuration for VMSingle StreamAggrConfig *StreamAggrConfig `json:"streamAggrConfig,omitempty"` + // Downsampling defines downsampling rules for VMSingle. + // Requires enterprise license. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#downsampling + // +optional + Downsampling *DownsamplingConfig `json:"downsampling,omitempty"` + // RetentionFilters defines per-series retention filters for VMSingle. + // Requires enterprise license. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention-filters + // +optional + RetentionFilters []RetentionFilter `json:"retentionFilters,omitempty"` // APIServerConfig allows specifying a host and auth methods to access apiserver. // If left empty, VMSingle is assumed to run inside of the cluster // and will discover API servers automatically and use the pod's CA certificate @@ -392,6 +400,27 @@ func (cr *VMSingle) Validate() error { return err } } + if cr.Spec.Downsampling != nil { + if err := cr.Spec.Downsampling.validate(cr.Spec.License); err != nil { + return err + } + } + if len(cr.Spec.RetentionFilters) > 0 { + if !cr.Spec.License.IsProvided() { + return fmt.Errorf("it is required to provide license key for retentionFilters. See [here](https://docs.victoriametrics.com/victoriametrics/enterprise/)") + } + if err := cr.Spec.License.validate(); err != nil { + return err + } + for i := range cr.Spec.RetentionFilters { + if err := cr.Spec.RetentionFilters[i].validate(); err != nil { + return fmt.Errorf("retentionFilters[%d]: %w", i, err) + } + } + if err := validateRetentionFiltersAgainstPeriod(cr.Spec.RetentionFilters, cr.Spec.RetentionPeriod, "retentionFilters"); err != nil { + return err + } + } scrapeClassNames := sets.New[string]() defaultScrapeClass := false for _, sc := range cr.Spec.ScrapeClasses { diff --git a/api/operator/v1beta1/vmsingle_types_test.go b/api/operator/v1beta1/vmsingle_types_test.go index 696a1e769..787307ab8 100644 --- a/api/operator/v1beta1/vmsingle_types_test.go +++ b/api/operator/v1beta1/vmsingle_types_test.go @@ -7,6 +7,8 @@ import ( "k8s.io/utils/ptr" ) +var testLicense = &License{Key: ptr.To("test-license-key")} + func TestVMSingle_Validate(t *testing.T) { f := func(spec VMSingleSpec, wantErr bool) { t.Helper() @@ -52,4 +54,162 @@ func TestVMSingle_Validate(t *testing.T) { }, }, }, true) + + // downsampling without license + f(VMSingleSpec{ + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "10m"}}}}, + }, + }, true) + + // downsampling with valid config + f(VMSingleSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "10m"}}}}, + }, + }, false) + + // downsampling with filter and dedupInterval + f(VMSingleSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Filter: `{env="prod"}`, Periods: []DownsamplingPeriod{{Offset: "90d", Interval: "1h"}}}}, + DedupInterval: "1m", + }, + }, false) + + // downsampling - multiple periods per rule + f(VMSingleSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{ + {Offset: "30d", Interval: "10m"}, + {Offset: "180d", Interval: "1h"}, + {Offset: "1y", Interval: "6h"}, + }}}, + }, + }, false) + + // downsampling - duplicate filter + f(VMSingleSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{ + {Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "10m"}}}, + {Periods: []DownsamplingPeriod{{Offset: "180d", Interval: "1h"}}}, + }, + }, + }, true) + + // downsampling - different filters are ok + f(VMSingleSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{ + {Filter: `{env="prod"}`, Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "10m"}}}, + {Filter: `{env="dev"}`, Periods: []DownsamplingPeriod{{Offset: "35d", Interval: "7m"}}}, + }, + }, + }, false) + + // downsampling - period intervals not multiples of each other within one rule + f(VMSingleSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{ + {Offset: "30d", Interval: "10m"}, + {Offset: "35d", Interval: "7m"}, + }}}, + }, + }, true) + + // downsampling - offset not a multiple of interval + f(VMSingleSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{{Offset: "1d", Interval: "7m"}}}}, + }, + }, true) + + // downsampling - invalid interval + f(VMSingleSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "bad"}}}}, + }, + }, true) + + // downsampling - invalid filter + f(VMSingleSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Filter: "not-a-filter", Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "10m"}}}}, + }, + }, true) + + // downsampling - invalid dedupInterval + f(VMSingleSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "10m"}}}}, + DedupInterval: "bad", + }, + }, true) + + // downsampling - period interval not a multiple of dedupInterval + f(VMSingleSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "10m"}}}}, + DedupInterval: "7m", + }, + }, true) + + // downsampling - period interval is a multiple of dedupInterval + f(VMSingleSpec{ + License: testLicense, + Downsampling: &DownsamplingConfig{ + Rules: []DownsamplingRule{{Periods: []DownsamplingPeriod{{Offset: "30d", Interval: "10m"}}}}, + DedupInterval: "5m", + }, + }, false) + + // retention filters without license + f(VMSingleSpec{ + RetentionFilters: []RetentionFilter{{Filter: `{env="dev"}`, Retention: "3d"}}, + }, true) + + // retention filters with valid config + f(VMSingleSpec{ + License: testLicense, + RetentionPeriod: "30", + RetentionFilters: []RetentionFilter{{Filter: `{env="dev"}`, Retention: "3d"}}, + }, false) + + // retention filters - invalid filter + f(VMSingleSpec{ + License: testLicense, + RetentionFilters: []RetentionFilter{{Filter: "not-a-filter", Retention: "3d"}}, + }, true) + + // retention filters - invalid retention + f(VMSingleSpec{ + License: testLicense, + RetentionFilters: []RetentionFilter{{Filter: `{env="dev"}`, Retention: "bad"}}, + }, true) + + // retention filters - retention exceeds retentionPeriod + f(VMSingleSpec{ + License: testLicense, + RetentionPeriod: "30d", + RetentionFilters: []RetentionFilter{{Filter: `{env="dev"}`, Retention: "1y"}}, + }, true) + + // retention filters - retention equal to retentionPeriod is ok + f(VMSingleSpec{ + License: testLicense, + RetentionPeriod: "1y", + RetentionFilters: []RetentionFilter{{Filter: `{env="dev"}`, Retention: "1y"}}, + }, false) } diff --git a/api/operator/v1beta1/zz_generated.deepcopy.go b/api/operator/v1beta1/zz_generated.deepcopy.go index f7ae3bbae..504995e37 100644 --- a/api/operator/v1beta1/zz_generated.deepcopy.go +++ b/api/operator/v1beta1/zz_generated.deepcopy.go @@ -1021,6 +1021,63 @@ func (in *DiscoverySelector) DeepCopy() *DiscoverySelector { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DownsamplingConfig) DeepCopyInto(out *DownsamplingConfig) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]DownsamplingRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DownsamplingConfig. +func (in *DownsamplingConfig) DeepCopy() *DownsamplingConfig { + if in == nil { + return nil + } + out := new(DownsamplingConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DownsamplingPeriod) DeepCopyInto(out *DownsamplingPeriod) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DownsamplingPeriod. +func (in *DownsamplingPeriod) DeepCopy() *DownsamplingPeriod { + if in == nil { + return nil + } + out := new(DownsamplingPeriod) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DownsamplingRule) DeepCopyInto(out *DownsamplingRule) { + *out = *in + if in.Periods != nil { + in, out := &in.Periods, &out.Periods + *out = make([]DownsamplingPeriod, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DownsamplingRule. +func (in *DownsamplingRule) DeepCopy() *DownsamplingRule { + if in == nil { + return nil + } + out := new(DownsamplingRule) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EC2Filter) DeepCopyInto(out *EC2Filter) { *out = *in @@ -3017,6 +3074,21 @@ func (in *RelabelConfig) DeepCopy() *RelabelConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RetentionFilter) DeepCopyInto(out *RetentionFilter) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RetentionFilter. +func (in *RetentionFilter) DeepCopy() *RetentionFilter { + if in == nil { + return nil + } + out := new(RetentionFilter) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RocketchatAttachmentAction) DeepCopyInto(out *RocketchatAttachmentAction) { *out = *in @@ -5839,6 +5911,11 @@ func (in *VMClusterSpec) DeepCopyInto(out *VMClusterSpec) { *out = new(License) (*in).DeepCopyInto(*out) } + if in.Downsampling != nil { + in, out := &in.Downsampling, &out.Downsampling + *out = new(DownsamplingConfig) + (*in).DeepCopyInto(*out) + } if in.VMSelect != nil { in, out := &in.VMSelect, &out.VMSelect *out = new(VMSelect) @@ -7024,6 +7101,16 @@ func (in *VMSingleSpec) DeepCopyInto(out *VMSingleSpec) { *out = new(StreamAggrConfig) (*in).DeepCopyInto(*out) } + if in.Downsampling != nil { + in, out := &in.Downsampling, &out.Downsampling + *out = new(DownsamplingConfig) + (*in).DeepCopyInto(*out) + } + if in.RetentionFilters != nil { + in, out := &in.RetentionFilters, &out.RetentionFilters + *out = make([]RetentionFilter, len(*in)) + copy(*out, *in) + } if in.APIServerConfig != nil { in, out := &in.APIServerConfig, &out.APIServerConfig *out = new(APIServerConfig) @@ -7189,6 +7276,11 @@ func (in *VMStorage) DeepCopyInto(out *VMStorage) { *out = new(VMBackup) (*in).DeepCopyInto(*out) } + if in.RetentionFilters != nil { + in, out := &in.RetentionFilters, &out.RetentionFilters + *out = make([]RetentionFilter, len(*in)) + copy(*out, *in) + } if in.ServiceSpec != nil { in, out := &in.ServiceSpec, &out.ServiceSpec *out = new(AdditionalServiceSpec) diff --git a/config/crd/overlay/crd.descriptionless.yaml b/config/crd/overlay/crd.descriptionless.yaml index 351980813..6ce92601b 100644 --- a/config/crd/overlay/crd.descriptionless.yaml +++ b/config/crd/overlay/crd.descriptionless.yaml @@ -15608,6 +15608,32 @@ spec: type: string clusterVersion: type: string + downsampling: + properties: + dedupInterval: + type: string + rules: + items: + properties: + filter: + type: string + periods: + items: + properties: + interval: + type: string + offset: + type: string + required: + - interval + - offset + type: object + type: array + required: + - periods + type: object + type: array + type: object imagePullSecrets: items: properties: @@ -17562,6 +17588,18 @@ spec: x-kubernetes-int-or-string: true type: object type: object + retentionFilters: + items: + properties: + filter: + type: string + retention: + type: string + required: + - filter + - retention + type: object + type: array revisionHistoryLimitCount: format: int32 type: integer @@ -40713,6 +40751,32 @@ spec: type: object dnsPolicy: type: string + downsampling: + properties: + dedupInterval: + type: string + rules: + items: + properties: + filter: + type: string + periods: + items: + properties: + interval: + type: string + offset: + type: string + required: + - interval + - offset + type: object + type: array + required: + - periods + type: object + type: array + type: object enableKubernetesAPISelectors: type: boolean enableServiceLinks: @@ -41356,6 +41420,18 @@ spec: x-kubernetes-int-or-string: true type: object type: object + retentionFilters: + items: + properties: + filter: + type: string + retention: + type: string + required: + - filter + - retention + type: object + type: array retentionPeriod: pattern: ^[0-9]+(h|d|w|y)?$ type: string diff --git a/config/crd/overlay/crd.yaml b/config/crd/overlay/crd.yaml index 394f2a09e..f1b7f5261 100644 --- a/config/crd/overlay/crd.yaml +++ b/config/crd/overlay/crd.yaml @@ -30383,6 +30383,59 @@ spec: ClusterVersion defines default images tag for all components. it can be overwritten with component specific image.tag value. type: string + downsampling: + description: |- + Downsampling defines downsampling rules applied to vmselect and vmstorage components. + Requires enterprise license. See https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#downsampling + properties: + dedupInterval: + description: |- + DedupInterval sets the deduplication interval used with downsampling. + It corresponds to -dedup.minScrapeInterval flag. + Must be a multiple of all configured downsampling intervals. + type: string + rules: + description: |- + Rules defines downsampling rules. + Each rule matches time series by an optional filter and applies downsampling after a given offset. + items: + description: |- + DownsamplingRule defines downsampling periods for an optional label filter. + All period intervals must be pairwise multiples of each other. + properties: + filter: + description: |- + Filter is an optional MetricsQL label filter for matching time series. + If not set, the rule is applied to all time series. + Example: '{env="prod"}' + type: string + periods: + description: Periods defines the downsampling time horizons + applied to matching series. + items: + description: DownsamplingPeriod defines a single downsampling + time horizon. + properties: + interval: + description: |- + Interval is the target downsampling resolution; only the last sample per interval is kept. + Example: "10m" + type: string + offset: + description: |- + Offset is the minimum age of data before downsampling is applied. + Example: "30d" + type: string + required: + - interval + - offset + type: object + type: array + required: + - periods + type: object + type: array + type: object imagePullSecrets: description: |- ImagePullSecrets An optional list of references to secrets in the same namespace @@ -34260,6 +34313,32 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + retentionFilters: + description: |- + RetentionFilters defines per-series retention filters for vmstorage. + Requires enterprise license. See https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#retention-filters + items: + description: |- + RetentionFilter defines per-series retention based on a label filter. + Requires enterprise license. + See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention-filters + properties: + filter: + description: |- + Filter is a MetricsQL label filter for matching time series. + Example: '{env="dev"}' + type: string + retention: + description: |- + Retention is the retention period for matching time series. + Must not exceed the global retentionPeriod. + Example: "3d", "1w" + type: string + required: + - filter + - retention + type: object + type: array revisionHistoryLimitCount: description: |- The number of old ReplicaSets to retain to allow rollback in deployment or @@ -81460,6 +81539,59 @@ spec: dnsPolicy: description: DNSPolicy sets DNS policy for the pod type: string + downsampling: + description: |- + Downsampling defines downsampling rules for VMSingle. + Requires enterprise license. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#downsampling + properties: + dedupInterval: + description: |- + DedupInterval sets the deduplication interval used with downsampling. + It corresponds to -dedup.minScrapeInterval flag. + Must be a multiple of all configured downsampling intervals. + type: string + rules: + description: |- + Rules defines downsampling rules. + Each rule matches time series by an optional filter and applies downsampling after a given offset. + items: + description: |- + DownsamplingRule defines downsampling periods for an optional label filter. + All period intervals must be pairwise multiples of each other. + properties: + filter: + description: |- + Filter is an optional MetricsQL label filter for matching time series. + If not set, the rule is applied to all time series. + Example: '{env="prod"}' + type: string + periods: + description: Periods defines the downsampling time horizons + applied to matching series. + items: + description: DownsamplingPeriod defines a single downsampling + time horizon. + properties: + interval: + description: |- + Interval is the target downsampling resolution; only the last sample per interval is kept. + Example: "10m" + type: string + offset: + description: |- + Offset is the minimum age of data before downsampling is applied. + Example: "30d" + type: string + required: + - interval + - offset + type: object + type: array + required: + - periods + type: object + type: array + type: object enableKubernetesAPISelectors: description: |- EnableKubernetesAPISelectors instructs vmagent or vmsingle to use CRD scrape objects spec.selectors for @@ -82766,6 +82898,32 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + retentionFilters: + description: |- + RetentionFilters defines per-series retention filters for VMSingle. + Requires enterprise license. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention-filters + items: + description: |- + RetentionFilter defines per-series retention based on a label filter. + Requires enterprise license. + See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention-filters + properties: + filter: + description: |- + Filter is a MetricsQL label filter for matching time series. + Example: '{env="dev"}' + type: string + retention: + description: |- + Retention is the retention period for matching time series. + Must not exceed the global retentionPeriod. + Example: "3d", "1w" + type: string + required: + - filter + - retention + type: object + type: array retentionPeriod: description: |- RetentionPeriod defines how long to retain stored metrics, specified as a duration (e.g., "1d", "1w", "1m"). diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c5127c719..95fb1437d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -17,11 +17,13 @@ aliases: * FEATURE: [vmoperator](https://docs.victoriametrics.com/operator/): added `VM_COMMON_LABELS` and `VM_COMMON_ANNOTATIONS` environment variables to apply common labels/annotations to all Kubernetes resources managed by the operator. These cannot override labels/annotations already set by the operator or via `spec.managedMetadata`. This also ensures HTTPRoutes and PVCs include ManagedMetadata labels and annotations * FEATURE: [vmoperator](https://docs.victoriametrics.com/operator/): support enableServiceLinks property in all CRs. See [#2194](https://github.com/VictoriaMetrics/operator/pull/2194). * FEATURE: [vmalertmanagerconfig](https://docs.victoriametrics.com/operator/resources/vmalertmanagerconfig/): add `url_file` and `alert_source_token_file` fields to `IncidentioConfig`, as file-based alternatives to `url` and `alert_source_token`. See [#2222](https://github.com/VictoriaMetrics/operator/issues/2222). +* FEATURE: [vmsingle](https://docs.victoriametrics.com/operator/resources/vmsingle/): added `spec.downsampling` for structured [downsampling](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#downsampling) configuration and `spec.retentionFilters` for structured [retention filters](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention-filters) configuration. Both require an enterprise license. +* FEATURE: [vmcluster](https://docs.victoriametrics.com/operator/resources/vmcluster/): added `spec.downsampling` for structured [downsampling](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#downsampling) configuration (applied to both vmselect and vmstorage) and `spec.vmstorage.retentionFilters` for structured [retention filters](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#retention-filters) configuration. Both require an enterprise license. -* BUGFIX: [vmagent](https://docs.victoriametrics.com/operator/resources/vmagent/), [vmanomaly](https://docs.victoriametrics.com/operator/resources/vmanomaly/): fix VPA scale subresource lookup failure when `spec.shardCount` is unset by always reporting at least 1 in `status.shards`. See [#2229](https://github.com/VictoriaMetrics/operator/issues/2229). -* BUGFIX: [vmagent](https://docs.victoriametrics.com/operator/resources/vmagent/): fix HPA targeting the underlying Deployment/StatefulSet (pod replicas) instead of the VMAgent CR scale subresource (`spec.shardCount`); HPA now correctly scales the number of shards. See [#2229](https://github.com/VictoriaMetrics/operator/issues/2229). * BUGFIX: [vmoperator](https://docs.victoriametrics.com/operator/): update status currentRevision and currentReplicas for StatefulSet with OnDelete update strategy. See [#1242](https://github.com/VictoriaMetrics/operator/issues/1242). * BUGFIX: [config-reloader](https://docs.victoriametrics.com/operator/): fix `configreloader_last_reload_success_timestamp_seconds` metric to report time in seconds instead of milliseconds. +* BUGFIX: [vmagent](https://docs.victoriametrics.com/operator/resources/vmagent/), [vmanomaly](https://docs.victoriametrics.com/operator/resources/vmanomaly/): fix VPA scale subresource lookup failure when `spec.shardCount` is unset by always reporting at least 1 in `status.shards`. See [#2229](https://github.com/VictoriaMetrics/operator/issues/2229). +* BUGFIX: [vmagent](https://docs.victoriametrics.com/operator/resources/vmagent/): fix HPA targeting the underlying Deployment/StatefulSet (pod replicas) instead of the VMAgent CR scale subresource (`spec.shardCount`); HPA now correctly scales the number of shards. See [#2229](https://github.com/VictoriaMetrics/operator/issues/2229). ## [v0.70.1](https://github.com/VictoriaMetrics/operator/releases/tag/v0.70.0) **Release date:** 20 May 2026 diff --git a/docs/api.md b/docs/api.md index d03fd43ab..0f390f4f3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1738,6 +1738,42 @@ Appears in: [VMAlertNotifierSpec](#vmalertnotifierspec) | labelSelector#
_[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#labelselector-v1-meta)_ | _(Required)_
| | namespaceSelector#
_[NamespaceSelector](#namespaceselector)_ | _(Required)_
| +#### DownsamplingConfig + +DownsamplingConfig defines downsampling configuration for VMSingle +Requires enterprise license. +See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#downsampling + +Appears in: [VMClusterSpec](#vmclusterspec), [VMSingleSpec](#vmsinglespec) + +| Field | Description | +| --- | --- | +| dedupInterval#
_string_ | _(Optional)_
DedupInterval sets the deduplication interval used with downsampling.
It corresponds to -dedup.minScrapeInterval flag.
Must be a multiple of all configured downsampling intervals. | +| rules#
_[DownsamplingRule](#downsamplingrule) array_ | _(Optional)_
Rules defines downsampling rules.
Each rule matches time series by an optional filter and applies downsampling after a given offset. | + +#### DownsamplingPeriod + +DownsamplingPeriod defines a single downsampling time horizon. + +Appears in: [DownsamplingRule](#downsamplingrule) + +| Field | Description | +| --- | --- | +| interval#
_string_ | _(Required)_
Interval is the target downsampling resolution; only the last sample per interval is kept.
Example: "10m" | +| offset#
_string_ | _(Required)_
Offset is the minimum age of data before downsampling is applied.
Example: "30d" | + +#### DownsamplingRule + +DownsamplingRule defines downsampling periods for an optional label filter. +All period intervals must be pairwise multiples of each other. + +Appears in: [DownsamplingConfig](#downsamplingconfig) + +| Field | Description | +| --- | --- | +| filter#
_string_ | _(Optional)_
Filter is an optional MetricsQL label filter for matching time series.
If not set, the rule is applied to all time series.
Example: '\{env="prod"\}' | +| periods#
_[DownsamplingPeriod](#downsamplingperiod) array_ | _(Required)_
Periods defines the downsampling time horizons applied to matching series. | + #### EC2Filter EC2Filter is the configuration for filtering EC2 instances. @@ -2775,6 +2811,19 @@ Appears in: [CommonRelabelParams](#commonrelabelparams), [CommonScrapeParams](#c | targetLabel#
_string_ | _(Optional)_
Label to which the resulting value is written in a replace action.
It is mandatory for replace actions. Regex capture groups are available. | | target_label#
_string_ | _(Optional)_
UnderScoreTargetLabel - additional form of target label - target_label
for compatibility with original relabel config.
if set both targetLabel and target_label, targetLabel has priority.
for details https://github.com/VictoriaMetrics/operator/issues/131 | +#### RetentionFilter + +RetentionFilter defines per-series retention based on a label filter. +Requires enterprise license. +See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention-filters + +Appears in: [VMSingleSpec](#vmsinglespec), [VMStorage](#vmstorage) + +| Field | Description | +| --- | --- | +| filter#
_string_ | _(Required)_
Filter is a MetricsQL label filter for matching time series.
Example: '\{env="dev"\}' | +| retention#
_string_ | _(Required)_
Retention is the retention period for matching time series.
Must not exceed the global retentionPeriod.
Example: "3d", "1w" | + #### RocketchatAttachmentAction RocketchatAttachmentAction defines message attachments @@ -4185,6 +4234,7 @@ Appears in: [VMCluster](#vmcluster), [VMDistributedZoneCluster](#vmdistributedzo | --- | --- | | clusterDomainName#
_string_ | _(Optional)_
ClusterDomainName defines domain name suffix for in-cluster dns addresses
aka .cluster.local
used by vminsert and vmselect to build vmstorage address | | clusterVersion#
_string_ | _(Optional)_
ClusterVersion defines default images tag for all components.
it can be overwritten with component specific image.tag value. | +| downsampling#
_[DownsamplingConfig](#downsamplingconfig)_ | _(Optional)_
Downsampling defines downsampling rules applied to vmselect and vmstorage components.
Requires enterprise license. See https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#downsampling | | imagePullSecrets#
_[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#localobjectreference-v1-core) array_ | _(Optional)_
ImagePullSecrets An optional list of references to secrets in the same namespace
to use for pulling images from registries
see https://kubernetes.io/docs/concepts/containers/images/#referring-to-an-imagepullsecrets-on-a-pod | | license#
_[License](#license)_ | _(Optional)_
License allows to configure license key to be used for enterprise features.
Using license key is supported starting from VictoriaMetrics v1.94.0.
See [here](https://docs.victoriametrics.com/victoriametrics/enterprise/) | | managedMetadata#
_[ManagedObjectsMetadata](#managedobjectsmetadata)_ | _(Required)_
ManagedMetadata defines metadata that will be added to the all objects
created by operator for the given CustomResource | @@ -4690,6 +4740,7 @@ Appears in: [VMSingle](#vmsingle) | disableSelfServiceScrape#
_boolean_ | _(Optional)_
DisableSelfServiceScrape controls creation of VMServiceScrape by operator
for the application.
Has priority over `VM_DISABLESELFSERVICESCRAPECREATION` operator env variable | | dnsConfig#
_[PodDNSConfig](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#poddnsconfig-v1-core)_ | _(Optional)_
Specifies the DNS parameters of a pod.
Parameters specified here will be merged to the generated DNS
configuration based on DNSPolicy. | | dnsPolicy#
_[DNSPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#dnspolicy-v1-core)_ | _(Optional)_
DNSPolicy sets DNS policy for the pod | +| downsampling#
_[DownsamplingConfig](#downsamplingconfig)_ | _(Optional)_
Downsampling defines downsampling rules for VMSingle.
Requires enterprise license. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#downsampling | | enableKubernetesAPISelectors#
_boolean_ | _(Optional)_
EnableKubernetesAPISelectors instructs vmagent or vmsingle to use CRD scrape objects spec.selectors for
Kubernetes API list and watch requests.
https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#list-and-watch-filtering
It could be useful to reduce Kubernetes API server resource usage for serving less than 100 CRD scrape objects in total. | | enableServiceLinks#
_boolean_ | _(Optional)_
EnableServiceLinks indicates whether information about services should be injected into pod's
environment variables, matching the syntax of Docker links.
Optional: Defaults to true. | | enforcedNamespaceLabel#
_string_ | _(Optional)_
EnforcedNamespaceLabel enforces adding a namespace label of origin for each alert
and metric that is user created. The label value will always be the namespace of the object that is
being created. | @@ -4741,6 +4792,7 @@ Appears in: [VMSingle](#vmsingle) | removePvcAfterDelete#
_boolean_ | _(Optional)_
RemovePvcAfterDelete - if true, controller adds ownership to pvc
and after VMSingle object deletion - pvc will be garbage collected
by controller manager | | replicaCount#
_integer_ | _(Optional)_
ReplicaCount is the expected size of the Application. | | resources#
_[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#resourcerequirements-v1-core)_ | _(Optional)_
Resources container resource request and limits, https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
if not defined default resources from operator config will be used | +| retentionFilters#
_[RetentionFilter](#retentionfilter) array_ | _(Optional)_
RetentionFilters defines per-series retention filters for VMSingle.
Requires enterprise license. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention-filters | | retentionPeriod#
_string_ | _(Optional)_
RetentionPeriod defines how long to retain stored metrics, specified as a duration (e.g., "1d", "1w", "1m").
Data with timestamps outside the RetentionPeriod is automatically deleted. The minimum allowed value is 1d, or 24h.
The default value is 1 (one month).
See [retention](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention) docs for details. | | revisionHistoryLimitCount#
_integer_ | _(Optional)_
The number of old ReplicaSets to retain to allow rollback in deployment or
maximum number of revisions that will be maintained in the Deployment revision history.
Has no effect at StatefulSets
Defaults to 10. | | runtimeClassName#
_string_ | _(Optional)_
RuntimeClassName - defines runtime class for kubernetes pod.
https://kubernetes.io/docs/concepts/containers/runtime-class/ | @@ -4848,6 +4900,7 @@ Appears in: [VMClusterSpec](#vmclusterspec) | readinessProbe#
_[Probe](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#probe-v1-core)_ | _(Optional)_
ReadinessProbe that will be added to CR pod | | replicaCount#
_integer_ | _(Optional)_
ReplicaCount is the expected size of the Application. | | resources#
_[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#resourcerequirements-v1-core)_ | _(Optional)_
Resources container resource request and limits, https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
if not defined default resources from operator config will be used | +| retentionFilters#
_[RetentionFilter](#retentionfilter) array_ | _(Optional)_
RetentionFilters defines per-series retention filters for vmstorage.
Requires enterprise license. See https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#retention-filters | | revisionHistoryLimitCount#
_integer_ | _(Optional)_
The number of old ReplicaSets to retain to allow rollback in deployment or
maximum number of revisions that will be maintained in the Deployment revision history.
Has no effect at StatefulSets
Defaults to 10. | | rollingUpdateStrategy#
_[StatefulSetUpdateStrategyType](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#statefulsetupdatestrategytype-v1-apps)_ | _(Optional)_
RollingUpdateStrategy defines strategy for application updates
Default is OnDelete, in this case operator handles update process
Can be changed for RollingUpdate | | rollingUpdateStrategyBehavior#
_[StatefulSetUpdateStrategyBehavior](#statefulsetupdatestrategybehavior)_ | _(Optional)_
RollingUpdateStrategyBehavior defines customized behavior for rolling updates.
It applies if the RollingUpdateStrategy is set to OnDelete, which is the default. | diff --git a/docs/resources/vmcluster.md b/docs/resources/vmcluster.md index 9d6f644d2..21826a98b 100644 --- a/docs/resources/vmcluster.md +++ b/docs/resources/vmcluster.md @@ -352,10 +352,11 @@ For using Enterprise version of [vmcluster](https://docs.victoriametrics.com/vic ### Downsampling -After that you can pass [Downsampling](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#downsampling) -flag to `VMCluster/vmselect` and `VMCluster/vmstorage` with [extraArgs](https://docs.victoriametrics.com/operator/resources/#extra-arguments) too. - -Here are complete example for [Downsampling](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#downsampling): +Use `spec.downsampling` to configure [Downsampling](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#downsampling). +The operator automatically applies the rules to both `vmselect` and `vmstorage`. +Each rule requires `offset` (how far back to downsample) and `interval` (target resolution). +An optional `filter` restricts the rule to matching time series. +The optional `dedupInterval` sets `-dedup.minScrapeInterval` on both components. ```yaml apiVersion: operator.victoriametrics.com/v1beta1 @@ -363,34 +364,38 @@ kind: VMCluster metadata: name: ent-example spec: - # enabling enterprise features license: keyRef: name: k8s-secret-that-contains-license key: key-in-a-secret-that-contains-license clusterVersion: v1.110.13-enterprise-cluster - vmselect: - # enabling enterprise features for vmselect - extraArgs: - # using enterprise features: Downsampling - # more details about downsampling you can read on https://docs.victoriametrics.com/victoriametrics/cluster-victoriaMetrics/#downsampling - downsampling.period: 30d:5m,180d:1h,1y:6h,2y:1d - vmstorage: - # enabling enterprise features for vmstorage - extraArgs: - # using enterprise features: Downsampling - # more details about downsampling you can read on https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#downsampling - downsampling.period: 30d:5m,180d:1h,1y:6h,2y:1d + downsampling: + dedupInterval: 1m + rules: + - periods: + - offset: 30d + interval: 5m + - offset: 180d + interval: 1h + - offset: 1y + interval: 6h + - filter: '{env="prod"}' + periods: + - offset: 30d + interval: 1m + - offset: 180d + interval: 10m # ...other fields... ``` -### Retention filters +You can read more about downsampling configuration on the [VictoriaMetrics cluster downsampling page](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#downsampling). -You can pass [Retention filters](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#retention-filters) -flag to `VMCluster/vmstorage` with [extraArgs](https://docs.victoriametrics.com/operator/resources/#extra-arguments). +### Retention filters -Here are complete example for [Retention filters](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#retention-filters): +Use `spec.vmstorage.retentionFilters` to configure [Retention filters](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#retention-filters) on `vmstorage`. +Each entry requires a MetricsQL label `filter` and a `retention` duration. +The global `spec.retentionPeriod` applies to all series that don't match any filter. ```yaml apiVersion: operator.victoriametrics.com/v1beta1 @@ -398,21 +403,24 @@ kind: VMCluster metadata: name: ent-example spec: - # enabling enterprise features license: keyRef: name: k8s-secret-that-contains-license key: key-in-a-secret-that-contains-license clusterVersion: v1.110.13-enterprise-cluster + retentionPeriod: "12" vmstorage: - extraArgs: - # using enterprise features: Retention filters - # more details about retention filters you can read on https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#retention-filters - retentionFilter: '{vm_account_id="5",env="dev"}:5d,{vm_account_id="5",env="prod"}:5y' + retentionFilters: + - filter: '{vm_account_id="5",env="dev"}' + retention: 5d + - filter: '{vm_account_id="5",env="prod"}' + retention: 5y # ...other fields... ``` +You can read more about retention filters configuration on the [VictoriaMetrics cluster retention filters page](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#retention-filters). + ### Advanced per-tenant statistic For using [Advanced per-tenant statistic](https://docs.victoriametrics.com/victoriametrics/pertenantstatistic/) diff --git a/docs/resources/vmsingle.md b/docs/resources/vmsingle.md index 64028fe77..f68c75c94 100644 --- a/docs/resources/vmsingle.md +++ b/docs/resources/vmsingle.md @@ -218,10 +218,10 @@ For using Enterprise version of [vmsingle](https://docs.victoriametrics.com/vict ### Downsampling -After that you can pass [Downsampling](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#downsampling) -flag to `VMSingle` with [extraArgs](https://docs.victoriametrics.com/operator/resources/#extra-arguments) too. - -Here are complete example for [Downsampling](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#downsampling): +Use `spec.downsampling` to configure [Downsampling](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#downsampling). +Each rule requires `offset` (how far back to downsample) and `interval` (target resolution). +An optional `filter` restricts the rule to matching time series. +The optional `dedupInterval` sets `-dedup.minScrapeInterval` for the instance. ```yaml apiVersion: operator.victoriametrics.com/v1beta1 @@ -229,24 +229,39 @@ kind: VMSingle metadata: name: ent-example spec: - # enabling enterprise features license: keyRef: name: k8s-secret-that-contains-license key: key-in-a-secret-that-contains-license image: tag: v1.110.13-enterprise - extraArgs: - # using enterprise features: Downsampling - # more details about downsampling you can read on https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#downsampling - downsampling.period: 30d:5m,180d:1h,1y:6h,2y:1d + downsampling: + dedupInterval: 1m + rules: + - periods: + - offset: 30d + interval: 5m + - offset: 180d + interval: 1h + - offset: 1y + interval: 6h + - filter: '{env="prod"}' + periods: + - offset: 30d + interval: 1m + - offset: 180d + interval: 10m # ...other fields... ``` +You can read more about downsampling configuration on the [VictoriaMetrics downsampling page](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#downsampling). + ### Retention filters -The same method is used to enable retention filters - here are complete example for [Retention filters](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention-filters). +Use `spec.retentionFilters` to configure [Retention filters](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention-filters). +Each entry requires a MetricsQL label `filter` and a `retention` duration. +The global `spec.retentionPeriod` applies to all series that don't match any filter. ```yaml apiVersion: operator.victoriametrics.com/v1beta1 @@ -254,21 +269,24 @@ kind: VMSingle metadata: name: ent-example spec: - # enabling enterprise features license: keyRef: name: k8s-secret-that-contains-license key: key-in-a-secret-that-contains-license image: tag: v1.110.13-enterprise - extraArgs: - # using enterprise features: Retention filters - # more details about retention filters you can read on https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention-filters - retentionFilter: '{team="juniors"}:3d,{env=~"dev|staging"}:30d' + retentionPeriod: "12" + retentionFilters: + - filter: '{team="juniors"}' + retention: 3d + - filter: '{env=~"dev|staging"}' + retention: 30d # ...other fields... ``` +You can read more about retention filters configuration on the [VictoriaMetrics retention filters page](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention-filters). + ### Backup automation You can check [vmbackupmanager documentation](https://docs.victoriametrics.com/victoriametrics/vmbackupmanager/) for backup automation. diff --git a/internal/controller/operator/factory/vmcluster/vmcluster.go b/internal/controller/operator/factory/vmcluster/vmcluster.go index 5fe9986cf..87b8376ac 100644 --- a/internal/controller/operator/factory/vmcluster/vmcluster.go +++ b/internal/controller/operator/factory/vmcluster/vmcluster.go @@ -584,7 +584,7 @@ func makePodSpecForVMSelect(cr *vmv1beta1.VMCluster) (*corev1.PodTemplateSpec, e replicationFactorIsSet = true } } - if !dedupIsSet { + if !dedupIsSet && (cr.Spec.Downsampling == nil || cr.Spec.Downsampling.DedupInterval == "") { args = append(args, "-dedup.minScrapeInterval=1ms") } if !replicationFactorIsSet { @@ -592,6 +592,21 @@ func makePodSpecForVMSelect(cr *vmv1beta1.VMCluster) (*corev1.PodTemplateSpec, e } } + if cr.Spec.Downsampling.HasAnyRule() { + for _, rule := range cr.Spec.Downsampling.Rules { + for _, p := range rule.Periods { + period := fmt.Sprintf("%s:%s", p.Offset, p.Interval) + if rule.Filter != "" { + period = rule.Filter + ":" + period + } + args = append(args, fmt.Sprintf("-downsampling.period=%s", period)) + } + } + } + if cr.Spec.Downsampling != nil && cr.Spec.Downsampling.DedupInterval != "" { + args = append(args, fmt.Sprintf("-dedup.minScrapeInterval=%s", cr.Spec.Downsampling.DedupInterval)) + } + storageNodeFlag := build.NewFlag("-storageNode", "") storageNodeIds := cr.AvailableStorageNodeIDs(vmv1beta1.ClusterComponentSelect) for idx, i := range storageNodeIds { @@ -1012,11 +1027,30 @@ func makePodSpecForVMStorage(ctx context.Context, cr *vmv1beta1.VMCluster) (*cor dedupIsSet = true } } - if !dedupIsSet { + if !dedupIsSet && (cr.Spec.Downsampling == nil || cr.Spec.Downsampling.DedupInterval == "") { args = append(args, "-dedup.minScrapeInterval=1ms") } } + if cr.Spec.Downsampling.HasAnyRule() { + for _, rule := range cr.Spec.Downsampling.Rules { + for _, p := range rule.Periods { + period := fmt.Sprintf("%s:%s", p.Offset, p.Interval) + if rule.Filter != "" { + period = rule.Filter + ":" + period + } + args = append(args, fmt.Sprintf("-downsampling.period=%s", period)) + } + } + } + if cr.Spec.Downsampling != nil && cr.Spec.Downsampling.DedupInterval != "" { + args = append(args, fmt.Sprintf("-dedup.minScrapeInterval=%s", cr.Spec.Downsampling.DedupInterval)) + } + + for _, rf := range cr.Spec.VMStorage.RetentionFilters { + args = append(args, fmt.Sprintf("-retentionFilter=%s:%s", rf.Filter, rf.Retention)) + } + var envs []corev1.EnvVar envs = append(envs, cr.Spec.VMStorage.ExtraEnvs...) diff --git a/internal/controller/operator/factory/vmsingle/vmsingle.go b/internal/controller/operator/factory/vmsingle/vmsingle.go index fc9107a05..2d1ad9f86 100644 --- a/internal/controller/operator/factory/vmsingle/vmsingle.go +++ b/internal/controller/operator/factory/vmsingle/vmsingle.go @@ -175,6 +175,25 @@ func newPodSpec(ctx context.Context, cr *vmv1beta1.VMSingle) (*corev1.PodTemplat args = append(args, fmt.Sprintf("-retentionPeriod=%s", cr.Spec.RetentionPeriod)) } + if cr.Spec.Downsampling.HasAnyRule() { + for _, rule := range cr.Spec.Downsampling.Rules { + for _, p := range rule.Periods { + period := fmt.Sprintf("%s:%s", p.Offset, p.Interval) + if rule.Filter != "" { + period = rule.Filter + ":" + period + } + args = append(args, fmt.Sprintf("-downsampling.period=%s", period)) + } + } + } + if cr.Spec.Downsampling != nil && cr.Spec.Downsampling.DedupInterval != "" { + args = append(args, fmt.Sprintf("-dedup.minScrapeInterval=%s", cr.Spec.Downsampling.DedupInterval)) + } + + for _, rf := range cr.Spec.RetentionFilters { + args = append(args, fmt.Sprintf("-retentionFilter=%s:%s", rf.Filter, rf.Retention)) + } + storagePath := dataDir if cr.Spec.StorageDataPath != "" { storagePath = cr.Spec.StorageDataPath