Repository: nginx/nginx-gateway-fabric

Issue:
NGF does not separate log format for multiple gateways?
**Describe the bug**
I am testing to separate traffics and logs for my applications. seems this is not working. I am using version 2.0.1.
My configuration: 3 apps in 2 namespaces and each app has its own gateway and log formatting configure

I was expecting to see only one formatting snipptsfilter be pushed to one gateway pod, but I am seeing following on app1 gateway: (similar on app2 and app3 gateway pods)

include /etc/nginx/includes/SnippetsFilter_http_app2_app2-http-logging.conf;
include /etc/nginx/includes/SnippetsFilter_http_app2_app3-http-logging.conf;
include /etc/nginx/includes/SnippetsFilter_http_app1_app1-http-logging.conf;

nginx will complain if the log_format is using same name, With different names, the incoming request is logged 3 times


**To Reproduce**
Steps to reproduce the behavior:
1. snippets deployments: (I used same configure to run test, they can be different)
`- apiVersion: gateway.nginx.org/v1alpha1
  kind: SnippetsFilter
  metadata:
    name: app1-http-logging
    namespace: app1
  spec:
    snippets:
    - context: http
      value: |
        log_format json_combined escape=json '{'
          "\"time\": \"$time_iso8601\","
          "\"time_local\": \"$time_local\","
          "\"x-forward-for\": \"$proxy_add_x_forwarded_for\","
          "\"server_addr\": \"$server_addr\","
          "\"server_name\": \"$server_name\","
          "\"TraceID\": \"$http_x_b3_traceid\""
        '}';
        access_log /dev/stdout json_combined;

- apiVersion: gateway.nginx.org/v1alpha1
  kind: SnippetsFilter
  metadata:
    name: app2-http-logging
    namespace: app2
  spec:
    snippets:
    - context: http
      value: |
        log_format json_combined escape=json '{'
          "\"time\": \"$time_iso8601\","
          "\"time_local\": \"$time_local\","
          "\"x-forward-for\": \"$proxy_add_x_forwarded_for\","
          "\"server_addr\": \"$server_addr\","
          "\"server_name\": \"$server_name\","
          "\"TraceID\": \"$http_x_b3_traceid\""
        '}';
        access_log /dev/stdout json_combined;

- apiVersion: gateway.nginx.org/v1alpha1
  kind: SnippetsFilter
  metadata:
    generation: 1
    name: app3-http-logging
    namespace: app2
  spec:
    snippets:
    - context: http
      value: |
        log_format json_combined escape=json '{'
          "\"time\": \"$time_iso8601\","
          "\"time_local\": \"$time_local\","
          "\"x-forward-for\": \"$proxy_add_x_forwarded_for\","
          "\"server_addr\": \"$server_addr\","
          "\"server_name\": \"$server_name\","
          "\"TraceID\": \"$http_x_b3_traceid\""
        '}';
        access_log /dev/stdout json_combined;`
3. View logs on 'controller pod'
```
{"level":"error","ts":"2025-06-30T14:26:21Z","logger":"eventHandler","msg":"Failed to update NGINX configuration","error":"msg: Config apply failed, rolling back config; error: failed validating config NGINX config test failed exit status 1: 2025/06/30 14:26:21 [emerg] 103640#103640: duplicate \"log_format\" name \"json_combined\" in /etc/nginx/includes/SnippetsFilter_http_app1_app1-http-logging.conf:37\nnginx: [emerg] duplicate \"log_format\" name \"json_combined\" in /etc/nginx/includes/SnippetsFilter_http_app1_app1-http-logging.conf:37\nnginx: configuration file /etc/nginx/nginx.conf test failed\n","stacktrace":"github.com/nginx/nginx-gateway-fabric/internal/controller.(*eventHandlerImpl).waitForStatusUpdates\n\t/home/runner/work/nginx-gateway-fabric/nginx-gateway-fabric/internal/controller/handler.go:262"}
```

If I give "log_format" different names, controller pod is not complaining any more and configure was reloaded successfully but request is logged 3 times

5. See error**Expected behavior**
A clear and concise description of what you expected to happen.

What am expecting is only log_format configuration intended to the application (by name and name space) is pushed to the gateway pod. So with my configuration, only following should be pushed to app1 gateway pod:

include /etc/nginx/includes/SnippetsFilter_http_app1_app1-http-logging.conf;

**Your environment**
* Version of the NGINX Gateway Fabric - release version or a specific commit. The first line of the nginx-gateway container logs includes the commit info.

NAMESPACE: nginx-gateway
STATUS: deployed
REVISION: 7
CHART: nginx-gateway-fabric
VERSION: 2.0.1
APP_VERSION: 2.0.1

* Version of Kubernetes
* Kubernetes platform (e.g. Mini-kube or GCP)
* Details on how you expose the NGINX Gateway Fabric Pod (e.g. Service of type LoadBalancer or port-forward)
* Logs of NGINX container: `kubectl -n <nginx-deployment-namespace> logs deployments/<nginx-deployment>`
* NGINX Configuration: `kubectl -n <nginx-deployment-namespace> exec -it deployments/<nginx-deployment> -- nginx -T`

**Additional context**
Add any other context about the problem here. Any log files you want to share.


Hints from issue comments:
@nginx-bot: Hi @hongyecc! Welcome to the project! 🎉

Thanks for opening this issue!
Be sure to check out our [Contributing Guidelines](../blob/main/CONTRIBUTING.md) and the [Issue Lifecycle](../blob/main/ISSUE_LIFECYCLE.md) while you wait for someone on the team to take a look at this.

<!-- nginx-bot-comment -->@mpstefan: Thanks for reporting this! We are looking into it ASAP.

Solution patch:
diff --git a/internal/controller/state/dataplane/configuration.go b/internal/controller/state/dataplane/configuration.go
index 6bded6ce..6943c4a2 100644
--- a/internal/controller/state/dataplane/configuration.go
+++ b/internal/controller/state/dataplane/configuration.go
@@ -44,7 +44,10 @@ func BuildConfiguration(
 		return config
 	}
 
-	baseHTTPConfig := buildBaseHTTPConfig(g, gateway)
+	// Get SnippetsFilters that are specifically referenced by routes attached to this gateway
+	gatewaySnippetsFilters := gateway.GetReferencedSnippetsFilters(g.Routes, g.SnippetsFilters)
+
+	baseHTTPConfig := buildBaseHTTPConfig(gateway, gatewaySnippetsFilters)
 
 	httpServers, sslServers := buildServers(gateway)
 	backendGroups := buildBackendGroups(append(httpServers, sslServers...))
@@ -77,7 +80,7 @@ func BuildConfiguration(
 		BaseHTTPConfig:   baseHTTPConfig,
 		Logging:          buildLogging(gateway),
 		NginxPlus:        nginxPlus,
-		MainSnippets:     buildSnippetsForContext(g.SnippetsFilters, ngfAPIv1alpha1.NginxContextMain),
+		MainSnippets:     buildSnippetsForContext(gatewaySnippetsFilters, ngfAPIv1alpha1.NginxContextMain),
 		AuxiliarySecrets: buildAuxiliarySecrets(g.PlusSecrets),
 	}
 
@@ -963,12 +966,15 @@ func CreateRatioVarName(ratio int32) string {
 }
 
 // buildBaseHTTPConfig generates the base http context config that should be applied to all servers.
-func buildBaseHTTPConfig(g *graph.Graph, gateway *graph.Gateway) BaseHTTPConfig {
+func buildBaseHTTPConfig(
+	gateway *graph.Gateway,
+	gatewaySnippetsFilters map[types.NamespacedName]*graph.SnippetsFilter,
+) BaseHTTPConfig {
 	baseConfig := BaseHTTPConfig{
 		// HTTP2 should be enabled by default
 		HTTP2:    true,
 		IPFamily: Dual,
-		Snippets: buildSnippetsForContext(g.SnippetsFilters, ngfAPIv1alpha1.NginxContextHTTP),
+		Snippets: buildSnippetsForContext(gatewaySnippetsFilters, ngfAPIv1alpha1.NginxContextHTTP),
 	}
 
 	// safe to access EffectiveNginxProxy since we only call this function when the Gateway is not nil.
diff --git a/internal/controller/state/graph/gateway.go b/internal/controller/state/graph/gateway.go
index ba174db0..e7129da5 100644
--- a/internal/controller/state/graph/gateway.go
+++ b/internal/controller/state/graph/gateway.go
@@ -3,6 +3,7 @@ package graph
 import (
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/validation/field"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 	v1 "sigs.k8s.io/gateway-api/apis/v1"
 
 	"github.com/nginx/nginx-gateway-fabric/internal/controller/config"
@@ -196,3 +197,70 @@ func validateGateway(gw *v1.Gateway, gc *GatewayClass, npCfg *NginxProxy) ([]con
 
 	return conds, valid
 }
+
+// GetReferencedSnippetsFilters returns all SnippetsFilters that are referenced by routes attached to this Gateway.
+func (g *Gateway) GetReferencedSnippetsFilters(
+	routes map[RouteKey]*L7Route,
+	allSnippetsFilters map[types.NamespacedName]*SnippetsFilter,
+) map[types.NamespacedName]*SnippetsFilter {
+	if len(routes) == 0 || len(allSnippetsFilters) == 0 {
+		return nil
+	}
+
+	gatewayNsName := client.ObjectKeyFromObject(g.Source)
+	referencedSnippetsFilters := make(map[types.NamespacedName]*SnippetsFilter)
+
+	for _, route := range routes {
+		if !route.Valid || !g.isRouteAttachedToGateway(route, gatewayNsName) {
+			continue
+		}
+
+		g.collectSnippetsFiltersFromRoute(route, allSnippetsFilters, referencedSnippetsFilters)
+	}
+
+	if len(referencedSnippetsFilters) == 0 {
+		return nil
+	}
+
+	return referencedSnippetsFilters
+}
+
+// isRouteAttachedToGateway checks if the given route is attached to this gateway.
+func (g *Gateway) isRouteAttachedToGateway(route *L7Route, gatewayNsName types.NamespacedName) bool {
+	for _, parentRef := range route.ParentRefs {
+		if parentRef.Gateway != nil && parentRef.Gateway.NamespacedName == gatewayNsName {
+			return true
+		}
+	}
+	return false
+}
+
+// collectSnippetsFiltersFromRoute extracts SnippetsFilters from a single route's rules.
+func (g *Gateway) collectSnippetsFiltersFromRoute(
+	route *L7Route,
+	allSnippetsFilters map[types.NamespacedName]*SnippetsFilter,
+	referencedFilters map[types.NamespacedName]*SnippetsFilter,
+) {
+	for _, rule := range route.Spec.Rules {
+		if !rule.Filters.Valid {
+			continue
+		}
+
+		for _, filter := range rule.Filters.Filters {
+			if filter.FilterType != FilterExtensionRef ||
+				filter.ResolvedExtensionRef == nil ||
+				filter.ResolvedExtensionRef.SnippetsFilter == nil {
+				continue
+			}
+
+			sf := filter.ResolvedExtensionRef.SnippetsFilter
+			nsName := client.ObjectKeyFromObject(sf.Source)
+
+			// Only include if it exists in the cluster-wide map and is valid
+			// Using the cluster-wide version ensures consistency and avoids duplicates
+			if clusterSF, exists := allSnippetsFilters[nsName]; exists && clusterSF.Valid {
+				referencedFilters[nsName] = clusterSF
+			}
+		}
+	}
+}



Test patch:
diff --git a/internal/controller/state/dataplane/configuration_test.go b/internal/controller/state/dataplane/configuration_test.go
index 95a4806e..12fc3d7e 100644
--- a/internal/controller/state/dataplane/configuration_test.go
+++ b/internal/controller/state/dataplane/configuration_test.go
@@ -2462,31 +2462,17 @@ func TestBuildConfiguration(t *testing.T) {
 				return g
 			}),
 			expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration {
-				conf.MainSnippets = []Snippet{
-					{
-						Name: createSnippetName(
-							ngfAPIv1alpha1.NginxContextMain,
-							client.ObjectKeyFromObject(sf1.Source),
-						),
-						Contents: "main snippet",
-					},
-				}
-				conf.BaseHTTPConfig.Snippets = []Snippet{
-					{
-						Name: createSnippetName(
-							ngfAPIv1alpha1.NginxContextHTTP,
-							client.ObjectKeyFromObject(sf1.Source),
-						),
-						Contents: "http snippet",
-					},
-				}
+				// With proper scoping, no snippets should be included since no routes
+				// attached to this gateway reference the SnippetsFilters
+				conf.MainSnippets = nil            // nil - no snippets should be included
+				conf.BaseHTTPConfig.Snippets = nil // nil - no snippets should be included
 				conf.HTTPServers = []VirtualServer{}
 				conf.SSLServers = []VirtualServer{}
 				conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{}
 
 				return conf
 			}),
-			msg: "SnippetsFilters with main and http snippet",
+			msg: "SnippetsFilters scoped per gateway - no routes reference SnippetsFilters",
 		},
 		{
 			graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph {
@@ -4488,7 +4474,10 @@ func TestBuildRewriteIPSettings(t *testing.T) {
 		t.Run(tc.msg, func(t *testing.T) {
 			t.Parallel()
 			g := NewWithT(t)
-			baseConfig := buildBaseHTTPConfig(tc.g, tc.g.Gateways[types.NamespacedName{}])
+			baseConfig := buildBaseHTTPConfig(
+				tc.g.Gateways[types.NamespacedName{}],
+				make(map[types.NamespacedName]*graph.SnippetsFilter),
+			)
 			g.Expect(baseConfig.RewriteClientIPSettings).To(Equal(tc.expRewriteIPSettings))
 		})
 	}
diff --git a/internal/controller/state/graph/gateway_test.go b/internal/controller/state/graph/gateway_test.go
index 26dbd375..f37349be 100644
--- a/internal/controller/state/graph/gateway_test.go
+++ b/internal/controller/state/graph/gateway_test.go
@@ -13,6 +13,7 @@ import (
 	v1 "sigs.k8s.io/gateway-api/apis/v1"
 	"sigs.k8s.io/gateway-api/apis/v1beta1"
 
+	ngfAPIv1alpha1 "github.com/nginx/nginx-gateway-fabric/apis/v1alpha1"
 	ngfAPIv1alpha2 "github.com/nginx/nginx-gateway-fabric/apis/v1alpha2"
 	"github.com/nginx/nginx-gateway-fabric/internal/controller/state/conditions"
 	"github.com/nginx/nginx-gateway-fabric/internal/framework/controller"
@@ -1582,3 +1583,200 @@ func TestValidateGatewayParametersRef(t *testing.T) {
 		})
 	}
 }
+
+func TestGetReferencedSnippetsFilters(t *testing.T) {
+	t.Parallel()
+
+	gw := &Gateway{
+		Source: &v1.Gateway{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "gateway-ns",
+				Name:      "test-gateway",
+			},
+		},
+	}
+
+	sf1 := &SnippetsFilter{
+		Source: &ngfAPIv1alpha1.SnippetsFilter{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "app1",
+				Name:      "app1-logging",
+			},
+		},
+		Valid: true,
+	}
+
+	sf2 := &SnippetsFilter{
+		Source: &ngfAPIv1alpha1.SnippetsFilter{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "app2",
+				Name:      "app2-logging",
+			},
+		},
+		Valid: true,
+	}
+
+	sf3Invalid := &SnippetsFilter{
+		Source: &ngfAPIv1alpha1.SnippetsFilter{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "app3",
+				Name:      "invalid-filter",
+			},
+		},
+		Valid: false,
+	}
+
+	routeAttachedToGateway := &L7Route{
+		Source: &v1.HTTPRoute{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "app1",
+				Name:      "attached-route",
+			},
+		},
+		Valid: true,
+		ParentRefs: []ParentRef{
+			{
+				Gateway: &ParentRefGateway{
+					NamespacedName: types.NamespacedName{
+						Namespace: "gateway-ns",
+						Name:      "test-gateway",
+					},
+				},
+			},
+		},
+		Spec: L7RouteSpec{
+			Rules: []RouteRule{
+				{
+					Filters: RouteRuleFilters{
+						Valid: true,
+						Filters: []Filter{
+							{
+								FilterType: FilterExtensionRef,
+								ResolvedExtensionRef: &ExtensionRefFilter{
+									SnippetsFilter: sf1,
+									Valid:          true,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	routeNotAttachedToGateway := &L7Route{
+		Source: &v1.HTTPRoute{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "app2",
+				Name:      "not-attached-route",
+			},
+		},
+		Valid: true,
+		ParentRefs: []ParentRef{
+			{
+				Gateway: &ParentRefGateway{
+					NamespacedName: types.NamespacedName{
+						Namespace: "other-gateway-ns",
+						Name:      "other-gateway",
+					},
+				},
+			},
+		},
+		Spec: L7RouteSpec{
+			Rules: []RouteRule{
+				{
+					Filters: RouteRuleFilters{
+						Valid: true,
+						Filters: []Filter{
+							{
+								FilterType: FilterExtensionRef,
+								ResolvedExtensionRef: &ExtensionRefFilter{
+									SnippetsFilter: sf2,
+									Valid:          true,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	routeWithInvalidFilter := &L7Route{
+		Source: &v1.HTTPRoute{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "app3",
+				Name:      "route-with-invalid-filter",
+			},
+		},
+		Valid: true,
+		ParentRefs: []ParentRef{
+			{
+				Gateway: &ParentRefGateway{
+					NamespacedName: types.NamespacedName{
+						Namespace: "gateway-ns",
+						Name:      "test-gateway",
+					},
+				},
+			},
+		},
+		Spec: L7RouteSpec{
+			Rules: []RouteRule{
+				{
+					Filters: RouteRuleFilters{
+						Valid: true,
+						Filters: []Filter{
+							{
+								FilterType: FilterExtensionRef,
+								ResolvedExtensionRef: &ExtensionRefFilter{
+									SnippetsFilter: sf3Invalid,
+									Valid:          false,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	routes := map[RouteKey]*L7Route{
+		{
+			NamespacedName: types.NamespacedName{Namespace: "app1", Name: "attached-route"},
+			RouteType:      RouteTypeHTTP,
+		}: routeAttachedToGateway,
+		{
+			NamespacedName: types.NamespacedName{Namespace: "app2", Name: "not-attached-route"},
+			RouteType:      RouteTypeHTTP,
+		}: routeNotAttachedToGateway,
+		{
+			NamespacedName: types.NamespacedName{Namespace: "app3", Name: "route-with-invalid-filter"},
+			RouteType:      RouteTypeHTTP,
+		}: routeWithInvalidFilter,
+	}
+
+	allSnippetsFilters := map[types.NamespacedName]*SnippetsFilter{
+		{Namespace: "app1", Name: "app1-logging"}:   sf1,
+		{Namespace: "app2", Name: "app2-logging"}:   sf2,
+		{Namespace: "app3", Name: "invalid-filter"}: sf3Invalid,
+	}
+
+	g := NewWithT(t)
+
+	result := gw.GetReferencedSnippetsFilters(routes, allSnippetsFilters)
+
+	// Should only include sf1 (valid filter from route attached to this gateway)
+	expectedResult := map[types.NamespacedName]*SnippetsFilter{
+		{Namespace: "app1", Name: "app1-logging"}: sf1,
+	}
+
+	g.Expect(result).To(Equal(expectedResult))
+
+	// Test with no routes
+	emptyResult := gw.GetReferencedSnippetsFilters(map[RouteKey]*L7Route{}, allSnippetsFilters)
+	g.Expect(emptyResult).To(BeEmpty())
+
+	// Test with routes but no snippets filters
+	emptyFilterResult := gw.GetReferencedSnippetsFilters(routes, map[types.NamespacedName]*SnippetsFilter{})
+	g.Expect(emptyFilterResult).To(BeEmpty())
+}