Skip to content

Commit 6b4f389

Browse files
feat: Add field to add unspecified value to metric (#996)
* Add field to add unspecified value to metric Signed-off-by: xuannam230201 <[email protected]> Signed-off-by: Nam Dang <[email protected]> * Update README.md to pass docs_check_format check Signed-off-by: Nam Dang <[email protected]> * Update format to pass pre-commit check Signed-off-by: Nam Dang <[email protected]> * Update based on comments and add more unit tests Signed-off-by: Nam Dang <[email protected]> --------- Signed-off-by: xuannam230201 <[email protected]> Signed-off-by: Nam Dang <[email protected]>
1 parent 99d8551 commit 6b4f389

File tree

3 files changed

+771
-6
lines changed

3 files changed

+771
-6
lines changed

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- [Replaces](#replaces)
2222
- [ShadowMode](#shadowmode)
2323
- [Including detailed metrics for unspecified values](#including-detailed-metrics-for-unspecified-values)
24+
- [Including descriptor values in metrics](#including-descriptor-values-in-metrics)
2425
- [Examples](#examples)
2526
- [Example 1](#example-1)
2627
- [Example 2](#example-2)
@@ -31,6 +32,7 @@
3132
- [Example 7](#example-7)
3233
- [Example 8](#example-8)
3334
- [Example 9](#example-9)
35+
- [Example 10](#example-10)
3436
- [Loading Configuration](#loading-configuration)
3537
- [File Based Configuration Loading](#file-based-configuration-loading)
3638
- [xDS Management Server Based Configuration Loading](#xds-management-server-based-configuration-loading)
@@ -282,6 +284,7 @@ descriptors:
282284
requests_per_unit: <see below: required>
283285
shadow_mode: (optional)
284286
detailed_metric: (optional)
287+
value_to_metric: (optional)
285288
descriptors: (optional block)
286289
- ... (nested repetition of above)
287290
```
@@ -336,6 +339,14 @@ Setting the `detailed_metric: true` for a descriptor will extend the metrics tha
336339

337340
NB! This should only be enabled in situations where the potentially large cardinality of metrics that this can lead to is acceptable.
338341

342+
### Including descriptor values in metrics
343+
344+
Setting `value_to_metric: true` (default: `false`) for a descriptor will include the descriptor's runtime value in the metric key, even when the descriptor value is not explicitly defined in the configuration. This allows you to track metrics per descriptor value when the value comes from the runtime request, providing visibility into different rate limit scenarios without needing to pre-define every possible value.
345+
346+
**Note:** If a value is explicitly specified in a descriptor (e.g., `value: "GET"`), that value is always included in the metric key regardless of the `value_to_metric` setting. The `value_to_metric` flag only affects descriptors where the value is not explicitly defined in the configuration.
347+
348+
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.
349+
339350
### Examples
340351

341352
#### Example 1
@@ -629,6 +640,58 @@ descriptors:
629640
requests_per_unit: 20
630641
```
631642
643+
#### Example 10
644+
645+
Using `value_to_metric: true` to include descriptor values in metrics when values are not explicitly defined in the configuration:
646+
647+
```yaml
648+
domain: example10
649+
descriptors:
650+
- key: route
651+
value_to_metric: true
652+
descriptors:
653+
- key: http_method
654+
value_to_metric: true
655+
descriptors:
656+
- key: subject_id
657+
rate_limit:
658+
unit: minute
659+
requests_per_unit: 60
660+
```
661+
662+
With this configuration, requests with different runtime values for `route` and `http_method` will generate separate metrics:
663+
664+
- Request: `route=api`, `http_method=GET`, `subject_id=123`
665+
- Metric key: `example10.route_api.http_method_GET.subject_id`
666+
667+
- Request: `route=web`, `http_method=POST`, `subject_id=456`
668+
- Metric key: `example10.route_web.http_method_POST.subject_id`
669+
670+
Without `value_to_metric: true`, both requests would use the same metric key: `example10.route.http_method.subject_id`.
671+
672+
When combined with wildcard matching, the full runtime value is included:
673+
674+
```yaml
675+
domain: example10_wildcard
676+
descriptors:
677+
- key: user
678+
value_to_metric: true
679+
descriptors:
680+
- key: action
681+
value: read*
682+
value_to_metric: true
683+
descriptors:
684+
- key: resource
685+
rate_limit:
686+
unit: minute
687+
requests_per_unit: 100
688+
```
689+
690+
- Request: `user=alice`, `action=readfile`, `resource=documents`
691+
- Metric key: `example10_wildcard.user_alice.action_readfile.resource`
692+
693+
Note: When `detailed_metric: true` is set on a descriptor, it takes precedence and `value_to_metric` is ignored for that descriptor.
694+
632695
## Loading Configuration
633696

634697
Rate limit service supports following configuration loading methods. You can define which methods to use by configuring environment variable `CONFIG_TYPE`.

src/config/config_impl.go

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type YamlDescriptor struct {
3232
Descriptors []YamlDescriptor
3333
ShadowMode bool `yaml:"shadow_mode"`
3434
DetailedMetric bool `yaml:"detailed_metric"`
35+
ValueToMetric bool `yaml:"value_to_metric"`
3536
}
3637

3738
type YamlRoot struct {
@@ -40,9 +41,10 @@ type YamlRoot struct {
4041
}
4142

4243
type rateLimitDescriptor struct {
43-
descriptors map[string]*rateLimitDescriptor
44-
limit *RateLimit
45-
wildcardKeys []string
44+
descriptors map[string]*rateLimitDescriptor
45+
limit *RateLimit
46+
wildcardKeys []string
47+
valueToMetric bool
4648
}
4749

4850
type rateLimitDomain struct {
@@ -68,6 +70,7 @@ var validKeys = map[string]bool{
6870
"name": true,
6971
"replaces": true,
7072
"detailed_metric": true,
73+
"value_to_metric": true,
7174
}
7275

7376
// Create a new rate limit config entry.
@@ -185,7 +188,7 @@ func (this *rateLimitDescriptor) loadDescriptors(config RateLimitConfigToLoad, p
185188

186189
logger.Debugf(
187190
"loading descriptor: key=%s%s", newParentKey, rateLimitDebugString)
188-
newDescriptor := &rateLimitDescriptor{map[string]*rateLimitDescriptor{}, rateLimit, nil}
191+
newDescriptor := &rateLimitDescriptor{map[string]*rateLimitDescriptor{}, rateLimit, nil, descriptorConfig.ValueToMetric}
189192
newDescriptor.loadDescriptors(config, newParentKey+".", descriptorConfig.Descriptors, statsManager)
190193
this.descriptors[finalKey] = newDescriptor
191194

@@ -262,7 +265,7 @@ func (this *rateLimitConfigImpl) loadConfig(config RateLimitConfigToLoad) {
262265
}
263266

264267
logger.Debugf("loading domain: %s", root.Domain)
265-
newDomain := &rateLimitDomain{rateLimitDescriptor{map[string]*rateLimitDescriptor{}, nil, nil}}
268+
newDomain := &rateLimitDomain{rateLimitDescriptor{map[string]*rateLimitDescriptor{}, nil, nil, false}}
266269
newDomain.loadDescriptors(config, root.Domain+".", root.Descriptors, this.statsManager)
267270
this.domains[root.Domain] = newDomain
268271
}
@@ -313,6 +316,10 @@ func (this *rateLimitConfigImpl) GetLimit(
313316
var detailedMetricFullKey strings.Builder
314317
detailedMetricFullKey.WriteString(domain)
315318

319+
// Build value_to_metric-enhanced metric key as we traverse
320+
var valueToMetricFullKey strings.Builder
321+
valueToMetricFullKey.WriteString(domain)
322+
316323
for i, entry := range descriptor.Entries {
317324
// First see if key_value is in the map. If that isn't in the map we look for just key
318325
// to check for a default value.
@@ -323,20 +330,61 @@ func (this *rateLimitConfigImpl) GetLimit(
323330

324331
logger.Debugf("looking up key: %s", finalKey)
325332
nextDescriptor := descriptorsMap[finalKey]
333+
matchedViaWildcard := false
326334

327335
if nextDescriptor == nil && len(prevDescriptor.wildcardKeys) > 0 {
328336
for _, wildcardKey := range prevDescriptor.wildcardKeys {
329337
if strings.HasPrefix(finalKey, strings.TrimSuffix(wildcardKey, "*")) {
330338
nextDescriptor = descriptorsMap[wildcardKey]
339+
matchedViaWildcard = true
331340
break
332341
}
333342
}
334343
}
335344

345+
matchedUsingValue := nextDescriptor != nil
336346
if nextDescriptor == nil {
337347
finalKey = entry.Key
338348
logger.Debugf("looking up key: %s", finalKey)
339349
nextDescriptor = descriptorsMap[finalKey]
350+
matchedUsingValue = false
351+
}
352+
353+
// Build value_to_metric metrics path for this level
354+
valueToMetricFullKey.WriteString(".")
355+
if nextDescriptor != nil {
356+
if matchedViaWildcard {
357+
if nextDescriptor.valueToMetric {
358+
valueToMetricFullKey.WriteString(entry.Key)
359+
if entry.Value != "" {
360+
valueToMetricFullKey.WriteString("_")
361+
valueToMetricFullKey.WriteString(entry.Value)
362+
}
363+
} else {
364+
valueToMetricFullKey.WriteString(entry.Key)
365+
}
366+
} else if matchedUsingValue {
367+
// Matched explicit key+value in config
368+
valueToMetricFullKey.WriteString(entry.Key)
369+
if entry.Value != "" {
370+
valueToMetricFullKey.WriteString("_")
371+
valueToMetricFullKey.WriteString(entry.Value)
372+
}
373+
} else {
374+
// Matched default key (no value) in config
375+
if nextDescriptor.valueToMetric {
376+
valueToMetricFullKey.WriteString(entry.Key)
377+
if entry.Value != "" {
378+
valueToMetricFullKey.WriteString("_")
379+
valueToMetricFullKey.WriteString(entry.Value)
380+
}
381+
} else {
382+
valueToMetricFullKey.WriteString(entry.Key)
383+
}
384+
}
385+
} else {
386+
// No next descriptor found; still append something deterministic
387+
valueToMetricFullKey.WriteString(entry.Key)
340388
}
341389

342390
if nextDescriptor != nil && nextDescriptor.limit != nil {
@@ -364,7 +412,21 @@ func (this *rateLimitConfigImpl) GetLimit(
364412

365413
// Replace metric with detailed metric, if leaf descriptor is detailed.
366414
if rateLimit != nil && rateLimit.DetailedMetric {
367-
rateLimit.Stats = this.statsManager.NewStats(detailedMetricFullKey.String())
415+
detailedKey := detailedMetricFullKey.String()
416+
rateLimit.Stats = this.statsManager.NewStats(detailedKey)
417+
rateLimit.FullKey = detailedKey
418+
}
419+
420+
// If not using detailed metric, but any value_to_metric path produced a different key,
421+
// override stats to use the value_to_metric-enhanced key
422+
if rateLimit != nil && !rateLimit.DetailedMetric {
423+
enhancedKey := valueToMetricFullKey.String()
424+
if enhancedKey != rateLimit.FullKey {
425+
// Recreate to ensure a clean stats struct, then set to enhanced stats
426+
rateLimit = NewRateLimit(rateLimit.Limit.RequestsPerUnit, rateLimit.Limit.Unit, this.statsManager.NewStats(rateLimit.FullKey), rateLimit.Unlimited, rateLimit.ShadowMode, rateLimit.Name, rateLimit.Replaces, rateLimit.DetailedMetric)
427+
rateLimit.Stats = this.statsManager.NewStats(enhancedKey)
428+
rateLimit.FullKey = enhancedKey
429+
}
368430
}
369431

370432
return rateLimit

0 commit comments

Comments
 (0)