diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java index 38f61386706a..f8720bd8961b 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java @@ -31,11 +31,13 @@ import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.QuotedCSV; import org.eclipse.jetty.http.pathmap.PathMappings; import org.eclipse.jetty.util.DateCache; import org.eclipse.jetty.util.NanoTime; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.component.ContainerLifeCycle; @@ -201,7 +203,6 @@ *

The query string, prepended with a ? if a query string exists, otherwise an empty string.

* * - * * * %r * @@ -214,13 +215,13 @@ *

The name of the Handler or Servlet generating the response (if any).

* * - * + * * %s * *

The HTTP response status code.

* * - * + * * %{format|timeZone|locale}t * *

The time at which the request was received.

@@ -262,7 +263,7 @@ *

The URL path requested, not including any query string.

* * - * + * * %X * *

The connection status when response is completed:

@@ -288,6 +289,39 @@ *

The value of the VARNAME response trailer.

* * + * + * %{OPTION}uri + * + *

The request URI.

+ *

The parameter is optional and may have the be one of the following options:

+ *
+ *
%uri
+ *
The entire request URI.
+ *
%{-query}uri
+ *
The entire request URI without the query.
+ *
%{-path,-query}uri
+ *
The request URI without path or query (so just `scheme://authority`).
+ *
%{scheme}uri
+ *
The scheme of the request URI.
+ *
%{authority}uri
+ *
The authority of the request URI.
+ *
%{path}uri
+ *
The path of the request URI.
+ *
%{query}uri
+ *
The query of the request URI.
+ *
%{host}uri
+ *
The host of the request URI.
+ *
%{port}uri
+ *
The port of the request URI.
+ *
+ * + * + * + * %{attributeName}attr + * + *

The value of the request attribute with the given name.

+ * + * * * */ @@ -309,6 +343,7 @@ public record LogDetail(String handlerName, String realPath) public static final String LOG_DETAIL = CustomRequestLog.class.getName() + ".logDetail"; private static final Logger LOG = LoggerFactory.getLogger(CustomRequestLog.class); private static final ThreadLocal _buffers = ThreadLocal.withInitial(() -> new StringBuilder(256)); + private static final Pattern PATTERN = Pattern.compile("^(?:%(?!?[0-9,]+)?(?:\\{(?[^}]+)})?(?(?:(?:ti)|(?:to)|(?:uri)|(?:attr)|[a-zA-Z%]))|(?[^%]+))(?.*)", Pattern.DOTALL | Pattern.MULTILINE); private final RequestLog.Writer _requestLogWriter; private final MethodHandle _logHandle; @@ -444,7 +479,7 @@ protected void doStart() throws Exception private static void append(StringBuilder buf, String s) { - if (s == null || s.length() == 0) + if (s == null || s.isEmpty()) buf.append('-'); else buf.append(s); @@ -489,8 +524,6 @@ private static List getTokens(String formatString) {PARAM} is an optional string parameter to the percent code. CODE is a 1 to 2 character string corresponding to a format code. */ - final Pattern PATTERN = Pattern.compile("^(?:%(?!?[0-9,]+)?(?:\\{(?[^}]+)})?(?(?:(?:ti)|(?:to)|[a-zA-Z%]))|(?[^%]+))(?.*)", Pattern.DOTALL | Pattern.MULTILINE); - List tokens = new ArrayList<>(); String remaining = formatString; while (remaining.length() > 0) @@ -788,6 +821,32 @@ else if ("d".equals(arg)) yield lookup.findStatic(CustomRequestLog.class, "logResponseTrailer", logTypeArg).bindTo(arg); } + case "uri" -> + { + if (arg == null) + arg = ""; + String method = switch (arg) + { + case "" -> "logRequestHttpUri"; + case "-query" -> "logRequestHttpUriWithoutQuery"; + case "-path,-query" -> "logRequestHttpUriWithoutPathQuery"; + case "scheme" -> "logRequestScheme"; + case "authority" -> "logRequestAuthority"; + case "path" -> "logUrlRequestPath"; + case "query" -> "logQueryString"; + case "host" -> "logRequestHttpUriHost"; + case "port" -> "logRequestHttpUriPort"; + default -> throw new IllegalArgumentException("Invalid arg for %uri"); + }; + + yield lookup.findStatic(CustomRequestLog.class, method, logType); + } + case "attr" -> + { + MethodType logRequestAttribute = methodType(void.class, String.class, StringBuilder.class, Request.class, Response.class); + yield lookup.findStatic(CustomRequestLog.class, "logRequestAttribute", logRequestAttribute).bindTo(arg); + } + default -> throw new IllegalArgumentException("Unsupported code %" + code); }; @@ -1139,4 +1198,74 @@ private static void logResponseTrailer(String arg, StringBuilder b, Request requ else b.append('-'); } + + @SuppressWarnings("unused") + private static void logRequestAuthority(StringBuilder b, Request request, Response response) + { + HttpURI httpURI = request.getHttpURI(); + if (httpURI.hasAuthority()) + append(b, httpURI.getAuthority()); + else + b.append('-'); + } + + @SuppressWarnings("unused") + private static void logRequestScheme(StringBuilder b, Request request, Response response) + { + append(b, request.getHttpURI().getScheme()); + } + + @SuppressWarnings("unused") + private static void logRequestHttpUri(StringBuilder b, Request request, Response response) + { + append(b, request.getHttpURI().toString()); + } + + @SuppressWarnings("unused") + private static void logRequestHttpUriWithoutQuery(StringBuilder b, Request request, Response response) + { + HttpURI.Mutable uri = HttpURI.build(request.getHttpURI()).query(null); + append(b, uri.toString()); + } + + @SuppressWarnings("unused") + private static void logRequestHttpUriWithoutPathQuery(StringBuilder b, Request request, Response response) + { + // HttpURI doesn't support null path so we do this manually. + HttpURI httpURI = request.getHttpURI(); + if (httpURI.getScheme() != null) + b.append(httpURI.getScheme()).append(':'); + if (httpURI.getHost() != null) + { + b.append("//"); + if (httpURI.getUser() != null) + b.append(httpURI.getUser()).append('@'); + b.append(httpURI.getHost()); + } + int normalizedPort = URIUtil.normalizePortForScheme(httpURI.getScheme(), httpURI.getPort()); + if (normalizedPort > 0) + b.append(':').append(normalizedPort); + } + + @SuppressWarnings("unused") + private static void logRequestHttpUriHost(StringBuilder b, Request request, Response response) + { + append(b, request.getHttpURI().getHost()); + } + + @SuppressWarnings("unused") + private static void logRequestHttpUriPort(StringBuilder b, Request request, Response response) + { + b.append(request.getHttpURI().getPort()); + } + + @SuppressWarnings("unused") + private static void logRequestAttribute(String arg, StringBuilder b, Request request, Response response) + { + Object attribute = request.getAttribute(arg); + if (attribute != null) + append(b, attribute.toString()); + else + b.append('-'); + } } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/CustomRequestLogTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/CustomRequestLogTest.java index 9921bba88c32..32ce7882fa02 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/CustomRequestLogTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/CustomRequestLogTest.java @@ -818,6 +818,207 @@ public boolean handle(Request request, Response response, Callback callback) assertThat(log, is("42")); } + @Test + public void testLogRequestHttpUri() throws Exception + { + start("%uri", new SimpleHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Content.Sink.write(response, false, "hello", Callback.NOOP); + callback.succeeded(); + return true; + } + }); + + HttpTester.Response response = getResponse("GET /path?hello=world#fragment HTTP/1.0\n\n"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String log = _logs.poll(5, TimeUnit.SECONDS); + assertThat(log, is("http://127.0.0.1:" + _serverConnector.getLocalPort() + "/path?hello=world#fragment")); + } + + @Test + public void testLogRequestHttpUriWithoutQuery() throws Exception + { + start("%{-query}uri", new SimpleHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Content.Sink.write(response, false, "hello", Callback.NOOP); + callback.succeeded(); + return true; + } + }); + + HttpTester.Response response = getResponse("GET /path?hello=world#fragment HTTP/1.0\n\n"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String log = _logs.poll(5, TimeUnit.SECONDS); + assertThat(log, is("http://127.0.0.1:" + _serverConnector.getLocalPort() + "/path")); + } + + @Test + public void testLogRequestHttpUriWithoutQueryAndPath() throws Exception + { + start("%{-path,-query}uri", new SimpleHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Content.Sink.write(response, false, "hello", Callback.NOOP); + callback.succeeded(); + return true; + } + }); + + HttpTester.Response response = getResponse("GET /path?hello=world#fragment HTTP/1.0\n\n"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String log = _logs.poll(5, TimeUnit.SECONDS); + assertThat(log, is("http://127.0.0.1:" + _serverConnector.getLocalPort())); + } + + @Test + public void testLogRequestHttpUriHost() throws Exception + { + start("%{host}uri", new SimpleHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Content.Sink.write(response, false, "hello", Callback.NOOP); + callback.succeeded(); + return true; + } + }); + + HttpTester.Response response = getResponse("GET /path?hello=world#fragment HTTP/1.0\n\n"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String log = _logs.poll(5, TimeUnit.SECONDS); + assertThat(log, is("127.0.0.1")); + } + + @Test + public void testLogRequestHttpUriPort() throws Exception + { + start("%{port}uri", new SimpleHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Content.Sink.write(response, false, "hello", Callback.NOOP); + callback.succeeded(); + return true; + } + }); + + HttpTester.Response response = getResponse("GET /path?hello=world#fragment HTTP/1.0\n\n"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String log = _logs.poll(5, TimeUnit.SECONDS); + assertThat(log, is(Integer.toString(_serverConnector.getLocalPort()))); + } + + @Test + public void testLogRequestHttpUriScheme() throws Exception + { + start("%{scheme}uri", new SimpleHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Content.Sink.write(response, false, "hello", Callback.NOOP); + callback.succeeded(); + return true; + } + }); + + HttpTester.Response response = getResponse("GET /path?hello=world#fragment HTTP/1.0\n\n"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String log = _logs.poll(5, TimeUnit.SECONDS); + assertThat(log, is("http")); + } + + @Test + public void testLogRequestHttpUriAuthority() throws Exception + { + start("%{authority}uri", new SimpleHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Content.Sink.write(response, false, "hello", Callback.NOOP); + callback.succeeded(); + return true; + } + }); + + HttpTester.Response response = getResponse("GET /path?hello=world#fragment HTTP/1.0\n\n"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String log = _logs.poll(5, TimeUnit.SECONDS); + assertThat(log, is("127.0.0.1:" + _serverConnector.getLocalPort())); + } + + @Test + public void testLogRequestHttpUriPath() throws Exception + { + start("%{path}uri", new SimpleHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Content.Sink.write(response, false, "hello", Callback.NOOP); + callback.succeeded(); + return true; + } + }); + + HttpTester.Response response = getResponse("GET /path?hello=world#fragment HTTP/1.0\n\n"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String log = _logs.poll(5, TimeUnit.SECONDS); + assertThat(log, is("/path")); + } + + @Test + public void testLogRequestHttpUriQuery() throws Exception + { + start("%{query}uri", new SimpleHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Content.Sink.write(response, false, "hello", Callback.NOOP); + callback.succeeded(); + return true; + } + }); + + HttpTester.Response response = getResponse("GET /path?hello=world#fragment HTTP/1.0\n\n"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String log = _logs.poll(5, TimeUnit.SECONDS); + assertThat(log, is("?hello=world")); + } + + @Test + public void testLogRequestAttribute() throws Exception + { + start("%{myAttribute}attr", new SimpleHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + request.setAttribute("myAttribute", "value1234"); + Content.Sink.write(response, false, "hello", Callback.NOOP); + callback.succeeded(); + return true; + } + }); + + HttpTester.Response response = getResponse("GET /?hello=world HTTP/1.0\n\n"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String log = _logs.poll(5, TimeUnit.SECONDS); + assertThat(log, is("value1234")); + } + class TestRequestLogWriter implements RequestLog.Writer { @Override