Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- [ShadowMode](#shadowmode)
- [Including detailed metrics for unspecified values](#including-detailed-metrics-for-unspecified-values)
- [Including descriptor values in metrics](#including-descriptor-values-in-metrics)
- [Limit override descriptor key config](#limit-override-descriptor-key-config)
- [Sharing thresholds for wildcard matches](#sharing-thresholds-for-wildcard-matches)
- [Examples](#examples)
- [Example 1](#example-1)
Expand Down Expand Up @@ -353,6 +354,46 @@ Setting `value_to_metric: true` (default: `false`) for a descriptor will include

When combined with wildcard matching, the full runtime value is included in the metric key, not just the wildcard prefix. This feature works independently of `detailed_metric` - when `detailed_metric` is set, it takes precedence and `value_to_metric` is ignored.

### Limit override descriptor key config

When Envoy sends a [rate limit override](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/rate_limit_filter#config-http-filters-rate-limit-override) on a descriptor (`limit` set in the request), the service builds the stats key from the descriptor entries in the request. By default, every non-empty entry value is appended to the key (legacy behavior), which can produce high metric cardinality when many descriptor entries are present.

`value_to_metric` and `detailed_metric` apply to limits matched from the rate limit **configuration**. They do not control stats keys for Envoy **overrides**.

To selectively include entry values in override stats keys, set `DESCRIPTOR_KEY_CONFIG` to the path of a YAML file (see [`examples/ratelimit/descriptor_key.yaml`](examples/ratelimit/descriptor_key.yaml)). The file is loaded once when the configuration provider starts (file or xDS). Changing the file requires a process restart.

```yaml
default:
include_entry_value_for_keys: []

domains:
example:
include_entry_value_for_keys:
- key1
- key2
```

- **`default`**: used for any domain not listed under `domains`.
- **`domains`**: per-domain allowlists of entry **keys** whose runtime **values** are appended as `_<value>` in the stats key.
- Unlisted entry keys contribute only the key name (no value suffix).

If `DESCRIPTOR_KEY_CONFIG` is unset, all non-empty entry values are included (backward compatible).

**Example:** for domain `example`, descriptor entries `key1=value1`, `key2=value2`, `key3=value3`, and the YAML above:

- Default (no config): `example.key1_value1.key2_value2.key3_value3`
- With config: `example.key1_value1.key2_value2.key3`

This affects **stats keys only** for override limits. Redis cache keys are unchanged. Limits matched from the rate limit configuration file continue to use the existing stats key rules (`detailed_metric`, `value_to_metric`, etc.).

The configuration checker accepts the same file:

```bash
ratelimit_config_check \
-config_dir=examples/ratelimit/config \
-descriptor_key_config=examples/ratelimit/descriptor_key.yaml
```

### Sharing thresholds for wildcard matches

Setting `share_threshold: true` (default: `false`) for a descriptor with a wildcard value (ending with `*`) allows all values matching that wildcard to share the same rate limit threshold, instead of using isolated thresholds for each matching value.
Expand Down Expand Up @@ -1045,6 +1086,7 @@ ratelimit.service.rate_limit.messaging.auth-service.over_limit.shadow_mode: 1
## Statistics options

1. `EXTRA_TAGS`: set to `"<k1:v1>,<k2:v2>"` to tag all emitted stats with the provided tags. You might want to tag build commit or release version, for example.
2. `DESCRIPTOR_KEY_CONFIG`: path to a YAML file that controls which descriptor entry values are included in stats keys for Envoy limit overrides. See [Limit override descriptor key config](#limit-override-descriptor-key-config). Loaded at startup; restart required to pick up changes.

## DogStatsD

Expand Down
18 changes: 18 additions & 0 deletions examples/ratelimit/descriptor_key.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Controls which descriptor entry keys include their runtime value in rate limit stats key
# for Envoy limit overrides (descriptorKey). This config file only affects the stats key, the
# cache key generation is unaffected.
#
# When DESCRIPTOR_KEY_CONFIG points at this file:
# - Listed entries append "_<value>" to the rate limit key segment.
# - Unlisted entries contribute only the entry name, reducing cardinality.
#
# Omit DESCRIPTOR_KEY_CONFIG to keep legacy behavior (all non-empty values included).

default:
include_entry_value_for_keys: []

domains:
example:
include_entry_value_for_keys:
- key1
- key2
2 changes: 1 addition & 1 deletion src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,5 @@ type RateLimitConfigLoader interface {
// @param mergeDomainConfigs defines whether multiple configurations referencing the same domain will be merged or rejected throwing an error.
// @return a new configuration.
// @throws RateLimitConfigError if the configuration could not be created.
Load(configs []RateLimitConfigToLoad, statsManager stats.Manager, mergeDomainConfigs bool) RateLimitConfig
Load(configs []RateLimitConfigToLoad, statsManager stats.Manager, mergeDomainConfigs bool, descriptorKeyConfig *DescriptorKeyConfig) RateLimitConfig
}
24 changes: 16 additions & 8 deletions src/config/config_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ type rateLimitDomain struct {
}

type rateLimitConfigImpl struct {
domains map[string]*rateLimitDomain
statsManager stats.Manager
mergeDomainConfigs bool
domains map[string]*rateLimitDomain
statsManager stats.Manager
mergeDomainConfigs bool
descriptorKeyConfig *DescriptorKeyConfig
}

var validKeys = map[string]bool{
Expand Down Expand Up @@ -457,7 +458,7 @@ func (this *rateLimitConfigImpl) GetLimit(
}

if descriptor.GetLimit() != nil {
rateLimitKey := descriptorKey(domain, descriptor)
rateLimitKey := descriptorKey(domain, descriptor, this.descriptorKeyConfig)
rateLimitOverrideUnit := pb.RateLimitResponse_RateLimit_Unit(descriptor.GetLimit().GetUnit())
// When limit override is provided by envoy config, we don't want to enable shadow_mode
rateLimit = NewRateLimit(
Expand Down Expand Up @@ -676,14 +677,14 @@ func (this *rateLimitConfigImpl) IsEmptyDomains() bool {
return len(this.domains) == 0
}

func descriptorKey(domain string, descriptor *pb_struct.RateLimitDescriptor) string {
func descriptorKey(domain string, descriptor *pb_struct.RateLimitDescriptor, cfg *DescriptorKeyConfig) string {
rateLimitKey := ""
for _, entry := range descriptor.Entries {
if rateLimitKey != "" {
rateLimitKey += "."
}
rateLimitKey += entry.Key
if entry.Value != "" {
if entry.Value != "" && cfg.IncludeEntryValueForKey(domain, entry.Key) {
rateLimitKey += "_" + entry.Value
}
}
Expand Down Expand Up @@ -722,8 +723,14 @@ func ConfigFileContentToYaml(fileName, content string) *YamlRoot {
// @return a new config.
func NewRateLimitConfigImpl(
configs []RateLimitConfigToLoad, statsManager stats.Manager, mergeDomainConfigs bool,
descriptorKeyConfig *DescriptorKeyConfig,
) RateLimitConfig {
ret := &rateLimitConfigImpl{map[string]*rateLimitDomain{}, statsManager, mergeDomainConfigs}
ret := &rateLimitConfigImpl{
domains: map[string]*rateLimitDomain{},
statsManager: statsManager,
mergeDomainConfigs: mergeDomainConfigs,
descriptorKeyConfig: descriptorKeyConfig,
}
for _, config := range configs {
ret.loadConfig(config)
}
Expand All @@ -735,8 +742,9 @@ type rateLimitConfigLoaderImpl struct{}

func (this *rateLimitConfigLoaderImpl) Load(
configs []RateLimitConfigToLoad, statsManager stats.Manager, mergeDomainConfigs bool,
descriptorKeyConfig *DescriptorKeyConfig,
) RateLimitConfig {
return NewRateLimitConfigImpl(configs, statsManager, mergeDomainConfigs)
return NewRateLimitConfigImpl(configs, statsManager, mergeDomainConfigs, descriptorKeyConfig)
}

// @return a new default config loader implementation.
Expand Down
84 changes: 84 additions & 0 deletions src/config/descriptor_key_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package config

import (
"fmt"
"os"

"gopkg.in/yaml.v2"
)

// DescriptorKeyConfig controls which descriptor entries include their runtime
// value in rate limit stats keys built by descriptorKey for Envoy limit overrides.
type DescriptorKeyConfig struct {
defaultKeys map[string]struct{}
domainKeys map[string]map[string]struct{}
}

type yamlDescriptorKeyRoot struct {
Default *yamlDescriptorKeyRule `yaml:"default"`
Domains map[string]yamlDescriptorKeyRule `yaml:"domains"`
}

type yamlDescriptorKeyRule struct {
IncludeEntryValueForKeys []string `yaml:"include_entry_value_for_keys"`
}

// LoadDescriptorKeyConfig reads descriptor key rules from a YAML file.
// Returns nil if path is empty (legacy behavior: include all non-empty values).
func LoadDescriptorKeyConfig(path string) (*DescriptorKeyConfig, error) {
if path == "" {
return nil, nil
}

content, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("descriptor key config: %w", err)
}
return ParseDescriptorKeyConfig(content)
}

func ParseDescriptorKeyConfig(content []byte) (*DescriptorKeyConfig, error) {
var root yamlDescriptorKeyRoot
if err := yaml.Unmarshal(content, &root); err != nil {
return nil, fmt.Errorf("descriptor key config: %w", err)
}

ret := &DescriptorKeyConfig{
defaultKeys: EntriesToSet(nil),
domainKeys: map[string]map[string]struct{}{},
}

if root.Default != nil {
ret.defaultKeys = EntriesToSet(root.Default.IncludeEntryValueForKeys)
}

for domain, rule := range root.Domains {
ret.domainKeys[domain] = EntriesToSet(rule.IncludeEntryValueForKeys)
}

return ret, nil
}

func EntriesToSet(entries []string) map[string]struct{} {
entrySet := make(map[string]struct{}, len(entries))
for _, entry := range entries {
entrySet[entry] = struct{}{}
}
return entrySet
}

// IncludeEntryValueForKey reports whether entry.Value should be appended for this domain/key.
// When cfg is nil, all non-empty values are included (backward compatible).
func (cfg *DescriptorKeyConfig) IncludeEntryValueForKey(domain, entry string) bool {
if cfg == nil {
return true
}

if domainKeys, ok := cfg.domainKeys[domain]; ok {
_, include := domainKeys[entry]
return include
}

_, include := cfg.defaultKeys[entry]
return include
}
12 changes: 9 additions & 3 deletions src/config_check_cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/envoyproxy/ratelimit/src/config"
)

func loadConfigs(allConfigs []config.RateLimitConfigToLoad, mergeDomainConfigs bool) {
func loadConfigs(allConfigs []config.RateLimitConfigToLoad, mergeDomainConfigs bool, descriptorKeyConfigPath string) {
defer func() {
err := recover()
if err != nil {
Expand All @@ -23,14 +23,20 @@ func loadConfigs(allConfigs []config.RateLimitConfigToLoad, mergeDomainConfigs b
}
}()
statsManager := stats.NewStatManager(gostats.NewStore(gostats.NewNullSink(), false), settings.NewSettings())
config.NewRateLimitConfigImpl(allConfigs, statsManager, mergeDomainConfigs)
descriptorKeyConfig, err := config.LoadDescriptorKeyConfig(descriptorKeyConfigPath)
if err != nil {
panic(err)
}
config.NewRateLimitConfigImpl(allConfigs, statsManager, mergeDomainConfigs, descriptorKeyConfig)
}

func main() {
configDirectory := flag.String(
"config_dir", "", "path to directory containing rate limit configs")
mergeDomainConfigs := flag.Bool(
"merge_domain_configs", false, "whether to merge configurations, referencing the same domain")
descriptorKeyConfigPath := flag.String(
"descriptor_key_config", "", "path to descriptor_key.yaml (optional)")
flag.Parse()
fmt.Printf("checking rate limit configs...\n")
fmt.Printf("loading config directory: %s\n", *configDirectory)
Expand All @@ -54,6 +60,6 @@ func main() {
allConfigs = append(allConfigs, config.RateLimitConfigToLoad{Name: finalPath, ConfigYaml: configYaml})
}

loadConfigs(allConfigs, *mergeDomainConfigs)
loadConfigs(allConfigs, *mergeDomainConfigs, *descriptorKeyConfigPath)
fmt.Printf("all rate limit configs ok\n")
}
18 changes: 18 additions & 0 deletions src/provider/descriptor_key_config_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package provider

import (
logger "github.com/sirupsen/logrus"

"github.com/envoyproxy/ratelimit/src/config"
)

func loadDescriptorKeyConfigOrPanic(path string) *config.DescriptorKeyConfig {
cfg, err := config.LoadDescriptorKeyConfig(path)
if err != nil {
logger.Fatalf("failed to load descriptor key config from %q: %v", path, err)
}
if cfg != nil {
logger.Infof("loaded descriptor key config from %q", path)
}
return cfg
}
4 changes: 3 additions & 1 deletion src/provider/file_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type FileProvider struct {
runtimeWatchRoot bool
rootStore gostats.Store
statsManager stats.Manager
descriptorKeyConfig *config.DescriptorKeyConfig
}

func (p *FileProvider) ConfigUpdateEvent() <-chan ConfigUpdateEvent {
Expand Down Expand Up @@ -64,7 +65,7 @@ func (p *FileProvider) sendEvent() {
}

rlSettings := settings.NewSettings()
newConfig := p.loader.Load(files, p.statsManager, rlSettings.MergeDomainConfigurations)
newConfig := p.loader.Load(files, p.statsManager, rlSettings.MergeDomainConfigurations, p.descriptorKeyConfig)

p.configUpdateEventChan <- &ConfigUpdateEventImpl{config: newConfig}
}
Expand Down Expand Up @@ -111,6 +112,7 @@ func NewFileProvider(settings settings.Settings, statsManager stats.Manager, roo
runtimeWatchRoot: settings.RuntimeWatchRoot,
rootStore: rootStore,
statsManager: statsManager,
descriptorKeyConfig: loadDescriptorKeyConfigOrPanic(settings.DescriptorKeyConfigPath),
}
p.setupRuntime()
go p.watch()
Expand Down
4 changes: 3 additions & 1 deletion src/provider/xds_grpc_sotw_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type XdsGrpcSotwProvider struct {
adsClient sotw.ADSClient
// connectionRetryChannel is the channel which trigger true for connection issues
connectionRetryChannel chan bool
descriptorKeyConfig *config.DescriptorKeyConfig
}

// NewXdsGrpcSotwProvider initializes xDS listener and returns the xDS provider.
Expand All @@ -51,6 +52,7 @@ func NewXdsGrpcSotwProvider(settings settings.Settings, statsManager stats.Manag
connectionRetryChannel: make(chan bool),
loader: config.NewRateLimitConfigLoaderImpl(),
adsClient: sotw.NewADSClient(ctx, getClientNode(settings), resource.RateLimitConfigType),
descriptorKeyConfig: loadDescriptorKeyConfigOrPanic(settings.DescriptorKeyConfigPath),
}
go p.initXdsClient()
return p
Expand Down Expand Up @@ -180,7 +182,7 @@ func (p *XdsGrpcSotwProvider) sendConfigs(resources []*anypb.Any) {
conf = append(conf, config.RateLimitConfigToLoad{Name: confPb.Name, ConfigYaml: configYaml})
}
rlSettings := settings.NewSettings()
rlsConf := p.loader.Load(conf, p.statsManager, rlSettings.MergeDomainConfigurations)
rlsConf := p.loader.Load(conf, p.statsManager, rlSettings.MergeDomainConfigurations, p.descriptorKeyConfig)
p.configUpdateEventChan <- &ConfigUpdateEventImpl{config: rlsConf}
p.adsClient.Ack()
}
Expand Down
11 changes: 6 additions & 5 deletions src/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,12 @@ type Settings struct {
PrometheusResponseTimeAsMilliseconds bool `envconfig:"PROMETHEUS_RESPONSE_TIME_AS_MILLISECONDS" default:"false"`

// Settings for rate limit configuration
RuntimePath string `envconfig:"RUNTIME_ROOT" default:"/srv/runtime_data/current"`
RuntimeSubdirectory string `envconfig:"RUNTIME_SUBDIRECTORY"`
RuntimeAppDirectory string `envconfig:"RUNTIME_APPDIRECTORY" default:"config"`
RuntimeIgnoreDotFiles bool `envconfig:"RUNTIME_IGNOREDOTFILES" default:"false"`
RuntimeWatchRoot bool `envconfig:"RUNTIME_WATCH_ROOT" default:"true"`
DescriptorKeyConfigPath string `envconfig:"DESCRIPTOR_KEY_CONFIG" default:""`
RuntimePath string `envconfig:"RUNTIME_ROOT" default:"/srv/runtime_data/current"`
RuntimeSubdirectory string `envconfig:"RUNTIME_SUBDIRECTORY"`
RuntimeAppDirectory string `envconfig:"RUNTIME_APPDIRECTORY" default:"config"`
RuntimeIgnoreDotFiles bool `envconfig:"RUNTIME_IGNOREDOTFILES" default:"false"`
RuntimeWatchRoot bool `envconfig:"RUNTIME_WATCH_ROOT" default:"true"`

// Settings for all cache types
ExpirationJitterMaxSeconds int64 `envconfig:"EXPIRATION_JITTER_MAX_SECONDS" default:"300"`
Expand Down
Loading