From 09cf9f7e3fc9fb3fc2256f18ea10c479404bc06e Mon Sep 17 00:00:00 2001 From: Brahim Raddahi Date: Thu, 7 Mar 2024 17:20:25 +0100 Subject: [PATCH] consolidate rerouted requests in access log --- docs/src/main/asciidoc/http-reference.adoc | 15 + .../vertx/http/runtime/AccessLogConfig.java | 6 + .../vertx/http/runtime/VertxHttpRecorder.java | 3 +- .../attribute/ExchangeAttributeParser.java | 15 +- .../attribute/QueryParameterAttribute.java | 14 +- .../attribute/QueryStringAttribute.java | 20 +- .../attribute/RequestLineAttribute.java | 29 +- .../attribute/RequestMethodAttribute.java | 14 +- .../attribute/RequestPathAttribute.java | 15 +- .../attribute/RequestURLAttribute.java | 14 +- .../filters/OriginalRequestContext.java | 67 ++++ .../filters/accesslog/AccessLogHandler.java | 17 +- .../ConsolidateReroutedRequestsTest.java | 317 ++++++++++++++++++ 13 files changed, 520 insertions(+), 26 deletions(-) create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/OriginalRequestContext.java create mode 100644 extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/attribute/ConsolidateReroutedRequestsTest.java diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index ddcb5da59e615..6f90ed502831c 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -442,6 +442,21 @@ include::{generated-dir}/config/quarkus-vertx-http-config-group-access-log-confi |Vert.x MDC data (e.g. 'traceId' for OpenTelemetry) | | `%{X,mdc-key}` |=== +Set `quarkus.http.access-log.consolidate-rerouted-requests=true` to enable support for the modifier `<`. This modifier can be used for requests that have been internally redirected to consult the original request. +The following attributes support this modifier: + + +[frame="topbot",options="header"] +|=== +|Attribute |Short Form|Long Form +|First line of the request | `% * Tokens are created according to the following rules: *

- * %a - % followed by single character. %% is an escape for a literal % + * % attributes = new ArrayList<>(); int pos = 0; - int state = 0; //0 = literal, 1 = %, 2 = %{, 3 = $, 4 = ${ + int state = 0; //0 = literal, 1 = %, 2 = %{, 3 = $, 4 = ${, 5 = %< for (int i = 0; i < valueString.length(); ++i) { char c = valueString.charAt(i); switch (state) { @@ -80,6 +80,8 @@ public ExchangeAttribute parse(final String valueString) { case 1: { if (c == '{') { state = 2; + } else if (c == '<') { + state = 5; } else if (c == '%') { //literal percent attributes.add(wrap(new ConstantExchangeAttribute("%"))); @@ -123,13 +125,20 @@ public ExchangeAttribute parse(final String valueString) { } break; } + case 5: { + attributes.add(wrap(parseSingleToken(valueString.substring(pos, i + 1)))); + pos = i + 1; + state = 0; + break; + } } } switch (state) { case 0: case 1: - case 3: { + case 3: + case 5: { if (pos != valueString.length()) { attributes.add(wrap(parseSingleToken(valueString.substring(pos)))); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/QueryParameterAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/QueryParameterAttribute.java index 9bf0a2dd6c033..6c2093bbafbfe 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/QueryParameterAttribute.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/QueryParameterAttribute.java @@ -3,6 +3,7 @@ import java.util.ArrayDeque; import java.util.List; +import io.quarkus.vertx.http.runtime.filters.OriginalRequestContext; import io.vertx.ext.web.RoutingContext; /** @@ -11,14 +12,17 @@ public class QueryParameterAttribute implements ExchangeAttribute { private final String parameter; + private final boolean useOriginalRequest; - public QueryParameterAttribute(String parameter) { + public QueryParameterAttribute(String parameter, boolean useOriginalRequest) { this.parameter = parameter; + this.useOriginalRequest = useOriginalRequest; } @Override public String readAttribute(final RoutingContext exchange) { - List res = exchange.queryParams().getAll(parameter); + List res = useOriginalRequest ? OriginalRequestContext.getAllQueryParams(exchange, parameter) + : exchange.queryParams().getAll(parameter); if (res == null) { return null; } else if (res.isEmpty()) { @@ -57,7 +61,11 @@ public String name() { public ExchangeAttribute build(final String token) { if (token.startsWith("%{q,") && token.endsWith("}")) { final String qp = token.substring(4, token.length() - 1); - return new QueryParameterAttribute(qp); + return new QueryParameterAttribute(qp, false); + } + if (token.startsWith("%{ getAllQueryParams(RoutingContext rc, String name) { + OriginalRequestContext originalRequestContext = rc.get(RC_DATA_KEY); + if (originalRequestContext == null) + return null; + return originalRequestContext.queryParams.getAll(name); + } + + private final HttpMethod method; + private final String uri; + private final String path; + private final String query; + private final MultiMap queryParams; + + public OriginalRequestContext(RoutingContext rc) { + this.method = rc.request().method(); + this.uri = rc.request().uri(); + this.path = rc.request().path(); + this.query = rc.request().query(); + this.queryParams = rc.queryParams(); + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/AccessLogHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/AccessLogHandler.java index 41c2f87ec1c62..9d8f401b94b2a 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/AccessLogHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/AccessLogHandler.java @@ -27,6 +27,7 @@ import io.quarkus.vertx.http.runtime.attribute.ExchangeAttribute; import io.quarkus.vertx.http.runtime.attribute.ExchangeAttributeParser; import io.quarkus.vertx.http.runtime.attribute.SubstituteEmptyWrapper; +import io.quarkus.vertx.http.runtime.filters.OriginalRequestContext; import io.quarkus.vertx.http.runtime.filters.QuarkusRequestWrapper; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; @@ -94,13 +95,16 @@ public class AccessLogHandler implements Handler { private final AccessLogReceiver accessLogReceiver; private final String formatString; + private final boolean consolidateReroutedRequests; private final ExchangeAttribute tokens; private final Pattern excludePattern; - public AccessLogHandler(final AccessLogReceiver accessLogReceiver, final String formatString, ClassLoader classLoader, + public AccessLogHandler(final AccessLogReceiver accessLogReceiver, final String formatString, + boolean consolidateReroutedRequests, ClassLoader classLoader, Optional excludePattern) { this.accessLogReceiver = accessLogReceiver; this.formatString = handleCommonNames(formatString); + this.consolidateReroutedRequests = consolidateReroutedRequests; this.tokens = new ExchangeAttributeParser(classLoader, Collections.singletonList(new SubstituteEmptyWrapper("-"))) .parse(this.formatString); if (excludePattern.isPresent()) { @@ -110,9 +114,11 @@ public AccessLogHandler(final AccessLogReceiver accessLogReceiver, final String } } - public AccessLogHandler(final AccessLogReceiver accessLogReceiver, String formatString, final ExchangeAttribute attribute) { + public AccessLogHandler(final AccessLogReceiver accessLogReceiver, String formatString, boolean consolidateReroutedRequests, + final ExchangeAttribute attribute) { this.accessLogReceiver = accessLogReceiver; this.formatString = handleCommonNames(formatString); + this.consolidateReroutedRequests = consolidateReroutedRequests; this.tokens = attribute; this.excludePattern = null; } @@ -142,12 +148,19 @@ public void handle(RoutingContext rc) { return; } } + if (consolidateReroutedRequests && rc.get(OriginalRequestContext.RC_DATA_KEY, null) != null) { + rc.next(); + return; + } QuarkusRequestWrapper.get(rc.request()).addRequestDoneHandler(new Handler() { @Override public void handle(Void event) { accessLogReceiver.logMessage(tokens.readAttribute(rc)); } }); + if (consolidateReroutedRequests) { + rc.put(OriginalRequestContext.RC_DATA_KEY, new OriginalRequestContext(rc)); + } rc.next(); } diff --git a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/attribute/ConsolidateReroutedRequestsTest.java b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/attribute/ConsolidateReroutedRequestsTest.java new file mode 100644 index 0000000000000..a2395b18c1e15 --- /dev/null +++ b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/attribute/ConsolidateReroutedRequestsTest.java @@ -0,0 +1,317 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.netty.handler.codec.http.QueryStringDecoder; +import io.quarkus.vertx.http.runtime.filters.QuarkusRequestWrapper; +import io.quarkus.vertx.http.runtime.filters.accesslog.AccessLogHandler; +import io.quarkus.vertx.http.runtime.filters.accesslog.AccessLogReceiver; +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.http.HttpVersion; +import io.vertx.core.net.HostAndPort; +import io.vertx.core.net.impl.HostAndPortImpl; +import io.vertx.ext.web.handler.HttpException; +import io.vertx.ext.web.impl.RoutingContextImpl; + +public class ConsolidateReroutedRequestsTest { + + @Test + public void testDisabledNoReroutesRequestLineOnly() { + test( + false, + Arrays.asList("%r", "%{REQUEST_LINE}"), + Optional.empty(), + HttpMethod.GET, + HttpVersion.HTTP_1_1, + "https", + new HostAndPortImpl("example.org", 443), + "/path1", + "q1=v1", + Collections.emptyList(), + Arrays.asList( + "GET /path1?q1=v1 HTTP/1.1")); + } + + @Test + public void testDisabled2ReroutesRequestLineOnly() { + test( + false, + Arrays.asList("%r", "%{REQUEST_LINE}"), + Optional.empty(), + HttpMethod.GET, + HttpVersion.HTTP_1_1, + "https", + new HostAndPortImpl("example.org", 443), + "/path1", + "q1=v1", + Arrays.asList( + new Reroute(null, "/path2", "q2=v2"), + new Reroute(null, "/path3", "q3=v3")), + Arrays.asList( + "GET /path3?q3=v3 HTTP/1.1", + "GET /path3?q3=v3 HTTP/1.1", + "GET /path3?q3=v3 HTTP/1.1")); + } + + @Test + public void testDisabled1RerouteRequestPathOnly() { + test( + false, + Arrays.asList("%R", "%{REQUEST_PATH}"), + Optional.empty(), + HttpMethod.GET, + HttpVersion.HTTP_1_1, + "https", + new HostAndPortImpl("example.org", 443), + "/path1", + "q1=v1", + Arrays.asList( + new Reroute(null, "/path2", "q2=v2")), + Arrays.asList( + "/path2", + "/path2")); + } + + @Test + public void testDisabled2ReroutesOriginalRequestLineOnly() { + test( + false, + Arrays.asList("% equivalentPatterns, + Optional excludePattern, + HttpMethod httpMethod, + HttpVersion httpVersion, + String scheme, + HostAndPort authority, + String path, + String query, + List reroutes, + List expectedAccessLogEntries) { + for (String pattern : equivalentPatterns) { + test(consolidateReroutedRequests, pattern, excludePattern, httpMethod, httpVersion, scheme, authority, path, query, + reroutes, expectedAccessLogEntries); + } + } + + private void test( + boolean consolidateReroutedRequests, + String pattern, + Optional excludePattern, + HttpMethod httpMethod, + HttpVersion httpVersion, + String scheme, + HostAndPort authority, + String path, + String query, + List reroutes, + List expectedAccessLogEntries) { + List accessLogEntries = new ArrayList<>(); + AccessLogReceiver receiver = new AccessLogReceiver() { + @Override + public void logMessage(String message) { + accessLogEntries.add(message); + } + }; + + AccessLogHandler accessLogHandler = new AccessLogHandler(receiver, pattern, consolidateReroutedRequests, + getClass().getClassLoader(), excludePattern); + QuarkusRequestWrapper request = Mockito.mock(QuarkusRequestWrapper.class); + Mockito.when(request.getCookie(QuarkusRequestWrapper.FAKE_COOKIE_NAME)).thenReturn(request.new QuarkusCookie()); + Mockito.when(request.version()).thenReturn(httpVersion); + Mockito.when(request.scheme()).thenReturn(scheme); + Mockito.when(request.authority()).thenReturn(authority); + Mockito.when(request.method()).thenReturn(httpMethod); + Mockito.when(request.path()).thenReturn(path); + Mockito.when(request.query()).thenReturn(query); + String uri = uriFromPathAndQuery(path, query); + Mockito.when(request.uri()).thenReturn(uri); + + HttpServerResponse response = Mockito.mock(HttpServerResponse.class); + Mockito.when(request.response()).thenReturn(response); + + List> requestDoneHandlers = new ArrayList<>(); + Mockito.doAnswer(i -> { + requestDoneHandlers.add(i.getArgument(0)); + return null; + }).when(request).addRequestDoneHandler(any()); + + RoutingContextImpl rc = Mockito.mock(RoutingContextImpl.class); + Mockito.when(rc.queryParams()).thenReturn(decodeQueryParams(uri)); + Mockito.when(rc.get(anyString())).thenCallRealMethod(); + Mockito.when(rc.get(anyString(), any())).thenCallRealMethod(); + Mockito.when(rc.put(anyString(), any())).thenCallRealMethod(); + Mockito.when(rc.request()).thenReturn(request); + + accessLogHandler.handle(rc); + for (Reroute reroute : reroutes) { + reroute.apply(rc, request); + accessLogHandler.handle(rc); + } + for (Handler requestDoneHandler : requestDoneHandlers) { + requestDoneHandler.handle(null); + } + Assertions.assertEquals(expectedAccessLogEntries, accessLogEntries); + } + + private static MultiMap decodeQueryParams(String uri) { + try { + MultiMap queryParams = MultiMap.caseInsensitiveMultiMap(); + Map> decodedParams = new QueryStringDecoder(uri).parameters(); + for (Map.Entry> entry : decodedParams.entrySet()) { + queryParams.add(entry.getKey(), entry.getValue()); + } + return queryParams; + } catch (IllegalArgumentException e) { + throw new HttpException(400, "Error while decoding query params", e); + } + } + + private static String uriFromPathAndQuery(String path, String query) { + String uri = null; + if (path != null) + uri = path; + if (query != null) + uri += "?" + query; + return uri; + } + + private static class Reroute { + private HttpMethod httpMethod; + private String path; + private String query; + + public Reroute(HttpMethod httpMethod, String path, String query) { + this.httpMethod = httpMethod; + this.path = path; + this.query = query; + } + + public void apply(RoutingContextImpl rc, QuarkusRequestWrapper request) { + if (httpMethod != null) + Mockito.when(request.method()).thenReturn(httpMethod); + if (path != null) + Mockito.when(request.path()).thenReturn(path); + if (query != null) + Mockito.when(request.query()).thenReturn(query); + String uri = uriFromPathAndQuery(path, query); + if (uri != null) { + Mockito.when(request.uri()).thenReturn(uri); + Mockito.when(rc.queryParams()).thenReturn(decodeQueryParams(uri)); + } + } + } + +}