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..871b108264 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,16 @@ public Tags contribute(Context context) { return tags; } + /** The {@link UserAgent#product()} from the request context will be used if possible */ 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, so use the full USER_AGENT header + 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))); + } +}