Skip to content

Commit 167d0f8

Browse files
authored
add support for response dynamic metadata (#1027)
* add support for response dynamic metadata Signed-off-by: zirain <zirain2009@gmail.com> * address Colin's comment Signed-off-by: zirain <zirain2009@gmail.com> --------- Signed-off-by: zirain <zirain2009@gmail.com>
1 parent f52a616 commit 167d0f8

File tree

4 files changed

+142
-12
lines changed

4 files changed

+142
-12
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20250812085011-4cf7e8485428
1313
github.com/go-kit/log v0.2.1
1414
github.com/golang/mock v1.6.0
15+
github.com/google/go-cmp v0.7.0
1516
github.com/google/uuid v1.6.0
1617
github.com/gorilla/mux v1.8.1
1718
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0

src/service/ratelimit.go

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import (
1010
"go.opentelemetry.io/otel"
1111
"go.opentelemetry.io/otel/attribute"
1212
"go.opentelemetry.io/otel/trace"
13+
"google.golang.org/protobuf/types/known/structpb"
1314

1415
"github.com/envoyproxy/ratelimit/src/settings"
1516
"github.com/envoyproxy/ratelimit/src/stats"
1617

1718
"github.com/envoyproxy/ratelimit/src/utils"
1819

1920
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
21+
ratelimitv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/common/ratelimit/v3"
2022
pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3"
2123
logger "github.com/sirupsen/logrus"
2224
"golang.org/x/net/context"
@@ -38,18 +40,19 @@ type RateLimitServiceServer interface {
3840
}
3941

4042
type service struct {
41-
configLock sync.RWMutex
42-
configUpdateEvent <-chan provider.ConfigUpdateEvent
43-
config config.RateLimitConfig
44-
cache limiter.RateLimitCache
45-
stats stats.ServiceStats
46-
health *server.HealthChecker
47-
customHeadersEnabled bool
48-
customHeaderLimitHeader string
49-
customHeaderRemainingHeader string
50-
customHeaderResetHeader string
51-
customHeaderClock utils.TimeSource
52-
globalShadowMode bool
43+
configLock sync.RWMutex
44+
configUpdateEvent <-chan provider.ConfigUpdateEvent
45+
config config.RateLimitConfig
46+
cache limiter.RateLimitCache
47+
stats stats.ServiceStats
48+
health *server.HealthChecker
49+
customHeadersEnabled bool
50+
customHeaderLimitHeader string
51+
customHeaderRemainingHeader string
52+
customHeaderResetHeader string
53+
customHeaderClock utils.TimeSource
54+
globalShadowMode bool
55+
responseDynamicMetadataEnabled bool
5356
}
5457

5558
func (this *service) SetConfig(updateEvent provider.ConfigUpdateEvent, healthyWithAtLeastOneConfigLoad bool) {
@@ -84,6 +87,7 @@ func (this *service) SetConfig(updateEvent provider.ConfigUpdateEvent, healthyWi
8487

8588
rlSettings := settings.NewSettings()
8689
this.globalShadowMode = rlSettings.GlobalShadowMode
90+
this.responseDynamicMetadataEnabled = rlSettings.ResponseDynamicMetadata
8791

8892
if rlSettings.RateLimitResponseHeadersEnabled {
8993
this.customHeadersEnabled = true
@@ -239,10 +243,72 @@ func (this *service) shouldRateLimitWorker(
239243
this.stats.GlobalShadowMode.Inc()
240244
}
241245

246+
// If response dynamic data enabled, set dynamic data on response.
247+
if this.responseDynamicMetadataEnabled {
248+
response.DynamicMetadata = ratelimitToMetadata(request)
249+
}
250+
242251
response.OverallCode = finalCode
243252
return response
244253
}
245254

255+
func ratelimitToMetadata(req *pb.RateLimitRequest) *structpb.Struct {
256+
fields := make(map[string]*structpb.Value)
257+
258+
// Domain
259+
fields["domain"] = structpb.NewStringValue(req.Domain)
260+
261+
// Descriptors
262+
descriptorsValues := make([]*structpb.Value, 0, len(req.Descriptors))
263+
for _, descriptor := range req.Descriptors {
264+
s := descriptorToStruct(descriptor)
265+
if s == nil {
266+
continue
267+
}
268+
descriptorsValues = append(descriptorsValues, structpb.NewStructValue(s))
269+
}
270+
fields["descriptors"] = structpb.NewListValue(&structpb.ListValue{
271+
Values: descriptorsValues,
272+
})
273+
274+
// HitsAddend
275+
if hitsAddend := req.GetHitsAddend(); hitsAddend != 0 {
276+
fields["hitsAddend"] = structpb.NewNumberValue(float64(hitsAddend))
277+
}
278+
279+
return &structpb.Struct{Fields: fields}
280+
}
281+
282+
func descriptorToStruct(descriptor *ratelimitv3.RateLimitDescriptor) *structpb.Struct {
283+
if descriptor == nil {
284+
return nil
285+
}
286+
287+
fields := make(map[string]*structpb.Value)
288+
289+
// Entries
290+
entriesValues := make([]*structpb.Value, 0, len(descriptor.Entries))
291+
for _, entry := range descriptor.Entries {
292+
val := fmt.Sprintf("%s=%s", entry.GetKey(), entry.GetValue())
293+
entriesValues = append(entriesValues, structpb.NewStringValue(val))
294+
}
295+
fields["entries"] = structpb.NewListValue(&structpb.ListValue{
296+
Values: entriesValues,
297+
})
298+
299+
// Limit
300+
if descriptor.GetLimit() != nil {
301+
fields["limit"] = structpb.NewStringValue(descriptor.Limit.String())
302+
}
303+
304+
// HitsAddend
305+
if hitsAddend := descriptor.GetHitsAddend(); hitsAddend != nil {
306+
fields["hitsAddend"] = structpb.NewNumberValue(float64(hitsAddend.GetValue()))
307+
}
308+
309+
return &structpb.Struct{Fields: fields}
310+
}
311+
246312
func (this *service) rateLimitLimitHeader(descriptor *pb.RateLimitResponse_DescriptorStatus) *core.HeaderValue {
247313
// Limit header only provides the mandatory part from the spec, the actual limit
248314
// the optional quota policy is currently not provided

src/service/ratelimit_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package ratelimit
2+
3+
import (
4+
"testing"
5+
6+
ratelimitv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/common/ratelimit/v3"
7+
pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3"
8+
"github.com/google/go-cmp/cmp"
9+
"github.com/stretchr/testify/require"
10+
"google.golang.org/protobuf/encoding/protojson"
11+
"google.golang.org/protobuf/testing/protocmp"
12+
"google.golang.org/protobuf/types/known/structpb"
13+
)
14+
15+
func TestRatelimitToMetadata(t *testing.T) {
16+
cases := []struct {
17+
name string
18+
req *pb.RateLimitRequest
19+
expected string
20+
}{
21+
{
22+
name: "Single descriptor with single entry",
23+
req: &pb.RateLimitRequest{
24+
Domain: "fake-domain",
25+
Descriptors: []*ratelimitv3.RateLimitDescriptor{
26+
{
27+
Entries: []*ratelimitv3.RateLimitDescriptor_Entry{
28+
{
29+
Key: "key1",
30+
Value: "val1",
31+
},
32+
},
33+
},
34+
},
35+
},
36+
expected: `{
37+
"descriptors": [
38+
{
39+
"entries": [
40+
"key1=val1"
41+
]
42+
}
43+
],
44+
"domain": "fake-domain"
45+
}`,
46+
},
47+
}
48+
49+
for _, tc := range cases {
50+
t.Run(tc.name, func(t *testing.T) {
51+
got := ratelimitToMetadata(tc.req)
52+
expected := &structpb.Struct{}
53+
err := protojson.Unmarshal([]byte(tc.expected), expected)
54+
require.NoError(t, err)
55+
56+
if diff := cmp.Diff(got, expected, protocmp.Transform()); diff != "" {
57+
t.Errorf("diff: %s", diff)
58+
}
59+
})
60+
}
61+
}

src/settings/settings.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ type Settings struct {
210210
// Should the ratelimiting be running in Global shadow-mode, ie. never report a ratelimit status, unless a rate was provided from envoy as an override
211211
GlobalShadowMode bool `envconfig:"SHADOW_MODE" default:"false"`
212212

213+
ResponseDynamicMetadata bool `envconfig:"RESPONSE_DYNAMIC_METADATA" default:"false"`
214+
213215
// Allow merging of multiple yaml files referencing the same domain
214216
MergeDomainConfigurations bool `envconfig:"MERGE_DOMAIN_CONFIG" default:"false"`
215217

0 commit comments

Comments
 (0)