Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of DefaultHttpLogFormatter #524

Merged
merged 5 commits into from
Jun 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 20 additions & 10 deletions logbook-api/src/main/java/org/zalando/logbook/RequestURI.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,36 @@ enum Component {
}

static String reconstruct(final HttpRequest request) {
return reconstruct(request, EnumSet.allOf(Component.class));
final StringBuilder url = new StringBuilder();
reconstruct(request, url);
return url.toString();
}

static void reconstruct(final HttpRequest request, StringBuilder output) {
reconstruct(request, EnumSet.allOf(Component.class), output);
}

static String reconstruct(final HttpRequest request, final Component... components) {
return reconstruct(request, EnumSet.copyOf(asList(components)));
final StringBuilder url = new StringBuilder();
reconstruct(request, EnumSet.copyOf(asList(components)), url);
return url.toString();
}

static String reconstruct(final HttpRequest request, final Set<Component> components) {
final StringBuilder url = new StringBuilder();
reconstruct(request, components, url);
return url.toString();
}

private static String reconstruct(final HttpRequest request, final Set<Component> components) {
private static void reconstruct(final HttpRequest request, final Set<Component> components, StringBuilder url) {
final String scheme = request.getScheme();
final String host = request.getHost();
final Optional<Integer> port = request.getPort();
final String path = request.getPath();
final String query = request.getQuery();

final StringBuilder url = new StringBuilder();

if (components.contains(SCHEME)) {
url.append(scheme).append(":");
url.append(scheme).append(':');
}

if (components.contains(AUTHORITY)) {
Expand All @@ -63,13 +75,11 @@ private static String reconstruct(final HttpRequest request, final Set<Component
if (components.contains(QUERY) && !query.isEmpty()) {
url.append('?').append(query);
}

return url.toString();
}

private static boolean isNotStandardPort(final String scheme, final int port) {
return "http".equals(scheme) && port != 80 ||
"https".equals(scheme) && port != 443;
return ("http".equals(scheme) && port != 80) ||
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are not really needed, are they?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly, no, it was intended as an improvement in readability, forgot to comment.

("https".equals(scheme) && port != 443);
}

}
17 changes: 15 additions & 2 deletions logbook-api/src/test/java/org/zalando/logbook/RequestURITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.zalando.logbook.RequestURI.Component;

import java.util.EnumSet;
import java.util.Optional;

import static org.hamcrest.MatcherAssert.assertThat;
Expand Down Expand Up @@ -101,13 +103,24 @@ void shouldReconstructWithoutSchemeAuthorityAndPath() {

@Test
void shouldUseComponentValueOf() {
RequestURI.Component.valueOf("SCHEME");
Component.valueOf("SCHEME");
}

@Test
void shouldUseOriginValueOf() {
Origin.valueOf("LOCAL");
}


@Test
void shouldReconstructUsingBuilder() {
StringBuilder builder = new StringBuilder();
reconstruct(request, builder);
assertThat(builder.toString(), is("http://localhost/admin?limit=1"));
}

@Test
void shouldReconstructSpecificComponents() {
String r = reconstruct(request, EnumSet.of(SCHEME, AUTHORITY, PATH));
assertThat(r, is("http://localhost/admin"));
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,56 @@
package org.zalando.logbook;

import org.apiguardian.api.API;
import static org.apiguardian.api.API.Status.STABLE;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.STABLE;
import static org.zalando.logbook.Origin.REMOTE;
import org.apiguardian.api.API;

@API(status = STABLE)
public final class DefaultHttpLogFormatter implements HttpLogFormatter {

/**
* Produces an HTTP-like request in individual lines.
*
* @param precorrelation the request correlation
* @param request the HTTP request
* @return a line-separated HTTP request
* @throws IOException if reading body fails
*/

@Override
public String format(final Precorrelation precorrelation, final HttpRequest request) throws IOException {
return format(prepare(precorrelation, request));
public String format(Precorrelation precorrelation, HttpRequest request) throws IOException {
final String correlationId = precorrelation.getId();

String body = request.getBodyAsString();

StringBuilder builder = new StringBuilder(body.length() + 2048);

builder.append(direction(request));
builder.append(" Request: ");
builder.append(correlationId);
builder.append('\n');

builder.append(request.getMethod());
builder.append(' ');
RequestURI.reconstruct(request, builder);
builder.append(' ');
builder.append(request.getProtocolVersion());
builder.append('\n');

writeHeaders(builder, request);

if (!body.isEmpty()) {
builder.append('\n');
builder.append(body);
} else {
builder.setLength(builder.length() - 1); // discard last newline
}

return builder.toString();
}

/**
Expand All @@ -32,95 +63,66 @@ public String format(final Precorrelation precorrelation, final HttpRequest requ
* @see #format(List)
* @see StructuredHttpLogFormatter#prepare(Precorrelation, HttpRequest)
*/
@API(status = EXPERIMENTAL)
public List<String> prepare(final Precorrelation precorrelation, final HttpRequest request) throws IOException {
final String requestLine = String.format("%s %s %s", request.getMethod(), request.getRequestUri(),
request.getProtocolVersion());
return prepare(request, "Request", precorrelation.getId(), requestLine);
}

@Override
public String format(final Correlation correlation, final HttpResponse response) throws IOException {
return format(prepare(correlation, response));
}

/**
* Produces an HTTP-like response in individual lines.
* <p>
* Pr@param correlation the response correlation
*
* @param correlation the correlated request and response pair
* @return a line-separated HTTP response
* @throws IOException if reading body fails
* @see #prepare(Precorrelation, HttpRequest)
* @see #format(List)
* @see StructuredHttpLogFormatter#prepare(Correlation, HttpResponse)
*/
@API(status = EXPERIMENTAL)
public List<String> prepare(final Correlation correlation, final HttpResponse response) throws IOException {
final StringBuilder statusLineBuilder = new StringBuilder(64);
statusLineBuilder.append(response.getProtocolVersion());
statusLineBuilder.append(' ');
statusLineBuilder.append(response.getStatus());
final String correlationId = correlation.getId();

String body = response.getBodyAsString();

StringBuilder builder = new StringBuilder(body.length() + 2048);

builder.append(direction(response));
builder.append(" Response: ");
builder.append(correlationId);
builder.append("\nDuration: ");
builder.append(correlation.getDuration().toMillis());
builder.append(" ms\n");

builder.append(response.getProtocolVersion());
builder.append(' ');
builder.append(response.getStatus());
final String reasonPhrase = response.getReasonPhrase();
if(reasonPhrase != null) {
statusLineBuilder.append(' ');
statusLineBuilder.append(reasonPhrase);
builder.append(' ');
builder.append(reasonPhrase);
}
return prepare(response, "Response", correlation.getId(),
"Duration: " + correlation.getDuration().toMillis() + " ms", statusLineBuilder.toString());
}

private <H extends HttpMessage> List<String> prepare(final H message, final String type,
final String correlationId, final String... prefixes) throws IOException {
final List<String> lines = new ArrayList<>();

lines.add(direction(message) + " " + type + ": " + correlationId);
Collections.addAll(lines, prefixes);
lines.addAll(formatHeaders(message));

builder.append('\n');

final String body = message.getBodyAsString();
writeHeaders(builder, response);

if (!body.isEmpty()) {
lines.add("");
lines.add(body);
builder.append('\n');
builder.append(body);
} else {
builder.setLength(builder.length() - 1); // discard last newline
}

return lines;
}

private String direction(final HttpMessage request) {
return request.getOrigin() == REMOTE ? "Incoming" : "Outgoing";
}

private List<String> formatHeaders(final HttpMessage message) {
return message.getHeaders().entrySet().stream()
.collect(toMap(Map.Entry::getKey, this::formatHeaderValues))
.entrySet().stream()
.map(this::formatHeader)
.collect(toList());
}

private String formatHeaderValues(final Map.Entry<String, List<String>> entry) {
return String.join(", ", entry.getValue());

return builder.toString();
}

private String formatHeader(final Map.Entry<String, String> entry) {
return String.format("%s: %s", entry.getKey(), entry.getValue());
private void writeHeaders(StringBuilder builder, HttpMessage httpMessage) {
Map<String, List<String>> headers = httpMessage.getHeaders();

if(!headers.isEmpty()) {
for (Entry<String, List<String>> entry : headers.entrySet()) {
builder.append(entry.getKey());
builder.append(": ");
List<String> headerValues = entry.getValue();
if(!headerValues.isEmpty()) {
for(String value : entry.getValue()) {
builder.append(value);
builder.append(", ");
}
builder.setLength(builder.length() - 2); // discard last comma
}
builder.append('\n');
}
}
}

/**
* Renders an HTTP-like message into a printable string.
*
* @param lines lines of an HTTP message
* @return the whole message as a single string, separated by new lines
* @see #prepare(Precorrelation, HttpRequest)
* @see #prepare(Correlation, HttpResponse)
* @see StructuredHttpLogFormatter#format(Map)
*/
@API(status = EXPERIMENTAL)
public String format(final List<String> lines) {
return String.join("\n", lines);
private String direction(final HttpMessage request) {
return request.getOrigin() == Origin.REMOTE ? "Incoming" : "Outgoing";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
import org.zalando.logbook.DefaultLogbook.SimplePrecorrelation;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import static java.time.Clock.systemUTC;
import static java.time.Duration.ofMillis;
Expand Down Expand Up @@ -131,5 +136,30 @@ void shouldLogResponseForUnknownStatusCode() throws IOException {
"\n" +
"{\"success\":true}"));
}


@Test
void shouldLogResponseForEmptyHeader() throws IOException {
final String correlationId = "2d51bc02-677e-11e5-8b9b-10ddb1ee7671";

Map<String, List<String>> headers = new TreeMap<>();
headers.put("Content-Type", Arrays.asList("application/json"));
headers.put("X-Empty-Header", Collections.emptyList());

final HttpResponse response = MockHttpResponse.create()
.withProtocolVersion("HTTP/1.0")
.withOrigin(Origin.REMOTE)
.withStatus(201)
.withHeaders(headers)
.withBodyAsString("{\"success\":true}");

final String http = unit.format(new SimpleCorrelation(correlationId, ofMillis(125)), response);

assertThat(http, is("Incoming Response: 2d51bc02-677e-11e5-8b9b-10ddb1ee7671\n" +
"Duration: 125 ms\n" +
"HTTP/1.0 201 Created\n" +
"Content-Type: application/json\n" +
"X-Empty-Header: \n" +
"\n" +
"{\"success\":true}"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ private void shouldLogRequestBody(final String contentType, final String content
assertThat(request, endsWith(
"GET http://localhost/api/sync HTTP/1.1\n" +
"Accept: application/json\n" +
"Host: localhost\n" +
"Content-Type: " + contentType + "\n" +
"Host: localhost\n" +
"\n" +
content));
}
Expand All @@ -116,8 +116,8 @@ void shouldNotLogFormRequestOff() throws Exception {
assertThat(request, endsWith(
"GET http://localhost/api/sync HTTP/1.1\n" +
"Accept: application/json\n" +
"Host: localhost\n" +
"Content-Type: application/x-www-form-urlencoded"));
"Content-Type: application/x-www-form-urlencoded\n" +
"Host: localhost"));
}

@Test
Expand Down