package com.equalexperts.logging.impl; import com.equalexperts.logging.DiagnosticContextSupplier; import com.equalexperts.logging.LogMessage; import com.equalexperts.logging.OpsLogger; import java.time.Clock; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.Future; import java.util.concurrent.LinkedTransferQueue; import java.util.function.Consumer; import static java.util.stream.Collectors.toList; /** * Asynchronous OpsLogger which puts the record to be logged in a transferQueue and * returns immediately which allows for better performance at the expense of not * necessarily having everything logged if the JVM shuts down unexpectedly. * A background thread is responsible for emptying the transferQueue. */ public class AsyncOpsLogger<T extends Enum<T> & LogMessage> implements OpsLogger<T> { static final int MAX_BATCH_SIZE = 100; private final Future<?> processingThread; private final LinkedTransferQueue<Optional<LogicalLogRecord<T>>> transferQueue; private final Clock clock; private final DiagnosticContextSupplier diagnosticContextSupplier; private final Destination<T> destination; private final Consumer<Throwable> errorHandler; private final boolean closeable; public AsyncOpsLogger(Clock clock, DiagnosticContextSupplier diagnosticContextSupplier, Destination<T> destination, Consumer<Throwable> errorHandler, LinkedTransferQueue<Optional<LogicalLogRecord<T>>> transferQueue, AsyncExecutor executor) { this.clock = clock; this.diagnosticContextSupplier = diagnosticContextSupplier; this.destination = destination; this.errorHandler = errorHandler; this.transferQueue = transferQueue; processingThread = executor.execute(this::process); this.closeable = true; } private AsyncOpsLogger(Clock clock, DiagnosticContextSupplier diagnosticContextSupplier, Destination<T> destination, Consumer<Throwable> errorHandler, LinkedTransferQueue<Optional<LogicalLogRecord<T>>> transferQueue, Future<?> processingThread, boolean closeable) { this.clock = clock; this.diagnosticContextSupplier = diagnosticContextSupplier; this.destination = destination; this.errorHandler = errorHandler; this.transferQueue = transferQueue; this.processingThread = processingThread; this.closeable = closeable; } @Override public void log(T message, Object... details) { try { DiagnosticContext diagnosticContext = new DiagnosticContext(diagnosticContextSupplier); LogicalLogRecord<T> record = new LogicalLogRecord<>(clock.instant(), diagnosticContext, message, Optional.empty(), details); transferQueue.put(Optional.of(record)); } catch (Throwable t) { errorHandler.accept(t); } } @Override public void logThrowable(T message, Throwable cause, Object... details) { try { DiagnosticContext diagnosticContext = new DiagnosticContext(diagnosticContextSupplier); LogicalLogRecord<T> record = new LogicalLogRecord<>(clock.instant(), diagnosticContext, message, Optional.of(cause), details); transferQueue.put(Optional.of(record)); } catch (Throwable t) { errorHandler.accept(t); } } @Override public AsyncOpsLogger<T> with(DiagnosticContextSupplier override) { return new AsyncOpsLogger<>(clock, override, destination, errorHandler, transferQueue, processingThread, false); } @Override public void close() throws Exception { if (closeable) { try { transferQueue.put(Optional.empty()); //an empty optional is the shutdown signal processingThread.get(); } finally { destination.close(); } } } private void process() { /* An empty optional on the queue is the shutdown signal */ boolean run = true; do { try { List<Optional<LogicalLogRecord<T>>> messages = waitForNextBatch(); List<LogicalLogRecord<T>> logRecords = messages.stream() .filter(Optional::isPresent) .map(Optional::get) .collect(toList()); if (logRecords.size() < messages.size()) { run = false; //shutdown signal detected } processBatch(logRecords); } catch (Throwable t) { errorHandler.accept(t); } } while (run); } private void processBatch(List<LogicalLogRecord<T>> batch) throws Exception { if (batch.isEmpty()) { return; } destination.beginBatch(); for (LogicalLogRecord<T> record : batch) { try { destination.publish(record); } catch (Throwable t) { errorHandler.accept(t); } } destination.endBatch(); } private List<Optional<LogicalLogRecord<T>>> waitForNextBatch() throws InterruptedException { List<Optional<LogicalLogRecord<T>>> result = new ArrayList<>(); result.add(transferQueue.take()); //a blocking operation transferQueue.drainTo(result, MAX_BATCH_SIZE - 1); return result; } public Clock getClock() { return clock; } public Destination<T> getDestination() { return destination; } public DiagnosticContextSupplier getDiagnosticContextSupplier() { return diagnosticContextSupplier; } public Consumer<Throwable> getErrorHandler() { return errorHandler; } public LinkedTransferQueue<Optional<LogicalLogRecord<T>>> getTransferQueue() { return transferQueue; } }