From 69fa8aff1560e44b82cf43c96f955035ea3164d6 Mon Sep 17 00:00:00 2001 From: zirain Date: Wed, 10 Dec 2025 16:58:45 +0800 Subject: [PATCH 1/2] add support for response dynamic metadata Signed-off-by: zirain --- go.mod | 1 + src/service/ratelimit.go | 85 ++++++++++++++++++++++++++++++----- src/service/ratelimit_test.go | 61 +++++++++++++++++++++++++ src/settings/settings.go | 2 + 4 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 src/service/ratelimit_test.go diff --git a/go.mod b/go.mod index 1cae8a28b..2c9cf881e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20250812085011-4cf7e8485428 github.com/go-kit/log v0.2.1 github.com/golang/mock v1.6.0 + github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 diff --git a/src/service/ratelimit.go b/src/service/ratelimit.go index 88a039ef9..66f05fbf4 100644 --- a/src/service/ratelimit.go +++ b/src/service/ratelimit.go @@ -10,6 +10,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/types/known/structpb" "github.com/envoyproxy/ratelimit/src/settings" "github.com/envoyproxy/ratelimit/src/stats" @@ -17,6 +18,7 @@ import ( "github.com/envoyproxy/ratelimit/src/utils" core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + ratelimitv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/common/ratelimit/v3" pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3" logger "github.com/sirupsen/logrus" "golang.org/x/net/context" @@ -38,18 +40,19 @@ type RateLimitServiceServer interface { } type service struct { - configLock sync.RWMutex - configUpdateEvent <-chan provider.ConfigUpdateEvent - config config.RateLimitConfig - cache limiter.RateLimitCache - stats stats.ServiceStats - health *server.HealthChecker - customHeadersEnabled bool - customHeaderLimitHeader string - customHeaderRemainingHeader string - customHeaderResetHeader string - customHeaderClock utils.TimeSource - globalShadowMode bool + configLock sync.RWMutex + configUpdateEvent <-chan provider.ConfigUpdateEvent + config config.RateLimitConfig + cache limiter.RateLimitCache + stats stats.ServiceStats + health *server.HealthChecker + customHeadersEnabled bool + customHeaderLimitHeader string + customHeaderRemainingHeader string + customHeaderResetHeader string + customHeaderClock utils.TimeSource + globalShadowMode bool + responseDynamicMetadataEnabled bool } func (this *service) SetConfig(updateEvent provider.ConfigUpdateEvent, healthyWithAtLeastOneConfigLoad bool) { @@ -84,6 +87,7 @@ func (this *service) SetConfig(updateEvent provider.ConfigUpdateEvent, healthyWi rlSettings := settings.NewSettings() this.globalShadowMode = rlSettings.GlobalShadowMode + this.responseDynamicMetadataEnabled = rlSettings.ResponseDynamicMetadata if rlSettings.RateLimitResponseHeadersEnabled { this.customHeadersEnabled = true @@ -239,10 +243,67 @@ func (this *service) shouldRateLimitWorker( this.stats.GlobalShadowMode.Inc() } + // If response dynamic data enabled, set dynamic data on response. + if this.responseDynamicMetadataEnabled { + response.DynamicMetadata = ratelimitToMetadata(request) + } + response.OverallCode = finalCode return response } +func ratelimitToMetadata(req *pb.RateLimitRequest) *structpb.Struct { + dm, _ := structpb.NewStruct(nil) + dm.Fields = make(map[string]*structpb.Value) + // Domain + dm.Fields["domain"] = structpb.NewStringValue(req.Domain) + // Descriptors + descriptorsValues := make([]*structpb.Value, 0, len(req.Descriptors)) + for _, descriptor := range req.Descriptors { + s := descriptorToStruct(descriptor) + if s == nil { + continue + } + descriptorsValues = append(descriptorsValues, structpb.NewStructValue(s)) + } + dm.Fields["descriptors"] = structpb.NewListValue(&structpb.ListValue{ + Values: descriptorsValues, + }) + // HitsAddend + if hitsAddend := req.GetHitsAddend(); hitsAddend != 0 { + dm.Fields["hitsAddend"] = structpb.NewNumberValue(float64(hitsAddend)) + } + return dm +} + +func descriptorToStruct(descriptor *ratelimitv3.RateLimitDescriptor) *structpb.Struct { + if descriptor == nil { + return nil + } + s, _ := structpb.NewStruct(nil) + s.Fields = make(map[string]*structpb.Value) + + // Entities + entriesValues := make([]*structpb.Value, 0, len(descriptor.Entries)) + for _, entry := range descriptor.Entries { + val := fmt.Sprintf("%s=%s", entry.GetKey(), entry.GetValue()) + entriesValues = append(entriesValues, structpb.NewStringValue(val)) + } + s.Fields["entries"] = structpb.NewListValue(&structpb.ListValue{ + Values: entriesValues, + }) + // Limit + if descriptor.GetLimit() != nil { + s.Fields["limit"] = structpb.NewStringValue(descriptor.Limit.String()) + } + // HitsAddend + if hitsAddend := descriptor.GetHitsAddend(); hitsAddend != nil { + s.Fields["hitsAddend"] = structpb.NewNumberValue(float64(hitsAddend.GetValue())) + } + + return s +} + func (this *service) rateLimitLimitHeader(descriptor *pb.RateLimitResponse_DescriptorStatus) *core.HeaderValue { // Limit header only provides the mandatory part from the spec, the actual limit // the optional quota policy is currently not provided diff --git a/src/service/ratelimit_test.go b/src/service/ratelimit_test.go new file mode 100644 index 000000000..8cd4f2ee3 --- /dev/null +++ b/src/service/ratelimit_test.go @@ -0,0 +1,61 @@ +package ratelimit + +import ( + "testing" + + ratelimitv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/common/ratelimit/v3" + pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/structpb" +) + +func TestRatelimitToMetadata(t *testing.T) { + cases := []struct { + name string + req *pb.RateLimitRequest + expected string + }{ + { + name: "Single descriptor with single entry", + req: &pb.RateLimitRequest{ + Domain: "fake-domain", + Descriptors: []*ratelimitv3.RateLimitDescriptor{ + { + Entries: []*ratelimitv3.RateLimitDescriptor_Entry{ + { + Key: "key1", + Value: "val1", + }, + }, + }, + }, + }, + expected: `{ + "descriptors": [ + { + "entries": [ + "key1=val1" + ] + } + ], + "domain": "fake-domain" +}`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := ratelimitToMetadata(tc.req) + expected := &structpb.Struct{} + err := protojson.Unmarshal([]byte(tc.expected), expected) + require.NoError(t, err) + + if diff := cmp.Diff(got, expected, protocmp.Transform()); diff != "" { + t.Errorf("diff: %s", diff) + } + }) + } +} diff --git a/src/settings/settings.go b/src/settings/settings.go index 2c14a478e..1d48d8715 100644 --- a/src/settings/settings.go +++ b/src/settings/settings.go @@ -202,6 +202,8 @@ type Settings struct { // 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 GlobalShadowMode bool `envconfig:"SHADOW_MODE" default:"false"` + ResponseDynamicMetadata bool `envconfig:"RESPONSE_DYNAMIC_METADATA" default:"false"` + // Allow merging of multiple yaml files referencing the same domain MergeDomainConfigurations bool `envconfig:"MERGE_DOMAIN_CONFIG" default:"false"` From f16d00488e25174d08fc0c2eb1b740193d916f3a Mon Sep 17 00:00:00 2001 From: zirain Date: Fri, 12 Dec 2025 08:46:45 +0800 Subject: [PATCH 2/2] address Colin's comment Signed-off-by: zirain --- src/service/ratelimit.go | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/service/ratelimit.go b/src/service/ratelimit.go index 66f05fbf4..6df7fc4c0 100644 --- a/src/service/ratelimit.go +++ b/src/service/ratelimit.go @@ -253,10 +253,11 @@ func (this *service) shouldRateLimitWorker( } func ratelimitToMetadata(req *pb.RateLimitRequest) *structpb.Struct { - dm, _ := structpb.NewStruct(nil) - dm.Fields = make(map[string]*structpb.Value) + fields := make(map[string]*structpb.Value) + // Domain - dm.Fields["domain"] = structpb.NewStringValue(req.Domain) + fields["domain"] = structpb.NewStringValue(req.Domain) + // Descriptors descriptorsValues := make([]*structpb.Value, 0, len(req.Descriptors)) for _, descriptor := range req.Descriptors { @@ -266,42 +267,46 @@ func ratelimitToMetadata(req *pb.RateLimitRequest) *structpb.Struct { } descriptorsValues = append(descriptorsValues, structpb.NewStructValue(s)) } - dm.Fields["descriptors"] = structpb.NewListValue(&structpb.ListValue{ + fields["descriptors"] = structpb.NewListValue(&structpb.ListValue{ Values: descriptorsValues, }) + // HitsAddend if hitsAddend := req.GetHitsAddend(); hitsAddend != 0 { - dm.Fields["hitsAddend"] = structpb.NewNumberValue(float64(hitsAddend)) + fields["hitsAddend"] = structpb.NewNumberValue(float64(hitsAddend)) } - return dm + + return &structpb.Struct{Fields: fields} } func descriptorToStruct(descriptor *ratelimitv3.RateLimitDescriptor) *structpb.Struct { if descriptor == nil { return nil } - s, _ := structpb.NewStruct(nil) - s.Fields = make(map[string]*structpb.Value) - // Entities + fields := make(map[string]*structpb.Value) + + // Entries entriesValues := make([]*structpb.Value, 0, len(descriptor.Entries)) for _, entry := range descriptor.Entries { val := fmt.Sprintf("%s=%s", entry.GetKey(), entry.GetValue()) entriesValues = append(entriesValues, structpb.NewStringValue(val)) } - s.Fields["entries"] = structpb.NewListValue(&structpb.ListValue{ + fields["entries"] = structpb.NewListValue(&structpb.ListValue{ Values: entriesValues, }) + // Limit if descriptor.GetLimit() != nil { - s.Fields["limit"] = structpb.NewStringValue(descriptor.Limit.String()) + fields["limit"] = structpb.NewStringValue(descriptor.Limit.String()) } + // HitsAddend if hitsAddend := descriptor.GetHitsAddend(); hitsAddend != nil { - s.Fields["hitsAddend"] = structpb.NewNumberValue(float64(hitsAddend.GetValue())) + fields["hitsAddend"] = structpb.NewNumberValue(float64(hitsAddend.GetValue())) } - return s + return &structpb.Struct{Fields: fields} } func (this *service) rateLimitLimitHeader(descriptor *pb.RateLimitResponse_DescriptorStatus) *core.HeaderValue {