LogEncoder.java
package io.jstach.rainbowgum;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import io.jstach.rainbowgum.LogOutput.ContentType;
import io.jstach.rainbowgum.LogOutput.ContentType.StandardContentType;
import io.jstach.rainbowgum.LogOutput.WriteMethod;
import io.jstach.rainbowgum.format.TTLL;
import io.jstach.rainbowgum.format.StandardEventFormatter;
/**
* Encodes a {@link LogEvent} into a buffer of its choosing. While the {@link Buffer} does
* not need to be thread-safe the encoder itself should be.
* <p>
* An appender typically calls an encoder by first {@linkplain #buffer(BufferHints)
* creating a buffer} that the encoder knows about or reusing an existing {@link Buffer}
* the encoder knows about.
* <p>
* The {@linkplain #encode(LogEvent, Buffer) encoding into a buffer} typically happens
* outside of lock to minimize lock contention. Thus the appender promises not to share a
* buffer at the same time with other threads as well as only use a buffer the log encoder
* created at some point.
* <p>
* Once encoding is done the appender than typically enters into a lock (appenders
* attached to an async publisher may not need to use a lock) where the appender will ask
* the buffer to {@linkplain Buffer#drain(LogOutput, LogEvent) drain} its contents into
* the output.
* <p>
* Because {@link Buffer} is not a specific implementation the Encoder typically casts the
* buffer to the expected concrete implementation. {@link AbstractEncoder} can make this
* logic easier and is recommended to extend it.
* <p>
* Given the complexity of encoders it is recommend use the much easier to implement
* interface of {@link LogFormatter} and convert it to an encoder with
* {@link #of(LogFormatter)}.
*
* @see LogFormatter
* @see Buffer
* @see LogAppender
*/
public interface LogEncoder {
/**
* Creates a <strong>new</strong> buffer. The encoder should not try to reuse buffers
* as that is the responsibility of the {@linkplain LogAppender appender} (and
* possibly {@link LogOutput} but usually not). Hints can be retrieved by call
* {@link LogOutput#bufferHints()}.
* @param hints hints are like size and storage type etc.
* @return a new buffer.
* @apiNote hints can be retrieved by calling {@link LogOutput#bufferHints()} the
* reason the output itself is not passed is to prevent the buffer from using the
* output directly at an inappropriate time as well as the rare possibility of the
* buffer being used by multiple outputs.
*/
public Buffer buffer(BufferHints hints);
/**
* Encodes an event to the buffer. It is recommended that the encoder call
* {@link Buffer#clear()} before using.
* @param event log event.
* @param buffer buffer created from {@link #buffer(BufferHints)}.
*/
public void encode(LogEvent event, Buffer buffer);
/**
* Creates an encoder from a formatter that encodes with
* {@link StandardCharsets#UTF_8}. For anything more than the default charset/content
* type/max buffer size use {@link #builder(LogFormatter)}.
* @param formatter formatter.
* @return encoder.
*/
public static LogProvider<LogEncoder> of(LogFormatter formatter) {
return builder(formatter).build();
}
/**
* Creates a builder for an encoder backed by the given formatter.
* @param formatter formatter.
* @return builder.
*/
public static Builder builder(LogFormatter formatter) {
return new Builder(formatter);
}
/**
* Creates a builder for an encoder backed by the default TTLL
* ({@link io.jstach.rainbowgum.format.StandardEventFormatter}) formatter.
* @return builder.
*/
public static Builder builder() {
return new Builder(StandardEventFormatter.builder().build());
}
/**
* Builds a {@link LogEncoder} out of a {@link LogFormatter}.
*
* @apiNote created with {@link LogEncoder#builder(LogFormatter)} or
* {@link LogEncoder#builder()}.
*/
public static final class Builder {
private final LogFormatter formatter;
private @Nullable Charset charset;
private @Nullable ContentType contentType;
private int maxBufferSize = -1;
private int initialBufferSize = DirectByteBufferBuffer.DEFAULT_INITIAL_BYTE_CAPACITY;
private @Nullable Boolean useGetBytes;
private @Nullable String description;
private Builder(LogFormatter formatter) {
this.formatter = Objects.requireNonNull(formatter);
}
/**
* A human readable description of the formatter/pattern this encoder was built
* from (e.g. the actual pattern string), surfaced by
* {@link LogReporter.Reportable} instead of the built encoder only ever showing
* up as its bare concrete class name. Purely diagnostic: has no effect on
* encoding itself.
* @param description description, or <code>null</code> for none.
* @return this.
*/
public Builder description(@Nullable String description) {
this.description = description;
return this;
}
/**
* Charset to encode with. Only affects outputs whose
* {@link BufferHints#writeMethod()} is {@link WriteMethod#BYTES} or
* {@link WriteMethod#BYTE_BUFFER} - the {@link WriteMethod#STRING} path hands a
* plain {@link String} to {@link LogOutput#write(LogEvent, String)}, whose own
* default implementation is fixed to {@link StandardCharsets#UTF_8} regardless of
* this setting, but no built-in output actually uses that write method. Defaults
* to {@link StandardCharsets#UTF_8}, or to {@link #contentType(ContentType)}'s
* own charset if that was set and this was not. If both are set this charset is
* what is actually used to encode; {@link #contentType(ContentType)} still
* controls what is reported to the output.
* @param charset charset to encode with.
* @return this.
*/
public Builder charset(Charset charset) {
this.charset = Objects.requireNonNull(charset);
return this;
}
/**
* Content type reported to the output. Use this when the formatter produces
* something other than plain text. Defaults to
* {@link StandardContentType#TEXT_PLAIN} with whatever charset is in effect (see
* {@link #charset(Charset)}).
* @param contentType content type reported to the output.
* @return this.
*/
public Builder contentType(ContentType contentType) {
this.contentType = Objects.requireNonNull(contentType);
return this;
}
/**
* Configures a maximum buffer size - see {@link Buffer#isOversized()}. This is a
* <strong>soft ceiling, not a hard cap</strong>: a single event can still grow
* the buffer past this size while it is being encoded (nothing here truncates or
* rejects an oversized event), it just means the buffer's backing storage gets
* shrunk back down afterward instead of being kept at its grown size indefinitely
* - a quota that gets reclaimed after the fact, not enforced during the write.
* This, like charset, is a property of the encoder/buffer itself, not of whatever
* ends up reusing the buffer (e.g. an appender's {@code ThreadLocal}-cached one)
* - the buffer decides for itself, inside its own {@link Buffer#clear()}, whether
* to shrink.
* @param maxBufferSize a negative value disables {@link Buffer#isOversized()}
* entirely (the default).
* @return this.
*/
public Builder maxBufferSize(int maxBufferSize) {
this.maxBufferSize = maxBufferSize;
return this;
}
/**
* The capacity a fresh buffer is allocated with before any event has been encoded
* into it. Defaults to
* {@link DirectByteBufferBuffer#DEFAULT_INITIAL_BYTE_CAPACITY}.
* @param initialBufferSize initial buffer capacity, must be positive.
* @return this.
* @throws IllegalArgumentException if not positive.
*/
public Builder initialBufferSize(int initialBufferSize) {
if (initialBufferSize <= 0) {
throw new IllegalArgumentException("initialBufferSize must be positive: " + initialBufferSize);
}
this.initialBufferSize = initialBufferSize;
return this;
}
/**
* If set, converts to bytes with {@link String#getBytes(Charset)} on the fully
* formatted string instead of the default
* {@link java.nio.charset.CharsetEncoder}-based copy directly into a reused byte
* buffer. Only affects {@link WriteMethod#BYTES}/{@link WriteMethod#BYTE_BUFFER}
* outputs - always calls the byte[] overload directly regardless of which of
* those two hints an output actually declares.
* <p>
* If never called, this resolves to {@code true} instead of the historical
* {@code false} default when {@link LogProperties#GLOBAL_OPTIMIZE_PROPERTY} is
* enabled and the process is running as a GraalVM native image - see that
* property's javadoc for what it currently resolves to and why that is not a
* contract. Calling this method, either way, always wins over that.
* @param useGetBytes default is false, unless left unset and overridden by
* {@link LogProperties#GLOBAL_OPTIMIZE_PROPERTY}.
* @return this.
*/
public Builder useGetBytes(boolean useGetBytes) {
this.useGetBytes = useGetBytes;
return this;
}
/**
* Builds the encoder.
* @return encoder.
*/
public LogProvider<LogEncoder> build() {
Charset c = charset;
ContentType ct = contentType;
if (ct != null) {
if (c == null) {
c = ct.charsetOrNull();
}
}
if (c == null) {
c = StandardCharsets.UTF_8;
}
if (ct == null) {
ct = ContentType.of(StandardContentType.TEXT_PLAIN.contentType(), c);
}
var resolvedCharset = c;
var resolvedContentType = ct;
Boolean explicitUseGetBytes = useGetBytes;
String resolvedDescription = description;
return (n, config) -> {
/*
* Resolved lazily, inside this lambda, not eagerly above: the global
* optimize flag is only guaranteed set by the time a real LogConfig
* exists (see DefaultLogConfig's constructor), which this lambda's config
* parameter proves has already happened - resolving it eagerly in build()
* could run before any LogConfig was ever constructed.
*/
boolean resolvedUseGetBytes = explicitUseGetBytes != null ? explicitUseGetBytes
: AbstractLogAppender.globalOptimizeEnabled && AbstractLogAppender.isNativeImageRuntime();
return new FormatterEncoder(formatter, resolvedCharset, resolvedContentType, maxBufferSize,
initialBufferSize, resolvedUseGetBytes, config.metrics(), resolvedDescription);
};
}
}
/**
* Provides a lazy loaded encoder from a URI.
* @param uri uri.
* @return provider of encoder.
*/
public static LogProvider<LogEncoder> of(URI uri) {
return of(LogProviderRef.of(uri));
}
/**
* Provides the standard TTLL encoder.
* @return provider of encoder.
*/
public static LogProvider<LogEncoder> ofTTLL() {
return of(LogProviderRef.of(URI.create(TTLL.SCHEMA)));
}
/**
* Provides a lazy loaded encoder from a provider ref.
* @param ref uri.
* @return provider of output.
* @apiNote the provider may throw an {@link UncheckedIOException}.
*/
public static LogProvider<LogEncoder> of(LogProviderRef ref) {
return (s, c) -> {
return c.encoderRegistry().provide(ref).provide(s, c);
};
}
/**
* Finds output based on URI.
*/
public interface EncoderProvider {
/**
* Loads an encoder from a URI.
* @param ref reference to provider usually just a uri.
* @return output.
* @throws LogProviderRef.NotFoundException if there is no registered provider.
*/
LogProvider<LogEncoder> provide(LogProviderRef ref) throws LogProviderRef.NotFoundException;
/**
* Convenience method to register encoders that require no configuration.
* @param encoder already configured encoder.
* @return encoder provider.
*/
static EncoderProvider of(LogProvider<LogEncoder> encoder) {
return ref -> encoder;
}
}
/**
* Encoders buffer.
*/
public interface Buffer extends AutoCloseable {
/**
* The appender will call this usually within a lock to transfer content from the
* buffer to the output.
* @param output output to receive content.
* @param event log event.
*/
public void drain(LogOutput output, LogEvent event);
/**
* Prepare the buffer for reuse.
* <p>
* An appender may not call clear before being passed to the encoder so the
* encoder should do its own clearing.
* <p>
* Built-in implementations also shrink their own backing storage back down here
* if {@link #isOversized()} - see that method - so a single unusually large event
* does not permanently bloat a buffer that gets reused for many more events after
* it.
*/
public void clear();
/**
* Whether this buffer has grown large enough (its capacity, not how much of that
* capacity the last event actually used) that its backing storage is worth
* shrinking back down rather than kept at its grown size indefinitely - reused
* buffers only grow on their own, they never shrink back down without deliberate
* help. This is a soft ceiling checked between events, not a hard cap enforced
* during one: nothing here stops a single event from growing the buffer past
* whatever threshold it was configured with while that event is being encoded.
* Built-in implementations consult this themselves inside {@link #clear()} to
* decide whether to shrink their own backing storage in place (e.g.
* {@link StringBuilder#trimToSize()}, or reallocating a smaller
* {@link java.nio.ByteBuffer}) - most callers do not need to call this directly;
* it remains here mainly for tests/observability and for a custom {@link Buffer}
* implementation that wants the same self-shrinking behavior.
* <p>
* What "large enough" means, and whether it means anything at all, is entirely up
* to the buffer/encoder - the same way charset is an encoder concern rather than
* something an appender configures (see
* {@link LogEncoder#builder(LogFormatter)}). The default implementation returns
* {@code false} - a buffer implementation that does not override this, or an
* encoder that never configured a threshold, never shrinks.
* @return {@code true} if this buffer's backing storage has grown past whatever
* threshold it was configured with.
*/
default boolean isOversized() {
return false;
}
/**
* Convenience that will call clear.
*/
@Override
default void close() {
clear();
}
}
/**
* Hints the output can pass to the encoder for creating buffers like max size and
* storage style of the buffer etc.
*
* @apiNote There is no guarantees the encoder/buffer will honor these hints.
*/
public interface BufferHints {
/*
* TODO should we seal this?
*/
/**
* The preferred write style of the output.
* @return write method.
* @apiNote {@link WriteMethod} implements this interface for convenience.
*/
LogOutput.WriteMethod writeMethod();
/**
* Maximum size of the buffer. This is a way for the encoder to say it can only
* handle so much data per event.
* @return a negative number indicates size is not important.
*/
default int maximumSize() {
return -1;
}
}
/**
* Abstract encoder that will cast the buffer to the desired implementation. Extend to
* make creating encoders easier.
*
* @param <T> buffer type.
*/
abstract class AbstractEncoder<T extends Buffer> implements LogEncoder {
/**
* Do nothing constructor.
*/
protected AbstractEncoder() {
}
/**
* Create a specific buffer implementation.
* @param hints buffer creation hints.
* @return buffer
*/
protected abstract T doBuffer(BufferHints hints);
/**
* A type safe version of {@link #encode(LogEvent, Buffer)}.
* @param event event.
* @param buffer casted buffer.
*/
protected abstract void doEncode(LogEvent event, T buffer);
@Override
public final Buffer buffer(BufferHints hints) {
return doBuffer(hints);
}
@SuppressWarnings("unchecked")
@Override
public final void encode(LogEvent event, Buffer buffer) {
doEncode(event, (T) buffer);
}
}
}
/**
* A {@link LogEncoder.Buffer} that formats a {@link LogEvent} through a
* {@link LogFormatter} into some intermediate text representation before
* {@linkplain #drain(LogOutput, LogEvent) draining} it to the output - the common shape
* shared by {@link StringBuilderBuffer} and {@link DirectByteBufferBuffer}, letting
* {@link FormatterEncoder} dispatch to either without needing to know which concrete
* implementation it has.
*/
sealed interface TextBuffer extends LogEncoder.Buffer {
/**
* Formats {@code event} with {@code formatter} into this buffer's own backing
* storage, ready to be {@linkplain #drain(LogOutput, LogEvent) drained}. Does not
* call {@link #clear()} first - the caller is expected to do that.
* @param formatter formatter to format with.
* @param event event to format.
*/
public void encodeToBuffer(LogFormatter formatter, LogEvent event);
}
/**
* A buffer that simply wraps a {@link StringBuilder}. Direct access to the
* {@link StringBuilder} is available as the field {@link #stringBuilder}.
*
* @see AbstractEncoder
*/
final class StringBuilderBuffer implements TextBuffer {
/**
* Underlying StringBuilder.
*/
public final StringBuilder stringBuilder;
private final int maxBufferSize;
private final LogMetrics metrics;
/**
* Creates a StringBuilder based buffer that reports {@link #isOversized()} once
* {@code sb}'s capacity exceeds {@code maxBufferSize}, reporting
* {@link LogMetrics#BUFFER_TRIMMED_METRIC} to {@code metrics} each time
* {@link #clear()} actually shrinks the backing storage.
* @param sb string builder.
* @param maxBufferSize a negative value disables {@link #isOversized()} entirely
* (always {@code false}).
* @param metrics where to report {@link LogMetrics#BUFFER_TRIMMED_METRIC}.
* @return buffer.
*/
public static StringBuilderBuffer of(StringBuilder sb, int maxBufferSize, LogMetrics metrics) {
return new StringBuilderBuffer(sb, maxBufferSize, metrics);
}
private StringBuilderBuffer(StringBuilder stringBuilder, int maxBufferSize, LogMetrics metrics) {
super();
this.stringBuilder = stringBuilder;
this.maxBufferSize = maxBufferSize;
this.metrics = metrics;
}
@Override
public void drain(LogOutput output, LogEvent event) {
output.write(event, stringBuilder.toString());
}
@Override
public void clear() {
stringBuilder.setLength(0);
// setLength(0) never touches capacity, so isOversized() here still
// reflects growth from whatever was just written - trimToSize()
// mutates the StringBuilder in place, no reassignment needed (works
// fine through the public final field above).
if (isOversized()) {
stringBuilder.trimToSize();
metrics.warnCounter(LogMetrics.BUFFER_TRIMMED_METRIC, 1);
}
}
@Override
public boolean isOversized() {
return maxBufferSize >= 0 && stringBuilder.capacity() > maxBufferSize;
}
@Override
public void encodeToBuffer(LogFormatter formatter, LogEvent event) {
formatter.format(stringBuilder, event);
}
}
/**
* A buffer that formats into a reused {@link StringBuilder} (like
* {@link StringBuilderBuffer}) but converts to bytes with
* {@link String#getBytes(Charset)} on the fully formatted string, instead of
* {@link DirectByteBufferBuffer}'s {@link java.nio.charset.CharsetEncoder}-based copy
* directly into a reused byte buffer. A separate class from
* {@link DirectByteBufferBuffer} rather than a branch inside it because the two hold
* fundamentally different backing storage ({@link StringBuilder} plus a scratch
* {@code byte[]} here, a reused {@link ByteBuffer} there) - {@link #isOversized()} only
* tracks {@link #stringBuilder}'s capacity, not the scratch {@code byte[]}, since the
* array is always sized exactly to the last event's encoded length and never retains
* excess capacity the way a reused buffer can.
*
* @see LogEncoder.Builder#useGetBytes(boolean)
*/
final class StringBuilderBufferBytes implements TextBuffer {
private static final byte[] EMPTY_BYTES = new byte[0];
final StringBuilder stringBuilder;
private final Charset charset;
private final LogOutput.ContentType contentType;
private final int maxBufferSize;
private final LogMetrics metrics;
private byte[] bytes = EMPTY_BYTES;
StringBuilderBufferBytes(StringBuilder stringBuilder, Charset charset, LogOutput.ContentType contentType,
int maxBufferSize, LogMetrics metrics) {
super();
this.stringBuilder = stringBuilder;
this.charset = charset;
this.contentType = contentType;
this.maxBufferSize = maxBufferSize;
this.metrics = metrics;
}
@Override
public void drain(LogOutput output, LogEvent event) {
output.write(event, bytes, 0, bytes.length, contentType);
}
@Override
public void clear() {
stringBuilder.setLength(0);
bytes = EMPTY_BYTES;
if (isOversized()) {
stringBuilder.trimToSize();
metrics.warnCounter(LogMetrics.BUFFER_TRIMMED_METRIC, 1);
}
}
@Override
public boolean isOversized() {
return maxBufferSize >= 0 && stringBuilder.capacity() > maxBufferSize;
}
@Override
public void encodeToBuffer(LogFormatter formatter, LogEvent event) {
formatter.format(stringBuilder, event);
bytes = stringBuilder.toString().getBytes(charset);
}
}
/**
* A buffer that formats into a reused {@link StringBuilder} (like
* {@link StringBuilderBuffer}) but encodes directly into a reused {@link ByteBuffer} via
* a {@link CharsetEncoder}, instead of going through an intermediate {@code String} and
* {@code byte[]} the way {@code LogOutput.write(LogEvent, String)}'s default
* implementation does. This is the same technique
* <a href="https://logging.apache.org/log4j/2.x/manual/garbagefree.html">Log4j2's
* garbage-free logging</a> uses.
* <p>
* The {@link CharsetEncoder} work happens in {@link #encodeToByteBuffer()}, called by
* {@link #encodeToBuffer(LogFormatter, LogEvent)} - i.e. before
* {@link #drain(LogOutput, LogEvent) drain}, which appenders that separate formatting
* from writing ({@code LockThreadLocalBufferLogAppender},
* {@code SynchronizedThreadLocalBufferLogAppender}) call outside their lock.
* {@code drain} - called from inside the lock - therefore does nothing but write the
* already-encoded bytes, matching how Log4j2 confines the actual character-to-byte
* transcoding to a thread-local scratch buffer and synchronizes only the final copy into
* the shared destination.
* <p>
* Constructed for a specific {@link LogOutput.WriteMethod} - either
* {@link LogOutput.WriteMethod#BYTE_BUFFER}, in which case {@link #drain} calls
* {@link LogOutput#write(LogEvent, ByteBuffer, LogOutput.ContentType)} directly, or
* {@link LogOutput.WriteMethod#BYTES}, in which case {@link #drain} calls
* {@link LogOutput#write(LogEvent, byte[], int, int, LogOutput.ContentType)} using the
* backing array directly, rather than going through
* {@code LogOutput#write(LogEvent, ByteBuffer, ContentType)}'s default implementation
* (which would allocate a fresh {@code byte[]} copy every call for an output that does
* not itself implement that overload).
* <p>
* Not thread-safe. Like all {@link LogEncoder.Buffer}s it relies on the appender to
* guarantee no overlapping use.
*/
final class DirectByteBufferBuffer implements TextBuffer {
/**
* Default initial byte buffer capacity, matching Log4j2's own
* {@code log4j2.encoderByteBufferSize} default.
*/
public static final int DEFAULT_INITIAL_BYTE_CAPACITY = 8192;
/**
* The buffer the formatter writes characters into. Sized in the constructor with
* {@code initialByteCapacity} (chars, not bytes, but close enough as a starting guess
* and cheaper than the alternative) instead of defaulting to {@link StringBuilder}'s
* own default capacity (16) - otherwise every fresh buffer pays for several growth
* reallocations on essentially every first event, the exact kind of per-event cost
* {@link #byteBuffer} below is already sized up front to avoid.
*/
public final StringBuilder stringBuilder;
private CharBuffer charBuffer;
private final CharsetEncoder charsetEncoder;
private final LogOutput.WriteMethod writeMethod;
private final LogOutput.ContentType contentType;
private final int maxBufferSize;
private final int initialByteCapacity;
private final LogMetrics metrics;
private ByteBuffer byteBuffer;
/**
* Creates a buffer with the given write method, initial byte capacity, charset,
* content type to report to the output, and maximum combined size (see
* {@link #isOversized()}).
* @param writeMethod which {@link LogOutput#write} overload {@link #drain} should
* call - {@link LogOutput.WriteMethod#BYTES} or
* {@link LogOutput.WriteMethod#BYTE_BUFFER}.
* @param initialByteCapacity initial capacity of the byte buffer. It will grow
* (doubling, or to whatever a single event needs if larger) as needed and the grown
* capacity is kept for subsequent events.
* @param charset charset to encode with.
* @param contentType content type reported to {@link LogOutput#write}. Its
* {@link LogOutput.ContentType#charsetOrNull() charset}, if specified, should
* normally match {@code charset}.
* @param maxBufferSize a negative value disables {@link #isOversized()} entirely
* (always {@code false}). Since the direct buffer allocates the initial character and
* byte storage up front and counts both in {@link #isOversized()} regardless of how
* much of either store an event has actually used, a low {@code maxBufferSize} can
* make {@link #isOversized()} unconditionally {@code true} from the very first event
* - {@code maxBufferSize} should normally be set well above the combined initial
* retained capacity.
* @param metrics where to report {@link LogMetrics#BUFFER_TRIMMED_METRIC} each time
* {@link #clear()} actually shrinks the backing storage.
*/
DirectByteBufferBuffer(LogOutput.WriteMethod writeMethod, int initialByteCapacity, Charset charset,
LogOutput.ContentType contentType, int maxBufferSize, LogMetrics metrics) {
this.writeMethod = writeMethod;
this.stringBuilder = new StringBuilder(initialByteCapacity);
this.charBuffer = CharBuffer.allocate(initialByteCapacity);
this.byteBuffer = ByteBuffer.allocate(initialByteCapacity);
this.charsetEncoder = charset.newEncoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
this.contentType = contentType;
this.maxBufferSize = maxBufferSize;
this.initialByteCapacity = initialByteCapacity;
this.metrics = metrics;
}
/**
* Writes the already-encoded bytes to the output. Assumes
* {@link #encodeToBuffer(LogFormatter, LogEvent)} has already been called for this
* event - see the class doc for why that step happens there instead of here.
*/
@Override
public void drain(LogOutput output, LogEvent event) {
switch (writeMethod) {
case BYTE_BUFFER -> output.write(event, byteBuffer, contentType);
case BYTES -> output.write(event, byteBuffer.array(), byteBuffer.arrayOffset() + byteBuffer.position(),
byteBuffer.remaining(), contentType);
case STRING ->
throw new IllegalStateException("DirectByteBufferBuffer does not support WriteMethod.STRING");
}
}
@Override
public void encodeToBuffer(LogFormatter formatter, LogEvent event) {
formatter.format(stringBuilder, event);
encodeToByteBuffer();
}
/**
* Encodes the current contents of {@link #stringBuilder} into the reused
* {@link ByteBuffer}, growing it first if needed. Deliberately not called from
* {@link #drain(LogOutput, LogEvent)} - see the class doc.
*/
private void encodeToByteBuffer() {
int maxBytes = (int) Math.ceil(stringBuilder.length() * (double) charsetEncoder.maxBytesPerChar());
if (byteBuffer.capacity() < maxBytes) {
byteBuffer = ByteBuffer.allocate(Math.max(maxBytes, byteBuffer.capacity() * 2));
}
byteBuffer.clear();
charsetEncoder.reset();
int length = stringBuilder.length();
if (charBuffer.capacity() < length) {
charBuffer = CharBuffer.allocate(length);
}
stringBuilder.getChars(0, length, charBuffer.array(), charBuffer.arrayOffset());
charBuffer.limit(length);
charBuffer.position(0);
var result = charsetEncoder.encode(charBuffer, byteBuffer, true);
if (result.isError()) {
try {
result.throwException();
}
catch (CharacterCodingException e) {
throw new UncheckedIOException(e);
}
}
/*
* maxBytes above already sized the buffer to fit the worst case so flush should
* never overflow, but check defensively rather than silently truncate.
*/
var flushResult = charsetEncoder.flush(byteBuffer);
if (flushResult.isOverflow()) {
throw new IllegalStateException("CharsetEncoder flush overflowed despite pre-sized buffer");
}
byteBuffer.flip();
}
@Override
public void clear() {
stringBuilder.setLength(0);
charBuffer.clear();
// Checked before byteBuffer is touched below, so this still reflects
// growth from whatever was just drained. stringBuilder.trimToSize()
// mutates in place; byteBuffer and charBuffer aren't final (they're already
// reassigned during growth in encodeToByteBuffer()) so reallocating them back
// down here needs no new field-mutability changes either.
if (isOversized()) {
stringBuilder.trimToSize();
charBuffer = CharBuffer.allocate(initialByteCapacity);
byteBuffer = ByteBuffer.allocate(initialByteCapacity);
metrics.warnCounter(LogMetrics.BUFFER_TRIMMED_METRIC, 1);
}
}
@Override
public boolean isOversized() {
return maxBufferSize >= 0
&& (stringBuilder.capacity() + charBuffer.capacity() + byteBuffer.capacity()) > maxBufferSize;
}
CharBuffer charBuffer() {
return this.charBuffer;
}
}
/*
* Not an AbstractEncoder since it needs to hand out either a StringBuilderBuffer or a
* DirectByteBufferBuffer depending on the output's WriteMethod hint - see buffer(hints).
*/
final class FormatterEncoder implements LogEncoder, LogReporter.Reportable {
private final LogFormatter formatter;
private final Charset charset;
private final ContentType contentType;
private final int maxBufferSize;
private final int initialBufferSize;
private final boolean useGetBytes;
private final LogMetrics metrics;
private final @Nullable String description;
FormatterEncoder(LogFormatter formatter, Charset charset, ContentType contentType, int maxBufferSize,
int initialBufferSize, boolean useGetBytes, LogMetrics metrics, @Nullable String description) {
super();
this.formatter = formatter;
this.charset = charset;
this.contentType = contentType;
this.maxBufferSize = maxBufferSize;
this.initialBufferSize = initialBufferSize;
this.useGetBytes = useGetBytes;
this.metrics = metrics;
this.description = description;
}
@Override
public void report(Appendable out) throws IOException {
out.append("contentType=").append(contentType.contentType());
Charset cs = contentType.charsetOrNull();
if (cs != null) {
out.append("; charset=").append(cs.name());
}
/*
* A formatter that is Reportable, such as TTLL, describes itself when the encoder
* was not given a description.
*/
String d = description;
if (d == null && formatter instanceof LogReporter.Reportable r) {
d = LogReporter.Reportable.toString(r, "");
}
if (d != null && !d.isEmpty()) {
out.append(", description=\"").append(d).append("\"");
}
}
@Override
public Buffer buffer(BufferHints hints) {
return switch (hints.writeMethod()) {
case STRING -> StringBuilderBuffer.of(new StringBuilder(initialBufferSize), maxBufferSize, metrics);
case BYTES,
BYTE_BUFFER ->
useGetBytes
? new StringBuilderBufferBytes(new StringBuilder(initialBufferSize), charset, contentType,
maxBufferSize, metrics)
: new DirectByteBufferBuffer(hints.writeMethod(), initialBufferSize, charset, contentType,
maxBufferSize, metrics);
};
}
@Override
public void encode(LogEvent event, Buffer buffer) {
switch (buffer) {
case TextBuffer tb -> {
tb.clear();
tb.encodeToBuffer(formatter, event);
}
default -> throw new IllegalStateException("Unsupported buffer: " + buffer.getClass());
}
}
}