LogOutput.java

package io.jstach.rainbowgum;

import java.io.FileDescriptor;
import java.io.Flushable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;

import org.jspecify.annotations.Nullable;

import io.jstach.rainbowgum.LogAppender.AppenderFlag;
import io.jstach.rainbowgum.LogEncoder.Buffer;
import io.jstach.rainbowgum.LogEncoder.BufferHints;
import io.jstach.rainbowgum.annotation.CaseChanging;

/**
 * A resource that can be written to usually in binary. In other logging frameworks this
 * is often called an "Appender" not be confused with RainbowGum's appenders. LogOutputs
 * will be written synchronously by the appender either through locks or some other
 * mechanism so other than output external specific locking (e.g file locking or db
 * transaction) the output does not have to deal with that. <strong> In other words there
 * will be no overlapping write/flush/close calls as the appender and publisher combo
 * promises that. </strong>
 * <p>
 * To simplyfy flushing and corruption prevention each call to the write methods MUST be
 * the contents of the entire event! However the output should not flush unless
 * {@link #flush()} is called by the appender. That is the appender
 * <strong>usually</strong> decides when to flush.
 * <p>
 * The write methods do not throw {@link IOException} on purpose as it is implementations
 * responsibility to handle errors and to be resilient on their own.
 * <p>
 * An output that needs a particular kind of encoding - or cannot function with just any
 * encoder - implements {@link ProvidesEncoder} to supply its own, either as a mandatory,
 * non-overridable choice or as an overridable default; see {@link ProvidesEncoder} for
 * details.
 *
 * @see LogOutput.OutputProvider
 * @see LogOutput.ProvidesEncoder
 * @see Buffer
 * @see AppenderFlag#DISABLE_IMMEDIATE_FLUSH
 * @apiNote if for some reason the output needs to share events with other threads call
 * {@link LogEvent#freeze()} which will return an immutable event.
 */
public interface LogOutput extends LogLifecycle, Flushable {

	/**
	 * {@link FileDescriptor#err} URI scheme.
	 */
	static final String STDERR_SCHEME = "stderr";

	/**
	 * {@link FileDescriptor#out} URI scheme.
	 */
	static final String STDOUT_SCHEME = "stdout";

	/**
	 * File URI scheme.
	 */
	static final String FILE_SCHEME = "file";

	/**
	 * Standard OUT URI.
	 */
	static final URI STDOUT_URI = URI.create(STDOUT_SCHEME + ":///");

	/**
	 * Standard ERR URI
	 */
	static final URI STDERR_URI = URI.create(STDERR_SCHEME + ":///");

	/**
	 * Provides a lazy loaded output from a URI.
	 * @param uri uri.
	 * @return provider of output.
	 * @apiNote the provider may throw an {@link UncheckedIOException}.
	 */
	private static LogProvider<LogOutput> of(URI uri) {
		/*
		 * TODO this should probably be public.
		 */
		return of(LogProviderRef.of(uri));
	}

	/**
	 * Provides a lazy loaded output from a URI.
	 * @param ref uri.
	 * @return provider of output.
	 * @apiNote the provider may throw an {@link UncheckedIOException}.
	 */
	public static LogProvider<LogOutput> of(LogProviderRef ref) {
		return (s, c) -> {
			return c.outputRegistry().provide(ref).provide(s, c);
		};
	}

	/**
	 * Standard out output. The default implementation <em>uses whatever is set to
	 * {@link System#out} at provision time</em>. <strong>If {@link System#out} is rebound
	 * the output will not be updated!</strong> Use
	 * {@link io.jstach.rainbowgum.output.StdOutOutputBuilder} with
	 * {@link io.jstach.rainbowgum.output.ConsoleStream#FOLLOW} to read it on every write
	 * instead.
	 * @return output.
	 */
	public static LogProvider<LogOutput> ofStandardOut() {
		return of(LogOutput.STDOUT_URI);
	}

	/**
	 * Standard err output. The default implementation <em>uses whatever is set to
	 * {@link System#err} at provision time</em>. <strong>If {@link System#err} is rebound
	 * the output will not be updated!</strong> Use
	 * {@link io.jstach.rainbowgum.output.StdErrOutputBuilder} with
	 * {@link io.jstach.rainbowgum.output.ConsoleStream#FOLLOW} to read it on every write
	 * instead.
	 * @return output.
	 */
	public static LogProvider<LogOutput> ofStandardErr() {
		return of(LogOutput.STDERR_URI);
	}

	/**
	 * Finds output based on URI.
	 */
	public interface OutputProvider {

		/**
		 * Creates an output ready to be injected with config.
		 * @param ref a URI reference to the output.
		 * @return provider of output.
		 */
		LogProvider<LogOutput> provide(LogProviderRef ref);

	}

	/**
	 * Marks an output that supplies its own {@link LogEncoder} instead of relying
	 * entirely on {@link LogAppender#APPENDER_ENCODER_PROPERTY} or
	 * {@link LogEncoderRegistry#encoderForOutputType(OutputType)}. {@link #policy()}
	 * decides whether that encoder can still be overridden:
	 * <ul>
	 * <li>{@link Policy#MANDATORY}: {@link #encoder(String, LogConfig)} is the only
	 * encoder this output will ever use. Configuring another one anyway -
	 * programmatically via {@link LogAppender.Builder#encoder}, or via
	 * {@link LogAppender#APPENDER_ENCODER_PROPERTY} - is a
	 * {@link LogProperty.ValidationException}, not a silent override.
	 * <li>{@link Policy#DEFAULT}: {@link #encoder(String, LogConfig)} is used only if
	 * nothing else is configured; either of the above always wins over it.
	 * </ul>
	 * Neither policy is consulted at all if an output does not implement this interface
	 * in the first place - such an output has no opinion on its own encoder, and
	 * resolution proceeds exactly as if this interface did not exist.
	 *
	 * @see LogAppender.Builder#build()
	 */
	public interface ProvidesEncoder {

		/**
		 * The encoder this output supplies, given the same {@code name}/{@code config}
		 * the output itself was provided with.
		 * @param name appender name.
		 * @param config config.
		 * @return encoder, see {@link #policy()} for whether it can be overridden.
		 * @apiNote this method is only ever called once per appender resolution, so an
		 * implementation does not need to memoize the returned encoder itself.
		 */
		LogEncoder encoder(String name, LogConfig config);

		/**
		 * Whether {@link #encoder(String, LogConfig)} can be overridden by an explicitly
		 * configured encoder. Defaults to {@link Policy#DEFAULT} - an implementation only
		 * needs to override this to return {@link Policy#MANDATORY}.
		 * @return policy.
		 */
		default Policy policy() {
			return Policy.DEFAULT;
		}

		/**
		 * Whether a {@link ProvidesEncoder}'s
		 * {@link ProvidesEncoder#encoder(String, LogConfig)} can be overridden by
		 * another, explicitly configured encoder.
		 */
		enum Policy {

			/**
			 * The output's own encoder is the only one allowed. Explicitly configuring
			 * another encoder anyway, programmatically or via property, is a validation
			 * failure rather than a silent override.
			 */
			MANDATORY,

			/**
			 * The output's own encoder is used only as a fallback: any explicitly
			 * configured encoder, programmatic or property, wins over it.
			 */
			DEFAULT;

		}

	}

	/**
	 * The content type of the binary data passed to the output from an encoder.
	 */
	public interface ContentType {

		/**
		 * Content type of binary datay passed to output.
		 * @return content type.
		 */
		String contentType();

		/**
		 * The charset the bytes are encoded with, or <code>null</code> if there is no
		 * single fixed charset for this content type. Both built-in
		 * {@link StandardContentType} constants are always {@link StandardCharsets#UTF_8}
		 * - {@link StandardContentType#APPLICATION_JSON} by spec, and
		 * {@link StandardContentType#TEXT_PLAIN} because UTF-8 is standard enough on
		 * modern Java (e.g. {@code String#getBytes()}'s no-charset overload) to treat as
		 * the assumed default rather than "unspecified" - an encoder actually configured
		 * with a different charset (see {@link LogEncoder#builder(LogFormatter)}) reports
		 * a different content type instead, not {@link StandardContentType#TEXT_PLAIN}.
		 * @return charset or <code>null</code> if not fixed/known.
		 */
		@Nullable Charset charsetOrNull();

		/**
		 * Finds the matching {@link StandardContentType} for the given content type and
		 * charset if one exists, otherwise creates a new plain {@link ContentType}.
		 * @param contentType content type, e.g. <code>text/plain</code>.
		 * @param charsetOrNull charset or <code>null</code> if not fixed/known.
		 * @return a {@link StandardContentType} if one matches, otherwise a plain
		 * {@link ContentType}.
		 */
		static ContentType of(String contentType, @Nullable Charset charsetOrNull) {
			for (var standard : StandardContentType.values()) {
				if (standard.contentType().equals(contentType)
						&& Objects.equals(standard.charsetOrNull(), charsetOrNull)) {
					return standard;
				}
			}
			return new SimpleContentType(contentType, charsetOrNull);
		}

		/**
		 * Builtin content types.
		 *
		 * @apiNote additional "standard" content types maybe added in the future before
		 * 1.0 thus one should expect the number of enum symbols to change and is not safe
		 * to pattern match on without a default.
		 */
		@CaseChanging
		public enum StandardContentType implements ContentType {

			/**
			 * text/plain
			 */
			TEXT_PLAIN() {
				@Override
				public String contentType() {
					return "text/plain";
				}

				@Override
				public Charset charsetOrNull() {
					return StandardCharsets.UTF_8;
				}
			},
			/**
			 * application/json
			 */
			APPLICATION_JSON() {
				@Override
				public String contentType() {
					return "application/json";
				}

				@Override
				public Charset charsetOrNull() {
					return StandardCharsets.UTF_8;
				}
			}

		}

	}

	/**
	 * The uri of the output.
	 * @return uri.
	 * @throws UnsupportedOperationException if this output does not support URI
	 * representation.
	 *
	 */
	public URI uri() throws UnsupportedOperationException;

	@Override
	default void start(LogConfig config) {
	}

	/**
	 * Write a batch of events by default calls
	 * {@link #write(LogEvent[], int, LogEncoder, Buffer)}. <strong>DO NOT MODIFY THE
	 * ARRAY</strong>. Do not use the <code>length</code> of the passed in array but
	 * instead use <code>count</code> parameter.
	 * @param events an array guaranteed to be smaller than count.
	 * @param count the number of items.
	 * @param encoder encoder to use for encoding.
	 */
	default void write(LogEvent[] events, int count, LogEncoder encoder) {
		try (var buffer = encoder.buffer(this.bufferHints())) {
			write(events, count, encoder, buffer);
		}
	}

	/**
	 * Write a batch of events reusing the provided buffer and by default calling
	 * {@link #write(LogEvent, Buffer)} for each event synchronously.
	 * <p>
	 * The batching is usually a result of an asynchronous publisher so the maximum size
	 * is configured there with bufferSize. If this method is implemented there are some
	 * caveats.
	 * <p>
	 * <strong>DO NOT MODIFY THE ARRAY</strong>. It is reused. Do not use the
	 * <code>length</code> of the passed in array but instead use <code>count</code>
	 * parameter. Consequently do not share the array or buffer with other threads however
	 * individual events can be shared by calling {@link LogEvent#freeze()} to guarantee
	 * that which is a safe no-op if it is already immutable.
	 * <p>
	 * If the output plans on grouping the events into a single blob (byte array or byte
	 * buffer) a different output can be passed to the encoder other than itself (which is
	 * the default).
	 * @param events an array guaranteed to be smaller than count.
	 * @param count the number of items.
	 * @param encoder encoder to use for encoding.
	 * @param buffer buffer to reuse. Do not share it with other threads!
	 * @apiNote If the output wants to fan-out or encode with other threads the buffer
	 * passed should not be used and instead {@link LogEncoder#buffer(BufferHints)} should
	 * be called to get a new buffer for each thread. This is generally not desirable for
	 * most resources such as a file or database as sending a batch is much faster (single
	 * writer principle).
	 */
	default void write(LogEvent[] events, int count, LogEncoder encoder, Buffer buffer) {
		for (int i = 0; i < count; i++) {
			buffer.clear();
			var event = events[i];
			encoder.encode(event, buffer);
			write(event, buffer);
		}
	}

	/**
	 * Writes a prepared buffer and by default drains the buffer to this output.
	 * @param event event.
	 * @param buffer buffer that is already filled. Do not share it with other threads!
	 * @apiNote the buffer already has the encoding.
	 */
	default void write(LogEvent event, Buffer buffer) {
		buffer.drain(this, event);
	}

	/**
	 * Writes a string and by default with charset {@link StandardCharsets#UTF_8}.
	 * @param event event associated with string.
	 * @param s string.
	 */
	default void write(LogEvent event, String s) {
		write(event, s.getBytes(StandardCharsets.UTF_8), ContentType.StandardContentType.TEXT_PLAIN);
	}

	/**
	 * Analogous to {@link OutputStream#write(byte[])}.
	 * @param event event associated with bytes.
	 * @param bytes data to be written.
	 * @param contentType content type of the bytes.
	 */
	default void write(LogEvent event, byte[] bytes, ContentType contentType) {
		write(event, bytes, 0, bytes.length, contentType);
	}

	/**
	 * Analogous to {@link OutputStream#write(byte[], int, int)}.
	 * @param event event associated with bytes.
	 * @param bytes data to be written.
	 * @param off offset.
	 * @param len length.
	 * @param contentType content type of the bytes.
	 */
	public void write(LogEvent event, byte[] bytes, int off, int len, ContentType contentType);

	/**
	 * Write event with byte buffer. The buffer is expected to be ready for reading in the
	 * standard {@link ByteBuffer} idiom: <code>position</code> at the start of the
	 * content and <code>limit</code> at its end (e.g. just {@linkplain ByteBuffer#flip()
	 * flipped} after being filled, or created with {@link ByteBuffer#wrap(byte[])}).
	 * @param event event.
	 * @param buf byte buffer ready for reading (flipped).
	 * @param contentType content type of the buf
	 */
	default void write(LogEvent event, ByteBuffer buf, ContentType contentType) {
		byte[] arr = new byte[buf.remaining()];
		buf.get(arr);
		write(event, arr, contentType);
	}

	/**
	 * Flushes if the output is maintaining a buffer. By default an appender will call
	 * this after each event in synchronous mode or after a batched group of events if in
	 * asynchronous mode unless {@link LogAppender.AppenderFlag#DISABLE_IMMEDIATE_FLUSH}
	 * is set.
	 * @see LogAppender.AppenderFlag#DISABLE_IMMEDIATE_FLUSH
	 */
	@Override
	public void flush();

	/**
	 * Gross categorization of an output.
	 * @return output type.
	 */
	public OutputType type();

	/**
	 * Hints to the buffer like what write style of the output and maximum buffer size.
	 * The output should still honor all write methods but this signals to the encoder
	 * which style it prefers.
	 * @return write mode.
	 */
	default BufferHints bufferHints() {
		return WriteMethod.BYTES;
	}

	/**
	 * Attempts to reopen the output if supported and <strong>SHOULD only be called by the
	 * appender</strong>! This call is mainly used for external log rotation systems such
	 * as logrotate or an aggregator agent. The default implementation does nothing -
	 * reopening is opt-in per output.
	 * @throws RuntimeException implementations are free to throw on failure - the caller
	 * (the appender) is responsible for catching this and reporting it to the alert
	 * system rather than propagating it.
	 */
	default void reopen() {
	}

	/**
	 * The preferred write style of an output. The output should still honor all write
	 * methods but this signals to the encoder which style it prefers.
	 */
	public enum WriteMethod implements BufferHints {

		/**
		 * Prefer calling {@link LogOutput#write(LogEvent, String)}.
		 */
		STRING,
		/**
		 * Prefer calling {@link LogOutput#write(LogEvent, byte[], ContentType)} or
		 * {@link LogOutput#write(LogEvent, byte[], int, int, ContentType)}.
		 */
		BYTES,
		/**
		 * Prefer calling {@link LogOutput#write(LogEvent, ByteBuffer, ContentType)}.
		 */
		BYTE_BUFFER;

		@Override
		public WriteMethod writeMethod() {
			return this;
		}

	}

	/**
	 * Output type useful for defaults and categorization.
	 */
	@CaseChanging
	public enum OutputType {

		/**
		 * Standard out.
		 */
		CONSOLE_OUT,
		/**
		 * Standard err.
		 */
		CONSOLE_ERR,
		/**
		 * Output targets a file.
		 */
		FILE,
		/**
		 * Output targets something over a network.
		 */
		NETWORK,
		/**
		 * Output targets in memory.
		 */
		MEMORY;

	}

	/**
	 * Will close the output and should only be called by the owning appender.
	 */
	@Override
	void close();

	/**
	 * Marker interface that the output is thread safe.
	 */
	public interface ThreadSafeLogOutput extends LogOutput {

	}

	/**
	 * Abstract output on output stream.
	 */
	abstract class AbstractOutputStreamOutput implements LogOutput {

		/**
		 * passed in uri
		 */
		protected final URI uri;

		/**
		 * output stream to write to.
		 */
		protected final OutputStream outputStream;

		/*
		 * A log call can still be in flight (e.g. routed through SLF4J from a shutdown
		 * hook) after something outside the normal appender/publisher lifecycle - like
		 * LoggingSystem.cleanUp() - has already closed this output directly. Once closed,
		 * write/flush become no-ops instead of throwing so that race does not surface as
		 * an uncaught exception on whatever thread is still trying to log.
		 */
		private final AtomicBoolean closed = new AtomicBoolean();

		/**
		 * Adapts an OutputStream to an Output.
		 * @param uri usually comes from output registry.
		 * @param outputStream output stream to adapt.
		 */
		protected AbstractOutputStreamOutput(URI uri, OutputStream outputStream) {
			super();
			this.uri = uri;
			this.outputStream = outputStream;
		}

		@Override
		public URI uri() {
			return uri;
		}

		@Override
		public void write(LogEvent event, byte[] bytes, int off, int len, ContentType contentType) {
			if (closed.get()) {
				return;
			}
			try {
				outputStream.write(bytes, off, len);
			}
			catch (IOException e) {
				throw new UncheckedIOException(e);
			}
		}

		@Override
		public void flush() {
			if (closed.get()) {
				return;
			}
			try {
				outputStream.flush();
			}
			catch (IOException e) {
				throw new UncheckedIOException(e);
			}
		}

		@Override
		public void close() {
			if (!closed.compareAndSet(false, true)) {
				return;
			}
			try {
				outputStream.close();
			}
			catch (IOException e) {
				throw new UncheckedIOException(e);
			}
		}

		@Override
		public BufferHints bufferHints() {
			return WriteMethod.BYTES;
		}

	}

}

/**
 * A plain {@link LogOutput.ContentType} for content types not covered by
 * {@link LogOutput.ContentType.StandardContentType}. Created via
 * {@link LogOutput.ContentType#of(String, Charset)} (or reuses a matching
 * {@link LogOutput.ContentType.StandardContentType} instead if one matches).
 *
 * @param contentType content type, e.g. <code>text/plain</code>.
 * @param charsetOrNull charset or <code>null</code> if not fixed/known.
 */
record SimpleContentType(String contentType, @Nullable Charset charsetOrNull) implements LogOutput.ContentType {
}