Repository: envoyproxy/gateway
Pull request #5498: fix: otel sink json access logging without text field
Author: login: kraashen
Labels: name: cherrypick/release-v1.2.8
name: cherrypick/release-v1.3.2
Created: 2025-03-14T09:03:37Z | Merged: 2025-03-22T23:08:57Z

**What type of PR is this?**

This PR attempts to fix issue #5481 by passing only the JSON attributes to the otel formatter builder. It does this by leaving the text format empty when JSON type is specified but text is not.

**What this PR does / why we need it**:

As described in the linked issue, this leads to a situation where HTTPRoutes are not registering to listeners due to error returned in situations where JSON logging is used. This issue seemed to be rising when OpenTelemetry sink type is used without `text` field in the configuration.

Tested this in a local cluster calling the test API, and logs seemed to be produced in Loki when queried with `exporter=OTEL` label.
![image](https://github.com/user-attachments/assets/689415c0-164a-4f3d-81ba-c95678602d34)


**Which issue(s) this PR fixes**:

Fixes #5481

<!--
For any non-trivial changes, you need to provide a brief description of the changes in the release notes.
Please add the description to the release-notes/current.yaml file and include this file in the PR.
-->
Release Notes: Yes

**Note**

I have little to none prior experience of Envoy Gateway development. Went through the [guidelines](https://gateway.envoyproxy.io/contributions/develop/) with best effort, but I'm a bit unsure if there are any possible other downstream consequences to this change. Any advice would be appreciated!

Linked issue #5481: OpenTelemetry access log sink failure with "otel.Text is nil" error (EG v1.3.1)
*Description*:
After upgrading our development clusters to v1.3.1 of Envoy Gateway, we noticed that none of our HTTPRoutes in the cluster were not registering to the proxy listeners, making all services unavailable. This was validated with `egctl config envoy-proxy listener` and `... routes`, showing that none of the configured routes were registered.

```
❯ curl https://<url> -vvv
* Host <url>:443 was resolved.
* IPv6: (none)
* IPv4: ...
*   Trying ...
* Connected to ... port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to ...:443
* Closing connection
curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to ...:443 

❯ openssl s_client -connect ...:443
Connecting to ...
CONNECTED(00000005)
4088A9F501000000:error:0A000126:SSL routines::unexpected eof while reading:ssl/record/rec_layer_s3.c:693:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 0 bytes and written 343 bytes
Verification: OK
---
New, (NONE), Cipher is (NONE)
Protocol: TLSv1.3
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
```

In the envoy logs, these log lines were visible:

```
2025-03-12T11:13:00.577Z	ERROR	xds-translator	runner/runner.go:85	failed to translate xds ir	{"runner": "xds-translator", "error": "otel.Text is nil\notel.Text is nil"}
2025-03-12T11:13:00.577Z	INFO	provider.demo-keyauth.demo-keyauth	kubernetes/status_updater.go:109	status unchanged, bypassing update	{"runner": "provider"}
2025-03-12T11:13:00.577Z	INFO	provider	kubernetes/status_updater.go:145	received a status update	{"runner": "provider", "namespace": "<namespace>", "name": "<name>"}
2025-03-12T11:13:00.577Z	ERROR	watchable	message/watchutil.go:80	observed an error	{"runner": "xds-translator", "error": "otel.Text is nil\notel.Text is nil"}
```

After disabling the `openTelemetry` sink from the proxy access log config, the routes were configured and services became accessible.

The lines generating the logs seems to originate here: https://github.com/envoyproxy/gateway/blob/df2bb96c32604554f50ec08702cbea834ce968f5/internal/xds/translator/accesslog.go#L339-L341

When downgrading back to v1.3.0, the existing telemetry sink configuration works again.

*Repro steps*:

Envoy proxy configmap includes following telemetry config:

```
...
  telemetry:
    accessLog:
      settings:
      - format:
          json:
            ...
          type: JSON
        sinks:
        - openTelemetry:
            host: telemetry-opentelemetry-collector.telemetry.svc.cluster.local
            port: 4317
            resources:
              k8s.cluster.name: <cluster-name>
          type: OpenTelemetry
        - file:
            path: /dev/stdout
          type: File
```

*Environment*:

`envoy-gateway v1.3.1`, templated and installed via latest Helm Chart.

*Logs*:

Included in description.
Linked issue comments: user: login: zirain
created_at: 2025-03-12T13:00:03Z
body: cc @guydc 
author_association: MEMBER

Diff:
diff --git a/internal/xds/translator/accesslog.go b/internal/xds/translator/accesslog.go
index 991c8e342c..304f22da49 100644
--- a/internal/xds/translator/accesslog.go
+++ b/internal/xds/translator/accesslog.go
@@ -336,10 +336,10 @@ func buildXdsAccessLog(al *ir.AccessLog, accessLogType ir.ProxyAccessLogType) ([
 			ResourceAttributes: convertToKeyValueList(otel.Resources, false),
 		}
 
-		if otel.Text == nil {
-			return nil, errors.New("otel.Text is nil")
+		var format string
+		if otel.Text != nil && *otel.Text != "" {
+			format = *otel.Text
 		}
-		format := *otel.Text
 
 		if format != "" {
 			al.Body = &otlpcommonv1.AnyValue{
@@ -349,9 +349,17 @@ func buildXdsAccessLog(al *ir.AccessLog, accessLogType ir.ProxyAccessLogType) ([
 			}
 		}
 
-		al.Attributes = convertToKeyValueList(otel.Attributes, true)
+		var attrs map[string]string
+		if len(otel.Attributes) != 0 {
+			attrs = otel.Attributes
+		} else if len(otel.Attributes) == 0 && format == "" {
+			// if there are no attributes and text format is unset, use the default EnvoyJSONLogFields
+			attrs = EnvoyJSONLogFields
+		}
+
+		al.Attributes = convertToKeyValueList(attrs, true)
+		formatters := accessLogOpenTelemetryFormatters(format, attrs)
 
-		formatters := accessLogOpenTelemetryFormatters(format, otel.Attributes)
 		if len(formatters) != 0 {
 			al.Formatters = formatters
 		}
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index b332fc8301..3b02988dcc 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -36,6 +36,7 @@ bug fixes: |
   Added support for BackendTLSPolicy and EnvoyExtensionPolicy parsing in Standalone mode.
   Retrigger reconciliation when backendRef of type ServiceImport is updated or when EndpointSlice(s) for a ServiceImport are updated.
   Fix not logging an error and returning it in the K8s Reconcile method when a GatewayClass is not accepted.
+  Fix allowing empty text field for opentelemetry sink when using JSON format.
 
 # Enhancements that improve performance.
 performance improvements: |
diff --git a/test/e2e/testdata/accesslog-otel-default.yaml b/test/e2e/testdata/accesslog-otel-default.yaml
new file mode 100644
index 0000000000..764e8e02e5
--- /dev/null
+++ b/test/e2e/testdata/accesslog-otel-default.yaml
@@ -0,0 +1,61 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: accesslog-gtw
+  namespace: gateway-conformance-infra
+spec:
+  gatewayClassName: {GATEWAY_CLASS_NAME}
+  listeners:
+    - name: http
+      port: 80
+      protocol: HTTP
+      allowedRoutes:
+        namespaces:
+          from: Same
+  infrastructure:
+    parametersRef:
+      group: gateway.envoyproxy.io
+      kind: EnvoyProxy
+      name: accesslog-otel
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: EnvoyProxy
+metadata:
+  name: accesslog-otel
+  namespace: gateway-conformance-infra
+spec:
+  ipFamily: IPv4
+  telemetry:
+    accessLog:
+      settings:
+        - matches:
+            - "'x-envoy-logged' in request.headers"
+          sinks:
+            - type: OpenTelemetry
+              openTelemetry:
+                backendRefs:
+                  - name: otel-collector
+                    namespace: monitoring
+                    port: 4317
+                resources:
+                  k8s.cluster.name: "envoy-gateway"
+  shutdown:
+    drainTimeout: 5s
+    minDrainDuration: 1s
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: accesslog-otel
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: accesslog-gtw
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /otel
+      backendRefs:
+        - name: infra-backend-v2
+          port: 8080
diff --git a/test/e2e/testdata/accesslog-otel-json.yaml b/test/e2e/testdata/accesslog-otel-json.yaml
new file mode 100644
index 0000000000..219315112c
--- /dev/null
+++ b/test/e2e/testdata/accesslog-otel-json.yaml
@@ -0,0 +1,79 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: accesslog-gtw
+  namespace: gateway-conformance-infra
+spec:
+  gatewayClassName: {GATEWAY_CLASS_NAME}
+  listeners:
+    - name: http
+      port: 80
+      protocol: HTTP
+      allowedRoutes:
+        namespaces:
+          from: Same
+  infrastructure:
+    parametersRef:
+      group: gateway.envoyproxy.io
+      kind: EnvoyProxy
+      name: accesslog-otel
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: EnvoyProxy
+metadata:
+  name: accesslog-otel
+  namespace: gateway-conformance-infra
+spec:
+  ipFamily: IPv4
+  telemetry:
+    accessLog:
+      settings:
+        - format:
+            type: JSON
+            json:
+              time: "%START_TIME%"
+              method: "%REQ(:METHOD)%"
+              metadata: "%METADATA(ROUTE:envoy-gateway:resources)%"
+              x-envoy-original-path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
+              protocol: "%PROTOCOL%"
+              responseCode: "%RESPONSE_CODE%"
+              responseFlags: "%RESPONSE_FLAGS%"
+              bytesReceived: "%BYTES_RECEIVED%"
+              bytesSent: "%BYTES_SENT%"
+              duration: "%DURATION%"
+              x-forwarded-for: "%REQ(X-FORWARDED-FOR)%"
+              user-agent: "%REQ(USER-AGENT)%"
+              x-request-id: "%REQ(X-REQUEST-ID)%"
+              authority: "%REQ(:AUTHORITY)%"
+              upstreamHost: "%UPSTREAM_HOST%"
+          matches:
+            - "'x-envoy-logged' in request.headers"
+          sinks:
+            - type: OpenTelemetry
+              openTelemetry:
+                backendRefs:
+                  - name: otel-collector
+                    namespace: monitoring
+                    port: 4317
+                resources:
+                  k8s.cluster.name: "envoy-gateway"
+  shutdown:
+    drainTimeout: 5s
+    minDrainDuration: 1s
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: accesslog-otel
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: accesslog-gtw
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /otel
+      backendRefs:
+        - name: infra-backend-v2
+          port: 8080
diff --git a/test/e2e/tests/accesslog.go b/test/e2e/tests/accesslog.go
index ea57734957..5df762868d 100644
--- a/test/e2e/tests/accesslog.go
+++ b/test/e2e/tests/accesslog.go
@@ -21,7 +21,7 @@ import (
 )
 
 func init() {
-	ConformanceTests = append(ConformanceTests, FileAccessLogTest, OpenTelemetryTest, ALSTest)
+	ConformanceTests = append(ConformanceTests, FileAccessLogTest, OpenTelemetryTestText, OpenTelemetryTestJSON, ALSTest, OpenTelemetryTestJSONAsDefault)
 }
 
 var FileAccessLogTest = suite.ConformanceTest{
@@ -116,9 +116,9 @@ var FileAccessLogTest = suite.ConformanceTest{
 	},
 }
 
-var OpenTelemetryTest = suite.ConformanceTest{
-	ShortName:   "OpenTelemetryAccessLog",
-	Description: "Make sure OpenTelemetry access log is working",
+var OpenTelemetryTestText = suite.ConformanceTest{
+	ShortName:   "OpenTelemetryTextAccessLog",
+	Description: "Make sure OpenTelemetry text access log is working",
 	Manifests:   []string{"testdata/accesslog-otel.yaml"},
 	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
 		labels := map[string]string{
@@ -169,6 +169,112 @@ var OpenTelemetryTest = suite.ConformanceTest{
 	},
 }
 
+var OpenTelemetryTestJSONAsDefault = suite.ConformanceTest{
+	ShortName:   "OpenTelemetryAccessLogJSONAsDefault",
+	Description: "Make sure OpenTelemetry JSON access log is working as default when no format or type is specified",
+	Manifests:   []string{"testdata/accesslog-otel-default.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		labels := map[string]string{
+			"k8s_namespace_name": "envoy-gateway-system",
+			"exporter":           "OTLP",
+		}
+
+		ns := "gateway-conformance-infra"
+		routeNN := types.NamespacedName{Name: "accesslog-otel", Namespace: ns}
+		gwNN := types.NamespacedName{Name: "accesslog-gtw", Namespace: ns}
+		gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+
+		t.Run("Positive", func(t *testing.T) {
+			expectedResponse := httputils.ExpectedResponse{
+				Request: httputils.Request{
+					Path: "/otel",
+					Headers: map[string]string{
+						"x-envoy-logged": "1",
+					},
+				},
+				Response: httputils.Response{
+					StatusCode: 200,
+				},
+				Namespace: ns,
+			}
+			// make sure listener is ready
+			httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+
+			runLogTest(t, suite, gwAddr, expectedResponse, labels, "", 1)
+		})
+
+		t.Run("Negative", func(t *testing.T) {
+			expectedResponse := httputils.ExpectedResponse{
+				Request: httputils.Request{
+					Path: "/otel",
+					// envoy will not log this request without the header x-envoy-logged
+				},
+				Response: httputils.Response{
+					StatusCode: 200,
+				},
+				Namespace: ns,
+			}
+			// make sure listener is ready
+			httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+
+			runLogTest(t, suite, gwAddr, expectedResponse, labels, "", 0)
+		})
+	},
+}
+
+var OpenTelemetryTestJSON = suite.ConformanceTest{
+	ShortName:   "OpenTelemetryAccessLogJSON",
+	Description: "Make sure OpenTelemetry JSON access log is working with custom JSON attributes",
+	Manifests:   []string{"testdata/accesslog-otel-json.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		labels := map[string]string{
+			"k8s_namespace_name": "envoy-gateway-system",
+			"exporter":           "OTLP",
+		}
+
+		ns := "gateway-conformance-infra"
+		routeNN := types.NamespacedName{Name: "accesslog-otel", Namespace: ns}
+		gwNN := types.NamespacedName{Name: "accesslog-gtw", Namespace: ns}
+		gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+
+		t.Run("Positive", func(t *testing.T) {
+			expectedResponse := httputils.ExpectedResponse{
+				Request: httputils.Request{
+					Path: "/otel",
+					Headers: map[string]string{
+						"x-envoy-logged": "1",
+					},
+				},
+				Response: httputils.Response{
+					StatusCode: 200,
+				},
+				Namespace: ns,
+			}
+			// make sure listener is ready
+			httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+
+			runLogTest(t, suite, gwAddr, expectedResponse, labels, "", 1)
+		})
+
+		t.Run("Negative", func(t *testing.T) {
+			expectedResponse := httputils.ExpectedResponse{
+				Request: httputils.Request{
+					Path: "/otel",
+					// envoy will not log this request without the header x-envoy-logged
+				},
+				Response: httputils.Response{
+					StatusCode: 200,
+				},
+				Namespace: ns,
+			}
+			// make sure listener is ready
+			httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+
+			runLogTest(t, suite, gwAddr, expectedResponse, labels, "", 0)
+		})
+	},
+}
+
 var ALSTest = suite.ConformanceTest{
 	ShortName:   "ALS",
 	Description: "Make sure ALS access log is working",


Reviews:
user: login: zirain
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T10:28:04Z
user: login: kraashen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T18:43:07Z
user: login: zirain
body: LGTM, defer to @guydc 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-16T01:10:24Z
user: login: kraashen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T18:15:36Z
user: login: zirain
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T03:52:00Z
user: login: kraashen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T12:37:54Z
user: login: guydc
body: LGTM, thanks for fixing this and adding a test!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T21:53:39Z
user: login: arkodg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T22:06:20Z
user: login: arkodg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T22:07:33Z
user: login: kraashen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T07:28:51Z
user: login: arkodg
body: LGTM thanks !
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T18:51:29Z
user: login: arkodg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T18:54:35Z

Review comments:
diff_hunk: @@ -336,12 +336,10 @@ func buildXdsAccessLog(al *ir.AccessLog, accessLogType ir.ProxyAccessLogType) ([
 			ResourceAttributes: convertToKeyValueList(otel.Resources, false),
 		}
 
-		if otel.Text == nil {
-			return nil, errors.New("otel.Text is nil")
-		}
-		format := *otel.Text
+		var format string
path: internal/xds/translator/accesslog.go
user: login: zirain
body: In v1.3.0-, we use `EnvoyTextLogFormat` as default value when unset, cc @guydc 
created_at: 2025-03-15T10:28:04Z
author_association: MEMBER
diff_hunk: @@ -336,12 +336,10 @@ func buildXdsAccessLog(al *ir.AccessLog, accessLogType ir.ProxyAccessLogType) ([
 			ResourceAttributes: convertToKeyValueList(otel.Resources, false),
 		}
 
-		if otel.Text == nil {
-			return nil, errors.New("otel.Text is nil")
-		}
-		format := *otel.Text
+		var format string
path: internal/xds/translator/accesslog.go
user: login: kraashen
body: Yep. As the `Text` format as default logging format was superceded by JSON in v1.3.1, it's default value was removed. That's where I presumed that text logging (also in `opentelemetry` sink) would require defining the logging format, thus having no preset value as default. Would it be better if a default format would still be provided?
created_at: 2025-03-15T18:43:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -336,12 +336,10 @@ func buildXdsAccessLog(al *ir.AccessLog, accessLogType ir.ProxyAccessLogType) ([
 			ResourceAttributes: convertToKeyValueList(otel.Resources, false),
 		}
 
-		if otel.Text == nil {
-			return nil, errors.New("otel.Text is nil")
-		}
-		format := *otel.Text
+		var format string
+		if otel.Text != nil && *otel.Text != "" {
+			format = *otel.Text
path: internal/xds/translator/accesslog.go
user: login: kraashen
body: Should we also do a separate check for JSON logging? I.e. check if JSON attributes are defined or not, and in case not, default to `EnvoyJSONLogFields`?

Something along the lines

```go
if len(otel.Attributes) != 0 {
	al.Attributes = convertToKeyValueList(otel.Attributes, true)
	formatters = accessLogOpenTelemetryFormatters(format, otel.Attributes)
} else {
	// if there are no attributes, use the default EnvoyJSONLogFields
	al.Attributes = convertToKeyValueList(EnvoyJSONLogFields, true)
	formatters = accessLogOpenTelemetryFormatters(format, EnvoyJSONLogFields)
}
```

created_at: 2025-03-16T18:15:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -336,12 +336,10 @@ func buildXdsAccessLog(al *ir.AccessLog, accessLogType ir.ProxyAccessLogType) ([
 			ResourceAttributes: convertToKeyValueList(otel.Resources, false),
 		}
 
-		if otel.Text == nil {
-			return nil, errors.New("otel.Text is nil")
-		}
-		format := *otel.Text
+		var format string
+		if otel.Text != nil && *otel.Text != "" {
+			format = *otel.Text
path: internal/xds/translator/accesslog.go
user: login: zirain
body: sounds goo to me, cc @guydc 
created_at: 2025-03-18T03:51:59Z
author_association: MEMBER
diff_hunk: @@ -336,12 +336,10 @@ func buildXdsAccessLog(al *ir.AccessLog, accessLogType ir.ProxyAccessLogType) ([
 			ResourceAttributes: convertToKeyValueList(otel.Resources, false),
 		}
 
-		if otel.Text == nil {
-			return nil, errors.New("otel.Text is nil")
-		}
-		format := *otel.Text
+		var format string
+		if otel.Text != nil && *otel.Text != "" {
+			format = *otel.Text
path: internal/xds/translator/accesslog.go
user: login: kraashen
body: Sorry this took a bit of time. Went through the OpenTelemetry specs and documentation on the field. Implemented following:

- When no text or JSON type is set, default to `EnvoyJSONLogFields`, similar to previous implementation with the respective text format prior to v1.3.1/v1.2.7

The JSON fields are populated to the accessLog `Attributes` field as before with the JSON fields. Went through the [OpenTelemetry specification discussion](https://github.com/open-telemetry/opentelemetry-specification/issues/1613) on this and relevant [docs](https://github.com/dynatrace-oss-contrib/semantic-conventions/blob/f6af53c0c580f75ebcc7c373d51d92017d9ed2f1/specification/logs/data-model.md#field-attributes) and to me it seems that this should be an OK way to go.
created_at: 2025-03-18T12:37:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -349,9 +349,17 @@ func buildXdsAccessLog(al *ir.AccessLog, accessLogType ir.ProxyAccessLogType) ([
 			}
 		}
 
-		al.Attributes = convertToKeyValueList(otel.Attributes, true)
+		var attrs map[string]string
path: internal/xds/translator/accesslog.go
user: login: arkodg
body: doesnt this feel weird, if we have a format its part of `Body`, else `Attributes` 
created_at: 2025-03-20T22:06:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -349,9 +349,17 @@ func buildXdsAccessLog(al *ir.AccessLog, accessLogType ir.ProxyAccessLogType) ([
 			}
 		}
 
-		al.Attributes = convertToKeyValueList(otel.Attributes, true)
+		var attrs map[string]string
path: internal/xds/translator/accesslog.go
user: login: arkodg
body: can we move this to another GH issue and only have the fix around
```
		if otel.Text != nil && *otel.Text != "" {
			format = *otel.Text
```
in this PR so we can add it into v1.3.2 , releasing soon			
created_at: 2025-03-20T22:07:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -349,9 +349,17 @@ func buildXdsAccessLog(al *ir.AccessLog, accessLogType ir.ProxyAccessLogType) ([
 			}
 		}
 
-		al.Attributes = convertToKeyValueList(otel.Attributes, true)
+		var attrs map[string]string
path: internal/xds/translator/accesslog.go
user: login: kraashen
body: Can be done! Only concern is, how does this behave in case no output type or format/attributes are defined? As JSON logging is now default, does it render logs empty for users who have neither defined? Previously in v1.3.0, the `EnvoyTextLogFormat` was set as default and then it was checked if log format was defined. https://github.com/envoyproxy/gateway/blob/v1.3.0/internal/xds/translator/accesslog.go#L325-L336
created_at: 2025-03-21T07:28:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -349,9 +349,17 @@ func buildXdsAccessLog(al *ir.AccessLog, accessLogType ir.ProxyAccessLogType) ([
 			}
 		}
 
-		al.Attributes = convertToKeyValueList(otel.Attributes, true)
+		var attrs map[string]string
path: internal/xds/translator/accesslog.go
user: login: arkodg
body: this looks good, sorry for the confusion
created_at: 2025-03-22T18:54:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-14T09:47:29Z
body: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/5498?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) Report
Attention: Patch coverage is `66.66667%` with `4 lines` in your changes missing coverage. Please review.
> Project coverage is 65.26%. Comparing base [(`580686a`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/580686ad3d3c2e7d84748ad1d80bbf1a311e4da3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`2153e2d`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/2153e2d39e7e0f99772a2a01ecfb4a8aba5d17a0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/envoyproxy/gateway/pull/5498?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) | Patch % | Lines |
|---|---|---|
| [internal/xds/translator/accesslog.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5498?src=pr&el=tree&filepath=internal%2Fxds%2Ftranslator%2Faccesslog.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwveGRzL3RyYW5zbGF0b3IvYWNjZXNzbG9nLmdv) | 66.66% | [3 Missing and 1 partial :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5498?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #5498      +/-   ##
==========================================
+ Coverage   65.25%   65.26%   +0.01%     
==========================================
  Files         213      213              
  Lines       34063    34069       +6     
==========================================
+ Hits        22228    22236       +8     
+ Misses      10500    10499       -1     
+ Partials     1335     1334       -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/5498?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: arkodg
created_at: 2025-03-22T18:54:57Z
body: @kraashen can you fix DCO and force push 
author_association: CONTRIBUTOR