From c54f3450111b6386eb6af3de73267beb48f6a57f Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Wed, 12 Aug 2026 14:25:51 -0700 Subject: [PATCH 1/4] fix: #2541 use UserAgent from RequestContext for the metrics tag Falls back to the full header when there is no RequestContext. --- .../TenantRequestMetricsTagProvider.java | 29 ++++--- .../TenantRequestMetricsTagProviderTest.java | 87 +++++++++++++++++++ 2 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 src/test/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProviderTest.java diff --git a/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java b/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java index 87176dfe91..c1425a46a1 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java @@ -1,26 +1,24 @@ package io.stargate.sgv2.jsonapi.metrics; import static io.stargate.sgv2.jsonapi.metrics.MetricsConstants.UNKNOWN_VALUE; +import static io.stargate.sgv2.jsonapi.util.StringUtil.isNullOrBlank; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; import io.quarkus.micrometer.runtime.HttpServerMetricsTagsContributor; import io.stargate.sgv2.jsonapi.api.request.RequestContext; +import io.stargate.sgv2.jsonapi.api.request.UserAgent; import io.stargate.sgv2.jsonapi.api.v1.metrics.MetricsConfig; import io.vertx.core.http.HttpServerRequest; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ContextNotActiveException; import jakarta.inject.Inject; import jakarta.ws.rs.core.HttpHeaders; -import java.util.regex.Pattern; /** Tags provider for http request metrics. It provides tenant id and user agent as tags. */ @ApplicationScoped public class TenantRequestMetricsTagProvider implements HttpServerMetricsTagsContributor { - // split pattern for the user agent, extract only first part of the agent - private static final Pattern USER_AGENT_SPLIT = Pattern.compile("[\\s/]"); - /** The configuration for metrics. */ private final MetricsConfig.TenantRequestCounterConfig config; @@ -70,17 +68,20 @@ public Tags contribute(Context context) { return tags; } + /** + * The {@link UserAgent#product()} from the request context, so the processing of the header is + * done in the one place. + */ private String getUserAgentValue(HttpServerRequest request) { - String headerString = request.getHeader(HttpHeaders.USER_AGENT); - if (null != headerString && !headerString.isBlank()) { - String[] split = USER_AGENT_SPLIT.split(headerString); - if (split.length > 0) { - return split[0]; - } else { - return headerString; - } - } else { - return UNKNOWN_VALUE; + + String userAgent; + try { + userAgent = requestContext.userAgent().product(); + } catch (ContextNotActiveException | IllegalStateException e) { + // no request context, see contribute() above. The full header is all we have, better than + // reporting the agent as unknown. + userAgent = request.getHeader(HttpHeaders.USER_AGENT); } + return isNullOrBlank(userAgent) ? UNKNOWN_VALUE : userAgent; } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProviderTest.java b/src/test/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProviderTest.java new file mode 100644 index 0000000000..37b47f2bad --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProviderTest.java @@ -0,0 +1,87 @@ +package io.stargate.sgv2.jsonapi.metrics; + +import static io.restassured.RestAssured.given; +import static io.stargate.sgv2.jsonapi.metrics.MetricsConstants.UNKNOWN_VALUE; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import io.restassured.http.ContentType; +import io.restassured.response.ValidatableResponse; +import io.stargate.sgv2.jsonapi.api.v1.GeneralResource; +import io.stargate.sgv2.jsonapi.config.constants.HttpConstants; +import jakarta.ws.rs.core.HttpHeaders; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** Tests for the user agent tag {@link TenantRequestMetricsTagProvider} adds to request metrics. */ +@QuarkusTest +@TestProfile(TenantRequestMetricsTagProviderTest.UserAgentTagProfile.class) +public class TenantRequestMetricsTagProviderTest { + + private static final String FULL_AGENT = "langflow/1.4.2 langchain/0.3.59 astrapy/2.0.1"; + private static final String COMMAND = "{\"noSuchCommand\": {}}"; + + /** The user agent tag is off by default. */ + public static class UserAgentTagProfile implements QuarkusTestProfile { + + @Override + public boolean disableGlobalTestResources() { + return true; + } + + @Override + public Map getConfigOverrides() { + return ImmutableMap.of( + "stargate.metrics.tenant-request-counter.user-agent-tag-enabled", "true"); + } + } + + @Test + public void productWhenRequestContext() { + postCommand(FULL_AGENT).statusCode(200); + assertUserAgentTag("langflow"); + } + + @Test + public void fullAgentWhenNoRequestContext() { + // no token, the request is rejected before the RequestContext is created + given() + .contentType(ContentType.JSON) + .header(HttpHeaders.USER_AGENT, FULL_AGENT) + .body(COMMAND) + .when() + .post(GeneralResource.BASE_PATH) + .then() + .statusCode(401); + + assertUserAgentTag(FULL_AGENT); + } + + @Test + public void unknownWhenNoAgent() { + postCommand("").statusCode(200); + assertUserAgentTag(UNKNOWN_VALUE); + } + + private ValidatableResponse postCommand(String userAgent) { + return given() + .contentType(ContentType.JSON) + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, "token") + .header(HttpHeaders.USER_AGENT, userAgent) + .body(COMMAND) + .when() + .post(GeneralResource.BASE_PATH) + .then(); + } + + private void assertUserAgentTag(String expected) { + String metrics = given().when().get("/metrics").then().statusCode(200).extract().asString(); + + assertThat(metrics.lines().filter(line -> line.startsWith("http_server_requests_")).toList()) + .describedAs("user agent tag for %s", expected) + .anyMatch(line -> line.contains("user_agent=\"%s\"".formatted(expected))); + } +} From 012bc4458e73d819b38b31ef0fb71bf8692ea004 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Wed, 12 Aug 2026 14:49:29 -0700 Subject: [PATCH 2/4] Update TenantRequestMetricsTagProvider.java --- .../sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java b/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java index c1425a46a1..c64b3d025d 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java @@ -69,8 +69,7 @@ public Tags contribute(Context context) { } /** - * The {@link UserAgent#product()} from the request context, so the processing of the header is - * done in the one place. + * The {@link UserAgent#product()} from the request context */ private String getUserAgentValue(HttpServerRequest request) { From 87dc44d3f8fdd913aa5049fe14a4333b60ea39bb Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Wed, 12 Aug 2026 15:09:21 -0700 Subject: [PATCH 3/4] style: format metrics tag provider --- .../sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java b/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java index c64b3d025d..26e91c047f 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java @@ -68,9 +68,7 @@ public Tags contribute(Context context) { return tags; } - /** - * The {@link UserAgent#product()} from the request context - */ + /** The {@link UserAgent#product()} from the request context */ private String getUserAgentValue(HttpServerRequest request) { String userAgent; From 1174abaebab9b3b457ac119bbd81ff9f1ce3c5e4 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Wed, 12 Aug 2026 15:11:42 -0700 Subject: [PATCH 4/4] Update TenantRequestMetricsTagProvider.java --- .../jsonapi/metrics/TenantRequestMetricsTagProvider.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java b/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java index 26e91c047f..871b108264 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/metrics/TenantRequestMetricsTagProvider.java @@ -68,15 +68,14 @@ public Tags contribute(Context context) { return tags; } - /** The {@link UserAgent#product()} from the request context */ + /** The {@link UserAgent#product()} from the request context will be used if possible */ private String getUserAgentValue(HttpServerRequest request) { String userAgent; try { userAgent = requestContext.userAgent().product(); } catch (ContextNotActiveException | IllegalStateException e) { - // no request context, see contribute() above. The full header is all we have, better than - // reporting the agent as unknown. + // no request context, so use the full USER_AGENT header userAgent = request.getHeader(HttpHeaders.USER_AGENT); } return isNullOrBlank(userAgent) ? UNKNOWN_VALUE : userAgent;