1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00
kyverno/pkg/config/metricsconfig.go
NeuroticalT 370abe257e
Fix: metrics exposure inconsistencies and unwanted side-effects (#10016)
* Change: metrics exposure improvement

Signed-off-by: Tamas Eger <tamas.eger@instructure.com>

* Fix: addressing linter errors

Signed-off-by: Tamas Eger <tamas.eger@instructure.com>

* Fix: unit test assert failure

Signed-off-by: Tamas Eger <tamas.eger@instructure.com>

---------

Signed-off-by: Tamas Eger <tamas.eger@instructure.com>
Co-authored-by: Tamas Eger <tamas.eger@instructure.com>
2024-04-22 07:33:04 +00:00

260 lines
7 KiB
Go

package config
import (
"maps"
"slices"
"sync"
"time"
"go.opentelemetry.io/otel/attribute"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
corev1 "k8s.io/api/core/v1"
)
// MetricsConfig stores the config for metrics
type MetricsConfiguration interface {
// GetExcludeNamespaces returns the namespaces to ignore for metrics exposure
GetExcludeNamespaces() []string
// GetIncludeNamespaces returns the namespaces to specifically consider for metrics exposure
GetIncludeNamespaces() []string
// GetMetricsRefreshInterval returns the refresh interval for the metrics
GetMetricsRefreshInterval() time.Duration
// CheckNamespace returns `true` if the namespace has to be considered
CheckNamespace(string) bool
// GetBucketBoundaries returns the bucket boundaries for Histogram metrics
GetBucketBoundaries() []float64
// BuildMeterProviderViews returns OTL view removing attributes which were disabled in the config
BuildMeterProviderViews() []sdkmetric.View
// Load loads configuration from a configmap
Load(*corev1.ConfigMap)
// OnChanged adds a callback to be invoked when the configuration is reloaded
OnChanged(func())
}
// metricsConfig stores the config for metrics
type metricsConfig struct {
namespaces namespacesConfig
metricsRefreshInterval time.Duration
bucketBoundaries []float64
metricsExposure map[string]metricExposureConfig
mux sync.RWMutex
callbacks []func()
}
// NewDefaultMetricsConfiguration ...
func NewDefaultMetricsConfiguration() *metricsConfig {
config := metricsConfig{}
config.reset()
return &config
}
func (cd *metricsConfig) OnChanged(callback func()) {
cd.mux.Lock()
defer cd.mux.Unlock()
cd.callbacks = append(cd.callbacks, callback)
}
// GetExcludeNamespaces returns the namespaces to ignore for metrics exposure
func (mcd *metricsConfig) GetExcludeNamespaces() []string {
mcd.mux.RLock()
defer mcd.mux.RUnlock()
return mcd.namespaces.ExcludeNamespaces
}
// GetIncludeNamespaces returns the namespaces to specifically consider for metrics exposure
func (mcd *metricsConfig) GetIncludeNamespaces() []string {
mcd.mux.RLock()
defer mcd.mux.RUnlock()
return mcd.namespaces.IncludeNamespaces
}
// GetBucketBoundaries returns the bucket boundaries for Histogram metrics
func (mcd *metricsConfig) GetBucketBoundaries() []float64 {
mcd.mux.RLock()
defer mcd.mux.RUnlock()
return mcd.bucketBoundaries
}
func (mcd *metricsConfig) BuildMeterProviderViews() []sdkmetric.View {
mcd.mux.RLock()
defer mcd.mux.RUnlock()
views := []sdkmetric.View{}
if len(mcd.metricsExposure) > 0 {
metricsExposure := maps.Clone(mcd.metricsExposure)
views = append(views, func(i sdkmetric.Instrument) (sdkmetric.Stream, bool) {
s := sdkmetric.Stream{Name: i.Name, Description: i.Description, Unit: i.Unit}
config, exists := metricsExposure[i.Name]
if !exists {
return s, false
}
if config.Enabled != nil && !*config.Enabled {
s.Aggregation = sdkmetric.AggregationDrop{}
return s, true
}
if len(config.DisabledLabelDimensions) > 0 {
s.AttributeFilter = func(kv attribute.KeyValue) bool {
return !slices.Contains(config.DisabledLabelDimensions, string(kv.Key))
}
}
if len(config.BucketBoundaries) > 0 {
aggregation := sdkmetric.DefaultAggregationSelector(i.Kind)
switch a := aggregation.(type) {
case sdkmetric.AggregationExplicitBucketHistogram:
a.Boundaries = config.BucketBoundaries
a.NoMinMax = false
s.Aggregation = a
}
}
return s, true
})
}
return views
}
// GetMetricsRefreshInterval returns the refresh interval for the metrics
func (mcd *metricsConfig) GetMetricsRefreshInterval() time.Duration {
mcd.mux.RLock()
defer mcd.mux.RUnlock()
return mcd.metricsRefreshInterval
}
// CheckNamespace returns `true` if the namespace has to be considered
func (mcd *metricsConfig) CheckNamespace(namespace string) bool {
mcd.mux.RLock()
defer mcd.mux.RUnlock()
// TODO(eddycharly): check we actually need `"-"`
if namespace == "" || namespace == "-" {
return true
}
if slices.Contains(mcd.namespaces.ExcludeNamespaces, namespace) {
return false
}
if len(mcd.namespaces.IncludeNamespaces) == 0 {
return true
}
return slices.Contains(mcd.namespaces.IncludeNamespaces, namespace)
}
func (mcd *metricsConfig) Load(cm *corev1.ConfigMap) {
if cm != nil {
mcd.load(cm)
} else {
mcd.unload()
}
}
func (cd *metricsConfig) load(cm *corev1.ConfigMap) {
logger := logger.WithValues("name", cm.Name, "namespace", cm.Namespace)
cd.mux.Lock()
defer cd.mux.Unlock()
defer cd.notify()
data := cm.Data
if data == nil {
data = map[string]string{}
}
// reset
cd.reset()
// load metricsRefreshInterval
metricsRefreshInterval, ok := data["metricsRefreshInterval"]
if !ok {
logger.Info("metricsRefreshInterval not set")
} else {
logger := logger.WithValues("metricsRefreshInterval", metricsRefreshInterval)
metricsRefreshInterval, err := time.ParseDuration(metricsRefreshInterval)
if err != nil {
logger.Error(err, "failed to parse metricsRefreshInterval")
} else {
cd.metricsRefreshInterval = metricsRefreshInterval
logger.Info("metricsRefreshInterval configured")
}
}
// load namespaces
namespaces, ok := data["namespaces"]
if !ok {
logger.Info("namespaces not set")
} else {
logger := logger.WithValues("namespaces", namespaces)
namespaces, err := parseIncludeExcludeNamespacesFromNamespacesConfig(namespaces)
if err != nil {
logger.Error(err, "failed to parse namespaces")
} else {
cd.namespaces = namespaces
logger.Info("namespaces configured")
}
}
// load bucket boundaries
bucketBoundariesString, ok := data["bucketBoundaries"]
if !ok {
logger.Info("bucketBoundaries not set")
} else {
logger := logger.WithValues("bucketBoundaries", bucketBoundariesString)
bucketBoundaries, err := parseBucketBoundariesConfig(bucketBoundariesString)
if err != nil {
logger.Error(err, "failed to parse bucketBoundariesString")
} else {
cd.bucketBoundaries = bucketBoundaries
logger.Info("bucketBoundaries configured")
}
}
// load include resource details
metricsExposureString, ok := data["metricsExposure"]
if !ok {
logger.Info("metricsExposure not set")
} else {
logger := logger.WithValues("metricsExposure", metricsExposureString)
metricsExposure, err := parseMetricExposureConfig(metricsExposureString, cd.bucketBoundaries)
if err != nil {
logger.Error(err, "failed to parse metricsExposure")
} else {
cd.metricsExposure = metricsExposure
logger.Info("metricsExposure configured")
}
}
}
func (mcd *metricsConfig) unload() {
mcd.mux.Lock()
defer mcd.mux.Unlock()
defer mcd.notify()
mcd.reset()
}
func (mcd *metricsConfig) reset() {
mcd.metricsRefreshInterval = 0
mcd.namespaces = namespacesConfig{
IncludeNamespaces: []string{},
ExcludeNamespaces: []string{},
}
mcd.bucketBoundaries = []float64{
0.005,
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1,
2.5,
5,
10,
15,
20,
25,
30,
}
mcd.metricsExposure = map[string]metricExposureConfig{}
}
func (mcd *metricsConfig) notify() {
for _, callback := range mcd.callbacks {
callback()
}
}