Skip to content

Commit

Permalink
[plugin-rest-api] Introduce extended logging for HTTP messages (#4537)
Browse files Browse the repository at this point in the history
Co-authored-by: draker94 <noreply@github.com>
  • Loading branch information
draker94 and web-flow authored Nov 29, 2023
1 parent 71c00c9 commit d924404
Show file tree
Hide file tree
Showing 10 changed files with 503 additions and 18 deletions.
6 changes: 6 additions & 0 deletions docs/modules/plugins/pages/plugin-rest-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ include::partial$plugin-installation.adoc[]
|`<empty>`
|The property family to set HTTP headers for all outgoing requests, e.g. rest-api.http.header.my-sample-header=my-sample-value

|`rest-api.http.extended-logging`
a|`true`
`false`
|`false`
|Enable logging of HTTP request/response headers and bodies (applied to the following content types only: `text/*`, `application/json`, `application/xml`)

|===

See xref:ROOT:tests-configuration.adoc#_http_configuration[HTTP configuration] for more fine-grained control over the HTTP interactions.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright 2019-2023 the original author or authors.
*
* 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
*
* https://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 org.vividus.http;

import static org.apache.hc.core5.http.ContentType.APPLICATION_JSON;
import static org.apache.hc.core5.http.ContentType.APPLICATION_XML;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.hc.core5.http.EntityDetails;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpEntityContainer;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpRequestInterceptor;
import org.apache.hc.core5.http.ProtocolVersion;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.spi.LoggingEventBuilder;
import org.vividus.http.client.HttpResponse;
import org.vividus.http.handler.HttpResponseHandler;

public class ExtendedHttpLoggingInterceptor implements HttpRequestInterceptor, HttpResponseHandler
{
private static final Logger LOGGER = LoggerFactory.getLogger(HttpRequestExecutor.class);

private static final String NEW_LINE = System.lineSeparator();
private static final String HEADERS_FORMAT = String.format("%nHeaders:%n{}");
private static final String BODY_FORMAT = String.format("%nBody:%n{}");

private static final Set<String> LOGGED_CONTENT_TYPES = Set.of(APPLICATION_JSON.getMimeType(),
APPLICATION_XML.getMimeType());

private final boolean extendedLogging;

public ExtendedHttpLoggingInterceptor(boolean extendedLogging)
{
this.extendedLogging = extendedLogging;
}

@Override
public void process(HttpRequest request, EntityDetails entityDetails, HttpContext context) throws IOException
{
String argBrackets = " {}";
LoggingEventBuilder loggingEventBuilder = LOGGER.atInfo();
StringBuilder loggerFormat = new StringBuilder("Request:");

ProtocolVersion protocolVersion = request.getVersion();
if (protocolVersion != null)
{
loggingEventBuilder = loggingEventBuilder.addArgument(protocolVersion);
loggerFormat.append(argBrackets);
}
loggingEventBuilder = loggingEventBuilder.addArgument(request);
loggerFormat.append(argBrackets);

if (extendedLogging)
{
loggingEventBuilder = loggingEventBuilder.addArgument(
() -> Stream.of(request.getHeaders()).map(Object::toString).collect(Collectors.joining(NEW_LINE)));
loggerFormat.append(HEADERS_FORMAT);

if (request instanceof HttpEntityContainer httpEntityContainer)
{
HttpEntity entity = httpEntityContainer.getEntity();
if (entity instanceof StringEntity stringEntity)
{
loggingEventBuilder = loggingEventBuilder.addArgument(() ->
{
try
{
return new String(stringEntity.getContent().readAllBytes(), StandardCharsets.UTF_8);
}
catch (IOException e)
{
return "Unable to get body content: " + e.getMessage();
}
});
loggerFormat.append(BODY_FORMAT);
}
else if (entity != null)
{
int bodySizeInBytes = entity.getContent().available();
loggingEventBuilder = loggingEventBuilder.addArgument(bodySizeInBytes);
loggerFormat.append(NEW_LINE).append("Body: {} bytes of binary data");
}
}
}
loggingEventBuilder.log(loggerFormat.toString());
}

@Override
public void handle(HttpResponse httpResponse)
{
LoggingEventBuilder loggingEventBuilder = LOGGER.atInfo().addArgument(httpResponse.getStatusCode())
.addArgument(httpResponse.getFrom());
StringBuilder loggerFormat = new StringBuilder("Response: status code {}, {}");

if (extendedLogging)
{
Header[] headers = httpResponse.getResponseHeaders();
loggingEventBuilder = loggingEventBuilder
.addArgument(() -> Stream.of(headers).map(Object::toString).collect(Collectors.joining(NEW_LINE)));
loggerFormat.append(HEADERS_FORMAT);

if (httpResponse.getResponseBody() != null)
{
String mimeType = MimeTypeUtils.getMimeTypeFromHeadersWithDefault(headers);
if (mimeType.startsWith("text/") || LOGGED_CONTENT_TYPES.contains(mimeType))
{
loggingEventBuilder = loggingEventBuilder.addArgument(httpResponse::getResponseBodyAsString);
loggerFormat.append(BODY_FORMAT);
}
}
}
loggingEventBuilder.log(loggerFormat.toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2019-2023 the original author or authors.
*
* 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
*
* https://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 org.vividus.http;

import java.util.Optional;
import java.util.stream.Stream;

import org.apache.commons.lang3.StringUtils;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HeaderElement;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.message.MessageSupport;

public final class MimeTypeUtils
{
private MimeTypeUtils()
{
}

/**
* Tries to get value of the "Content-Type" header (case-insensitive).
* If the header does not exist or empty, returns text ("text/plain") MIME type
* @param headers Headers to get MIME type
* @return MIME type
*/
public static String getMimeTypeFromHeadersWithDefault(Header... headers)
{
return getMimeTypeFromHeaders(headers).orElseGet(ContentType.DEFAULT_TEXT::getMimeType);
}

public static Optional<String> getMimeTypeFromHeaders(Header... headers)
{
return Stream.of(headers)
.filter(h -> HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(h.getName())
&& StringUtils.isNotBlank(h.getValue()))
.findFirst()
.map(MessageSupport::parse)
.map(elements -> elements[0])
.map(HeaderElement::getName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,14 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

import org.apache.commons.lang3.StringUtils;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.EntityDetails;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HeaderElement;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpEntityContainer;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpRequestInterceptor;
import org.apache.hc.core5.http.message.MessageSupport;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -63,7 +58,7 @@ public void process(HttpRequest request, EntityDetails entityDetails, HttpContex
HttpEntity entity = httpEntityContainer.getEntity();
if (entity != null)
{
mimeType = getMimeType(request.getHeaders())
mimeType = MimeTypeUtils.getMimeTypeFromHeaders(request.getHeaders())
.orElseGet(() ->
Optional.ofNullable(ContentType.parseLenient(entity.getContentType()))
.orElse(ContentType.DEFAULT_TEXT).getMimeType()
Expand All @@ -88,7 +83,7 @@ public void handle(HttpResponse response) throws IOException
{
Header[] headers = response.getResponseHeaders();
String attachmentTitle = String.format("Response: %s %s", response.getMethod(), response.getFrom());
String mimeType = getMimeType(headers).orElseGet(ContentType.DEFAULT_TEXT::getMimeType);
String mimeType = MimeTypeUtils.getMimeTypeFromHeadersWithDefault(headers);
attachApiMessage(attachmentTitle, headers, response.getResponseBody(), mimeType, response.getStatusCode());
}

Expand All @@ -102,15 +97,4 @@ private void attachApiMessage(String title, Header[] headers, byte[] body, Strin

attachmentPublisher.publishAttachment("/org/vividus/http/attachment/api-message.ftl", dataMap, title);
}

private Optional<String> getMimeType(Header... headers)
{
return Stream.of(headers)
.filter(h -> HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(h.getName())
&& StringUtils.isNotBlank(h.getValue()))
.findFirst()
.map(MessageSupport::parse)
.map(elements -> elements[0])
.map(HeaderElement::getName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Loggers>
<Logger name="org.vividus.http.HttpRequestExecutor" level="INFO" additivity="false">
<AppenderRef ref="console" />
<AppenderRef ref="file" />
</Logger>
</Loggers>
</Configuration>
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Default API endpoint
rest-api.http.endpoint=
rest-api.http.cookie-store-level=global
rest-api.http.extended-logging=false
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
<constructor-arg value="rest-api.http.header." />
</bean>
</property>
<property name="firstRequestInterceptor" ref="extendedHttpLoggingInterceptor"/>
<property name="lastRequestInterceptor" ref="publishingAttachmentInterceptor" />
<property name="lastResponseInterceptor">
<bean class="org.vividus.http.SavingConnectionDetailsHttpResponseInterceptor">
Expand All @@ -69,6 +70,7 @@
</property>
<property name="httpResponseHandlers">
<list>
<ref bean="extendedHttpLoggingInterceptor" />
<ref bean="publishingAttachmentInterceptor" />
</list>
</property>
Expand All @@ -80,6 +82,10 @@
<constructor-arg ref="softAssert" />
</bean>

<bean id="extendedHttpLoggingInterceptor" class="org.vividus.http.ExtendedHttpLoggingInterceptor">
<constructor-arg value="${rest-api.http.extended-logging}" />
</bean>

<bean id="publishingAttachmentInterceptor" class="org.vividus.http.PublishingAttachmentInterceptor" />

<bean id="httpCookieSteps" class="org.vividus.steps.api.HttpCookieSteps"/>
Expand Down
Loading

0 comments on commit d924404

Please sign in to comment.