diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 4ef26fb408..493f4325a1 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -283,7 +283,7 @@ modules = [ [[package]] org = "ballerina" name = "observe" -version = "1.2.2" +version = "1.2.3" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] diff --git a/ballerina/http_log_manager.bal b/ballerina/http_log_manager.bal index 67928b082b..28b9a88c97 100644 --- a/ballerina/http_log_manager.bal +++ b/ballerina/http_log_manager.bal @@ -35,9 +35,13 @@ public type TraceLogAdvancedConfiguration record {| # Represents HTTP access log configuration. # # + console - Boolean value to enable or disable console access logs +# + format - The format of access logs to be printed (either `flat` or `json`) +# + attributes - The list of attributes of access logs to be printed # + path - Optional file path to store access logs public type AccessLogConfiguration record {| boolean console = false; + string format = "flat"; + string[] attributes?; string path?; |}; diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java index b31d83f132..48b1915153 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java @@ -323,9 +323,12 @@ public final class HttpConstants { public static final String HTTP_TRACE_LOG_ENABLED = "http.tracelog.enabled"; public static final String HTTP_ACCESS_LOG = "http.accesslog"; public static final String HTTP_ACCESS_LOG_ENABLED = "http.accesslog.enabled"; + public static final String HTTP_LOG_FORMAT_JSON = "json"; // TraceLog and AccessLog configs public static final BString HTTP_LOG_CONSOLE = StringUtils.fromString("console"); + public static final BString HTTP_LOG_FORMAT = StringUtils.fromString("format"); + public static final BString HTTP_LOG_ATTRIBUTES = StringUtils.fromString("attributes"); public static final BString HTTP_LOG_FILE_PATH = StringUtils.fromString("path"); public static final BString HTTP_TRACE_LOG_HOST = StringUtils.fromString("host"); public static final BString HTTP_TRACE_LOG_PORT = StringUtils.fromString("port"); diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/logging/HttpLogManager.java b/native/src/main/java/io/ballerina/stdlib/http/api/logging/HttpLogManager.java index 1cd7666f49..cc067ecb6e 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/logging/HttpLogManager.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/logging/HttpLogManager.java @@ -20,6 +20,7 @@ import io.ballerina.runtime.api.values.BMap; import io.ballerina.runtime.api.values.BString; +import io.ballerina.stdlib.http.api.logging.accesslog.HttpAccessLogConfig; import io.ballerina.stdlib.http.api.logging.formatters.HttpAccessLogFormatter; import io.ballerina.stdlib.http.api.logging.formatters.HttpTraceLogFormatter; import io.ballerina.stdlib.http.api.logging.formatters.JsonLogFormatter; @@ -70,6 +71,7 @@ public HttpLogManager(boolean traceLogConsole, BMap traceLogAdvancedConfig, BMap this.protocol = protocol.getValue(); this.setHttpTraceLogHandler(traceLogConsole, traceLogAdvancedConfig); this.setHttpAccessLogHandler(accessLogConfig); + HttpAccessLogConfig.initializeHttpAccessLogConfig(accessLogConfig); } /** @@ -166,5 +168,4 @@ public void setHttpAccessLogHandler(BMap accessLogConfig) { stdErr.println("ballerina: " + protocol + " access log enabled"); } } - } diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/logging/accesslog/HttpAccessLogConfig.java b/native/src/main/java/io/ballerina/stdlib/http/api/logging/accesslog/HttpAccessLogConfig.java new file mode 100644 index 0000000000..41ba7ec13a --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/http/api/logging/accesslog/HttpAccessLogConfig.java @@ -0,0 +1,56 @@ +package io.ballerina.stdlib.http.api.logging.accesslog; + +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static io.ballerina.stdlib.http.api.HttpConstants.HTTP_LOG_ATTRIBUTES; +import static io.ballerina.stdlib.http.api.HttpConstants.HTTP_LOG_FORMAT; +import static io.ballerina.stdlib.http.api.HttpConstants.HTTP_LOG_FORMAT_JSON; + +public class HttpAccessLogConfig { + private static final Set EXCLUDED_ATTRIBUTES = new HashSet<>(List.of( + "http_referrer", "http_user_agent", "http_x_forwarded_for" + )); + private static BMap accessLogConfig = null; + + public static void initializeHttpAccessLogConfig(BMap accessLogConfigFromBallerina) { + accessLogConfig = accessLogConfigFromBallerina; + } + + public static List getCustomHeaders() { + List attributes = getAccessLogAttributes(); + if (attributes == null) { + return Collections.emptyList(); + } + + return attributes.stream() + .filter(attr -> attr.startsWith("http_") && !EXCLUDED_ATTRIBUTES.contains(attr)) + .map(attr -> attr.substring(5)) + .collect(Collectors.toList()); + } + + public static HttpAccessLogFormat getAccessLogFormat() { + BString logFormat = accessLogConfig.getStringValue(HTTP_LOG_FORMAT); + if (logFormat.getValue().equals(HTTP_LOG_FORMAT_JSON)) { + return HttpAccessLogFormat.JSON; + } + return HttpAccessLogFormat.FLAT; + } + + public static List getAccessLogAttributes() { + BArray logAttributes = accessLogConfig.getArrayValue(HTTP_LOG_ATTRIBUTES); + if (logAttributes != null) { + return Arrays.stream(logAttributes.getStringArray()) + .collect(Collectors.toList()); + } + return null; + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/logging/accesslog/HttpAccessLogFormat.java b/native/src/main/java/io/ballerina/stdlib/http/api/logging/accesslog/HttpAccessLogFormat.java new file mode 100644 index 0000000000..4ac452c0f6 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/http/api/logging/accesslog/HttpAccessLogFormat.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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.ballerina.stdlib.http.api.logging.accesslog; + +public enum HttpAccessLogFormat { + FLAT, JSON +} diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/logging/accesslog/HttpAccessLogMessage.java b/native/src/main/java/io/ballerina/stdlib/http/api/logging/accesslog/HttpAccessLogMessage.java new file mode 100644 index 0000000000..78947170c9 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/http/api/logging/accesslog/HttpAccessLogMessage.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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.ballerina.stdlib.http.api.logging.accesslog; + +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; + +public class HttpAccessLogMessage { + private String ip; + private Calendar dateTime; + private String requestMethod; + private String requestUri; + private String scheme; + private int status; + private long requestBodySize; + private long responseBodySize; + private long requestTime; + private String httpReferrer; + private String httpUserAgent; + private String httpXForwardedFor; + private String host; + private int port; + private Map customHeaders; + + public HttpAccessLogMessage() { + this.customHeaders = new HashMap<>(); + } + + public HttpAccessLogMessage(String ip, Calendar dateTime, String requestMethod, String requestUri, String scheme, + int status, long responseBodySize, String httpReferrer, String httpUserAgent) { + this.ip = ip; + this.dateTime = dateTime; + this.requestMethod = requestMethod; + this.requestUri = requestUri; + this.scheme = scheme; + this.status = status; + this.responseBodySize = responseBodySize; + this.httpReferrer = httpReferrer; + this.httpUserAgent = httpUserAgent; + this.customHeaders = new HashMap<>(); + } + + public Calendar getDateTime() { + return dateTime; + } + + public void setDateTime(Calendar dateTime) { + this.dateTime = dateTime; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getRequestMethod() { + return requestMethod; + } + + public void setRequestMethod(String requestMethod) { + this.requestMethod = requestMethod; + } + + public String getRequestUri() { + return requestUri; + } + + public void setRequestUri(String requestUri) { + this.requestUri = requestUri; + } + + public String getScheme() { + return scheme; + } + + public void setScheme(String scheme) { + this.scheme = scheme; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public long getRequestBodySize() { + return requestBodySize; + } + + public void setRequestBodySize(Long requestBodySize) { + this.requestBodySize = requestBodySize; + } + + public long getResponseBodySize() { + return responseBodySize; + } + + public void setResponseBodySize(Long responseBodySize) { + this.responseBodySize = responseBodySize; + } + + public long getRequestTime() { + return requestTime; + } + + public void setRequestTime(Long requestTime) { + this.requestTime = requestTime; + } + + public String getHttpUserAgent() { + return httpUserAgent; + } + + public String getHttpReferrer() { + return httpReferrer; + } + + public void setHttpReferrer(String httpReferrer) { + this.httpReferrer = httpReferrer; + } + + public void setHttpUserAgent(String httpUserAgent) { + this.httpUserAgent = httpUserAgent; + } + + public String getHttpXForwardedFor() { + return httpXForwardedFor; + } + + public void setHttpXForwardedFor(String httpXForwardedFor) { + this.httpXForwardedFor = httpXForwardedFor; + } + + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return this.port; + } + + public void setPort(int port) { + this.port = port; + } + + public Map getCustomHeaders() { + return customHeaders; + } + + public void setCustomHeaders(Map customHeaders) { + this.customHeaders = customHeaders; + } + + public void putCustomHeader(String headerKey, String headerValue) { + this.customHeaders.put(headerKey, headerValue); + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/logging/accesslog/HttpAccessLogger.java b/native/src/main/java/io/ballerina/stdlib/http/api/logging/accesslog/HttpAccessLogger.java new file mode 100644 index 0000000000..c938cdfc15 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/http/api/logging/accesslog/HttpAccessLogger.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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.ballerina.stdlib.http.api.logging.accesslog; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import io.netty.util.internal.logging.InternalLogLevel; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static io.ballerina.stdlib.http.transport.contract.Constants.ACCESS_LOG; + +public class HttpAccessLogger { + private static final InternalLogger ACCESS_LOGGER = InternalLoggerFactory.getInstance(ACCESS_LOG); + + public static boolean isEnabled() { + return ACCESS_LOGGER.isEnabled(InternalLogLevel.INFO); + } + + public static void log(HttpAccessLogMessage inboundMessage, List outboundMessages) { + String formattedAccessLogMessage = formatAccessLogMessage(inboundMessage, outboundMessages, + HttpAccessLogConfig.getAccessLogFormat(), HttpAccessLogConfig.getAccessLogAttributes()); + ACCESS_LOGGER.log(InternalLogLevel.INFO, formattedAccessLogMessage); + } + + private static String formatAccessLogMessage(HttpAccessLogMessage inboundMessage, + List outboundMessages, HttpAccessLogFormat format, + List attributes) { + if (format == HttpAccessLogFormat.FLAT) { + Map inboundMap = mapAccessLogMessage(inboundMessage, format, attributes); + String inboundFormatted = inboundMap.values().stream() + .filter(Objects::nonNull) + .collect(Collectors.joining(" ")); + + if (!outboundMessages.isEmpty()) { + String outboundFormatted = outboundMessages.stream() + .map(outboundMsg -> mapAccessLogMessage(outboundMsg, format, attributes)) + .map(outboundMap -> outboundMap.values().stream() + .filter(Objects::nonNull) + .collect(Collectors.joining(" "))) + .collect(Collectors.joining(" ")); + + return inboundFormatted + " \"~\" " + outboundFormatted; + } else { + return inboundFormatted; + } + } else { + Map inboundMap = mapAccessLogMessage(inboundMessage, format, attributes); + Gson gson = new Gson(); + JsonObject jsonObject = new JsonObject(); + + inboundMap.forEach(jsonObject::addProperty); + + if (!outboundMessages.isEmpty()) { + JsonArray upstreamArray = new JsonArray(); + for (HttpAccessLogMessage outboundMessage : outboundMessages) { + Map outboundMap = mapAccessLogMessage(outboundMessage, format, attributes); + JsonObject outboundJson = gson.toJsonTree(outboundMap).getAsJsonObject(); + upstreamArray.add(outboundJson); + } + jsonObject.add("upstream", upstreamArray); + } + return gson.toJson(jsonObject); + } + } + + private static Map mapAccessLogMessage(HttpAccessLogMessage httpAccessLogMessage, + HttpAccessLogFormat format, List attributes) { + List allAttributes = List.of("ip", "date_time", "request", "request_method", "request_uri", + "scheme", "status", "request_body_size", "response_body_size", "request_time", "http_referrer", + "http_user_agent", "http_x_forwarded_for"); + List defaultAttributes = List.of("ip", "date_time", "request", "status", "response_body_size", + "http_referrer", "http_user_agent"); + + Map attributeValues = new LinkedHashMap<>(); + allAttributes.forEach(attr -> attributeValues.put(attr, null)); + + if (attributes != null) { + attributes.forEach(attr -> { + attributeValues.put(attr, formatAccessLogAttribute(httpAccessLogMessage, format, attr)); + }); + } else { + defaultAttributes.forEach(attr -> + attributeValues.put(attr, formatAccessLogAttribute(httpAccessLogMessage, format, attr))); + } + return attributeValues; + } + + private static String formatAccessLogAttribute(HttpAccessLogMessage httpAccessLogMessage, + HttpAccessLogFormat format, String attribute) { + return switch (attribute) { + case "ip" -> httpAccessLogMessage.getIp(); + case "date_time" -> + String.format("[%1$td/%1$tb/%1$tY:%1$tT.%1$tL %1$tz]", httpAccessLogMessage.getDateTime()); + case "request_method" -> httpAccessLogMessage.getRequestMethod(); + case "request_uri" -> httpAccessLogMessage.getRequestUri(); + case "scheme" -> httpAccessLogMessage.getScheme(); + case "request" -> String.format(format == HttpAccessLogFormat.FLAT ? + "\"%1$s %2$s %3$s\"" : "%1$s %2$s %3$s", httpAccessLogMessage.getRequestMethod(), + httpAccessLogMessage.getRequestUri(), httpAccessLogMessage.getScheme()); + case "status" -> String.valueOf(httpAccessLogMessage.getStatus()); + case "request_body_size" -> String.valueOf(httpAccessLogMessage.getRequestBodySize()); + case "response_body_size" -> String.valueOf(httpAccessLogMessage.getResponseBodySize()); + case "request_time" -> String.valueOf(httpAccessLogMessage.getRequestTime()); + case "http_referrer" -> String.format(format == HttpAccessLogFormat.FLAT ? + "\"%1$s\"" : "%1$s", getHyphenForNull(httpAccessLogMessage.getHttpReferrer())); + case "http_user_agent" -> String.format(format == HttpAccessLogFormat.FLAT ? + "\"%1$s\"" : "%1$s", getHyphenForNull(httpAccessLogMessage.getHttpUserAgent())); + case "http_x_forwarded_for" -> getHyphenForNull(httpAccessLogMessage.getHttpXForwardedFor()); + default -> getCustomHeaderValueForAttribute(httpAccessLogMessage, attribute); + }; + } + + private static String getCustomHeaderValueForAttribute(HttpAccessLogMessage httpAccessLogMessage, + String attribute) { + Map customHeaders = httpAccessLogMessage.getCustomHeaders(); + if (attribute.startsWith("http_")) { + String customHeaderKey = attribute.substring(5).toLowerCase(Locale.getDefault()); + return customHeaders.getOrDefault(customHeaderKey, "-"); + } + return null; + } + + private static String getHyphenForNull(String value) { + return value == null ? "-" : value; + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/logging/formatters/HttpAccessLogFormatter.java b/native/src/main/java/io/ballerina/stdlib/http/api/logging/formatters/HttpAccessLogFormatter.java index ad676230c6..3330eaf7a1 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/logging/formatters/HttpAccessLogFormatter.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/logging/formatters/HttpAccessLogFormatter.java @@ -29,7 +29,6 @@ * @since 0.965 */ public class HttpAccessLogFormatter extends Formatter { - private static final String format = HttpLogManager.getLogManager().getProperty( HttpAccessLogFormatter.class.getCanonicalName() + ".format"); diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contract/Constants.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contract/Constants.java index 5f9f282e7a..b21025ba78 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contract/Constants.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contract/Constants.java @@ -158,6 +158,7 @@ public final class Constants { public static final String HTTP_REASON_PHRASE = "HTTP_REASON_PHRASE"; public static final String CHNL_HNDLR_CTX = "CHNL_HNDLR_CTX"; + public static final String OUTBOUND_ACCESS_LOG_MESSAGE = "OUTBOUND_ACCESS_LOG_MESSAGE"; public static final String SRC_HANDLER = "SRC_HANDLER"; public static final String POOLED_BYTE_BUFFER_FACTORY = "POOLED_BYTE_BUFFER_FACTORY"; diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/DefaultHttpClientConnector.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/DefaultHttpClientConnector.java index 1b6a34b4fc..d570ef4619 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/DefaultHttpClientConnector.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/DefaultHttpClientConnector.java @@ -19,6 +19,7 @@ package io.ballerina.stdlib.http.transport.contractimpl; +import io.ballerina.stdlib.http.api.logging.accesslog.HttpAccessLogMessage; import io.ballerina.stdlib.http.transport.contract.Constants; import io.ballerina.stdlib.http.transport.contract.HttpClientConnector; import io.ballerina.stdlib.http.transport.contract.HttpResponseFuture; @@ -57,8 +58,10 @@ import java.io.IOException; import java.net.InetSocketAddress; +import java.util.Calendar; import java.util.NoSuchElementException; +import static io.ballerina.stdlib.http.transport.contract.Constants.OUTBOUND_ACCESS_LOG_MESSAGE; import static io.ballerina.stdlib.http.transport.contract.Constants.REMOTE_SERVER_CLOSED_BEFORE_INITIATING_OUTBOUND_REQUEST; /** @@ -148,6 +151,10 @@ public HttpResponseFuture send(HttpCarbonMessage httpOutboundRequest) { } public HttpResponseFuture send(OutboundMsgHolder outboundMsgHolder, HttpCarbonMessage httpOutboundRequest) { + HttpAccessLogMessage outboundAccessLogMessage = new HttpAccessLogMessage(); + outboundAccessLogMessage.setDateTime(Calendar.getInstance()); + httpOutboundRequest.setProperty(OUTBOUND_ACCESS_LOG_MESSAGE, outboundAccessLogMessage); + final HttpResponseFuture httpResponseFuture; Object sourceHandlerObject = httpOutboundRequest.getProperty(Constants.SRC_HANDLER); @@ -191,6 +198,7 @@ public HttpResponseFuture send(OutboundMsgHolder outboundMsgHolder, HttpCarbonMe new RequestWriteStarter(outboundMsgHolder, activeHttp2ClientChannel).startWritingContent(); httpResponseFuture = outboundMsgHolder.getResponseFuture(); httpResponseFuture.notifyResponseHandle(new ResponseHandle(outboundMsgHolder)); + return httpResponseFuture; } } diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceHandler.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceHandler.java index b419b326d9..4ea94255dc 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceHandler.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceHandler.java @@ -18,6 +18,7 @@ package io.ballerina.stdlib.http.transport.contractimpl.listener.http2; +import io.ballerina.stdlib.http.api.logging.accesslog.HttpAccessLogMessage; import io.ballerina.stdlib.http.transport.contract.Constants; import io.ballerina.stdlib.http.transport.contract.ServerConnectorFuture; import io.ballerina.stdlib.http.transport.contract.exceptions.ServerConnectorException; @@ -50,6 +51,8 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -81,6 +84,7 @@ public final class Http2SourceHandler extends ChannelInboundHandlerAdapter { private SocketAddress remoteAddress; private ChannelGroup allChannels; private ChannelGroup listenerChannels; + private List httpAccessLogMessages; Http2SourceHandler(HttpServerChannelInitializer serverChannelInitializer, Http2ConnectionEncoder encoder, String interfaceId, Http2Connection conn, ServerConnectorFuture serverConnectorFuture, @@ -94,6 +98,7 @@ public final class Http2SourceHandler extends ChannelInboundHandlerAdapter { this.targetChannelPool = new ConcurrentHashMap<>(); this.allChannels = allChannels; this.listenerChannels = listenerChannels; + this.httpAccessLogMessages = new ArrayList<>(); setRemoteFlowController(); setDataEventListeners(); } @@ -291,4 +296,12 @@ public Http2ServerChannel getHttp2ServerChannel() { public SocketAddress getRemoteAddress() { return remoteAddress; } + + public void addHttpAccessLogMessage(HttpAccessLogMessage httpAccessLogMessage) { + this.httpAccessLogMessages.add(httpAccessLogMessage); + } + + public List getHttpAccessLogMessages() { + return this.httpAccessLogMessages; + } } diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/states/http2/SendingEntityBody.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/states/http2/SendingEntityBody.java index 5bf39f733f..7fe4eeae0e 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/states/http2/SendingEntityBody.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/states/http2/SendingEntityBody.java @@ -18,6 +18,8 @@ package io.ballerina.stdlib.http.transport.contractimpl.listener.states.http2; +import io.ballerina.stdlib.http.api.logging.accesslog.HttpAccessLogMessage; +import io.ballerina.stdlib.http.api.logging.accesslog.HttpAccessLogger; import io.ballerina.stdlib.http.transport.contract.HttpResponseFuture; import io.ballerina.stdlib.http.transport.contract.ServerConnectorFuture; import io.ballerina.stdlib.http.transport.contract.exceptions.ServerConnectorException; @@ -47,20 +49,18 @@ import io.netty.handler.codec.http2.Http2Exception; import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.HttpConversionUtil; -import io.netty.util.internal.logging.InternalLogLevel; -import io.netty.util.internal.logging.InternalLogger; -import io.netty.util.internal.logging.InternalLoggerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.ArrayList; import java.util.Calendar; +import java.util.List; -import static io.ballerina.stdlib.http.transport.contract.Constants.ACCESS_LOG; -import static io.ballerina.stdlib.http.transport.contract.Constants.ACCESS_LOG_FORMAT; import static io.ballerina.stdlib.http.transport.contract.Constants.HTTP_X_FORWARDED_FOR; import static io.ballerina.stdlib.http.transport.contract.Constants.IDLE_TIMEOUT_TRIGGERED_WHILE_WRITING_OUTBOUND_RESPONSE_BODY; import static io.ballerina.stdlib.http.transport.contract.Constants.REMOTE_CLIENT_CLOSED_WHILE_WRITING_OUTBOUND_RESPONSE_BODY; +import static io.ballerina.stdlib.http.transport.contract.Constants.SRC_HANDLER; import static io.ballerina.stdlib.http.transport.contract.Constants.TO; import static io.ballerina.stdlib.http.transport.contractimpl.common.states.Http2StateUtil.validatePromisedStreamState; @@ -72,7 +72,6 @@ public class SendingEntityBody implements ListenerState { private static final Logger LOG = LoggerFactory.getLogger(SendingEntityBody.class); - private static final InternalLogger ACCESS_LOGGER = InternalLoggerFactory.getInstance(ACCESS_LOG); private final Http2MessageStateContext http2MessageStateContext; private final ChannelHandlerContext ctx; @@ -167,10 +166,6 @@ private void writeContent(Http2OutboundRespListener http2OutboundRespListener, HttpCarbonMessage outboundResponseMsg, HttpContent httpContent, int streamId) throws Http2Exception { if (httpContent instanceof LastHttpContent) { - if (serverChannelInitializer.isHttpAccessLogEnabled()) { - logAccessInfo(outboundResponseMsg, streamId); - } - final LastHttpContent lastContent = (httpContent == LastHttpContent.EMPTY_LAST_CONTENT) ? new DefaultLastHttpContent() : (LastHttpContent) httpContent; HttpHeaders trailers = lastContent.trailingHeaders(); @@ -185,6 +180,9 @@ private void writeContent(Http2OutboundRespListener http2OutboundRespListener, inboundRequestMsg); } http2OutboundRespListener.removeDefaultResponseWriter(); + if (serverChannelInitializer.isHttpAccessLogEnabled()) { + logAccessInfo(http2OutboundRespListener.getInboundRequestMsg(), outboundResponseMsg, streamId); + } http2MessageStateContext .setListenerState(new ResponseCompleted(http2OutboundRespListener, http2MessageStateContext)); } else { @@ -214,8 +212,9 @@ private void writeData(HttpContent httpContent, int streamId, boolean endStream) } } - private void logAccessInfo(HttpCarbonMessage outboundResponseMsg, int streamId) { - if (!ACCESS_LOGGER.isEnabled(InternalLogLevel.INFO)) { + private void logAccessInfo(HttpCarbonMessage inboundRequestMsg, HttpCarbonMessage outboundResponseMsg, + int streamId) { + if (!HttpAccessLogger.isEnabled()) { return; } if (originalStreamId != streamId) { // Skip access logs for server push messages @@ -252,8 +251,18 @@ private void logAccessInfo(HttpCarbonMessage outboundResponseMsg, int streamId) // Populate response parameters int statusCode = Util.getHttpResponseStatus(outboundResponseMsg).code(); - ACCESS_LOGGER.log(InternalLogLevel.INFO, String.format( - ACCESS_LOG_FORMAT, remoteAddress, inboundRequestArrivalTime, method, uri, protocol, - statusCode, contentLength, referrer, userAgent)); + long requestTime = Calendar.getInstance().getTimeInMillis() - inboundRequestArrivalTime.getTimeInMillis(); + HttpAccessLogMessage inboundMessage = new HttpAccessLogMessage(remoteAddress, + inboundRequestArrivalTime, method, uri, protocol, statusCode, contentLength, referrer, userAgent); + inboundMessage.setRequestBodySize((long) inboundRequestMsg.getContentSize()); + inboundMessage.setRequestTime(requestTime); + + List outboundMessages = new ArrayList<>(); + Object sourceHandlerObject = inboundRequestMsg.getProperty(SRC_HANDLER); + + if (sourceHandlerObject instanceof Http2SourceHandler http2SourceHandler) { + outboundMessages.addAll(http2SourceHandler.getHttpAccessLogMessages()); + } + HttpAccessLogger.log(inboundMessage, outboundMessages); } } diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/sender/states/ReceivingEntityBody.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/sender/states/ReceivingEntityBody.java index 22aebfc1cc..a94c723820 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/sender/states/ReceivingEntityBody.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/sender/states/ReceivingEntityBody.java @@ -18,21 +18,36 @@ package io.ballerina.stdlib.http.transport.contractimpl.sender.states; +import io.ballerina.stdlib.http.api.logging.accesslog.HttpAccessLogConfig; +import io.ballerina.stdlib.http.api.logging.accesslog.HttpAccessLogMessage; +import io.ballerina.stdlib.http.transport.contract.Constants; import io.ballerina.stdlib.http.transport.contract.HttpResponseFuture; import io.ballerina.stdlib.http.transport.contractimpl.common.states.SenderReqRespStateManager; import io.ballerina.stdlib.http.transport.contractimpl.common.states.StateUtil; +import io.ballerina.stdlib.http.transport.contractimpl.listener.http2.Http2SourceHandler; import io.ballerina.stdlib.http.transport.contractimpl.sender.TargetHandler; import io.ballerina.stdlib.http.transport.message.HttpCarbonMessage; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Calendar; + +import static io.ballerina.stdlib.http.transport.contract.Constants.HTTP_X_FORWARDED_FOR; import static io.ballerina.stdlib.http.transport.contract.Constants.IDLE_STATE_HANDLER; import static io.ballerina.stdlib.http.transport.contract.Constants.IDLE_TIMEOUT_TRIGGERED_WHILE_READING_INBOUND_RESPONSE_BODY; +import static io.ballerina.stdlib.http.transport.contract.Constants.OUTBOUND_ACCESS_LOG_MESSAGE; import static io.ballerina.stdlib.http.transport.contract.Constants.REMOTE_SERVER_CLOSED_WHILE_READING_INBOUND_RESPONSE_BODY; +import static io.ballerina.stdlib.http.transport.contract.Constants.TO; import static io.ballerina.stdlib.http.transport.contractimpl.common.Util.isKeepAlive; import static io.ballerina.stdlib.http.transport.contractimpl.common.Util.safelyRemoveHandlers; import static io.ballerina.stdlib.http.transport.contractimpl.common.states.StateUtil.ILLEGAL_STATE_ERROR; @@ -47,6 +62,7 @@ public class ReceivingEntityBody implements SenderState { private final SenderReqRespStateManager senderReqRespStateManager; private final TargetHandler targetHandler; + private Long contentLength = 0L; ReceivingEntityBody(SenderReqRespStateManager senderReqRespStateManager, TargetHandler targetHandler) { this.senderReqRespStateManager = senderReqRespStateManager; @@ -76,9 +92,11 @@ public void readInboundResponseEntityBody(ChannelHandlerContext ctx, HttpContent StateUtil.setInboundTrailersToNewMessage(((LastHttpContent) httpContent).trailingHeaders(), inboundResponseMsg); inboundResponseMsg.addHttpContent(httpContent); + contentLength += httpContent.content().readableBytes(); inboundResponseMsg.setLastHttpContentArrived(); targetHandler.resetInboundMsg(); safelyRemoveHandlers(targetHandler.getTargetChannel().getChannel().pipeline(), IDLE_STATE_HANDLER); + updateAccessLogInfo(targetHandler, inboundResponseMsg); senderReqRespStateManager.state = new EntityBodyReceived(senderReqRespStateManager); if (!isKeepAlive(targetHandler.getKeepAliveConfig(), @@ -88,6 +106,7 @@ public void readInboundResponseEntityBody(ChannelHandlerContext ctx, HttpContent targetHandler.getConnectionManager().returnChannel(targetHandler.getTargetChannel()); } else { inboundResponseMsg.addHttpContent(httpContent); + contentLength += httpContent.content().readableBytes(); } } @@ -105,4 +124,72 @@ public void handleIdleTimeoutConnectionClosure(TargetHandler targetHandler, handleIncompleteInboundMessage(targetHandler.getInboundResponseMsg(), IDLE_TIMEOUT_TRIGGERED_WHILE_READING_INBOUND_RESPONSE_BODY); } + + private void updateAccessLogInfo(TargetHandler targetHandler, + HttpCarbonMessage inboundResponseMsg) { + HttpCarbonMessage httpOutboundRequest = targetHandler.getOutboundRequestMsg(); + HttpAccessLogMessage outboundAccessLogMessage = + getTypedProperty(httpOutboundRequest, OUTBOUND_ACCESS_LOG_MESSAGE, HttpAccessLogMessage.class); + Http2SourceHandler http2SourceHandler = + getTypedProperty(httpOutboundRequest, Constants.SRC_HANDLER, Http2SourceHandler.class); + if (outboundAccessLogMessage == null || http2SourceHandler == null) { + return; + } + + SocketAddress remoteAddress = targetHandler.getTargetChannel().getChannel().remoteAddress(); + if (remoteAddress instanceof InetSocketAddress inetSocketAddress) { + InetAddress inetAddress = inetSocketAddress.getAddress(); + outboundAccessLogMessage.setIp(inetAddress.getHostAddress()); + outboundAccessLogMessage.setHost(inetAddress.getHostName()); + outboundAccessLogMessage.setPort(inetSocketAddress.getPort()); + } + if (outboundAccessLogMessage.getIp().startsWith("/")) { + outboundAccessLogMessage.setIp(outboundAccessLogMessage.getIp().substring(1)); + } + + // Populate with header parameters + HttpHeaders headers = httpOutboundRequest.getHeaders(); + if (headers.contains(HTTP_X_FORWARDED_FOR)) { + String forwardedHops = headers.get(HTTP_X_FORWARDED_FOR); + outboundAccessLogMessage.setHttpXForwardedFor(forwardedHops); + // If multiple IPs available, the first ip is the client + int firstCommaIndex = forwardedHops.indexOf(','); + outboundAccessLogMessage.setIp(firstCommaIndex != -1 ? + forwardedHops.substring(0, firstCommaIndex) : forwardedHops); + } + if (headers.contains(HttpHeaderNames.USER_AGENT)) { + outboundAccessLogMessage.setHttpUserAgent(headers.get(HttpHeaderNames.USER_AGENT)); + } + if (headers.contains(HttpHeaderNames.REFERER)) { + outboundAccessLogMessage.setHttpReferrer(headers.get(HttpHeaderNames.REFERER)); + } + HttpAccessLogConfig.getCustomHeaders().forEach(customHeader -> + outboundAccessLogMessage.putCustomHeader(customHeader, headers.contains(customHeader) ? + headers.get(customHeader) : "-")); + + outboundAccessLogMessage.setRequestMethod(httpOutboundRequest.getHttpMethod()); + outboundAccessLogMessage.setRequestUri((String) httpOutboundRequest.getProperty(TO)); + HttpMessage outboundRequest = httpOutboundRequest.getNettyHttpRequest(); + if (outboundRequest != null) { + outboundAccessLogMessage.setScheme(outboundRequest.protocolVersion().toString()); + } else { + outboundAccessLogMessage.setScheme(httpOutboundRequest.getHttpVersion()); + } + outboundAccessLogMessage.setRequestBodySize((long) httpOutboundRequest.getContentSize()); + outboundAccessLogMessage.setStatus(inboundResponseMsg.getHttpStatusCode()); + outboundAccessLogMessage.setResponseBodySize(contentLength); + long requestTime = Calendar.getInstance().getTimeInMillis() - + outboundAccessLogMessage.getDateTime().getTimeInMillis(); + outboundAccessLogMessage.setRequestTime(requestTime); + + http2SourceHandler.addHttpAccessLogMessage(outboundAccessLogMessage); + } + + private T getTypedProperty(HttpCarbonMessage request, String propertyName, Class type) { + Object property = request.getProperty(propertyName); + if (type.isInstance(property)) { + return type.cast(property); + } + return null; + } } diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/sender/states/http2/ReceivingEntityBody.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/sender/states/http2/ReceivingEntityBody.java index ac059d85c7..e7e3063e4f 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/sender/states/http2/ReceivingEntityBody.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/sender/states/http2/ReceivingEntityBody.java @@ -18,7 +18,11 @@ package io.ballerina.stdlib.http.transport.contractimpl.sender.states.http2; +import io.ballerina.stdlib.http.api.logging.accesslog.HttpAccessLogConfig; +import io.ballerina.stdlib.http.api.logging.accesslog.HttpAccessLogMessage; +import io.ballerina.stdlib.http.transport.contract.Constants; import io.ballerina.stdlib.http.transport.contractimpl.common.states.Http2MessageStateContext; +import io.ballerina.stdlib.http.transport.contractimpl.listener.http2.Http2SourceHandler; import io.ballerina.stdlib.http.transport.contractimpl.sender.http2.Http2ClientChannel; import io.ballerina.stdlib.http.transport.contractimpl.sender.http2.Http2TargetHandler; import io.ballerina.stdlib.http.transport.contractimpl.sender.http2.OutboundMsgHolder; @@ -31,13 +35,24 @@ import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http2.Http2Exception; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Calendar; + +import static io.ballerina.stdlib.http.transport.contract.Constants.HTTP_X_FORWARDED_FOR; +import static io.ballerina.stdlib.http.transport.contract.Constants.OUTBOUND_ACCESS_LOG_MESSAGE; import static io.ballerina.stdlib.http.transport.contract.Constants.REMOTE_SERVER_CLOSED_WHILE_READING_INBOUND_RESPONSE_BODY; import static io.ballerina.stdlib.http.transport.contract.Constants.REMOTE_SERVER_SENT_GOAWAY_WHILE_READING_INBOUND_RESPONSE_BODY; import static io.ballerina.stdlib.http.transport.contract.Constants.REMOTE_SERVER_SENT_RST_STREAM_WHILE_READING_INBOUND_RESPONSE_BODY; +import static io.ballerina.stdlib.http.transport.contract.Constants.TO; import static io.ballerina.stdlib.http.transport.contractimpl.common.states.Http2StateUtil.releaseContent; import static io.ballerina.stdlib.http.transport.contractimpl.common.states.StateUtil.handleIncompleteInboundMessage; @@ -53,6 +68,7 @@ public class ReceivingEntityBody implements SenderState { private final Http2TargetHandler http2TargetHandler; private final Http2ClientChannel http2ClientChannel; private final Http2TargetHandler.Http2RequestWriter http2RequestWriter; + private Long contentLength = 0L; ReceivingEntityBody(Http2TargetHandler http2TargetHandler, Http2TargetHandler.Http2RequestWriter http2RequestWriter) { @@ -135,6 +151,7 @@ private void onDataRead(Http2DataFrame http2DataFrame, OutboundMsgHolder outboun Http2MessageStateContext http2MessageStateContext) { int streamId = http2DataFrame.getStreamId(); ByteBuf data = http2DataFrame.getData(); + contentLength += data.readableBytes(); boolean endOfStream = http2DataFrame.isEndOfStream(); if (serverPush) { @@ -143,6 +160,7 @@ private void onDataRead(Http2DataFrame http2DataFrame, OutboundMsgHolder outboun onResponseDataRead(outboundMsgHolder, streamId, endOfStream, data); } if (endOfStream) { + updateAccessLogInfo(outboundMsgHolder); http2MessageStateContext.setSenderState(new EntityBodyReceived(http2TargetHandler, http2RequestWriter)); } } @@ -170,4 +188,71 @@ private void onResponseDataRead(OutboundMsgHolder outboundMsgHolder, int streamI responseMessage.addHttpContent(new DefaultHttpContent(data.retain())); } } + + private void updateAccessLogInfo(OutboundMsgHolder outboundMsgHolder) { + HttpCarbonMessage httpOutboundRequest = outboundMsgHolder.getRequest(); + HttpAccessLogMessage outboundAccessLogMessage = + getTypedProperty(httpOutboundRequest, OUTBOUND_ACCESS_LOG_MESSAGE, HttpAccessLogMessage.class); + Http2SourceHandler http2SourceHandler = + getTypedProperty(httpOutboundRequest, Constants.SRC_HANDLER, Http2SourceHandler.class); + if (outboundAccessLogMessage == null || http2SourceHandler == null) { + return; + } + + SocketAddress remoteAddress = http2TargetHandler.getHttp2ClientChannel().getChannel().remoteAddress(); + if (remoteAddress instanceof InetSocketAddress inetSocketAddress) { + InetAddress inetAddress = inetSocketAddress.getAddress(); + outboundAccessLogMessage.setIp(inetAddress.getHostAddress()); + outboundAccessLogMessage.setHost(inetAddress.getHostName()); + outboundAccessLogMessage.setPort(inetSocketAddress.getPort()); + } + if (outboundAccessLogMessage.getIp().startsWith("/")) { + outboundAccessLogMessage.setIp(outboundAccessLogMessage.getIp().substring(1)); + } + + // Populate with header parameters + HttpHeaders headers = httpOutboundRequest.getHeaders(); + if (headers.contains(HTTP_X_FORWARDED_FOR)) { + String forwardedHops = headers.get(HTTP_X_FORWARDED_FOR); + outboundAccessLogMessage.setHttpXForwardedFor(forwardedHops); + // If multiple IPs available, the first ip is the client + int firstCommaIndex = forwardedHops.indexOf(','); + outboundAccessLogMessage.setIp(firstCommaIndex != -1 ? + forwardedHops.substring(0, firstCommaIndex) : forwardedHops); + } + if (headers.contains(HttpHeaderNames.USER_AGENT)) { + outboundAccessLogMessage.setHttpUserAgent(headers.get(HttpHeaderNames.USER_AGENT)); + } + if (headers.contains(HttpHeaderNames.REFERER)) { + outboundAccessLogMessage.setHttpReferrer(headers.get(HttpHeaderNames.REFERER)); + } + HttpAccessLogConfig.getCustomHeaders().forEach(customHeader -> + outboundAccessLogMessage.putCustomHeader(customHeader, headers.contains(customHeader) ? + headers.get(customHeader) : "-")); + + outboundAccessLogMessage.setRequestMethod(httpOutboundRequest.getHttpMethod()); + outboundAccessLogMessage.setRequestUri((String) httpOutboundRequest.getProperty(TO)); + HttpMessage outboundRequest = httpOutboundRequest.getNettyHttpRequest(); + if (outboundRequest != null) { + outboundAccessLogMessage.setScheme(outboundRequest.protocolVersion().toString()); + } else { + outboundAccessLogMessage.setScheme(httpOutboundRequest.getHttpVersion()); + } + outboundAccessLogMessage.setRequestBodySize((long) httpOutboundRequest.getContentSize()); + outboundAccessLogMessage.setStatus(outboundMsgHolder.getResponse().getHttpStatusCode()); + outboundAccessLogMessage.setResponseBodySize(contentLength); + long requestTime = Calendar.getInstance().getTimeInMillis() - + outboundAccessLogMessage.getDateTime().getTimeInMillis(); + outboundAccessLogMessage.setRequestTime(requestTime); + + http2SourceHandler.addHttpAccessLogMessage(outboundAccessLogMessage); + } + + private T getTypedProperty(HttpCarbonMessage request, String propertyName, Class type) { + Object property = request.getProperty(propertyName); + if (type.isInstance(property)) { + return type.cast(property); + } + return null; + } } diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/message/HttpCarbonMessage.java b/native/src/main/java/io/ballerina/stdlib/http/transport/message/HttpCarbonMessage.java index c2d0e47f48..f10a1519c1 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/message/HttpCarbonMessage.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/message/HttpCarbonMessage.java @@ -79,6 +79,7 @@ public class HttpCarbonMessage { private String httpMethod; private String requestUrl; private Integer httpStatusCode; + private Integer contentSize = 0; private boolean contentReleased = false; public HttpCarbonMessage(HttpMessage httpMessage, Listener contentListener) { @@ -144,6 +145,9 @@ public synchronized void addHttpContent(HttpContent httpContent) { public HttpContent getHttpContent() { HttpContent httpContent = this.blockingEntityCollector.getHttpContent(); this.contentObservable.notifyGetListener(httpContent); + if (httpContent != null) { + this.setContentSize(httpContent.content().readableBytes()); + } return httpContent; } @@ -547,6 +551,14 @@ public boolean isPipeliningEnabled() { return pipeliningEnabled; } + public void setContentSize(Integer contentSize) { + this.contentSize = contentSize; + } + + public Integer getContentSize() { + return contentSize; + } + public void setContentReleased(boolean contentReleased) { this.contentReleased = contentReleased; }