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 {
}