feat: Add field to add unspecified value to metric#996
Conversation
8a137ce to
cf5f6fb
Compare
Signed-off-by: xuannam230201 <xuannam230201@gmail.com> Signed-off-by: Nam Dang <xuannam230201@gmail.com>
cf5f6fb to
9e9d3d5
Compare
Signed-off-by: Nam Dang <xuannam230201@gmail.com>
Signed-off-by: Nam Dang <xuannam230201@gmail.com>
|
Updated PR because of failed format checks: https://github.com/envoyproxy/ratelimit/actions/runs/19188015447/job/55114797917?pr=996. |
|
Hi @collin-lee , do you have any concerns/feedbacks on this PR (for this issue #994)? Is it good to go? |
See comments above regarding FullKey |
@collin-lee , I don't see any comments from you regarding FullKey. Could you please let me know what are your concerns? Thanks.
|
Weird... a few weeks ago I had made a comment and then the other day, I wrote this. It looks like my comments are pending so maybe that's why you don't see it. Looks like I didn't click on options in the dropdown to "publish" it. do you see it now? |
| if matchedViaWildcard { | ||
| if nextDescriptor.valueToMetric { | ||
| valueToMetricFullKey.WriteString(entry.Key) | ||
| valueToMetricFullKey.WriteString("_") |
There was a problem hiding this comment.
could entry.Value here be empty potentially? If so, maybe add a guard
if entry.Value != "" {
valueToMetricFullKey.WriteString("_")
valueToMetricFullKey.WriteString(entry.Value)
}
There was a problem hiding this comment.
Agreed! Let me add the guard and also unit test for this.
| // Recreate to ensure a clean stats struct, then set to enhanced stats | ||
| rateLimit = NewRateLimit(rateLimit.Limit.RequestsPerUnit, rateLimit.Limit.Unit, this.statsManager.NewStats(rateLimit.FullKey), rateLimit.Unlimited, rateLimit.ShadowMode, rateLimit.Name, rateLimit.Replaces, rateLimit.DetailedMetric) | ||
| rateLimit.Stats = this.statsManager.NewStats(enhancedKey) | ||
| } |
There was a problem hiding this comment.
Should we add after 419:
rateLimit.FullKey = enhancedKey
FullKey is used for logging/debugging (e.g., in base_limiter.go), Stats.Key is the actual metric key used for statistics. They should match so logging reflects the actual metrics being tracked
// Without the fix:
rateLimit.FullKey = "domain.route.http_method" // Old key (shown in logs)
rateLimit.Stats.Key = "domain.route_api.http_method_GET" // New key (used for metrics)
There was a problem hiding this comment.
@xuannam230201 see this comment about adding rateLimit.FullKey = enhancedKey after
Here's a test we could add to config_test.go:
`// TestValueToMetric_FullKeyMatchesStatsKey verifies that rateLimit.FullKey always matches
// rateLimit.Stats.Key. This is important for debugging and log/metric correlation.
// FullKey is used in debug logs, while Stats.Key is used for actual metrics.
func TestValueToMetric_FullKeyMatchesStatsKey(t *testing.T) {
asrt := assert.New(t)
store := stats.NewStore(stats.NewNullSink(), false)
cfg := []config.RateLimitConfigToLoad{
{
Name: "inline",
ConfigYaml: &config.YamlRoot{
Domain: "test-domain",
Descriptors: []config.YamlDescriptor{
{
Key: "route",
ValueToMetric: true,
Descriptors: []config.YamlDescriptor{
{
Key: "http_method",
ValueToMetric: true,
Descriptors: []config.YamlDescriptor{
{
Key: "subject_id",
RateLimit: &config.YamlRateLimit{
RequestsPerUnit: 60,
Unit: "minute",
},
},
},
},
},
},
},
},
},
}
rlConfig := config.NewRateLimitConfigImpl(cfg, mockstats.NewMockStatManager(store), false)
// Test case 1: value_to_metric enabled - FullKey should match Stats.Key
rl := rlConfig.GetLimit(
context.TODO(), "test-domain",
&pb_struct.RateLimitDescriptor{
Entries: []*pb_struct.RateLimitDescriptor_Entry{
{Key: "route", Value: "api"},
{Key: "http_method", Value: "GET"},
{Key: "subject_id", Value: "user123"},
},
},
)
asrt.NotNil(rl)
asrt.Equal(rl.FullKey, rl.Stats.Key, "FullKey should match Stats.Key when value_to_metric is enabled")
expectedKey := "test-domain.route_api.http_method_GET.subject_id"
asrt.Equal(expectedKey, rl.FullKey)
asrt.Equal(expectedKey, rl.Stats.Key)
// Test case 2: value_to_metric disabled - FullKey should still match Stats.Key
cfgNoValueToMetric := []config.RateLimitConfigToLoad{
{
Name: "inline",
ConfigYaml: &config.YamlRoot{
Domain: "test-domain-2",
Descriptors: []config.YamlDescriptor{
{
Key: "route",
Descriptors: []config.YamlDescriptor{
{
Key: "http_method",
Descriptors: []config.YamlDescriptor{
{
Key: "subject_id",
RateLimit: &config.YamlRateLimit{
RequestsPerUnit: 60,
Unit: "minute",
},
},
},
},
},
},
},
},
},
}
rlConfig2 := config.NewRateLimitConfigImpl(cfgNoValueToMetric, mockstats.NewMockStatManager(store), false)
rl2 := rlConfig2.GetLimit(
context.TODO(), "test-domain-2",
&pb_struct.RateLimitDescriptor{
Entries: []*pb_struct.RateLimitDescriptor_Entry{
{Key: "route", Value: "api"},
{Key: "http_method", Value: "GET"},
{Key: "subject_id", Value: "user123"},
},
},
)
asrt.NotNil(rl2)
asrt.Equal(rl2.FullKey, rl2.Stats.Key, "FullKey should match Stats.Key even when value_to_metric is disabled")
// Test case 3: detailed_metric enabled - FullKey should match Stats.Key
cfgDetailedMetric := []config.RateLimitConfigToLoad{
{
Name: "inline",
ConfigYaml: &config.YamlRoot{
Domain: "test-domain-3",
Descriptors: []config.YamlDescriptor{
{
Key: "route",
Descriptors: []config.YamlDescriptor{
{
Key: "http_method",
Descriptors: []config.YamlDescriptor{
{
Key: "subject_id",
DetailedMetric: true,
RateLimit: &config.YamlRateLimit{
RequestsPerUnit: 60,
Unit: "minute",
},
},
},
},
},
},
},
},
},
}
rlConfig3 := config.NewRateLimitConfigImpl(cfgDetailedMetric, mockstats.NewMockStatManager(store), false)
rl3 := rlConfig3.GetLimit(
context.TODO(), "test-domain-3",
&pb_struct.RateLimitDescriptor{
Entries: []*pb_struct.RateLimitDescriptor_Entry{
{Key: "route", Value: "api"},
{Key: "http_method", Value: "GET"},
{Key: "subject_id", Value: "user123"},
},
},
)
asrt.NotNil(rl3)
asrt.Equal(rl3.FullKey, rl3.Stats.Key, "FullKey should match Stats.Key when detailed_metric is enabled")
}
`
There was a problem hiding this comment.
Good to know that!
However, currently, I see that FullKey and Stat.Key in detailed_metric are not the same (because current code doesn't set FullKey to Stat.Key in detailed_metric).
I set FullKey to Stat.Key in both cases (detailed_metric and value_to_metric), and also added unit tests for these cases. Is it okay?
There was a problem hiding this comment.
ok great - thanks! LGTM
Signed-off-by: Nam Dang <xuannam230201@gmail.com>
|
@collin-lee, I updated PR to address your comments (and also added more unit tests). Could you please help to take a look at it? Thanks in advance! |
|
Hi @collin-lee , I noticed that a GitHub Action failed after this PR was merged: https://github.com/envoyproxy/ratelimit/actions/runs/19781727464/job/56682854936 It also appears that the workflow did not push a new image with new tag |
|
In that GHA, build for master tag was success, but for new tag (6b4f3896) was failed. I checked the main.yaml GHA and see that they (master and new tags) are applied the same command to build and push Docker image. As a result, I think that some tests were flaky which caused this issue. Does it make sense to you to re-trigger this GHA to see if it's success @collin-lee . The failed test case didn't relate to this change |



Add
value_to_metricfield to include descriptor values in metricsSummary
This PR adds a new optional field
value_to_metric(default:false) to each descriptor in the rate limit configuration. When enabled, it includes the descriptor's runtime value in the metric key, even when the descriptor value is not explicitly defined in the configuration. This provides visibility into different rate limit scenarios without needing to pre-define every possible value.Problem
Previously, when a descriptor matched a value that wasn't explicitly defined in the configuration (i.e., matched via a default key without value), the metric key would only include the descriptor key, not the actual runtime value. This made it difficult to track and analyze rate limiting metrics for different runtime values without using
detailed_metric, which includes values for all descriptors and can lead to high cardinality.Solution
The new
value_to_metricfield allows users to selectively include runtime values in metric keys for specific descriptors, providing granular control over metric cardinality while still maintaining visibility into important descriptor values.Behavior
Default behavior: When
value_to_metricisfalse(default) or not set, the behavior remains unchanged - descriptors matched via default keys only include the key name in metrics.With
value_to_metric: true: When enabled on a descriptor:domain.key_value.subkeyPrecedence: When
detailed_metric: trueis set on a descriptor, it takes precedence andvalue_to_metricis ignored for that descriptor (to maintain backward compatibility).Example
Configuration:
Requests:
route=api,http_method=GET,subject_id=123→ Metric:domain.route_api.http_method_GET.subject_idroute=web,http_method=POST,subject_id=456→ Metric:domain.route_web.http_method_POST.subject_idWithout
value_to_metric, both requests would use:domain.route.http_method.subject_idChanges
Code Changes
ValueToMetric boolfield toYamlDescriptorstructvalue_to_metrictovalidKeysmap for YAML validationvalueToMetric boolfield torateLimitDescriptorstruct to track the flag per descriptorloadDescriptorsto store thevalue_to_metricflag in descriptor nodesGetLimitto build avalue_to_metric-enhanced metric key when enabledvalue_to_metricis enabledTests
value_to_metricvalue_to_metricdetailed_metric(precedence)value_to_metricvalue_to_metricDocumentation
value_to_metricto descriptor list definition formatdoctocto regenerate)Testing
All existing tests continue to pass, ensuring backward compatibility. New tests verify:
value_to_metricfunctionalitydetailed_metricBackward Compatibility
This change is fully backward compatible:
false, so existing configurations continue to work unchanged