Skip to content
Merged
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 78 additions & 12 deletions src/service/ratelimit.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ 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"

"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"
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -239,10 +243,72 @@ 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 {
fields := make(map[string]*structpb.Value)

// Domain
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))
}
fields["descriptors"] = structpb.NewListValue(&structpb.ListValue{
Values: descriptorsValues,
})

// HitsAddend
if hitsAddend := req.GetHitsAddend(); hitsAddend != 0 {
fields["hitsAddend"] = structpb.NewNumberValue(float64(hitsAddend))
}

return &structpb.Struct{Fields: fields}
}

func descriptorToStruct(descriptor *ratelimitv3.RateLimitDescriptor) *structpb.Struct {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the review above regarding silently failing

suggestion
func descriptorToStruct(descriptor *ratelimitv3.RateLimitDescriptor) *structpb.Struct {
    if descriptor == nil {
        return nil
    }
    
    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))
    }
    fields["entries"] = structpb.NewListValue(&structpb.ListValue{
        Values: entriesValues,
    })
    
    // Limit
    if descriptor.GetLimit() != nil {
        fields["limit"] = structpb.NewStringValue(descriptor.Limit.String())
    }
    
    // HitsAddend
    if hitsAddend := descriptor.GetHitsAddend(); hitsAddend != nil {
        fields["hitsAddend"] = structpb.NewNumberValue(float64(hitsAddend.GetValue()))
    }
    
    return &structpb.Struct{Fields: fields}
}

if descriptor == nil {
return nil
}

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))
}
fields["entries"] = structpb.NewListValue(&structpb.ListValue{
Values: entriesValues,
})

// Limit
if descriptor.GetLimit() != nil {
fields["limit"] = structpb.NewStringValue(descriptor.Limit.String())
}

// HitsAddend
if hitsAddend := descriptor.GetHitsAddend(); hitsAddend != nil {
fields["hitsAddend"] = structpb.NewNumberValue(float64(hitsAddend.GetValue()))
}

return &structpb.Struct{Fields: fields}
}

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
Expand Down
61 changes: 61 additions & 0 deletions src/service/ratelimit_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
2 changes: 2 additions & 0 deletions src/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down
Loading