add flag allow-metric-labels to get the input metric label value allowlist.

This commit is contained in:
yoyinzyc 2021-02-23 15:21:46 -08:00
parent 3022b39817
commit a44bb76f1e
4 changed files with 243 additions and 11 deletions

View File

@ -18,6 +18,7 @@ package metrics
import ( import (
"fmt" "fmt"
"regexp"
"github.com/blang/semver" "github.com/blang/semver"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -29,6 +30,7 @@ import (
type Options struct { type Options struct {
ShowHiddenMetricsForVersion string ShowHiddenMetricsForVersion string
DisabledMetrics []string DisabledMetrics []string
AllowListMapping map[string]string
} }
// NewOptions returns default metrics options // NewOptions returns default metrics options
@ -38,12 +40,20 @@ func NewOptions() *Options {
// Validate validates metrics flags options. // Validate validates metrics flags options.
func (o *Options) Validate() []error { func (o *Options) Validate() []error {
var errs []error
err := validateShowHiddenMetricsVersion(parseVersion(version.Get()), o.ShowHiddenMetricsForVersion) err := validateShowHiddenMetricsVersion(parseVersion(version.Get()), o.ShowHiddenMetricsForVersion)
if err != nil { if err != nil {
return []error{err} errs = append(errs, err)
} }
return nil if err := validateAllowMetricLabel(o.AllowListMapping); err != nil {
errs = append(errs, err)
}
if len(errs) == 0 {
return nil
}
return errs
} }
// AddFlags adds flags for exposing component metrics. // AddFlags adds flags for exposing component metrics.
@ -63,6 +73,10 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) {
"This flag provides an escape hatch for misbehaving metrics. "+ "This flag provides an escape hatch for misbehaving metrics. "+
"You must provide the fully qualified metric name in order to disable it. "+ "You must provide the fully qualified metric name in order to disable it. "+
"Disclaimer: disabling metrics is higher in precedence than showing hidden metrics.") "Disclaimer: disabling metrics is higher in precedence than showing hidden metrics.")
fs.StringToStringVar(&o.AllowListMapping, "allow-metric-labels", o.AllowListMapping,
"The map from metric-label to value allow-list of this label. The key's format is <MetricName>,<LabelName>. "+
"The value's format is <allowed_value>,<allowed_value>..."+
"e.g. metric1,label1='v1,v2,v3', metric1,label2='v1,v2,v3' metric2,label1='v1,v2,v3'.")
} }
// Apply applies parameters into global configuration of metrics. // Apply applies parameters into global configuration of metrics.
@ -77,6 +91,9 @@ func (o *Options) Apply() {
for _, metricName := range o.DisabledMetrics { for _, metricName := range o.DisabledMetrics {
SetDisabledMetric(metricName) SetDisabledMetric(metricName)
} }
if o.AllowListMapping != nil {
SetLabelAllowListFromCLI(o.AllowListMapping)
}
} }
func validateShowHiddenMetricsVersion(currentVersion semver.Version, targetVersionStr string) error { func validateShowHiddenMetricsVersion(currentVersion semver.Version, targetVersionStr string) error {
@ -91,3 +108,18 @@ func validateShowHiddenMetricsVersion(currentVersion semver.Version, targetVersi
return nil return nil
} }
func validateAllowMetricLabel(allowListMapping map[string]string) error {
if allowListMapping == nil {
return nil
}
metricNameRegex := `[a-zA-Z_:][a-zA-Z0-9_:]*`
labelRegex := `[a-zA-Z_][a-zA-Z0-9_]*`
for k := range allowListMapping {
reg := regexp.MustCompile(metricNameRegex + `,` + labelRegex)
if reg.FindString(k) != k {
return fmt.Errorf("--allow-metric-labels must has a list of kv pair with format `metricName:labelName=labelValue, labelValue,...`")
}
}
return nil
}

View File

@ -0,0 +1,67 @@
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package metrics
import "testing"
func TestValidateAllowMetricLabel(t *testing.T) {
var tests = []struct {
name string
input map[string]string
expectedError bool
}{
{
"validated",
map[string]string{
"metric_name,label_name": "labelValue1,labelValue2",
},
false,
},
{
"metric name is not valid",
map[string]string{
"-metric_name,label_name": "labelValue1,labelValue2",
},
true,
},
{
"label name is not valid",
map[string]string{
"metric_name,:label_name": "labelValue1,labelValue2",
},
true,
},
{
"no label name",
map[string]string{
"metric_name": "labelValue1,labelValue2",
},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateAllowMetricLabel(tt.input)
if err == nil && tt.expectedError {
t.Error("Got error is nil, wanted error is not nil")
}
if err != nil && !tt.expectedError {
t.Errorf("Got error is %v, wanted no error", err)
}
})
}
}

View File

@ -18,10 +18,17 @@ package metrics
import ( import (
"fmt" "fmt"
"strings"
"sync" "sync"
"time" "time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"k8s.io/apimachinery/pkg/util/sets"
)
var (
labelValueAllowLists = map[string]*MetricLabelAllowList{}
allowListLock sync.RWMutex
) )
// KubeOpts is superset struct for prometheus.Opts. The prometheus Opts structure // KubeOpts is superset struct for prometheus.Opts. The prometheus Opts structure
@ -31,15 +38,16 @@ import (
// Name must be set to a non-empty string. DeprecatedVersion is defined only // Name must be set to a non-empty string. DeprecatedVersion is defined only
// if the metric for which this options applies is, in fact, deprecated. // if the metric for which this options applies is, in fact, deprecated.
type KubeOpts struct { type KubeOpts struct {
Namespace string Namespace string
Subsystem string Subsystem string
Name string Name string
Help string Help string
ConstLabels map[string]string ConstLabels map[string]string
DeprecatedVersion string DeprecatedVersion string
deprecateOnce sync.Once deprecateOnce sync.Once
annotateOnce sync.Once annotateOnce sync.Once
StabilityLevel StabilityLevel StabilityLevel StabilityLevel
LabelValueAllowLists *MetricLabelAllowList
} }
// BuildFQName joins the given three name components by "_". Empty name // BuildFQName joins the given three name components by "_". Empty name
@ -243,3 +251,49 @@ func (o *SummaryOpts) toPromSummaryOpts() prometheus.SummaryOpts {
BufCap: o.BufCap, BufCap: o.BufCap,
} }
} }
type MetricLabelAllowList struct {
labelToAllowList map[string]sets.String
}
func (allowList *MetricLabelAllowList) ConstrainToAllowedList(labelNameList, labelValueList []string) {
for index, value := range labelValueList {
name := labelNameList[index]
if allowValues, ok := allowList.labelToAllowList[name]; ok {
if !allowValues.Has(value) {
labelValueList[index] = "unexpected"
}
}
}
}
func (allowList *MetricLabelAllowList) ConstrainLabelMap(labels map[string]string) {
for name, value := range labels {
if allowValues, ok := allowList.labelToAllowList[name]; ok {
if !allowValues.Has(value) {
labels[name] = "unexpected"
}
}
}
}
func SetLabelAllowListFromCLI(allowListMapping map[string]string) {
allowListLock.Lock()
defer allowListLock.Unlock()
for metricLabelName, labelValues := range allowListMapping {
metricName := strings.Split(metricLabelName, ",")[0]
labelName := strings.Split(metricLabelName, ",")[1]
valueSet := sets.NewString(strings.Split(labelValues, ",")...)
allowList, ok := labelValueAllowLists[metricName]
if ok {
allowList.labelToAllowList[labelName] = valueSet
} else {
labelToAllowList := make(map[string]sets.String)
labelToAllowList[labelName] = valueSet
labelValueAllowLists[metricName] = &MetricLabelAllowList{
labelToAllowList,
}
}
}
}

View File

@ -17,9 +17,11 @@ limitations under the License.
package metrics package metrics
import ( import (
"reflect"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/util/sets"
) )
func TestDefaultStabilityLevel(t *testing.T) { func TestDefaultStabilityLevel(t *testing.T) {
@ -59,3 +61,80 @@ func TestDefaultStabilityLevel(t *testing.T) {
}) })
} }
} }
func TestConstrainToAllowedList(t *testing.T) {
allowList := &MetricLabelAllowList{
labelToAllowList: map[string]sets.String{
"label_a": sets.NewString("allow_value1", "allow_value2"),
},
}
labelNameList := []string{"label_a", "label_b"}
var tests = []struct {
name string
inputLabelValueList []string
outputLabelValueList []string
}{
{
"no unexpected value",
[]string{"allow_value1", "label_b_value"},
[]string{"allow_value1", "label_b_value"},
},
{
"with unexpected value",
[]string{"not_allowed", "label_b_value"},
[]string{"unexpected", "label_b_value"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
allowList.ConstrainToAllowedList(labelNameList, test.inputLabelValueList)
if !reflect.DeepEqual(test.inputLabelValueList, test.outputLabelValueList) {
t.Errorf("Got %v, expected %v", test.inputLabelValueList, test.outputLabelValueList)
}
})
}
}
func TestConstrainLabelMap(t *testing.T) {
allowList := &MetricLabelAllowList{
labelToAllowList: map[string]sets.String{
"label_a": sets.NewString("allow_value1", "allow_value2"),
},
}
var tests = []struct {
name string
inputLabelMap map[string]string
outputLabelMap map[string]string
}{
{
"no unexpected value",
map[string]string{
"label_a": "allow_value1",
"label_b": "label_b_value",
},
map[string]string{
"label_a": "allow_value1",
"label_b": "label_b_value",
},
},
{
"with unexpected value",
map[string]string{
"label_a": "not_allowed",
"label_b": "label_b_value",
},
map[string]string{
"label_a": "unexpected",
"label_b": "label_b_value",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
allowList.ConstrainLabelMap(test.inputLabelMap)
if !reflect.DeepEqual(test.inputLabelMap, test.outputLabelMap) {
t.Errorf("Got %v, expected %v", test.inputLabelMap, test.outputLabelMap)
}
})
}
}