Skip to content

Add support for percentage based request mirroring #3627

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jul 17, 2025
Merged
25 changes: 13 additions & 12 deletions internal/controller/nginx/config/http/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,19 @@ const (

// Location holds all configuration for an HTTP location.
type Location struct {
Path string
ProxyPass string
HTTPMatchKey string
Type LocationType
ProxySetHeaders []Header
ProxySSLVerify *ProxySSLVerify
Return *Return
ResponseHeaders ResponseHeaders
Rewrites []string
MirrorPaths []string
Includes []shared.Include
GRPC bool
Path string
ProxyPass string
HTTPMatchKey string
MirrorSplitClientsVariableName string
Type LocationType
ProxySetHeaders []Header
ProxySSLVerify *ProxySSLVerify
Return *Return
ResponseHeaders ResponseHeaders
Rewrites []string
MirrorPaths []string
Includes []shared.Include
GRPC bool
}

// Header defines an HTTP header to be passed to the proxied server.
Expand Down
164 changes: 137 additions & 27 deletions internal/controller/nginx/config/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,33 @@ type rewriteConfig struct {
MainRewrite string
}

// extractMirrorTargetsWithPercentages extracts mirror targets and their percentages from path rules.
func extractMirrorTargetsWithPercentages(pathRules []dataplane.PathRule) map[string]*float64 {
mirrorTargets := make(map[string]*float64)

for _, rule := range pathRules {
for _, matchRule := range rule.MatchRules {
for _, mirrorFilter := range matchRule.Filters.RequestMirrors {
if mirrorFilter.Target != nil {
if mirrorFilter.Percent == nil {
mirrorTargets[*mirrorFilter.Target] = helpers.GetPointer(100.0)
continue
}

percentage := mirrorFilter.Percent

if _, exists := mirrorTargets[*mirrorFilter.Target]; !exists ||
*percentage > *mirrorTargets[*mirrorFilter.Target] {
mirrorTargets[*mirrorFilter.Target] = percentage // set a higher percentage if it exists
}
}
}
}
}

return mirrorTargets
}

type httpMatchPairs map[string][]routeMatch

func createLocations(
Expand All @@ -239,6 +266,8 @@ func createLocations(
var rootPathExists bool
var grpcServer bool

mirrorPathToPercentage := extractMirrorTargetsWithPercentages(server.PathRules)

for pathRuleIdx, rule := range server.PathRules {
matches := make([]routeMatch, 0, len(rule.MatchRules))

Expand All @@ -250,6 +279,8 @@ func createLocations(
grpcServer = true
}

mirrorPercentage := mirrorPathToPercentage[rule.Path]

extLocations := initializeExternalLocations(rule, pathsAndTypes)
for i := range extLocations {
extLocations[i].Includes = createIncludesFromPolicyGenerateResult(
Expand All @@ -260,13 +291,12 @@ func createLocations(
if !needsInternalLocations(rule) {
for _, r := range rule.MatchRules {
extLocations = updateLocations(
r.Filters,
extLocations,
r,
rule,
extLocations,
server.Port,
rule.Path,
rule.GRPC,
keepAliveCheck,
mirrorPercentage,
)
}

Expand All @@ -283,13 +313,12 @@ func createLocations(
)

intLocation = updateLocation(
r.Filters,
intLocation,
r,
rule,
intLocation,
server.Port,
rule.Path,
rule.GRPC,
keepAliveCheck,
mirrorPercentage,
)

internalLocations = append(internalLocations, intLocation)
Expand Down Expand Up @@ -420,38 +449,68 @@ func initializeInternalLocation(

// updateLocation updates a location with any relevant configurations, like proxy_pass, filters, tls settings, etc.
func updateLocation(
filters dataplane.HTTPFilters,
location http.Location,
matchRule dataplane.MatchRule,
pathRule dataplane.PathRule,
location http.Location,
listenerPort int32,
path string,
grpc bool,
keepAliveCheck keepAliveChecker,
mirrorPercentage *float64,
) http.Location {
filters := matchRule.Filters
path := pathRule.Path
grpc := pathRule.GRPC

if filters.InvalidFilter != nil {
location.Return = &http.Return{Code: http.StatusInternalServerError}
return location
}

location = updateLocationMirrorRoute(location, path, grpc)
location.Includes = append(location.Includes, createIncludesFromLocationSnippetsFilters(filters.SnippetsFilters)...)

if filters.RequestRedirect != nil {
return updateLocationRedirectFilter(location, filters.RequestRedirect, listenerPort, path)
}

location = updateLocationRewriteFilter(location, filters.RequestURLRewrite, path)
location = updateLocationMirrorFilters(location, filters.RequestMirrors, path, mirrorPercentage)
location = updateLocationProxySettings(location, matchRule, grpc, keepAliveCheck)

return location
}

func updateLocationMirrorRoute(location http.Location, path string, grpc bool) http.Location {
if strings.HasPrefix(path, http.InternalMirrorRoutePathPrefix) {
location.Type = http.InternalLocationType
if grpc {
location.Rewrites = []string{"^ $request_uri break"}
}
}

location.Includes = append(location.Includes, createIncludesFromLocationSnippetsFilters(filters.SnippetsFilters)...)
return location
}

if filters.RequestRedirect != nil {
ret, rewrite := createReturnAndRewriteConfigForRedirectFilter(filters.RequestRedirect, listenerPort, path)
if rewrite.MainRewrite != "" {
location.Rewrites = append(location.Rewrites, rewrite.MainRewrite)
}
location.Return = ret
return location
func updateLocationRedirectFilter(
location http.Location,
redirectFilter *dataplane.HTTPRequestRedirectFilter,
listenerPort int32,
path string,
) http.Location {
ret, rewrite := createReturnAndRewriteConfigForRedirectFilter(redirectFilter, listenerPort, path)
if rewrite.MainRewrite != "" {
location.Rewrites = append(location.Rewrites, rewrite.MainRewrite)
}
location.Return = ret

return location
}

rewrites := createRewritesValForRewriteFilter(filters.RequestURLRewrite, path)
func updateLocationRewriteFilter(
location http.Location,
rewriteFilter *dataplane.HTTPURLRewriteFilter,
path string,
) http.Location {
rewrites := createRewritesValForRewriteFilter(rewriteFilter, path)
if rewrites != nil {
if location.Type == http.InternalLocationType && rewrites.InternalRewrite != "" {
location.Rewrites = append(location.Rewrites, rewrites.InternalRewrite)
Expand All @@ -461,12 +520,42 @@ func updateLocation(
}
}

for _, filter := range filters.RequestMirrors {
return location
}

func updateLocationMirrorFilters(
location http.Location,
mirrorFilters []*dataplane.HTTPRequestMirrorFilter,
path string,
mirrorPercentage *float64,
) http.Location {
for _, filter := range mirrorFilters {
if filter.Target != nil {
location.MirrorPaths = append(location.MirrorPaths, *filter.Target)
}
}

if location.MirrorPaths != nil {
location.MirrorPaths = deduplicateStrings(location.MirrorPaths)
}

// if mirrorPercentage is nil (no mirror filter configured) or 100.0, the split clients variable is not generated,
// and we let all traffic get mirrored.
if mirrorPercentage != nil && *mirrorPercentage != 100.0 {
location.MirrorSplitClientsVariableName = convertSplitClientVariableName(
fmt.Sprintf("%s_%.2f", path, *mirrorPercentage),
)
}

return location
}

func updateLocationProxySettings(
location http.Location,
matchRule dataplane.MatchRule,
grpc bool,
keepAliveCheck keepAliveChecker,
) http.Location {
extraHeaders := make([]http.Header, 0, 3)
if grpc {
extraHeaders = append(extraHeaders, grpcAuthorityHeader)
Expand Down Expand Up @@ -497,18 +586,24 @@ func updateLocation(
// updateLocations updates the existing locations with any relevant configurations, like proxy_pass,
// filters, tls settings, etc.
func updateLocations(
filters dataplane.HTTPFilters,
buildLocations []http.Location,
matchRule dataplane.MatchRule,
pathRule dataplane.PathRule,
buildLocations []http.Location,
listenerPort int32,
path string,
grpc bool,
keepAliveCheck keepAliveChecker,
mirrorPercentage *float64,
) []http.Location {
updatedLocations := make([]http.Location, len(buildLocations))

for i, loc := range buildLocations {
updatedLocations[i] = updateLocation(filters, loc, matchRule, listenerPort, path, grpc, keepAliveCheck)
updatedLocations[i] = updateLocation(
matchRule,
pathRule,
loc,
listenerPort,
keepAliveCheck,
mirrorPercentage,
)
}

return updatedLocations
Expand Down Expand Up @@ -962,3 +1057,18 @@ func getConnectionHeader(keepAliveCheck keepAliveChecker, backends []dataplane.B

return httpConnectionHeader
}

// deduplicateStrings removes duplicate strings from a slice while preserving order.
func deduplicateStrings(content []string) []string {
seen := make(map[string]struct{})
result := make([]string, 0, len(content))

for _, str := range content {
if _, exists := seen[str]; !exists {
seen[str] = struct{}{}
result = append(result, str)
}
}

return result
}
6 changes: 6 additions & 0 deletions internal/controller/nginx/config/servers_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ server {
internal;
{{ end }}

{{ if ne $l.MirrorSplitClientsVariableName "" -}}
if (${{ $l.MirrorSplitClientsVariableName }} = "") {
return 204;
}
{{- end }}

{{- range $i := $l.Includes }}
include {{ $i.Name }};
{{- end -}}
Expand Down
Loading
Loading