diff --git a/README.md b/README.md index 741b165f9..f44416845 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,8 @@ a JSON response body will **not** be escaped and represented as a string: #### Writing -Writing defines where formatted requests and responses are written to. Logbook comes with two implementations: Logger and Stream. +Writing defines where formatted requests and responses are written to. Logbook comes with three implementations: +Logger, Stream and Chunking. ##### Logger @@ -267,10 +268,18 @@ Logbook logbook = Logbook.builder() An alternative implementation is to log requests and responses to a `PrintStream`, e.g. `System.out` or `System.err`. This is usually a bad choice for running in production, but can sometimes be useful for short-term local development and/or investigation. +```java +``` + +##### Chunking + +The `ChunkingHttpLogWriter` will split long messages into smaller chunks and will write them individually while delegating to another writer: + ```java Logbook logbook = Logbook.builder() - .writer(new StreamHttpLogWriter(System.err)) + .writer(new ChunkingHttpLogWriter(1000, new DefaultHttpLogWriter())) .build(); + ``` ### Servlet @@ -381,6 +390,7 @@ The following tables show the available configuration: | `logbook.obfuscate.parameters` | List of parameter names that need obfuscation | `[access_token]` | | `logbook.write.category` | Changes the category of the [`DefaultHttpLogWriter`](#logger) | `org.zalando.logbook.Logbook` | | `logbook.write.level` | Changes the level of the [`DefaultHttpLogWriter`](#logger) | `TRACE` | +| `logbook.write.chunk-size` | Splits log lines into smaller chunks. | `0` (disabled) | ##### Example configuration @@ -401,6 +411,7 @@ logbook: write: category: http.wire-log level: INFO + chunk-size: 1000 ``` ## Known Issues diff --git a/logbook-core/src/main/java/org/zalando/logbook/ChunkingHttpLogWriter.java b/logbook-core/src/main/java/org/zalando/logbook/ChunkingHttpLogWriter.java new file mode 100644 index 000000000..5b64dfc99 --- /dev/null +++ b/logbook-core/src/main/java/org/zalando/logbook/ChunkingHttpLogWriter.java @@ -0,0 +1,42 @@ +package org.zalando.logbook; + +import org.zalando.logbook.DefaultLogbook.SimpleCorrelation; +import org.zalando.logbook.DefaultLogbook.SimplePrecorrelation; + +import java.io.IOException; +import java.util.regex.Pattern; + +public final class ChunkingHttpLogWriter implements HttpLogWriter { + + private final Pattern pattern; + private final HttpLogWriter writer; + + public ChunkingHttpLogWriter(final int size, final HttpLogWriter writer) { + this.pattern = Pattern.compile("(?<=\\G.{" + size + "})"); + this.writer = writer; + } + + @Override + public boolean isActive(final RawHttpRequest request) throws IOException { + return writer.isActive(request); + } + + @Override + public void writeRequest(final Precorrelation precorrelation) throws IOException { + for (final String part : split(precorrelation.getRequest())) { + writer.writeRequest(new SimplePrecorrelation<>(precorrelation.getId(), part)); + } + } + + @Override + public void writeResponse(final Correlation correlation) throws IOException { + for (final String part : split(correlation.getResponse())) { + writer.writeResponse(new SimpleCorrelation<>(correlation.getId(), correlation.getRequest(), part)); + } + } + + private String[] split(final String s) { + return pattern.split(s); + } + +} diff --git a/logbook-core/src/test/java/org/zalando/logbook/ChunkingHttpLogWriterTest.java b/logbook-core/src/test/java/org/zalando/logbook/ChunkingHttpLogWriterTest.java new file mode 100644 index 000000000..e619268c5 --- /dev/null +++ b/logbook-core/src/test/java/org/zalando/logbook/ChunkingHttpLogWriterTest.java @@ -0,0 +1,87 @@ +package org.zalando.logbook; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.runners.MockitoJUnitRunner; +import org.zalando.logbook.DefaultLogbook.SimplePrecorrelation; + +import java.io.IOException; +import java.util.List; + +import static java.util.stream.Collectors.toList; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public final class ChunkingHttpLogWriterTest { + + private final HttpLogWriter delegate = mock(HttpLogWriter.class); + private final HttpLogWriter unit = new ChunkingHttpLogWriter(10, delegate); + + @Captor + private ArgumentCaptor> requestCaptor; + + @Captor + private ArgumentCaptor> responseCaptor; + + @Test + public void shouldDelegateActive() throws IOException { + final RawHttpRequest request = mock(RawHttpRequest.class); + assertThat(unit.isActive(request), is(false)); + } + + @Test + public void shouldWriteSingleRequestIfLengthNotExceeded() throws IOException { + final List precorrelation = captureRequest("Hello"); + assertThat(precorrelation, contains("Hello")); + } + + @Test + public void shouldWriteRequestInChunksIfLengthExceeded() throws IOException { + final List precorrelation = captureRequest("Lorem ipsum dolor sit amet, consectetur adipiscing elit"); + assertThat(precorrelation, + contains("Lorem ipsu", "m dolor si", "t amet, co", "nsectetur ", "adipiscing", " elit")); + } + + private List captureRequest(final String request) throws IOException { + unit.writeRequest(new SimplePrecorrelation<>("id", request)); + + verify(delegate, atLeastOnce()).writeRequest(requestCaptor.capture()); + + return requestCaptor.getAllValues().stream() + .map(Precorrelation::getRequest) + .collect(toList()); + } + + @Test + public void shouldWriteSingleResponseIfLengthNotExceeded() throws IOException { + final List precorrelation = captureResponse("Hello"); + assertThat(precorrelation, contains("Hello")); + + } + + @Test + public void shouldWriteResponseInChunksIfLengthExceeded() throws IOException { + final List precorrelation = captureResponse("Lorem ipsum dolor sit amet, consectetur adipiscing elit"); + assertThat(precorrelation, + contains("Lorem ipsu", "m dolor si", "t amet, co", "nsectetur ", "adipiscing", " elit")); + } + + private List captureResponse(final String response) throws IOException { + unit.writeResponse(new DefaultLogbook.SimpleCorrelation<>("id", "", response)); + + verify(delegate, atLeastOnce()).writeResponse(responseCaptor.capture()); + + return responseCaptor.getAllValues().stream() + .map(Correlation::getResponse) + .collect(toList()); + } + + +} \ No newline at end of file diff --git a/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/LogbookAutoConfiguration.java b/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/LogbookAutoConfiguration.java index 7c4588fb5..aa430f083 100644 --- a/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/LogbookAutoConfiguration.java +++ b/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/LogbookAutoConfiguration.java @@ -19,6 +19,7 @@ import org.springframework.core.Ordered; import org.zalando.logbook.BodyFilter; import org.zalando.logbook.BodyFilters; +import org.zalando.logbook.ChunkingHttpLogWriter; import org.zalando.logbook.Conditions; import org.zalando.logbook.DefaultHttpLogFormatter; import org.zalando.logbook.DefaultHttpLogWriter; @@ -42,7 +43,6 @@ import javax.servlet.Filter; import java.util.List; -import java.util.Optional; import java.util.function.Predicate; import static java.util.stream.Collectors.toList; @@ -195,18 +195,18 @@ public HttpLogFormatter jsonFormatter( @Bean @ConditionalOnMissingBean(HttpLogWriter.class) public HttpLogWriter writer(final Logger httpLogger) { - final Level level = properties.getWrite().getLevel(); + final LogbookProperties.Write write = properties.getWrite(); + final Level level = write.getLevel(); + final int size = write.getChunkSize(); - return level == null ? - new DefaultHttpLogWriter(httpLogger) : - new DefaultHttpLogWriter(httpLogger, level); + final HttpLogWriter writer = new DefaultHttpLogWriter(httpLogger, level); + return size > 0 ? new ChunkingHttpLogWriter(size, writer) : writer; } @Bean @ConditionalOnMissingBean(name = "httpLogger") public Logger httpLogger() { - final String category = properties.getWrite().getCategory(); - return LoggerFactory.getLogger(Optional.ofNullable(category).orElseGet(Logbook.class::getName)); + return LoggerFactory.getLogger(properties.getWrite().getCategory()); } } diff --git a/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/LogbookProperties.java b/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/LogbookProperties.java index 7d2963c2b..1e1c8264a 100644 --- a/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/LogbookProperties.java +++ b/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/LogbookProperties.java @@ -2,6 +2,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.zalando.logbook.DefaultHttpLogWriter.Level; +import org.zalando.logbook.Logbook; import javax.annotation.Nullable; import java.util.ArrayList; @@ -43,10 +44,10 @@ public List getParameters() { public static class Write { - private String category; - private Level level; + private String category = Logbook.class.getName(); + private Level level = Level.TRACE; + private int chunkSize = 0; - @Nullable public String getCategory() { return category; } @@ -55,7 +56,6 @@ public void setCategory(final String category) { this.category = category; } - @Nullable public Level getLevel() { return level; } @@ -64,6 +64,14 @@ public void setLevel(final Level level) { this.level = level; } + public int getChunkSize() { + return chunkSize; + } + + public void setChunkSize(final int chunkSize) { + this.chunkSize = chunkSize; + } + } } diff --git a/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/WriteChunkingTest.java b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/WriteChunkingTest.java new file mode 100644 index 000000000..293955aa6 --- /dev/null +++ b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/WriteChunkingTest.java @@ -0,0 +1,28 @@ +package org.zalando.logbook.spring; + +import org.junit.Test; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; +import org.zalando.logbook.ChunkingHttpLogWriter; +import org.zalando.logbook.HttpLogWriter; + +import java.io.IOException; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hobsoft.hamcrest.compose.ComposeMatchers.hasFeature; +import static org.junit.Assert.assertThat; + +@TestPropertySource(properties = "logbook.write.chunk-size = 100") +public final class WriteChunkingTest extends AbstractTest { + + @Autowired + private HttpLogWriter writer; + + @Test + public void shouldUseChunkingWriter() throws IOException { + assertThat(writer, is(instanceOf(ChunkingHttpLogWriter.class))); + } + +}