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

Added support for embedded+compacted json bodies #11

Merged
merged 1 commit into from
Oct 8, 2015
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,27 @@
* #L%
*/

import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import com.google.common.net.MediaType;

import java.io.IOException;
import java.util.Collection;
import java.io.StringWriter;
import java.util.Map;
import java.util.function.Predicate;

public final class JsonHttpLogFormatter implements HttpLogFormatter {

private static final MediaType APPLICATION_JSON = MediaType.create("application", "json");
private static final CharMatcher PRETTY_PRINT = CharMatcher.anyOf("\n\t");

private final ObjectMapper mapper;

public JsonHttpLogFormatter() {
Expand All @@ -54,7 +64,7 @@ public String format(final Precorrelation<HttpRequest> precorrelation) throws IO
builder.put("uri", request.getRequestUri());

addUnless(builder, "headers", request.getHeaders().asMap(), Map::isEmpty);
addUnless(builder, "body", request.getBodyAsString(), String::isEmpty);
addBody(request, builder);

final ImmutableMap<String, Object> content = builder.build();

Expand All @@ -71,7 +81,7 @@ public String format(final Correlation<HttpRequest, HttpResponse> correlation) t
builder.put("correlation", correlationId);
builder.put("status", response.getStatus());
addUnless(builder, "headers", response.getHeaders().asMap(), Map::isEmpty);
addUnless(builder, "body", response.getBodyAsString(), String::isEmpty);
addBody(response, builder);

final ImmutableMap<String, Object> content = builder.build();

Expand All @@ -86,4 +96,74 @@ private static <T> void addUnless(final ImmutableMap.Builder<String, Object> tar
}
}

private void addBody(final HttpMessage request, final ImmutableMap.Builder<String, Object> builder) throws IOException {
final String body = request.getBodyAsString();

if (isJson(request.getContentType())) {
builder.put("body", new JsonBody(compactJson(body)));
} else {
addUnless(builder, "body", request.getBodyAsString(), String::isEmpty);
}
}

private boolean isJson(final String contentType) {
if (contentType.isEmpty()) {
return false;
}

final MediaType mediaType = MediaType.parse(contentType);

final boolean isJson = mediaType.is(APPLICATION_JSON);

final boolean isApplication = mediaType.is(MediaType.ANY_APPLICATION_TYPE);
final boolean isCustomJson = mediaType.subtype().endsWith("+json");

return isJson || isApplication && isCustomJson;
}

private String compactJson(final String json) throws IOException {
if (isAlreadyCompacted(json)) {
return json;
}

final StringWriter output = new StringWriter();
final JsonFactory factory = mapper.getFactory();
final JsonParser parser = factory.createParser(json);

final JsonGenerator generator = factory.createGenerator(output);

// see http://stackoverflow.com/questions/17354150/8-branches-for-try-with-resources-jacoco-coverage-possible
//noinspection TryFinallyCanBeTryWithResources - jacoco can't handle try-with correctly
try {
while (parser.nextToken() != null) {
generator.copyCurrentEvent(parser);
}
} finally {
generator.close();
}

return output.toString();
}

// this wouldn't catch spaces in json, but that's ok for our use case here
private boolean isAlreadyCompacted(final String json) {
return PRETTY_PRINT.matchesNoneOf(json);
}

private static final class JsonBody {

private final String json;

private JsonBody(final String json) {
this.json = json;
}

@JsonRawValue
@JsonValue
public String getJson() {
return json;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@

import static com.jayway.jsonassert.JsonAssert.with;
import static java.util.Collections.singletonList;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;

public final class JsonHttpLogFormatterTest {

Expand All @@ -43,8 +45,9 @@ public void shouldLogRequest() throws IOException {
final HttpRequest request = MockHttpRequest.builder()
.requestUri("/test")
.header("Accept", "application/json")
.header("Content-Type", "text/plain")
.body("Hello, world!")
.header("Date", "Tue, 15 Nov 1994 08:12:31 GMT")
.contentType("application/xml")
.body("<action>test</action>")
.build();

final String json = unit.format(new SimplePrecorrelation<>(correlationId, request));
Expand All @@ -56,8 +59,8 @@ public void shouldLogRequest() throws IOException {
.assertThat("$.uri", is("/test"))
.assertThat("$.headers.*", hasSize(2))
.assertThat("$.headers['Accept']", is(singletonList("application/json")))
.assertThat("$.headers['Content-Type']", is(singletonList("text/plain")))
.assertThat("$.body", is("Hello, world!"));
.assertThat("$.headers['Date']", is(singletonList("Tue, 15 Nov 1994 08:12:31 GMT")))
.assertThat("$.body", is("<action>test</action>"));
}

@Test
Expand Down Expand Up @@ -87,13 +90,69 @@ public void shouldLogRequestWithoutBody() throws IOException {
.assertThat("$", not(hasKey("body")));
}

@Test
public void shouldEmbedJsonRequestBodyAsIs() throws IOException {
final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
final HttpRequest request = MockHttpRequest.builder()
.contentType("application/json")
.body("{\"name\":\"Bob\"}")
.build();

final String json = unit.format(new SimplePrecorrelation<>(correlationId, request));

with(json)
.assertThat("$.body.name", is("Bob"));
}

@Test
public void shouldCompactEmbeddedJsonRequestBody() throws IOException {
final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
final HttpRequest request = MockHttpRequest.builder()
.contentType("application/json")
.body("{\n \"name\": \"Bob\"\n}")
.build();

final String json = unit.format(new SimplePrecorrelation<>(correlationId, request));

assertThat(json, containsString("{\"name\":\"Bob\"}"));
}

@Test
public void shouldEmbedCustomJsonRequestBodyAsIs() throws IOException {
final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
final HttpRequest request = MockHttpRequest.builder()
.contentType("application/custom+json")
.body("{\"name\":\"Bob\"}")
.build();

final String json = unit.format(new SimplePrecorrelation<>(correlationId, request));

with(json)
.assertThat("$.body.name", is("Bob"));
}

@Test
public void shouldNotEmbedCustomTextJsonRequestBodyAsIs() throws IOException {
final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
final HttpRequest request = MockHttpRequest.builder()
.contentType("text/custom+json")
.body("{\"name\":\"Bob\"}")
.build();

final String json = unit.format(new SimplePrecorrelation<>(correlationId, request));

with(json)
.assertThat("$.body", is("{\"name\":\"Bob\"}"));
}

@Test
public void shouldLogResponse() throws IOException {
final String correlationId = "53de2640-677d-11e5-bc84-10ddb1ee7671";
final HttpRequest request = MockHttpRequest.create();
final HttpResponse response = MockHttpResponse.builder()
.header("Content-Type", "application/json")
.body("{\"success\":true}")
.header("Date", "Tue, 15 Nov 1994 08:12:31 GMT")
.contentType("application/xml")
.body("<success>true<success>")
.build();

final String json = unit.format(new SimpleCorrelation<>(correlationId, request, response));
Expand All @@ -102,8 +161,8 @@ public void shouldLogResponse() throws IOException {
.assertThat("$.correlation", is("53de2640-677d-11e5-bc84-10ddb1ee7671"))
.assertThat("$.status", is(200))
.assertThat("$.headers.*", hasSize(1))
.assertThat("$.headers['Content-Type']", is(singletonList("application/json")))
.assertThat("$.body", is("{\"success\":true}"));
.assertThat("$.headers['Date']", is(singletonList("Tue, 15 Nov 1994 08:12:31 GMT")))
.assertThat("$.body", is("<success>true<success>"));
}

@Test
Expand Down Expand Up @@ -133,4 +192,63 @@ public void shouldLogResponseWithoutBody() throws IOException {
.assertThat("$", not(hasKey("body")));
}

@Test
public void shouldEmbedJsonResponseBodyAsIs() throws IOException {
final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
final HttpRequest request = MockHttpRequest.create();
final HttpResponse response = MockHttpResponse.builder()
.contentType("application/json")
.body("{\"name\":\"Bob\"}")
.build();

final String json = unit.format(new SimpleCorrelation<>(correlationId, request, response));

with(json)
.assertThat("$.body.name", is("Bob"));
}

@Test
public void shouldCompactEmbeddedJsonResponseBody() throws IOException {
final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
final HttpRequest request = MockHttpRequest.create();
final HttpResponse response = MockHttpResponse.builder()
.contentType("application/json")
.body("{\n \"name\": \"Bob\"\n}")
.build();

final String json = unit.format(new SimpleCorrelation<>(correlationId, request, response));

assertThat(json, containsString("{\"name\":\"Bob\"}"));
}

@Test
public void shouldEmbedCustomJsonResponseBodyAsIs() throws IOException {
final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
final HttpRequest request = MockHttpRequest.create();
final HttpResponse response = MockHttpResponse.builder()
.contentType("application/custom+json")
.body("{\"name\":\"Bob\"}")
.build();

final String json = unit.format(new SimpleCorrelation<>(correlationId, request, response));

with(json)
.assertThat("$.body.name", is("Bob"));
}

@Test
public void shouldNotEmbedCustomTextJsonResponseBodyAsIs() throws IOException {
final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
final HttpRequest request = MockHttpRequest.create();
final HttpResponse response = MockHttpResponse.builder()
.contentType("text/custom+json")
.body("{\"name\":\"Bob\"}")
.build();

final String json = unit.format(new SimpleCorrelation<>(correlationId, request, response));

with(json)
.assertThat("$.body", is("{\"name\":\"Bob\"}"));
}

}