Skip to content

Commit

Permalink
Cherry Pick: Adds ability to specify sanitizeOptions for prometheus m…
Browse files Browse the repository at this point in the history
…etrics (#3170) (#3174)

Adds ability to specify sanitizeOptions for prometheus metrics (#3170)

This PR adds the ability to set specific tally.SanitizeOptions via temporal configuration. If the configuration is not set, we rely on the default that is defined in code. Otherwise, we will generate a tally.SanitizeOptions by parsing the configuration file and converting it to a tally.SanitizeOptions.

Because configuration comes in via yaml, all relevant input fields are strings. The code converts the strings to runes and performs length check validations. Any invalid configuration will fail server startup.
  • Loading branch information
mastermanu authored and alexshtin committed Aug 2, 2022
1 parent 7d9b262 commit 26af874
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

.idea/
.vscode/settings.json
.vscode
*.code-workspace
.DS_Store
.coverage
Expand Down
109 changes: 107 additions & 2 deletions common/metrics/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
package metrics

import (
"errors"
"fmt"
"time"

"github.com/cactus/go-statsd-client/statsd"
Expand Down Expand Up @@ -134,6 +136,11 @@ type (
// on the specified listen address or registering a metric with the
// Prometheus. By default the registerer will panic.
OnError string `yaml:"onError"`

// Deprecated. SanitizeOptions is an optional field that enables a user to
// specify which characters are valid and/or should be replaced before metrics
// are emitted.
SanitizeOptions *SanitizeOptions `yaml:"sanitizeOptions"`
}
)

Expand All @@ -150,6 +157,23 @@ type SummaryObjective struct {
AllowedError float64 `yaml:"allowedError"`
}

type SanitizeRange struct {
StartRange string `yaml:"startRange"`
EndRange string `yaml:"endRange"`
}

type ValidCharacters struct {
Ranges []SanitizeRange `yaml:"ranges"`
SafeCharacters string `yaml:"safeChars"`
}

type SanitizeOptions struct {
NameCharacters *ValidCharacters `yaml:"nameChars"`
KeyCharacters *ValidCharacters `yaml:"keyChars"`
ValueCharacters *ValidCharacters `yaml:"valueChars"`
ReplacementCharacter string `yaml:"replacementChar"`
}

// Supported framework types
const (
// FrameworkTally tally framework id
Expand All @@ -175,7 +199,7 @@ const (
var (
safeCharacters = []rune{'_'}

sanitizeOptions = tally.SanitizeOptions{
defaultTallySanitizeOptions = tally.SanitizeOptions{
NameCharacters: tally.ValidCharacters{
Ranges: tally.AlphanumericRange,
Characters: safeCharacters,
Expand Down Expand Up @@ -265,7 +289,18 @@ func NewScope(logger log.Logger, c *Config) tally.Scope {
return newStatsdScope(logger, c)
}
if c.Prometheus != nil {
return newPrometheusScope(logger, convertPrometheusConfigToTally(c.Prometheus), &c.ClientConfig)
sanitizeOptions, err := convertSanitizeOptionsToTally(c.Prometheus)
if err != nil {
logger.Fatal("invalid sanitize options input on prometheus config", tag.Error(err))
return nil
}

return newPrometheusScope(
logger,
convertPrometheusConfigToTally(c.Prometheus),
sanitizeOptions,
&c.ClientConfig,
)
}
return tally.NoopScope
}
Expand All @@ -289,6 +324,14 @@ func convertPrometheusConfigToTally(
}
}

func convertSanitizeOptionsToTally(config *PrometheusConfig) (tally.SanitizeOptions, error) {
if config.SanitizeOptions == nil {
return defaultTallySanitizeOptions, nil
}

return config.SanitizeOptions.toTally()
}

func setDefaultPerUnitHistogramBoundaries(clientConfig *ClientConfig) {
buckets := maps.Clone(defaultPerUnitHistogramBoundaries)

Expand Down Expand Up @@ -338,6 +381,7 @@ func newStatsdScope(logger log.Logger, c *Config) tally.Scope {
func newPrometheusScope(
logger log.Logger,
config *prometheus.Configuration,
sanitizeOptions tally.SanitizeOptions,
clientConfig *ClientConfig,
) tally.Scope {
reporter, err := config.NewReporter(
Expand Down Expand Up @@ -396,3 +440,64 @@ func configExcludeTags(cfg ClientConfig) map[string]map[string]struct{} {
}
return tagsToFilter
}

func (s SanitizeRange) toTally() (tally.SanitizeRange, error) {
startRangeRunes := []rune(s.StartRange)
if len(startRangeRunes) != 1 {
return tally.SanitizeRange{}, fmt.Errorf("start range '%+v' must be a single rune", startRangeRunes)
}

endRangeRunes := []rune(s.EndRange)
if len(endRangeRunes) != 1 {
return tally.SanitizeRange{}, fmt.Errorf("end range '%+v' must be a single rune", endRangeRunes)
}

return tally.SanitizeRange([2]rune{startRangeRunes[0], endRangeRunes[0]}), nil
}

func (v ValidCharacters) toTally() (tally.ValidCharacters, error) {
var ranges []tally.SanitizeRange

for _, r := range v.Ranges {
tallyRange, err := r.toTally()
if err != nil {
return tally.ValidCharacters{}, err
}

ranges = append(ranges, tallyRange)
}

return tally.ValidCharacters{
Ranges: ranges,
Characters: []rune(v.SafeCharacters),
}, nil
}

func (s SanitizeOptions) toTally() (tally.SanitizeOptions, error) {
tallyNameChars, err := s.NameCharacters.toTally()
if err != nil {
return tally.SanitizeOptions{}, fmt.Errorf("invalid nameChars: %v", err)
}

tallyKeyChars, err := s.KeyCharacters.toTally()
if err != nil {
return tally.SanitizeOptions{}, fmt.Errorf("invalid keyChars: %v", err)
}

tallyValueChars, err := s.ValueCharacters.toTally()
if err != nil {
return tally.SanitizeOptions{}, fmt.Errorf("invalid valueChars: %v", err)
}

replacementChars := []rune(s.ReplacementCharacter)
if len(replacementChars) != 1 {
return tally.SanitizeOptions{}, errors.New("can only specify a single replacement character")
}

return tally.SanitizeOptions{
NameCharacters: tallyNameChars,
KeyCharacters: tallyKeyChars,
ValueCharacters: tallyValueChars,
ReplacementCharacter: replacementChars[0],
}, nil
}
36 changes: 36 additions & 0 deletions common/metrics/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,42 @@ func (s *MetricsSuite) TestPrometheus() {
s.NotNil(scope)
}

func (s *MetricsSuite) TestPrometheusWithSanitizeOptions() {
validChars := &ValidCharacters{
Ranges: []SanitizeRange{
{
StartRange: "a",
EndRange: "z",
},
{
StartRange: "A",
EndRange: "Z",
},
{
StartRange: "0",
EndRange: "9",
},
},
SafeCharacters: "-",
}

prom := &PrometheusConfig{
OnError: "panic",
TimerType: "histogram",
ListenAddress: "127.0.0.1:0",
SanitizeOptions: &SanitizeOptions{
NameCharacters: validChars,
KeyCharacters: validChars,
ValueCharacters: validChars,
ReplacementCharacter: "_",
},
}
config := new(Config)
config.Prometheus = prom
scope := NewScope(log.NewNoopLogger(), config)
s.NotNil(scope)
}

func (s *MetricsSuite) TestNoop() {
config := &Config{}
scope := NewScope(log.NewNoopLogger(), config)
Expand Down

0 comments on commit 26af874

Please sign in to comment.