Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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)));
}
}