From 840f9dc021241c0015b6bcc21bc2549e431a271d Mon Sep 17 00:00:00 2001 From: xjusko Date: Thu, 14 Mar 2024 13:25:39 +0100 Subject: [PATCH] [UNDERTOW-2372] Add request size and response size exchange attributes(sizes include headers). --- .../attribute/ExchangeAttributes.java | 8 ++ .../attribute/RequestSizeAttribute.java | 99 +++++++++++++++++ .../attribute/ResponseSizeAttribute.java | 103 ++++++++++++++++++ .../handlers/accesslog/AccessLogHandler.java | 3 + .../accesslog/ExtendedAccessLogParser.java | 6 + .../main/java/io/undertow/util/HeaderMap.java | 17 +++ ...ndertow.attribute.ExchangeAttributeBuilder | 3 + 7 files changed, 239 insertions(+) create mode 100644 core/src/main/java/io/undertow/attribute/RequestSizeAttribute.java create mode 100644 core/src/main/java/io/undertow/attribute/ResponseSizeAttribute.java diff --git a/core/src/main/java/io/undertow/attribute/ExchangeAttributes.java b/core/src/main/java/io/undertow/attribute/ExchangeAttributes.java index be68b99466..c68125c290 100644 --- a/core/src/main/java/io/undertow/attribute/ExchangeAttributes.java +++ b/core/src/main/java/io/undertow/attribute/ExchangeAttributes.java @@ -123,6 +123,14 @@ public static ExchangeAttribute threadName() { return ThreadNameAttribute.INSTANCE; } + public static ExchangeAttribute requestSize() { + return RequestSizeAttribute.INSTANCE; + } + + public static ExchangeAttribute responseSize() { + return ResponseSizeAttribute.INSTANCE; + } + public static ExchangeAttribute constant(String value) { return new ConstantExchangeAttribute(value); } diff --git a/core/src/main/java/io/undertow/attribute/RequestSizeAttribute.java b/core/src/main/java/io/undertow/attribute/RequestSizeAttribute.java new file mode 100644 index 0000000000..5026250d7d --- /dev/null +++ b/core/src/main/java/io/undertow/attribute/RequestSizeAttribute.java @@ -0,0 +1,99 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +/** + * Size of request in bytes, including headers, cannot be zero + * + * @author Marek Jusko + */ + +public class RequestSizeAttribute implements ExchangeAttribute{ + + public static final String REQUEST_SIZE_SHORT = "%E"; + public static final String REQUEST_SIZE = "%{REQUEST_SIZE}"; + public static final ExchangeAttribute INSTANCE = new RequestSizeAttribute(); + + @Override + public String readAttribute(HttpServerExchange exchange) { + // Initialize requestSize to 2 bytes for the CRLF at the end of headers string + long requestSize = 2; + + // Add the request content length if it is specified + if (exchange.getRequestContentLength() != -1) { + requestSize += exchange.getRequestContentLength(); + } + // Add the size of the request line + requestSize += calculateRequestLineSize(exchange); + + // Add the size of all headers + requestSize += exchange.getRequestHeaders().getHeadersBytes(); + + // Add 4 bytes per header for ": " and CRLF + requestSize += exchange.getRequestHeaders().size() * 4L; + + return Long.toString(requestSize); + } + + // Request-Line = Method SP Request-URI SP HTTP-Version CRLF + private long calculateRequestLineSize(HttpServerExchange exchange) { + // Initialize size to 4 bytes for the 2 spaces and CRLF in the request line + long size = 4; // 2 spaces + CRLF + + // Add the length of the protocol, request method, and request path + size += exchange.getProtocol().length(); + size += exchange.getRequestMethod().length(); + size += exchange.getRequestPath().length(); + + return size; + } + + @Override + public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Size of request, including headers", newValue); + } + + @Override + public String toString() { + return REQUEST_SIZE; + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Request size"; + } + + @Override + public ExchangeAttribute build(String token) { + if (token.equals(REQUEST_SIZE) || token.equals(REQUEST_SIZE_SHORT)) { + return RequestSizeAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/core/src/main/java/io/undertow/attribute/ResponseSizeAttribute.java b/core/src/main/java/io/undertow/attribute/ResponseSizeAttribute.java new file mode 100644 index 0000000000..a86ec2ee00 --- /dev/null +++ b/core/src/main/java/io/undertow/attribute/ResponseSizeAttribute.java @@ -0,0 +1,103 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.StatusCodes; + +/** + * Size of response in bytes, including headers + * + * @author Marek Jusko + */ + +public class ResponseSizeAttribute implements ExchangeAttribute{ + + public static final String RESPONSE_SIZE_SHORT = "%O"; + public static final String RESPONSE_SIZE = "%{RESPONSE_SIZE}"; + public static final ExchangeAttribute INSTANCE = new ResponseSizeAttribute(); + + @Override + public String readAttribute(HttpServerExchange exchange) { + if (exchange.getResponseHeaders().size() == 0) { + return "0"; + } + // Initialize requestSize to 2 bytes for the CRLF at the end of headers string + long responseSize = 2; + + // Add the number of bytes sent in the response body + responseSize += exchange.getResponseBytesSent(); + + // Add the size of the status line + responseSize += calculateStatusLineSize(exchange); + + // Add the size of the headers + responseSize += exchange.getResponseHeaders().getHeadersBytes(); + + // Add 4 bytes per header for ": " and CRLF + responseSize += exchange.getResponseHeaders().size() * 4L; + + return Long.toString(responseSize); + } + + // Status Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF + private long calculateStatusLineSize(HttpServerExchange exchange) { + // Initialize size to 7 bytes for the spaces, CRLF, and 3-digit status code + long size = 7; // 3 for status code + 2 for spaces + 2 for CRLF + + // Add the length of the HTTP version + size += exchange.getProtocol().length(); + + // Add the length of the status message + size += StatusCodes.getReason(exchange.getStatusCode()).length(); + + return size; + } + + @Override + public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Size of response, including headers", newValue); + } + + @Override + public String toString() { + return RESPONSE_SIZE; + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Response size"; + } + + @Override + public ExchangeAttribute build(String token) { + if (token.equals(RESPONSE_SIZE) || token.equals(RESPONSE_SIZE_SHORT)) { + return ResponseSizeAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/core/src/main/java/io/undertow/server/handlers/accesslog/AccessLogHandler.java b/core/src/main/java/io/undertow/server/handlers/accesslog/AccessLogHandler.java index e7693d29af..b47df02f70 100644 --- a/core/src/main/java/io/undertow/server/handlers/accesslog/AccessLogHandler.java +++ b/core/src/main/java/io/undertow/server/handlers/accesslog/AccessLogHandler.java @@ -67,6 +67,9 @@ *
  • %D - Time taken to process the request, in millis *
  • %T - Time taken to process the request, in seconds *
  • %I - current Request thread name (can compare later with stacktraces) + *
  • %E - Size of request in bytes, including headers, cannot be zero + *
  • %O - Size of response in bytes, including headers + * *

    In addition, the caller can specify one of the following aliases for * commonly utilized patterns:

    diff --git a/core/src/main/java/io/undertow/server/handlers/accesslog/ExtendedAccessLogParser.java b/core/src/main/java/io/undertow/server/handlers/accesslog/ExtendedAccessLogParser.java index 77458799a5..d8acdde093 100644 --- a/core/src/main/java/io/undertow/server/handlers/accesslog/ExtendedAccessLogParser.java +++ b/core/src/main/java/io/undertow/server/handlers/accesslog/ExtendedAccessLogParser.java @@ -39,9 +39,11 @@ import io.undertow.attribute.RequestMethodAttribute; import io.undertow.attribute.RequestProtocolAttribute; import io.undertow.attribute.RequestSchemeAttribute; +import io.undertow.attribute.RequestSizeAttribute; import io.undertow.attribute.RequestURLAttribute; import io.undertow.attribute.ResponseCodeAttribute; import io.undertow.attribute.ResponseHeaderAttribute; +import io.undertow.attribute.ResponseSizeAttribute; import io.undertow.attribute.ResponseTimeAttribute; import io.undertow.attribute.SecureExchangeAttribute; import io.undertow.attribute.SubstituteEmptyWrapper; @@ -249,6 +251,10 @@ protected ExchangeAttribute getLogElement(String token, PatternTokenizer tokeniz } } else if ("bytes".equals(token)) { return new BytesSentAttribute(true); + } else if ("responseSize".equals(token)) { + return ResponseSizeAttribute.INSTANCE; + } else if ("requestSize".equals(token)) { + return RequestSizeAttribute.INSTANCE; } else if ("cached".equals(token)) { /* I don't know how to evaluate this! */ return new ConstantExchangeAttribute("-"); diff --git a/core/src/main/java/io/undertow/util/HeaderMap.java b/core/src/main/java/io/undertow/util/HeaderMap.java index a6b7bc6b3c..cf50a37556 100644 --- a/core/src/main/java/io/undertow/util/HeaderMap.java +++ b/core/src/main/java/io/undertow/util/HeaderMap.java @@ -20,6 +20,7 @@ import static org.wildfly.common.Assert.checkNotNullParam; +import java.nio.charset.StandardCharsets; import java.util.AbstractCollection; import java.util.Arrays; import java.util.Collection; @@ -834,6 +835,22 @@ public boolean contains(String headerName) { return false; } + public long getHeadersBytes() { + long headersSize = 0; + long cookie = this.fastIterateNonEmpty(); + while (cookie != -1L) { + HeaderValues header = this.fiCurrent(cookie); + headersSize += header.getHeaderName().length(); // Size of the header name + for (String value : header) { + headersSize += value.getBytes(StandardCharsets.UTF_8).length; // Size of each header value + } + + // Get the next non-empty header cookie + cookie = this.fiNextNonEmpty(cookie); + } + return headersSize; + } + // compare @Override diff --git a/core/src/main/resources/META-INF/services/io.undertow.attribute.ExchangeAttributeBuilder b/core/src/main/resources/META-INF/services/io.undertow.attribute.ExchangeAttributeBuilder index eea0057862..065c4ad3b3 100644 --- a/core/src/main/resources/META-INF/services/io.undertow.attribute.ExchangeAttributeBuilder +++ b/core/src/main/resources/META-INF/services/io.undertow.attribute.ExchangeAttributeBuilder @@ -38,3 +38,6 @@ io.undertow.attribute.NullAttribute$Builder io.undertow.attribute.StoredResponse$Builder io.undertow.attribute.ResponseReasonPhraseAttribute$Builder io.undertow.attribute.RemoteObfuscatedIPAttribute$Builder +io.undertow.attribute.RequestSizeAttribute$Builder +io.undertow.attribute.ResponseSizeAttribute$Builder +