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());
		}
	}

}