From 03cc4810b3e8dcd985ea9ff8a7106ad7b65a31d6 Mon Sep 17 00:00:00 2001 From: Bertrand Renuart Date: Thu, 9 Sep 2021 09:05:03 +0200 Subject: [PATCH] Implement a pool of reusable JsonGenerator (#631) Implement a pool of reusable JsonGenerator similar to the ReusableByteBufferPool. `CompositeJsonFormatter` now acts as a factory creating JsonFormatter instances bound to an OutputStream. The output stream is backed by a ReusableByteBuffer. CompositeJsonEncoder/CompositeJsonLayout acquire a reusable JsonFormatter from the pool, write the event to it then flush the underlying buffer into a byte array, a string or an another output stream if it implements StreamingEncoder. This approach reuses the JsonGenerator and the associated memory buffers. It also reduces the number of memory copy operations required to move data to destination. Jackson buffer recycling is also disabled. It works by maintaining a pool of buffers per thread. This strategy works best when one JsonGenerator is created per thread, typically in J2EE environments - which is not the case here. Each JsonFormatter uses its own instance of JsonGenerator and is reused multiple times possibly on different threads. The memory buffers allocated by the JsonGenerator do not belong to a particular thread - hence the recycling feature should be disabled. --- .../composite/CompositeJsonFormatter.java | 76 ++++-- .../logback/encoder/CompositeJsonEncoder.java | 44 ++-- .../logback/layout/CompositeJsonLayout.java | 58 +++-- .../logback/util/ReusableByteBufferPool.java | 105 -------- .../util/ReusableJsonFormatterPool.java | 237 ++++++++++++++++++ ...oggingEventCompositeJsonFormatterTest.java | 4 +- .../encoder/CompositeJsonEncoderTest.java | 25 +- 7 files changed, 365 insertions(+), 184 deletions(-) delete mode 100644 src/main/java/net/logstash/logback/util/ReusableByteBufferPool.java create mode 100644 src/main/java/net/logstash/logback/util/ReusableJsonFormatterPool.java diff --git a/src/main/java/net/logstash/logback/composite/CompositeJsonFormatter.java b/src/main/java/net/logstash/logback/composite/CompositeJsonFormatter.java index 662faf73..3dc8f8ad 100644 --- a/src/main/java/net/logstash/logback/composite/CompositeJsonFormatter.java +++ b/src/main/java/net/logstash/logback/composite/CompositeJsonFormatter.java @@ -15,9 +15,9 @@ */ package net.logstash.logback.composite; +import java.io.Closeable; import java.io.IOException; import java.io.OutputStream; -import java.io.Writer; import java.util.Objects; import java.util.ServiceConfigurationError; @@ -34,6 +34,7 @@ import ch.qos.logback.core.spi.LifeCycle; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonFactory.Feature; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -71,7 +72,7 @@ public abstract class CompositeJsonFormatter jsonProviders = new JsonProviders(); + private JsonProviders jsonProviders = new JsonProviders<>(); private JsonEncoding encoding = JsonEncoding.UTF8; @@ -118,6 +119,41 @@ public boolean isStarted() { return started; } + /** + * Create a reusable {@link JsonFormatter} bound to the given {@link OutputStream}. + * + * @param outputStream the output stream used by the {@link JsonFormatter} + * @return {@link JsonFormatter} writing JSON content in the output stream + * @throws IOException thrown when unable to write in the output stream or when Jackson fails to produce JSON content + */ + public JsonFormatter createJsonFormatter(OutputStream outputStream) throws IOException { + if (!isStarted()) { + throw new IllegalStateException("Formatter is not started"); + } + + JsonGenerator generator = createGenerator(outputStream); + return new JsonFormatter(generator); + } + + + public class JsonFormatter implements Closeable { + private final JsonGenerator generator; + + public JsonFormatter(JsonGenerator generator) { + this.generator = Objects.requireNonNull(generator); + } + + public void writeEvent(Event event) throws IOException { + writeEventToGenerator(generator, event); + } + + @Override + public void close() throws IOException { + this.generator.close(); + } + } + + private JsonFactory createJsonFactory() { ObjectMapper objectMapper = new ObjectMapper() /* @@ -133,27 +169,23 @@ private JsonFactory createJsonFactory() { } } - return this.jsonFactoryDecorator.decorate(objectMapper.getFactory()); + return decorateFactory(objectMapper.getFactory()); } - public void writeEventToOutputStream(Event event, OutputStream outputStream) throws IOException { - try (JsonGenerator generator = createGenerator(outputStream)) { - writeEventToGenerator(generator, event); - } - /* - * Do not flush the outputStream. - * - * Allow something higher in the stack (e.g. the encoder/appender) - * to determine appropriate times to flush. - */ - } - - public void writeEventToWriter(Event event, Writer writer) throws IOException { - try (JsonGenerator generator = createGenerator(writer)) { - writeEventToGenerator(generator, event); - } + private JsonFactory decorateFactory(JsonFactory factory) { + return this.jsonFactoryDecorator.decorate(factory) + /* + * Jackson buffer recycling works by maintaining a pool of buffers per thread. This + * feature works best when one JsonGenerator is created per thread, typically in J2EE + * environments. + * + * Each JsonFormatter uses its own instance of JsonGenerator and is reused multiple times + * possibly on different threads. The memory buffers allocated by the JsonGenerator do + * not belong to a particular thread - hence the recycling feature should be disabled. + */ + .disable(Feature.USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING); } - + protected void writeEventToGenerator(JsonGenerator generator, Event event) throws IOException { if (!isStarted()) { throw new IllegalStateException("Encoding attempted before starting."); @@ -172,10 +204,6 @@ protected void prepareForDeferredProcessing(Event event) { private JsonGenerator createGenerator(OutputStream outputStream) throws IOException { return decorateGenerator(jsonFactory.createGenerator(outputStream, encoding)); } - - private JsonGenerator createGenerator(Writer writer) throws IOException { - return decorateGenerator(jsonFactory.createGenerator(writer)); - } private JsonGenerator decorateGenerator(JsonGenerator generator) { return this.jsonGeneratorDecorator.decorate(generator) diff --git a/src/main/java/net/logstash/logback/encoder/CompositeJsonEncoder.java b/src/main/java/net/logstash/logback/encoder/CompositeJsonEncoder.java index 03befc7d..ccec3dfb 100644 --- a/src/main/java/net/logstash/logback/encoder/CompositeJsonEncoder.java +++ b/src/main/java/net/logstash/logback/encoder/CompositeJsonEncoder.java @@ -18,13 +18,13 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; +import java.util.Objects; import net.logstash.logback.composite.CompositeJsonFormatter; import net.logstash.logback.composite.JsonProviders; import net.logstash.logback.decorate.JsonFactoryDecorator; import net.logstash.logback.decorate.JsonGeneratorDecorator; -import net.logstash.logback.util.ReusableByteBuffer; -import net.logstash.logback.util.ReusableByteBufferPool; +import net.logstash.logback.util.ReusableJsonFormatterPool; import ch.qos.logback.core.encoder.Encoder; import ch.qos.logback.core.encoder.EncoderBase; @@ -46,16 +46,12 @@ public abstract class CompositeJsonEncoder prefix; private Encoder suffix; private final CompositeJsonFormatter formatter; + private ReusableJsonFormatterPool formatterPool; private String lineSeparator = System.lineSeparator(); @@ -65,7 +61,7 @@ public abstract class CompositeJsonEncoder createFormatter(); @@ -75,12 +71,11 @@ public void encode(Event event, OutputStream outputStream) throws IOException { if (!isStarted()) { throw new IllegalStateException("Encoder is not started"); } - - encode(prefix, event, outputStream); - formatter.writeEventToOutputStream(event, outputStream); - encode(suffix, event, outputStream); - outputStream.write(lineSeparatorBytes); + try (ReusableJsonFormatterPool.ReusableJsonFormatter cachedFormatter = formatterPool.acquire()) { + encode(cachedFormatter, event); + cachedFormatter.getBuffer().writeTo(outputStream); + } } @Override @@ -89,18 +84,23 @@ public byte[] encode(Event event) { throw new IllegalStateException("Encoder is not started"); } - ReusableByteBuffer buffer = bufferPool.acquire(); - try { - encode(event, buffer); - return buffer.toByteArray(); + try (ReusableJsonFormatterPool.ReusableJsonFormatter cachedFormatter = formatterPool.acquire()) { + encode(cachedFormatter, event); + return cachedFormatter.getBuffer().toByteArray(); + } catch (IOException e) { addWarn("Error encountered while encoding log event. Event: " + event, e); return EMPTY_BYTES; - } finally { - bufferPool.release(buffer); } } + private void encode(ReusableJsonFormatterPool.ReusableJsonFormatter cachedFormatter, Event event) throws IOException { + encode(prefix, event, cachedFormatter.getBuffer()); + cachedFormatter.write(event); + encode(suffix, event, cachedFormatter.getBuffer()); + cachedFormatter.getBuffer().write(lineSeparatorBytes); + } + private void encode(Encoder encoder, Event event, OutputStream outputStream) throws IOException { if (encoder != null) { byte[] data = encoder.encode(event); @@ -117,7 +117,7 @@ public void start() { } super.start(); - this.bufferPool = new ReusableByteBufferPool(this.minBufferSize); + formatter.setContext(getContext()); formatter.start(); charset = Charset.forName(formatter.getEncoding()); @@ -126,6 +126,8 @@ public void start() { : this.lineSeparator.getBytes(charset); startWrapped(prefix); startWrapped(suffix); + + this.formatterPool = new ReusableJsonFormatterPool<>(formatter, minBufferSize); } @SuppressWarnings({ "unchecked", "rawtypes" }) @@ -168,6 +170,8 @@ public void stop() { formatter.stop(); stopWrapped(prefix); stopWrapped(suffix); + + this.formatterPool = null; } } diff --git a/src/main/java/net/logstash/logback/layout/CompositeJsonLayout.java b/src/main/java/net/logstash/logback/layout/CompositeJsonLayout.java index 9733126f..7356e41c 100644 --- a/src/main/java/net/logstash/logback/layout/CompositeJsonLayout.java +++ b/src/main/java/net/logstash/logback/layout/CompositeJsonLayout.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; +import java.util.Objects; import net.logstash.logback.composite.CompositeJsonFormatter; import net.logstash.logback.composite.JsonProviders; @@ -25,8 +26,7 @@ import net.logstash.logback.decorate.JsonGeneratorDecorator; import net.logstash.logback.encoder.CompositeJsonEncoder; import net.logstash.logback.encoder.SeparatorParser; -import net.logstash.logback.util.ReusableByteBuffer; -import net.logstash.logback.util.ReusableByteBufferPool; +import net.logstash.logback.util.ReusableJsonFormatterPool; import ch.qos.logback.core.Layout; import ch.qos.logback.core.LayoutBase; @@ -61,17 +61,13 @@ public abstract class CompositeJsonLayout */ private int minBufferSize = 1024; - /** - * Provides reusable byte buffers (initialized when layout is started) - */ - private ReusableByteBufferPool bufferPool; - private final CompositeJsonFormatter formatter; - + private ReusableJsonFormatterPool formatterPool; + public CompositeJsonLayout() { super(); - this.formatter = createFormatter(); + this.formatter = Objects.requireNonNull(createFormatter()); } protected abstract CompositeJsonFormatter createFormatter(); @@ -82,26 +78,29 @@ public String doLayout(Event event) { throw new IllegalStateException("Layout is not started"); } - ReusableByteBuffer buffer = this.bufferPool.acquire(); - try (OutputStreamWriter writer = new OutputStreamWriter(buffer)) { + try (ReusableJsonFormatterPool.ReusableJsonFormatter cachedFormatter = formatterPool.acquire()) { + writeEvent(cachedFormatter, event); + return new String(cachedFormatter.getBuffer().toByteArray()); + + } catch (IOException e) { + addWarn("Error formatting logging event", e); + return null; + + } + } + + private void writeEvent(ReusableJsonFormatterPool.ReusableJsonFormatter cachedFormatter, Event event) throws IOException { + try (Writer writer = new OutputStreamWriter(cachedFormatter.getBuffer())) { writeLayout(prefix, writer, event); - writeFormatter(writer, event); + cachedFormatter.write(event); writeLayout(suffix, writer, event); if (lineSeparator != null) { writer.write(lineSeparator); } writer.flush(); - - return new String(buffer.toByteArray()); - } catch (IOException e) { - addWarn("Error formatting logging event", e); - return null; - } finally { - bufferPool.release(buffer); } } - private void writeLayout(Layout wrapped, Writer writer, Event event) throws IOException { if (wrapped == null) { @@ -111,22 +110,25 @@ private void writeLayout(Layout wrapped, Writer writer, Event event) thro String str = wrapped.doLayout(event); if (str != null) { writer.write(str); + writer.flush(); } } - - private void writeFormatter(Writer writer, Event event) throws IOException { - this.formatter.writeEventToWriter(event, writer); - } @Override public void start() { + if (isStarted()) { + return; + } + super.start(); - this.bufferPool = new ReusableByteBufferPool(this.minBufferSize); formatter.setContext(getContext()); formatter.start(); + startWrapped(prefix); startWrapped(suffix); + + this.formatterPool = new ReusableJsonFormatterPool<>(formatter, minBufferSize); } private void startWrapped(Layout wrapped) { @@ -151,10 +153,16 @@ private void startWrapped(Layout wrapped) { @Override public void stop() { + if (!isStarted()) { + return; + } + super.stop(); formatter.stop(); stopWrapped(prefix); stopWrapped(suffix); + + this.formatterPool = null; } private void stopWrapped(Layout wrapped) { diff --git a/src/main/java/net/logstash/logback/util/ReusableByteBufferPool.java b/src/main/java/net/logstash/logback/util/ReusableByteBufferPool.java deleted file mode 100644 index 877e2203..00000000 --- a/src/main/java/net/logstash/logback/util/ReusableByteBufferPool.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2013-2021 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 - * - * 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 net.logstash.logback.util; - -import java.lang.ref.Reference; -import java.lang.ref.SoftReference; -import java.util.Deque; -import java.util.concurrent.ConcurrentLinkedDeque; - -/** - * A pool of {@link ReusableByteBuffer}. - * - *

The pool is technically unbounded but will never hold more buffers than the number of concurrent - * threads accessing it. Buffers are kept in the pool using soft references so they can be garbage - * collected by the JVM when running low in memory. - * - * @author brenuart - */ -public class ReusableByteBufferPool { - - /** - * Pool of reusable buffers. - */ - private final Deque> buffers = new ConcurrentLinkedDeque<>(); - - /** - * The capacity (in bytes) of the initial buffer that is reused across consecutive usages. - */ - private final int initialCapacity; - - /** - * Create a new buffer pool holding buffers with an initial capacity of {@code initialSize} bytes. - * - * @param intialCapacity the initial capacity of buffers created by this pool. - */ - public ReusableByteBufferPool(int intialCapacity) { - if (intialCapacity <= 0) { - throw new IllegalArgumentException("initialCapacity must be greater than 0"); - } - this.initialCapacity = intialCapacity; - } - - /** - * Create a new buffer pool holding buffers with a default initial capacity. - */ - public ReusableByteBufferPool() { - this(ReusableByteBuffer.DEFAULT_INITIAL_CAPACITY); - } - - /** - * Create a new buffer with an initial size of {@link #initialCapacity} bytes. - * - * @return a new buffer instance - */ - private ReusableByteBuffer createBuffer() { - return new ReusableByteBuffer(initialCapacity); - } - - /** - * Get a buffer from the pool or create a new one if none is available. - * The buffer must be returned to the pool after usage by a call to {@link #release(ReusableByteBuffer)}. - * - * @return a reusable byte buffer - */ - public ReusableByteBuffer acquire() { - ReusableByteBuffer buffer = null; - - while (buffer == null) { - Reference ref = buffers.poll(); - if (ref == null) { - break; - } - buffer = ref.get(); - } - - if (buffer == null) { - buffer = createBuffer(); - } - - return buffer; - } - - /** - * Return a buffer to the pool after usage. - * - * @param buffer the buffer to return to the pool. - */ - public void release(ReusableByteBuffer buffer) { - buffer.reset(); - this.buffers.add(new SoftReference<>(buffer)); - } -} diff --git a/src/main/java/net/logstash/logback/util/ReusableJsonFormatterPool.java b/src/main/java/net/logstash/logback/util/ReusableJsonFormatterPool.java new file mode 100644 index 00000000..cb970c01 --- /dev/null +++ b/src/main/java/net/logstash/logback/util/ReusableJsonFormatterPool.java @@ -0,0 +1,237 @@ +/* + * Copyright 2013-2021 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 + * + * 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 net.logstash.logback.util; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.util.Deque; +import java.util.Objects; +import java.util.concurrent.ConcurrentLinkedDeque; + +import net.logstash.logback.composite.CompositeJsonFormatter; +import net.logstash.logback.composite.CompositeJsonFormatter.JsonFormatter; + +import ch.qos.logback.core.spi.DeferredProcessingAware; + +/** + * Pool of {@link ReusableJsonFormatter} that can be safely reused multiple times. + * A {@link ReusableJsonFormatter} is made of an internal {@link ReusableByteBuffer} and a + * {@link CompositeJsonFormatter.JsonFormatter} bound to it. + * + *

Instances must be returned to the pool after use by calling {@link ReusableJsonFormatter#close()} + * or {@link #release(net.logstash.logback.util.ReusableJsonFormatterPool.ReusableJsonFormatter) + * release(ReusableJsonFormatter)}. + * + * Instances are not recycled (and therefore not returned to the pool) after their internal + * {@link CompositeJsonFormatter.JsonFormatter} threw an exception. This is to prevent reusing an + * instance whose internal components are potentially in an unpredictable state. + * + *

The internal byte buffer is created with an initial size of {@link #minBufferSize}. + * The buffer automatically grows above the {@code #minBufferSize} when needed to + * accommodate with larger events. However, only the first {@code minBufferSize} bytes + * will be reused by subsequent invocations. It is therefore strongly advised to set + * the minimum size at least equal to the average size of the encoded events to reduce + * unnecessary memory allocations and reduce pressure on the garbage collector. + * + *

The pool is technically unbounded but will never hold more entries than the number of concurrent + * threads accessing it. Entries are kept in the pool using soft references so they can be garbage + * collected by the JVM when running low in memory. + * + * @author brenuart + */ +public class ReusableJsonFormatterPool { + + /** + * The minimum size of the byte buffer used when encoding events. + */ + private final int minBufferSize; + + /** + * The factory used to create {@link JsonFormatter} instances + */ + private final CompositeJsonFormatter formatterFactory; + + /** + * The pool of reusable JsonFormatter instances. + * May be cleared by the GC when running low in memory. + * + * Note: + * JsonFormatters are not explicitly disposed when the GC clears the SoftReference. This means that + * the underlying Jackson JsonGenerator is not explicitly closed and the associated memory buffers + * are not returned to Jackson's internal memory pools. + * This behavior is desired and makes the associated memory immediately reclaimable - which is what + * we need since we are "running low in memory". + */ + private volatile SoftReference> formatters = new SoftReference<>(null); + + + public ReusableJsonFormatterPool(CompositeJsonFormatter formatterFactory, int minBufferSize) { + this.formatterFactory = Objects.requireNonNull(formatterFactory); + this.minBufferSize = minBufferSize; + } + + /** + * A reusable JsonFormatter holding a JsonFormatter writing inside a dedicated {@link ReusableByteBuffer}. + * Closing the instance returns it to the pool and makes it available for subsequent usage, unless the + * underlying {@link CompositeJsonFormatter.JsonFormatter} threw an exception during its use. + * + *

Note: usage is not thread-safe. + */ + public class ReusableJsonFormatter implements Closeable { + private ReusableByteBuffer buffer; + private CompositeJsonFormatter.JsonFormatter formatter; + private boolean recyclable = true; + + ReusableJsonFormatter(ReusableByteBuffer buffer, CompositeJsonFormatter.JsonFormatter formatter) { + this.buffer = Objects.requireNonNull(buffer); + this.formatter = Objects.requireNonNull(formatter); + } + + /** + * Return the underlying buffer into which the JsonFormatter is writing. + * + * @return the underlying byte buffer + */ + public ReusableByteBuffer getBuffer() { + assertNotDisposed(); + return buffer; + } + + /** + * Write the Event in JSON format into the enclosed buffer using the enclosed JsonFormatter. + * + * @param event the event to write + * @throws IOException thrown when the JsonFormatter has problem to convert the Event into JSON format + */ + public void write(Event event) throws IOException { + assertNotDisposed(); + + try { + this.formatter.writeEvent(event); + + } catch (IOException e) { + // Do not recycle the instance after an exception is thrown: the underlying + // JsonGenerator may not be in a safe state. + this.recyclable = false; + throw e; + } + } + + /** + * Close the JsonFormatter, release associated resources and return it to the pool. + */ + @Override + public void close() throws IOException { + release(this); + } + + /** + * Dispose associated resources + */ + protected void dispose() { + try { + this.formatter.close(); + } catch (IOException e) { + // ignore and proceed + } + + this.formatter = null; + this.buffer = null; + } + + protected boolean isDisposed() { + return buffer == null; + } + + protected void assertNotDisposed() { + if (isDisposed()) { + throw new IllegalStateException("Instance has been disposed and cannot be used anymore. Did you keep a reference to it after it is closed?"); + } + } + } + + + /** + * Get a {@link ReusableJsonFormatter} out of the pool, creating a new one if needed. + * The instance must be closed after use to return it to the pool. + * + * @return a {@link ReusableJsonFormatter} + * @throws IOException thrown when unable to create a new instance + */ + public ReusableJsonFormatter acquire() throws IOException { + ReusableJsonFormatter reusableFormatter = null; + + Deque cachedFormatters = formatters.get(); + if (cachedFormatters != null) { + reusableFormatter = cachedFormatters.poll(); + } + + if (reusableFormatter == null) { + reusableFormatter = createJsonFormatter(); + } + + return reusableFormatter; + } + + + /** + * Return an instance to the pool. + * An alternative is to call {@link ReusableJsonFormatter#close()}. + * + * @param reusableFormatter the instance to return to the pool + */ + public void release(ReusableJsonFormatter reusableFormatter) { + if (reusableFormatter == null) { + return; + } + + /* + * Dispose the formatter instead of returning to the pool when marked not recyclable + */ + if (!reusableFormatter.recyclable) { + reusableFormatter.dispose(); + return; + } + + Deque cachedFormatters = this.formatters.get(); + if (cachedFormatters == null) { + cachedFormatters = new ConcurrentLinkedDeque<>(); + this.formatters = new SoftReference<>(cachedFormatters); + } + + /* + * Reset the internal buffer and return the cached JsonFormatter to the pool. + */ + reusableFormatter.getBuffer().reset(); + cachedFormatters.addFirst(reusableFormatter); // try to reuse the same as much as we can -> add it first + } + + + /** + * Create a new {@link ReusableJsonFormatter} instance by allocating a new {@link ReusableByteBuffer} + * and a {@link CompositeJsonFormatter.JsonFormatter} bound to it. + * + * @return a new {@link ReusableJsonFormatter} + * @throws IOException thrown when the {@link CompositeJsonFormatter} is unable to create a new instance + * of {@link CompositeJsonFormatter.JsonFormatter}. + */ + protected ReusableJsonFormatter createJsonFormatter() throws IOException { + ReusableByteBuffer buffer = new ReusableByteBuffer(this.minBufferSize); + CompositeJsonFormatter.JsonFormatter jsonFormatter = this.formatterFactory.createJsonFormatter(buffer); + return new ReusableJsonFormatter(buffer, jsonFormatter); + } +} diff --git a/src/test/java/net/logstash/logback/composite/loggingevent/LoggingEventCompositeJsonFormatterTest.java b/src/test/java/net/logstash/logback/composite/loggingevent/LoggingEventCompositeJsonFormatterTest.java index f36d582f..f8e10c04 100644 --- a/src/test/java/net/logstash/logback/composite/loggingevent/LoggingEventCompositeJsonFormatterTest.java +++ b/src/test/java/net/logstash/logback/composite/loggingevent/LoggingEventCompositeJsonFormatterTest.java @@ -22,6 +22,7 @@ import java.io.IOException; import net.logstash.logback.argument.StructuredArguments; +import net.logstash.logback.composite.CompositeJsonFormatter.JsonFormatter; import net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder; import ch.qos.logback.classic.spi.ILoggingEvent; @@ -52,7 +53,8 @@ public void testDoesNotFailOnEmptyBeans() throws IOException { * This should not throw an exception, since SerializationFeature.FAIL_ON_EMPTY_BEANS is disabled */ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { - assertThatCode(() -> formatter.writeEventToOutputStream(event, bos)).doesNotThrowAnyException(); + JsonFormatter jsonFormatter = formatter.createJsonFormatter(bos); + assertThatCode(() -> jsonFormatter.writeEvent(event)).doesNotThrowAnyException(); } } diff --git a/src/test/java/net/logstash/logback/encoder/CompositeJsonEncoderTest.java b/src/test/java/net/logstash/logback/encoder/CompositeJsonEncoderTest.java index 3e8ad080..5412036b 100644 --- a/src/test/java/net/logstash/logback/encoder/CompositeJsonEncoderTest.java +++ b/src/test/java/net/logstash/logback/encoder/CompositeJsonEncoderTest.java @@ -20,7 +20,6 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -45,6 +44,7 @@ import ch.qos.logback.core.status.StatusManager; import ch.qos.logback.core.status.WarnStatus; import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonGenerator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -57,7 +57,7 @@ public class CompositeJsonEncoderTest { @InjectMocks - private final CompositeJsonEncoder encoder = new TestCompositeJsonEncoder(); + private final TestCompositeJsonEncoder encoder = new TestCompositeJsonEncoder(); private CompositeJsonFormatter formatter; @@ -218,20 +218,17 @@ public void testLineEndings() { /* - * Failure to encode log event should log an warning status + * Failure to encode event should log an warning status */ @Test public void testIOException() throws IOException { + encoder.exceptionToThrow = new IOException(); encoder.start(); - IOException exception = new IOException(); - - doThrow(exception).when(formatter).writeEventToOutputStream(eq(event), any(OutputStream.class)); - encoder.encode(event); verify(statusManager).add(new WarnStatus("Error encountered while encoding log event. " - + "Event: " + event, context, exception)); + + "Event: " + event, context, encoder.exceptionToThrow)); } @@ -258,9 +255,19 @@ public void testIOException_streaming() throws IOException { private static class TestCompositeJsonEncoder extends CompositeJsonEncoder { + private IOException exceptionToThrow; + @Override protected CompositeJsonFormatter createFormatter() { - CompositeJsonFormatter formatter = spy(new CompositeJsonFormatter(this) { }); + CompositeJsonFormatter formatter = spy(new CompositeJsonFormatter(this) { + @Override + protected void writeEventToGenerator(JsonGenerator generator, ILoggingEvent event) throws IOException { + if (exceptionToThrow != null) { + throw exceptionToThrow; + } + super.writeEventToGenerator(generator, event); + } + }); formatter.getProviders().addProvider(new TestJsonProvider()); return formatter; }