From c8b25d9b2b26d5048f199fdb1c2ac81652358f95 Mon Sep 17 00:00:00 2001 From: Aniket Kumar Date: Fri, 30 Oct 2026 22:43:45 +0530 Subject: [PATCH] fix(telemetry): export numeric attributes as long values Attributes.prepare() exported every attribute with AttributeKey.stringKey(), so http.response.status_code, http.request.resend_count and fga-client.request.batch_check_size reached OpenTelemetry as strings, which breaks numeric queries such as status_code >= 400. Export these three attributes (documented as int in docs/OpenTelemetry.md) with AttributeKey.longKey() instead, and omit values that do not parse as integers. All other attributes are still exported as strings. Note: queries that match these attributes as strings need updating. Fixes #291 --- .../dev/openfga/sdk/telemetry/Attributes.java | 19 ++++- .../openfga/sdk/telemetry/AttributesTest.java | 71 ++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/main/java/dev/openfga/sdk/telemetry/Attributes.java b/src/main/java/dev/openfga/sdk/telemetry/Attributes.java index 71289cda..084613d3 100644 --- a/src/main/java/dev/openfga/sdk/telemetry/Attributes.java +++ b/src/main/java/dev/openfga/sdk/telemetry/Attributes.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; /** * This class represents a collection of attributes used for telemetry purposes. @@ -88,6 +89,12 @@ public class Attributes { */ public static final Attribute USER_AGENT = new Attribute("user_agent.original"); + /** + * The attributes that are exported to OpenTelemetry as integers rather than strings. + */ + private static final Set LONG_ATTRIBUTES = + Set.of(FGA_CLIENT_REQUEST_BATCH_CHECK_SIZE, HTTP_REQUEST_RESEND_COUNT, HTTP_RESPONSE_STATUS_CODE); + /** * Prepares the attributes for OpenTelemetry publishing by converting them into the expected format. * @@ -126,7 +133,17 @@ public static io.opentelemetry.api.common.Attributes prepare( String attrVal = attributes.getOrDefault(attr, ""); - if (!isNullOrWhitespace(attrVal)) { + if (isNullOrWhitespace(attrVal)) { + continue; + } + + if (LONG_ATTRIBUTES.contains(attr)) { + try { + builder.put(AttributeKey.longKey(attr.getName()), Long.parseLong(attrVal.trim())); + } catch (NumberFormatException e) { + // Skip values that cannot be represented as an integer + } + } else { builder.put(AttributeKey.stringKey(attr.getName()), attrVal); } } diff --git a/src/test/java/dev/openfga/sdk/telemetry/AttributesTest.java b/src/test/java/dev/openfga/sdk/telemetry/AttributesTest.java index 12c392ff..3670f43e 100644 --- a/src/test/java/dev/openfga/sdk/telemetry/AttributesTest.java +++ b/src/test/java/dev/openfga/sdk/telemetry/AttributesTest.java @@ -1,5 +1,6 @@ package dev.openfga.sdk.telemetry; +import static io.opentelemetry.api.common.AttributeKey.longKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -51,6 +52,8 @@ void shouldPrepareFiltersAttributesFromDefaults() { TelemetryConfiguration.defaultAttributes().entrySet()) { defaultAttributes.put(entry.getKey(), entry.getKey().toString() + "-value"); } + defaultAttributes.put(Attributes.HTTP_REQUEST_RESEND_COUNT, "2"); + defaultAttributes.put(Attributes.HTTP_RESPONSE_STATUS_CODE, "200"); // not sent by default Map nonDefaultAttributes = Map.of(Attributes.FGA_CLIENT_USER, "user-value"); @@ -70,13 +73,79 @@ void shouldPrepareFiltersAttributesFromDefaults() { // then AttributesBuilder builder = io.opentelemetry.api.common.Attributes.builder(); for (Map.Entry entry : defaultAttributes.entrySet()) { - builder.put(stringKey(entry.getKey().getName()), entry.getValue()); + if (entry.getKey() == Attributes.HTTP_REQUEST_RESEND_COUNT + || entry.getKey() == Attributes.HTTP_RESPONSE_STATUS_CODE) { + builder.put(longKey(entry.getKey().getName()), Long.parseLong(entry.getValue())); + } else { + builder.put(stringKey(entry.getKey().getName()), entry.getValue()); + } } io.opentelemetry.api.common.Attributes expected = builder.build(); assertThat(result).isEqualTo(expected); } + @Test + void shouldPrepareNumericAttributesAsLongs() { + // given + Map attributes = Map.of( + Attributes.FGA_CLIENT_REQUEST_BATCH_CHECK_SIZE, "10", + Attributes.HTTP_REQUEST_RESEND_COUNT, "2", + Attributes.HTTP_RESPONSE_STATUS_CODE, "404", + Attributes.HTTP_REQUEST_METHOD, "POST"); + Metric metric = Histograms.REQUEST_DURATION; + + Map> attributeMap = Map.of( + Attributes.FGA_CLIENT_REQUEST_BATCH_CHECK_SIZE, Optional.empty(), + Attributes.HTTP_REQUEST_RESEND_COUNT, Optional.empty(), + Attributes.HTTP_RESPONSE_STATUS_CODE, Optional.empty(), + Attributes.HTTP_REQUEST_METHOD, Optional.empty()); + Configuration configuration = + new Configuration().telemetryConfiguration(new TelemetryConfiguration(Map.of(metric, attributeMap))); + + // when + io.opentelemetry.api.common.Attributes result = Attributes.prepare(attributes, metric, configuration); + + // then + io.opentelemetry.api.common.Attributes expected = io.opentelemetry.api.common.Attributes.builder() + .put(longKey(Attributes.FGA_CLIENT_REQUEST_BATCH_CHECK_SIZE.getName()), 10L) + .put(longKey(Attributes.HTTP_REQUEST_RESEND_COUNT.getName()), 2L) + .put(longKey(Attributes.HTTP_RESPONSE_STATUS_CODE.getName()), 404L) + .put(stringKey(Attributes.HTTP_REQUEST_METHOD.getName()), "POST") + .build(); + assertThat(result).isEqualTo(expected); + assertThat(result.get(stringKey(Attributes.HTTP_RESPONSE_STATUS_CODE.getName()))) + .isNull(); + } + + @Test + void shouldPrepareSkipsNumericAttributesThatAreNotNumbers() { + // given + Map attributes = new HashMap<>(); + attributes.put(Attributes.HTTP_RESPONSE_STATUS_CODE, "not-a-number"); + attributes.put(Attributes.HTTP_REQUEST_RESEND_COUNT, ""); + attributes.put(Attributes.FGA_CLIENT_REQUEST_BATCH_CHECK_SIZE, null); + attributes.put(Attributes.HTTP_REQUEST_METHOD, "POST"); + Metric metric = Histograms.REQUEST_DURATION; + + Map> attributeMap = Map.of( + Attributes.FGA_CLIENT_REQUEST_BATCH_CHECK_SIZE, Optional.empty(), + Attributes.HTTP_REQUEST_RESEND_COUNT, Optional.empty(), + Attributes.HTTP_RESPONSE_STATUS_CODE, Optional.empty(), + Attributes.HTTP_REQUEST_METHOD, Optional.empty()); + Configuration configuration = + new Configuration().telemetryConfiguration(new TelemetryConfiguration(Map.of(metric, attributeMap))); + + // when + io.opentelemetry.api.common.Attributes result = Attributes.prepare(attributes, metric, configuration); + + // then + io.opentelemetry.api.common.Attributes expected = io.opentelemetry.api.common.Attributes.builder() + .put(stringKey(Attributes.HTTP_REQUEST_METHOD.getName()), "POST") + .build(); + assertThat(result).isEqualTo(expected); + } + @Test void shouldConvertAttributesFromHttpResponse() { // given