package com.equalexperts.logging; import com.equalexperts.logging.impl.AsyncOpsLoggerFactory; import com.equalexperts.logging.impl.BasicOpsLoggerFactory; import com.equalexperts.logging.impl.InfrastructureFactory; import java.io.PrintStream; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; /** * <p>Constructs <code>OpsLogger</code> instances.</p> * * <p>Instances of this class are not thread-safe, and should not be accessed by multiple * threads.</p> * * @see OpsLogger */ public class OpsLoggerFactory { private Optional<PrintStream> loggerOutput = Optional.empty(); private Optional<Path> logfilePath = Optional.empty(); private boolean async = false; private Optional<Boolean> storeStackTracesInFilesystem = Optional.empty(); private Optional<Path> stackTraceStoragePath = Optional.empty(); private Optional<Consumer<Throwable>> errorHandler = Optional.empty(); private Optional<DiagnosticContextSupplier> contextSupplier = Optional.empty(); private Optional<OpsLogger<?>> cachedInstance = Optional.empty(); private AsyncOpsLoggerFactory asyncOpsLoggerFactory = new AsyncOpsLoggerFactory(); private BasicOpsLoggerFactory basicOpsLoggerFactory = new BasicOpsLoggerFactory(); /** * The destination for the log strings. A typical value is System.out. * @param printStream destination * @return <code>this</code> for further configuration */ public OpsLoggerFactory setDestination(PrintStream printStream) { validateParametersForSetDestination(printStream); clearCachedInstance(); loggerOutput = Optional.of(printStream); logfilePath = Optional.empty(); return this; } /** * The path of the file to print the log strings to. Is closed and reopened frequently to allow outside log rotation to work. * The path is used as-is. * @param path path for log file * @return <code>this</code> for further configuration */ public OpsLoggerFactory setPath(Path path) { validateParametersForSetPath(path); clearCachedInstance(); logfilePath = Optional.of(path).map(Path::toAbsolutePath); loggerOutput = Optional.empty(); return this; } /** * <p>Should stack traces be placed in individual files or printed along with the log statements?</p> * <p>If called with true, each unique stack trace will placed in its own file. (see setStackTraceStoragePath). * If called with false, stack traces will be printed to main log file.</p> * @param store true for separate files, false for inlined in log file * @return <code>this</code> for further configuration */ public OpsLoggerFactory setStoreStackTracesInFilesystem(boolean store) { clearCachedInstance(); storeStackTracesInFilesystem = Optional.of(store); if (!store) { stackTraceStoragePath = Optional.empty(); } return this; } /** * <p>Path to directory to contain individual stack trace files.</p> * <p> * Must be called with a valid path corresponding to a directory, where stack traces will be stored * (see setStoreStackTracesInFilesystem). If the directory does not exist, it will be created.</p> * @param directory valid path for target directory * @return <code>this</code> for further configuration */ public OpsLoggerFactory setStackTraceStoragePath(Path directory) { validateParametersForSetStackTraceStoragePath(directory); clearCachedInstance(); setStoreStackTracesInFilesystem(true); stackTraceStoragePath = Optional.of(directory); return this; } /** * <p>Handler for when exceptions occur when logging.</p> * <p> * If any exception is thrown "inside" this logger, it will caught and be passed on to this error handler, * which then is responsible for any further error handling. The log message causing the error, will not * be processed further.</p> * @param handler Consumer of Throwables handleling any exception encountered. * @return <code>this</code> for further configuration */ public OpsLoggerFactory setErrorHandler(Consumer<Throwable> handler) { clearCachedInstance(); errorHandler = Optional.ofNullable(handler); return this; } /** * <p>This method will be removed in a future release.</p> * * @param supplier the map supplier. (for example: <code>()->map</code>) * @deprecated Replaced by {@link #setGlobalDiagnosticContextSupplier(DiagnosticContextSupplier)}. * @return <code>this</code> for further configuration */ @Deprecated public OpsLoggerFactory setCorrelationIdSupplier(Supplier<Map<String,String>> supplier) { return setGlobalDiagnosticContextSupplier(supplier != null ? supplier::get : null); } /** * <p>Set the supplier of the map to print for each log entry.</p> * <p>The correlation id map is printed out as part of the message logged:</p> * <p>Example code: (where <code>setGlobalDiagnosticContextSupplier(()->map)</code> has been invoked in the OpsLoggerFactory * invocation, and Failure has the message code "FOO-012345")</p> * <pre> * map.put("A", "113"); * logger.log(Failure, new RuntimeException("Argh")); * </pre> * will give * <pre> * 2014-10-22T10:59:19.891Z,A=113,FOO-012345,Did not do anything. java.lang.RuntimeException: Argh (file:///tmp/stacktraces/stacktrace_7rSxGtIroLrznTg8bt1BrQ.txt) * </pre> * @param supplier the context supplier. (for example: <code>()->map</code>) * @return <code>this</code> for further configuration */ public OpsLoggerFactory setGlobalDiagnosticContextSupplier(DiagnosticContextSupplier supplier) { clearCachedInstance(); this.contextSupplier = Optional.ofNullable(supplier); return this; } /** * Enable/disable asynchronous logging. * * When disabled, the log(...) method call does not return until the message has been written to the target * file/output stream. * * When enabled, the log(...) method call pushes the log message object to an internal queue, and returns * immediately. The queue is emptied in order by a background thread. This can be very useful for keeping response * times low, but risks losing the log message objects still in the queue if the Java Virtual Machine is for any * reason abruptly terminated. * * If this method is not called, asynchronous logging is disabled. * * @param async true=async, false=sync * @return <code>this</code> for further configuration */ public OpsLoggerFactory setAsync(boolean async) { clearCachedInstance(); this.async = async; return this; } /** * Build and return the <code>OpsLogger</code> corresponding to the configuration provided. * * Calling <code>build</code> multiple times on a single instance of this class without * changing the configuration (by calling a <code>set</code> method) will return the * same <code>OpsLogger</code> instance each time. * * @param <T> LogMessage enum of all possible logger objects. * @return ready to use OpsLogger * @throws UncheckedIOException if a problem occurs creating parent directories for log files and/or stack traces */ @SuppressWarnings("unchecked") public <T extends Enum<T> & LogMessage> OpsLogger<T> build() throws UncheckedIOException { if (!cachedInstance.isPresent()) { cachedInstance = Optional.of(buildNewInstance()); } return (OpsLogger<T>) cachedInstance.get(); } private <T extends Enum<T> & LogMessage> OpsLogger<T> buildNewInstance() throws UncheckedIOException { InfrastructureFactory infrastructureFactory = new InfrastructureFactory(logfilePath, loggerOutput, storeStackTracesInFilesystem, stackTraceStoragePath, contextSupplier, errorHandler); if (async) { return asyncOpsLoggerFactory.build(infrastructureFactory); } return basicOpsLoggerFactory.build(infrastructureFactory); } private void clearCachedInstance() { cachedInstance = Optional.empty(); } private void validateParametersForSetDestination(PrintStream destination) { Objects.requireNonNull(destination, "Destination must not be null"); } private void validateParametersForSetStackTraceStoragePath(Path directory) { Objects.requireNonNull(directory, "path must not be null"); if (Files.exists(directory) && !Files.isDirectory(directory)) { throw new IllegalArgumentException("path must be a directory"); } } private void validateParametersForSetPath(Path path) { Objects.requireNonNull(path, "path must not be null"); if (Files.isDirectory(path)) { throw new IllegalArgumentException("Path must not be a directory"); } } //region test hooks for spying on internal factories void setAsyncOpsLoggerFactory(AsyncOpsLoggerFactory asyncOpsLoggerFactory) { this.asyncOpsLoggerFactory = asyncOpsLoggerFactory; } void setBasicOpsLoggerFactory(BasicOpsLoggerFactory basicOpsLoggerFactory) { this.basicOpsLoggerFactory = basicOpsLoggerFactory; } //endregion }