diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 229f1d344..7b7c321bc 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -653,7 +653,7 @@ - diff --git a/backends/jul-backend/src/main/java/io/spine/logging/backend/jul/AbstractJulBackend.java b/backends/jul-backend/src/main/java/io/spine/logging/backend/jul/AbstractJulBackend.java deleted file mode 100644 index 3b7cb2f8d..000000000 --- a/backends/jul-backend/src/main/java/io/spine/logging/backend/jul/AbstractJulBackend.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.backend.jul; - -import io.spine.logging.Level; -import io.spine.logging.backend.LoggerBackend; - -import java.util.logging.LogRecord; -import java.util.logging.Logger; - -import static io.spine.logging.Levels.toJavaLogging; - -/** - * An abstract implementation of {@code java.util.logging} (JUL) based backend. - * - *

This class handles everything except formatting of a log message - * and metadata. - * - * @see Original Java code for historical context. - */ -public abstract class AbstractJulBackend extends LoggerBackend { - // Set if any attempt at logging via the "forcing" logger fails due to an inability to set the - // log level in the forcing logger. This result is cached so we don't repeatedly trigger - // security exceptions every time something is logged. This field is only ever read or written - // to in cases where the LogManager returns subclasses of Logger. - private static volatile boolean cannotUseForcingLogger = false; - - private final Logger logger; - - // Internal constructor used by legacy callers - should be updated to just pass in the logging - // class name. This needs work to handle anonymous loggers however (if that's ever supported). - // TODO:2023-09-14:yevhenii.nadtochii: Should become `internal` when migrated to Kotlin. - // See issue: https://github.com/SpineEventEngine/logging/issues/47 - protected AbstractJulBackend(Logger logger) { - this.logger = logger; - } - - /** - * Constructs an abstract backend for the given class name. - * - *

Nested or inner class names (containing {@code $} are converted to names matching the - * standard JDK logger namespace by converting '$' to '.', but in future it is expected that - * nested and inner classes (especially anonymous ones) will have their names truncated to just - * the outer class name. There is no benefit to having loggers named after an inner/nested - * classes, and this distinction is expected to go away. - */ - protected AbstractJulBackend(String loggingClass) { - // TODO(b/27920233): Strip inner/nested classes when deriving logger name. - this(Logger.getLogger(loggingClass.replace('$', '.'))); - } - - @Override - public final String getLoggerName() { - return logger.getName(); - } - - @Override - public final boolean isLoggable(Level level) { - return logger.isLoggable(toJavaLogging(level)); - } - - /** - * Logs the given record using this backend. If {@code wasForced} is set, the backend will make a - * best effort attempt to bypass any log level restrictions in the underlying Java {@link Logger}, - * but there are circumstances in which this can fail. - */ - public final void log(LogRecord record, boolean wasForced) { - // Q: Why is the code below so complex ? - // - // The code below is (sadly) necessarily complex due to the need to cope with the possibility - // that the JDK logger instance we have is a subclass generated from a custom LogManager. - // - // Because the API docs for the "log(LogRecord)" method say that it can be overridden to capture - // all logging, we must call it if there's a chance it was overridden. - // - // However we cannot always call it directly if the log statement was forced because the default - // implementation of "log(LogRecord)" will will perform its own loggability check based only on - // the log level, and could discard forced log records. But at the same time, we must ensure - // that any handlers attached to that logger (and any parent loggers) will see all the log - // records, including "forced" ones. - // - // The only vaguely sane approach to this is to use a child logger when forcing is required. - // - // It seems reasonable to assume that if the logger we have is some special subclass (which - // might override the log(LogRecord) method) then any child logger we get from the LogManager - // will be overridden in the same way. Thus logging to our logger and logging to a child logger - // (which contains no additional handlers or filters) will have exactly the same effect, apart - // from the loggability check. - - // Do the fast boolean check (which normally succeeds) before calling isLoggable(). - if (!wasForced || logger.isLoggable(record.getLevel())) { - // Unforced log statements or forced log statements at or above the logger's level can be - // passed to the normal log(LogRecord) method. - logger.log(record); - } else { - // If logging has been forced for a log record which would otherwise be discarded, we cannot - // call our logger's log(LogRecord) method, so we must simulate its behavior in one of two - // ways. - // 1: Simulate the effect of calling the log(LogRecord) method directly (this is safe if the - // logger provided by the log manager was a normal Logger instance). - // 2: Obtain a "child" logger from the log manager which is set to log everything, and call - // its log(LogRecord) method instead (which should have the same overridden behavior and - // will still publish log records to our logger's handlers). - // - // In all cases we still call the filter (if one exists) even though we ignore the result. - // Use a local variable to avoid race conditions where the filter can be unset at any time. - var filter = logger.getFilter(); - if (filter != null) { - filter.isLoggable(record); - } - if (logger.getClass() == Logger.class || cannotUseForcingLogger) { - // If the Logger instance is not a subclass, its log(LogRecord) method cannot have been - // overridden. That means it's safe to just publish the log record directly to handlers - // to avoid the loggability check, which would otherwise discard the forced log record. - publish(logger, record); - } else { - // Hopefully rare situation in which the logger is subclassed _and_ the log record would - // normally be discarded based on its level. - forceLoggingViaChildLogger(record); - } - } - } - - // Documentation from public Java API documentation for java.util.logging.Logger: - // ---- - // It will then call a Filter (if present) to do a more detailed check on whether the record - // should be published. If that passes it will then publish the LogRecord to its output - // Handlers. By default, loggers also publish to their parent's Handlers, recursively up the - // tree. - // ---- - // The question of whether filtering is also done recursively of parent loggers seems to be - // answered by the documentation for setFilter(), which states: - // ""Set a filter to control output on _this_ Logger."" - // which implies that only the immediate filter should be checked before publishing recursively. - // - // Note: Normally it might be important to care about the number of stack frames being created - // if the log site information is inferred by the handlers (a handler at the root of the tree - // would get a lot of extra stack frames to search through). However for Flogger, the LogSite - // was already determined in the "shouldLog()" method because it's needed for things like - // rate limiting. Thus we don't have to care about using iterative methods vs recursion here. - private static void publish(Logger logger, LogRecord record) { - // Annoyingly this method appears to copy the array every time it is called, but there's - // nothing much we can do about this (and there could be synchronization issues even if we - // could access things directly because handlers can be changed at any time). Most of the - // time this returns the singleton empty array however, so it's not as bad as all that. - for (var handler : logger.getHandlers()) { - handler.publish(record); - } - if (logger.getUseParentHandlers()) { - logger = logger.getParent(); - if (logger != null) { - publish(logger, record); - } - } - } - - // WARNING: This code will fail for anonymous loggers (getName() == null) and when Flogger - // supports anonymous loggers it must ensure that this code path is avoided by not allowing - // subclasses of Logger to be used. - void forceLoggingViaChildLogger(LogRecord record) { - // Assume that nobody else will configure or manipulate loggers with this "secret" name. - var forcingLogger = getForcingLogger(logger); - - // This logger can be garbage collected at any time, so we must always reset any configuration. - // This code is subject to a bunch of unlikely race conditions if the logger is manipulated - // while these checks are being made, but there is nothing we can really do about this (the - // setting of log levels is not protected by synchronizing the logger instance). - try { - forcingLogger.setLevel(toJavaLogging(Level.ALL)); - } catch (SecurityException e) { - // If we're blocked from changing logging configuration then we cannot log "forced" log - // statements via the forcingLogger. Fall back to publishing them directly to the handlers - // (which may bypass logic present in an overridden log(LogRecord) method). - cannotUseForcingLogger = true; - // Log to the root logger to bypass any configuration that might drop this message. - Logger.getLogger("") - .log( - toJavaLogging(Level.SEVERE), - "Forcing log statements with Flogger has been partially disabled.\n" - + "The Flogger library cannot modify logger log levels, which is necessary to" - + " force log statements. This is likely due to an installed SecurityManager.\n" - + "Forced log statements will still be published directly to log handlers, but" - + " will not be visible to the 'log(LogRecord)' method of Logger subclasses.\n"); - publish(logger, record); - return; - } - // Assume any custom behaviour in our logger instance also exists for a child logger. - forcingLogger.log(record); - } - - // Pass in the logger (even though it's in our instance) so that it's accessible for testing - // without needing to make the field accessible. - // VisibleForTesting - // TODO:2023-09-14:yevhenii.nadtochii: Should become `internal` when migrated to Kotlin. - // See issue: https://github.com/SpineEventEngine/logging/issues/47 - protected Logger getForcingLogger(Logger parent) { - return Logger.getLogger(parent.getName() + ".__forced__"); - } -} diff --git a/backends/jul-backend/src/main/java/io/spine/logging/backend/jul/AbstractJulRecord.java b/backends/jul-backend/src/main/java/io/spine/logging/backend/jul/AbstractJulRecord.java deleted file mode 100644 index a8745681d..000000000 --- a/backends/jul-backend/src/main/java/io/spine/logging/backend/jul/AbstractJulRecord.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.backend.jul; - -import io.spine.logging.backend.AnyMessages; -import io.spine.logging.backend.LogData; -import io.spine.logging.backend.LogMessageFormatter; -import io.spine.logging.backend.Metadata; -import io.spine.logging.backend.MetadataProcessor; -import io.spine.logging.backend.SimpleMessageFormatter; - -import java.io.Serial; -import java.util.Arrays; -import java.util.ResourceBundle; -import java.util.logging.LogRecord; - -import static io.spine.logging.Levels.toJavaLogging; -import static java.time.Instant.ofEpochMilli; -import static java.util.Objects.requireNonNullElse; -import static java.util.concurrent.TimeUnit.NANOSECONDS; -import static java.util.logging.Level.WARNING; - -/** - * Abstract base for {@code java.util.logging} (JUL) log records. - * - *

This class supports three distinct modes of operation, depending on the state of the message - * and/or parameters: - * - *

Non-null message, {@code null} or empty parameters

- * - * This state is reached either when {@link #getMessage()} is first called, or if an explicit - * non-null message is set via {@link #setMessage(String)} (without setting any parameters). In - * this - * state, the message is considered to be formatted, and just returned via {@code getMessage()}. - * - *

Non-null message, non-empty parameters

- * - * This state is only reached if a user calls both {@link #setMessage(String)} and {@link - * #setParameters(Object[])}. In this state the message is treated as is it were a brace-format log - * message, and no formatting is attempted. Any relationship between this value, and the log message - * implied by the contained {@link LogData} and {@link Metadata} is lost. - * - *

For many reasons it is never a good idea for users to modify unknown {@link LogRecord} - * instances, but this does happen occasionally, so this class supports that in a best effort way, - * but users are always recommended to copy {@link LogRecord} instances if they need - * to modify them. - * - *

Corollary

- * - *

Because of the defined states above there are a few small, but necessary, changes to - * behaviour - * in this class as compared to the "vanilla" JDK {@link LogRecord}. - * - *

- * - * @see Original Java code for historical context. - */ -@SuppressWarnings("HardcodedLineSeparator") -public abstract class AbstractJulRecord extends LogRecord { - - private static final Object[] NO_PARAMETERS = new Object[0]; - - @Serial - private static final long serialVersionUID = 0L; - - private final LogData data; - private final MetadataProcessor metadata; - - /** - * Constructs a log record for normal logging without filling in format-specific fields. - * Subclasses calling this constructor are expected to additionally call {@link #setThrown} and - * perhaps {@link #setMessage} (depending on whether eager message caching is desired). - */ - @SuppressWarnings("OverridableMethodCallDuringObjectConstruction") - protected AbstractJulRecord(LogData data, Metadata scope) { - super(toJavaLogging(data.getLevel()), null); - this.data = data; - this.metadata = MetadataProcessor.forScopeAndLogSite(scope, data.getMetadata()); - - // Apply any data which is known or easily available without any effort. - var logSite = data.getLogSite(); - var timestampMillis = NANOSECONDS.toMillis(data.getTimestampNanos()); - setSourceClassName(logSite.getClassName()); - setSourceMethodName(logSite.getMethodName()); - setLoggerName(data.getLoggerName()); - setInstant(ofEpochMilli(timestampMillis)); - - // It was discovered that some null-hostile application code resets "parameters" to an empty - // array when it discovers null, so preempt that here by initializing the parameters array - // (but do it via the parent class method which doesn't have side effects). - // This should reduce the risk of needless message caching caused by calling - // `setParameters()` from the application code. - super.setParameters(NO_PARAMETERS); - } - - /** - * Constructs a log record in response to an exception during a previous logging attempt. A - * synthetic error message is generated from the original log data and the given exception is - * set - * as the cause. The level of this record is the maximum of WARNING or the original level. - */ - @SuppressWarnings("OverridableMethodCallDuringObjectConstruction") - protected AbstractJulRecord(RuntimeException error, LogData data, Metadata scope) { - this(data, scope); - // Re-target this log message as a warning (or above) since it indicates a real bug. - setLevel(data.getLevel().getValue() < WARNING.intValue() - ? WARNING - : toJavaLogging(data.getLevel())); - setThrown(error); - var errorMsg = - new StringBuilder("LOGGING ERROR: ").append(error.getMessage()) - .append('\n'); - safeAppend(data, errorMsg); - setMessage(errorMsg.toString()); - } - - /** - * Returns the formatter used when formatting {@link LogData}. This is not used if the log - * message was set explicitly, and can be overridden to supply a different formatter without - * necessarily requiring a new field in this class (to cut down on instance size). - */ - protected LogMessageFormatter getLogMessageFormatter() { - return SimpleMessageFormatter.getDefaultFormatter(); - } - - @Override - @SuppressWarnings("AssignmentToMethodParameter") // Special `null` treatment. - public final void setParameters(Object[] parameters) { - // IMPORTANT: We call getMessage() to cache the internal formatted message if someone indicates - // they want to change the parameters. This is to avoid a situation in which parameters are set, - // but the underlying message is still null. Do this first to switch internal states. - @SuppressWarnings("unused") - var unused = getMessage(); - // Now handle setting parameters as normal. - if (parameters == null) { - parameters = NO_PARAMETERS; - } - super.setParameters(parameters); - } - - @Override - public final void setMessage(String message) { - super.setMessage(requireNonNullElse(message, "")); - } - - @Override - public final String getMessage() { - var cachedMessage = super.getMessage(); - if (cachedMessage != null) { - return cachedMessage; - } - var formattedMessage = getLogMessageFormatter().format(data, metadata); - super.setMessage(formattedMessage); - return formattedMessage; - } - - /** - * No-op. - */ - @Override - public final void setResourceBundle(ResourceBundle bundle) { - } - - /** - * No-op. - */ - @Override - public final void setResourceBundleName(String name) { - } - - /** - * Returns the {@link LogData} instance encapsulating the current fluent log statement. - * - *

The LogData instance is effectively owned by this log record but must still be considered - * immutable by anyone using it (as it may be processed by multiple log handlers). - */ - public final LogData getLogData() { - return data; - } - - /** - * Returns the immutable {@link MetadataProcessor} which provides a unified view of scope and log - * site metadata. This should be used in preference to {@link Metadata} available from {@link - * LogData} which represents only the log site. - */ - public final MetadataProcessor getMetadataProcessor() { - return metadata; - } - - @Override - public String toString() { - // Note that this toString() method is _not_ safe against exceptions thrown by user toString(). - var out = new StringBuilder(); - out.append(getClass().getSimpleName()) - .append(" {\n message: ") - .append(getMessage()) - .append("\n arguments: ") - .append(getParameters() != null ? Arrays.asList(getParameters()) : "") - .append('\n'); - safeAppend(getLogData(), out); - out.append("\n}"); - return out.toString(); - } - - private static void safeAppend(LogData data, StringBuilder out) { - out.append(" original message: "); - out.append(AnyMessages.safeToString(data.getLiteralArgument())); - var metadata = data.getMetadata(); - if (metadata.size() > 0) { - out.append("\n metadata:"); - for (var n = 0; n < metadata.size(); n++) { - out.append("\n ") - .append(metadata.getKey(n) - .getLabel()) - .append(": ") - .append(AnyMessages.safeToString(metadata.getValue(n))); - } - } - out.append("\n level: ") - .append(AnyMessages.safeToString(data.getLevel())); - out.append("\n timestamp (nanos): ") - .append(data.getTimestampNanos()); - out.append("\n class: ") - .append(data.getLogSite() - .getClassName()); - out.append("\n method: ") - .append(data.getLogSite() - .getMethodName()); - out.append("\n line number: ") - .append(data.getLogSite() - .getLineNumber()); - } -} diff --git a/backends/jul-backend/src/main/java/io/spine/logging/backend/jul/JulRecord.java b/backends/jul-backend/src/main/java/io/spine/logging/backend/jul/JulRecord.java deleted file mode 100644 index dfdf2fd73..000000000 --- a/backends/jul-backend/src/main/java/io/spine/logging/backend/jul/JulRecord.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.backend.jul; - -import io.spine.logging.LogContext; -import io.spine.logging.backend.LogData; -import io.spine.logging.backend.Metadata; - -import java.util.logging.LogRecord; - -/** - * An eagerly evaluating {@link LogRecord} that can be passed to a normal - * log {@link java.util.logging.Handler Handler} instance for output. - * - * @see Original Java code for historical context. - */ -public final class JulRecord extends AbstractJulRecord { - /** Creates a {@link JulRecord} for a normal log statement from the given data. */ - public static JulRecord create(LogData data, Metadata scope) { - return new JulRecord(data, scope); - } - - /** @deprecated Use create(LogData data, Metadata scope) and pass scoped metadata in. */ - @Deprecated - public static JulRecord create(LogData data) { - return create(data, Metadata.empty()); - } - - /** Creates a {@link JulRecord} in the case of an error during logging. */ - public static JulRecord error(RuntimeException error, LogData data, Metadata scope) { - return new JulRecord(error, data, scope); - } - - /** @deprecated Use error(LogData data, Metadata scope) and pass scoped metadata in. */ - @Deprecated - public static JulRecord error(RuntimeException error, LogData data) { - return error(error, data, Metadata.empty()); - } - - private JulRecord(LogData data, Metadata scope) { - super(data, scope); - setThrown(getMetadataProcessor().getSingleValue(LogContext.Key.LOG_CAUSE)); - - // Calling getMessage() formats and caches the formatted message in the AbstractLogRecord. - // - // IMPORTANT: Conceptually there's no need to format the log message here, since backends can - // choose to format messages in different ways or log structurally, so it's not obviously a - // win to format things here first. Formatting would otherwise be done by AbstractLogRecord - // when getMessage() is called, and the results cached; so the only effect of being "lazy" - // should be that formatting (and thus calls to the toString() methods of arguments) happens - // later in the same log statement. - // - // However ... due to bad use of locking in core JDK log handler classes, any lazy formatting - // of log arguments (i.e. in the Handler's "publish()" method) can be done with locks held, - // and thus risks deadlock. We can mitigate the risk by formatting the message string early - // (i.e. here). This is wasteful in cases where this message is never needed (e.g. structured - // logging) but necessary when using many of the common JDK handlers (e.g. StreamHandler, - // FileHandler etc.) and it's impossible to know which handlers are being used. - String unused = getMessage(); - } - - private JulRecord(RuntimeException error, LogData data, Metadata scope) { - // In the case of an error, the base class handles everything as there's no specific formatting. - super(error, data, scope); - } -} diff --git a/backends/jul-backend/src/main/java/io/spine/logging/backend/jul/package-info.java b/backends/jul-backend/src/main/java/io/spine/logging/backend/jul/package-info.java deleted file mode 100644 index 0ce2a4dff..000000000 --- a/backends/jul-backend/src/main/java/io/spine/logging/backend/jul/package-info.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/** - * Contains implementation of {@code java.util.logging} (JUL) backend. - * - * @see Original Java code for historical context. - */ -@CheckReturnValue -package io.spine.logging.backend.jul; - -import com.google.errorprone.annotations.CheckReturnValue; diff --git a/backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/AbstractJulBackend.kt b/backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/AbstractJulBackend.kt new file mode 100644 index 000000000..e57b22f20 --- /dev/null +++ b/backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/AbstractJulBackend.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.logging.backend.jul + +import io.spine.annotation.VisibleForTesting +import io.spine.logging.Level +import io.spine.logging.Level.Companion.SEVERE +import io.spine.logging.backend.LoggerBackend +import io.spine.logging.toJavaLogging +import java.util.logging.LogRecord +import java.util.logging.Logger + +/** + * An abstract implementation of `java.util.logging` (JUL) based backend. + * + * This class handles everything except formatting of a log message and metadata. + * + * @see Original Java code for historical context. + */ +public abstract class AbstractJulBackend : LoggerBackend { + + private val logger: Logger + + /** + * Creates an abstract backend for the given [Logger]. + */ + internal constructor(logger: Logger) { + this.logger = logger + } + + /** + * Constructs an abstract backend for the given class name. + * + * Nested or inner class names (containing '$') are converted to names matching the + * standard JDK logger namespace by converting '$' to '.'. + */ + protected constructor(loggingClass: String) : this( + Logger.getLogger(loggingClass.replace('$', '.')) + ) + + public override val loggerName: String? + get() = logger.name + + public override fun isLoggable(level: Level): Boolean = logger.isLoggable(level.toJavaLogging()) + + /** + * Logs the given record using this backend. If [wasForced] is set, the backend will make a + * best effort attempt to bypass any log level restrictions in the underlying Java [Logger], + * but there are circumstances in which this can fail. + */ + public fun log(record: LogRecord, wasForced: Boolean) { + // Do the fast boolean check (which normally succeeds) before calling isLoggable(). + if (!wasForced || logger.isLoggable(record.level)) { + logger.log(record) + } else { + // In all cases we still call the filter (if one exists) + // even though we ignore the result. + // Use a local variable to avoid race conditions + // where the filter can be unset at any time. + val filter = logger.filter + filter?.isLoggable(record) + if (logger.javaClass == Logger::class.java || cannotUseForcingLogger) { + publish(logger, record) + } else { + forceLoggingViaChildLogger(record) + } + } + } + + private fun publish(logger: Logger, record: LogRecord) { + for (handler in logger.handlers) { + handler.publish(record) + } + if (logger.useParentHandlers) { + val parent = logger.parent + if (parent != null) { + publish(parent, record) + } + } + } + + /** + * Forces logging via a child logger, bypassing the parent handlers. + * + * WARNING: This code will fail for anonymous loggers `(getName() == null)` and + * when/if the Logging library supports anonymous loggers it must ensure that + * this code path is avoided by not allowing subclasses of `Logger` to be used. + */ + internal fun forceLoggingViaChildLogger(record: LogRecord) { + val forcingLogger = getForcingLogger(logger) + try { + forcingLogger.level = Level.ALL.toJavaLogging() + } catch (_: SecurityException) { + cannotUseForcingLogger = true + Logger.getLogger("").log( + SEVERE.toJavaLogging(), + """ + Forcing log statements with has been partially disabled. + The Logging library cannot modify logger log levels, which is necessary to + force log statements. This is likely due to an installed `SecurityManager`. + Forced log statements will still be published directly to log handlers, but + will not be visible to the `log(LogRecord)` method of `Logger` subclasses. + """.trimIndent() + ) + publish(logger, record) + return + } + forcingLogger.log(record) + } + + @VisibleForTesting + internal open fun getForcingLogger(parent: Logger): Logger = + Logger.getLogger(parent.name + ".__forced__") + + public companion object { + @Volatile + private var cannotUseForcingLogger: Boolean = false + } +} diff --git a/backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/AbstractJulRecord.kt b/backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/AbstractJulRecord.kt new file mode 100644 index 000000000..42784f428 --- /dev/null +++ b/backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/AbstractJulRecord.kt @@ -0,0 +1,227 @@ +/* + * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.logging.backend.jul + +import io.spine.logging.backend.LogData +import io.spine.logging.backend.LogMessageFormatter +import io.spine.logging.backend.Metadata +import io.spine.logging.backend.MetadataProcessor +import io.spine.logging.backend.SimpleMessageFormatter +import io.spine.logging.backend.safeToString +import io.spine.logging.toJavaLogging +import java.io.Serial +import java.time.Instant.ofEpochMilli +import java.util.* +import java.util.concurrent.TimeUnit.NANOSECONDS +import java.util.logging.Level.WARNING +import java.util.logging.LogRecord + +/** + * Abstract base for `java.util.logging` (JUL) log records. + * + * This class supports three distinct modes of operation, depending on the state of the message + * and/or parameters: + * + * ## Non-null message, `null` or empty parameters + * + * This state is reached either when [getMessage] is first called, or if an explicit + * non-null message is set via [setMessage] (without setting any parameters). + * In this state, the message is considered to be formatted, and just returned + * via [getMessage]. + * + * ## Non-null message, non-empty parameters + * + * This state is only reached if a user calls both [setMessage] and [setParameters]. + * In this state the message is treated as is it were a brace-format log + * message, and no formatting is attempted. + * Any relationship between this value, and the log message + * implied by the contained [LogData] and [Metadata] is lost. + * + * For many reasons it is never a good idea for users to modify unknown [LogRecord] + * instances, but this does happen occasionally, so this class supports that in a best effort way, + * but users are always recommended to copy [LogRecord] instances if they need + * to modify them. + * + * ## Corollary + * + * Because of the defined states above there are a few small, but necessary, changes to + * behaviour in this class as compared to the "vanilla" JDK [LogRecord]. + * + * * Since the "message" field being `null` indicates a private state, calling + * `setMessage(null)` from outside this class is equivalent to calling `setMessage("")`, + * and will not reset the instance to its initial "unformatted" state. + * This is within specification for [LogRecord] since the documentation for + * [getMessage] says that a return value of `null` is equivalent to the empty string. + * + * * Setting the parameters to `null` from outside this class will reset the parameters to + * a static singleton empty array. From outside this class, [getParameters] is never + * observed to contain `null`. This is also within specification for [LogRecord]. + * + * * Setting parameters from outside this class (to any value) will also result in the log + * message being formatted and cached (if it hadn't been set already). This is to avoid + * situations in which parameters are set, but the underlying message is still `null`. + * + * * `ResourceBundles` are not supported by `AbstractLogRecord` and any attempt to + * set them is ignored. + * + * @see Original Java code for historical context. + */ +@Suppress("HardcodedLineSeparator") +public abstract class AbstractJulRecord : LogRecord { + + private val data: LogData + private val metadata: MetadataProcessor + + /** + * Constructs a log record for normal logging without filling in format-specific fields. + * Subclasses calling this constructor are expected to additionally call [setThrown] and + * perhaps [setMessage] (depending on whether eager message caching is desired). + */ + @Suppress("LeakingThis") + protected constructor(data: LogData, scope: Metadata) : super( + data.level.toJavaLogging(), + null + ) { + this.data = data + this.metadata = MetadataProcessor.forScopeAndLogSite(scope, data.metadata) + + val logSite = data.logSite + val timestampMillis = NANOSECONDS.toMillis(data.timestampNanos) + sourceClassName = logSite.className + sourceMethodName = logSite.methodName + loggerName = data.loggerName + instant = ofEpochMilli(timestampMillis) + + // Pre-initialize parameters to avoid null-hostile application code changing it. + super.setParameters(NO_PARAMETERS) + } + + /** + * Constructs a log record in response to an exception during a previous logging attempt. + */ + @Suppress("LeakingThis") + protected constructor(error: RuntimeException, data: LogData, scope: Metadata) : this( + data, + scope + ) { + // Re-target this log message as a warning (or above) since it indicates a real bug. + level = if (data.level.value < WARNING.intValue()) WARNING else data.level.toJavaLogging() + thrown = error + val errorMsg = StringBuilder("LOGGING ERROR: ").append(error.message).append('\n') + safeAppend(data, errorMsg) + message = errorMsg.toString() + } + + /** + * Returns the formatter used when formatting [LogData]. + */ + protected open fun getLogMessageFormatter(): LogMessageFormatter = + SimpleMessageFormatter.getDefaultFormatter() + + override fun setParameters(parameters: Array?) { + // Cache the internal formatted message if someone indicates + // they want to change the parameters. + @Suppress("UNUSED_VARIABLE", "unused") + val unused = message + val nonNull = parameters ?: NO_PARAMETERS + super.setParameters(nonNull) + } + + override fun setMessage(message: String?) { + super.setMessage(message ?: "") + } + + override fun getMessage(): String { + val cached = super.getMessage() + if (cached != null) return cached + val formatted = getLogMessageFormatter().format(data, metadata) + super.setMessage(formatted) + return formatted + } + + /** No-op. */ + override fun setResourceBundle(bundle: ResourceBundle?): Unit = Unit + + /** No-op. */ + override fun setResourceBundleName(name: String?): Unit = Unit + + /** + * Returns the [LogData] instance encapsulating the current fluent log statement. + */ + public fun getLogData(): LogData = data + + /** + * Returns the immutable [MetadataProcessor] which provides a unified view of scope and log site metadata. + */ + public fun getMetadataProcessor(): MetadataProcessor = metadata + + @Suppress("SpreadOperator") + override fun toString(): String { + val out = StringBuilder() + out.append(javaClass.simpleName) + .append(" {\n message: ") + .append(message) + .append("\n arguments: ") + .append(if (parameters != null) listOf(*parameters) else "") + .append('\n') + safeAppend(getLogData(), out) + out.append("\n}") + return out.toString() + } + + private companion object { + @Serial + private const val serialVersionUID: Long = 0L + + private val NO_PARAMETERS: Array = arrayOf() + + private fun safeAppend(data: LogData, out: StringBuilder) { + out.append(" original message: ") + out.append(data.literalArgument.safeToString()) + val metadata = data.metadata + if (metadata.size() > 0) { + out.append("\n metadata:") + for (n in 0 until metadata.size()) { + out.append("\n ") + .append(metadata.getKey(n).label) + .append(": ") + .append(metadata.getValue(n).safeToString()) + } + } + out.append("\n level: ") + .append(data.level.safeToString()) + out.append("\n timestamp (nanos): ") + .append(data.timestampNanos) + out.append("\n class: ") + .append(data.logSite.className) + out.append("\n method: ") + .append(data.logSite.methodName) + out.append("\n line number: ") + .append(data.logSite.lineNumber) + } + } +} diff --git a/backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/JulBackend.kt b/backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/JulBackend.kt index caae81f72..f9a777d9a 100644 --- a/backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/JulBackend.kt +++ b/backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/JulBackend.kt @@ -44,12 +44,11 @@ import java.util.logging.Logger * * It allows forced publishing of logging records. * - * @param loggingClass - * a name of the logger created for this backend. A better name for - * the parameter would be `loggerName`, but we keep the naming - * consistent with the API we extend. Please also see the constructor - * of `AbstractBackend` which accepts `String` for the operation with - * the given class name. + * @param loggingClass A name of the logger created for this backend. + * A better name for the parameter would be `loggerName`, but we keep the naming + * consistent with the API we extend. Please also see the constructor + * of `AbstractBackend` which accepts `String` for the operation with + * the given class name. * @see AbstractJulBackend */ internal class JulBackend(loggingClass: String): AbstractJulBackend(loggingClass) { diff --git a/backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/JulRecord.kt b/backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/JulRecord.kt new file mode 100644 index 000000000..0b6890411 --- /dev/null +++ b/backends/jul-backend/src/main/kotlin/io/spine/logging/backend/jul/JulRecord.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.logging.backend.jul + +import io.spine.logging.LogContext +import io.spine.logging.backend.LogData +import io.spine.logging.backend.Metadata +import java.io.Serial + +/** + * An eagerly evaluating JUL [java.util.logging.LogRecord] that can be passed to a normal handler. + */ +public class JulRecord : AbstractJulRecord { + + public companion object { + + @Serial + private const val serialVersionUID: Long = 0L + + /** Creates a [JulRecord] for a normal log statement from the given data. */ + @JvmStatic + public fun create(data: LogData, scope: Metadata): JulRecord = JulRecord(data, scope) + + /** + * Deprecated. Use [create] and pass scoped metadata in. + */ + @Deprecated("Use create(LogData, Metadata)") + @JvmStatic + public fun create(data: LogData): JulRecord = create(data, Metadata.empty()) + + /** Creates a [JulRecord] in the case of an error during logging. */ + @JvmStatic + public fun error(error: RuntimeException, data: LogData, scope: Metadata): JulRecord = + JulRecord(error, data, scope) + + /** + * Deprecated. Use [error] and pass scoped metadata in. + */ + @Deprecated("Use error(RuntimeException, LogData, Metadata)") + @JvmStatic + public fun error(error: RuntimeException, data: LogData): JulRecord = + error(error, data, Metadata.empty()) + } + + private constructor(data: LogData, scope: Metadata): super(data, scope) { + thrown = getMetadataProcessor().getSingleValue(LogContext.Key.LOG_CAUSE) + // Force message formatting early to avoid deadlocks in some JUL handlers. + @Suppress("UNUSED_VARIABLE", "unused") + val unused = message + } + + private constructor(error: RuntimeException, data: LogData, scope: Metadata) : super( + error, + data, + scope + ) +} diff --git a/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/Log4j2BackendFactory.java b/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/Log4j2BackendFactory.java deleted file mode 100644 index de8190147..000000000 --- a/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/Log4j2BackendFactory.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2023, The Flogger Authors; 2023, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.backend.log4j2; - -import io.spine.logging.backend.LoggerBackend; -import io.spine.logging.backend.BackendFactory; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.core.Logger; - -/** - * Backend factory for Log4j2. - * - *

When using {@code io.spine.logging.backend.system.DefaultPlatform}, - * this factory will automatically be used if it is included on the classpath, - * and no other implementation of {@code BackendFactory} (other than the default - * implementation) is present. - * - *

To specify it more explicitly or to work around an issue where multiple - * backend implementations are on the classpath, you can set {@code flogger.backend_factory} - * system property to {@code io.spine.logging.backend.log4j2.Log4j2BackendFactory}. - */ -public final class Log4j2BackendFactory extends BackendFactory { - - // Must be public for ServiceLoader - public Log4j2BackendFactory() {} - - @Override - public LoggerBackend create(String loggingClass) { - - // Compute the logger name the same way as in `SimpleBackendFactory`. - var name = loggingClass.replace('$', '.'); - - // There is `log4j.Logger` interface and `log4j.core.Logger` implementation. - // Implementation exposes more methods that are needed by the backend. - // So, we have to cast an interface back to its implementation. - var logger = (Logger) LogManager.getLogger(name); - - return new Log4j2LoggerBackend(logger); - } - - /** - * Returns a fully-qualified name of this class. - */ - @Override - public String toString() { - return getClass().getName(); - } -} diff --git a/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/Log4j2LogEventUtil.java b/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/Log4j2LogEventUtil.java deleted file mode 100644 index a042919a8..000000000 --- a/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/Log4j2LogEventUtil.java +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright 2023, The Flogger Authors; 2023, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.backend.log4j2; - -import io.spine.logging.KeyValueHandler; -import io.spine.logging.Level; -import io.spine.logging.LogContext; -import io.spine.logging.MetadataKey; -import io.spine.logging.backend.LogData; -import io.spine.logging.backend.MetadataHandler; -import io.spine.logging.backend.Platform; -import io.spine.logging.backend.SimpleMessageFormatter; -import io.spine.logging.context.Tags; -import io.spine.logging.context.ScopedLoggingContext; -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.LoggerContext; -import org.apache.logging.log4j.core.config.DefaultConfiguration; -import org.apache.logging.log4j.core.impl.ContextDataFactory; -import org.apache.logging.log4j.core.impl.Log4jLogEvent; -import org.apache.logging.log4j.core.time.Instant; -import org.apache.logging.log4j.core.time.MutableInstant; -import org.apache.logging.log4j.message.SimpleMessage; -import org.apache.logging.log4j.util.StringMap; - -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import static io.spine.logging.Levels.toLevel; -import static io.spine.logging.backend.MetadataProcessor.forScopeAndLogSite; -import static java.util.Objects.requireNonNull; -import static java.util.concurrent.TimeUnit.NANOSECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; -import static java.util.logging.Level.WARNING; - -/** - * Helper to format {@link LogData}. - */ -final class Log4j2LogEventUtil { - - private Log4j2LogEventUtil() { - } - - static LogEvent toLog4jLogEvent(String loggerName, LogData logData) { - var metadata = forScopeAndLogSite(Platform.getInjectedMetadata(), logData.getMetadata()); - - /* - * If no configuration file could be located, Log4j2 will use the DefaultConfiguration. - * This will cause logging output to go to the console, - * and the context data will be ignored. - * This mechanism can be used to detect if a configuration file has been loaded - * (or if the default configuration was overwritten through the means of - * a configuration factory) by checking the - * type of the current configuration class. - * - * Be aware that the `LoggerContext` class is not part of Log4j2's public API and behavior - * can change with any minor release. - * - * For the future we are thinking about implementing a Flogger aware Log4j2 configuration - * (e.g., using a configuration builder with a custom ConfigurationFactory) to configure - * a formatter, which can perhaps be installed as default if nothing else is present. - * Then, we would not rely on Log4j2 internals. - */ - var ctx = LoggerContext.getContext(false); - var config = ctx.getConfiguration(); - String message; - if (config instanceof DefaultConfiguration) { - message = SimpleMessageFormatter.getDefaultFormatter() - .format(logData, metadata); - } else { - throw new IllegalStateException(String.format( - "Unable to format a message for the configuration: `%s`.", config - )); - } - - var thrown = metadata.getSingleValue(LogContext.Key.LOG_CAUSE); - return toLog4jLogEvent( - loggerName, logData, message, toLog4jLevel(logData.getLevel()), thrown - ); - } - - static LogEvent toLog4jLogEvent(String loggerName, RuntimeException error, LogData badData) { - var message = formatBadLogData(error, badData); - // Re-target this log message as a warning (or above) since it indicates a real bug. - var level = badData.getLevel().getValue() < WARNING.intValue() - ? toLevel(WARNING) - : badData.getLevel(); - return toLog4jLogEvent(loggerName, badData, message, toLog4jLevel(level), error); - } - - private static LogEvent toLog4jLogEvent( - String loggerName, - LogData logData, - String message, - org.apache.logging.log4j.Level level, - Throwable thrown) { - - var logSite = logData.getLogSite(); - var locationInfo = - new StackTraceElement( - logSite.getClassName(), - logSite.getMethodName(), - logSite.getFileName(), - logSite.getLineNumber()); - - return Log4jLogEvent.newBuilder() - .setLoggerName(loggerName) - .setLoggerFqcn(logData.getLoggerName()) - .setLevel(level) // this might be different from logData.getLevel() for errors. - .setMessage(new SimpleMessage(message)) - .setThreadName(Thread.currentThread().getName()) - .setInstant(getInstant(logData.getTimestampNanos())) - .setThrown(thrown) - .setIncludeLocation(true) - .setSource(locationInfo) - .setContextData(createContextMap(logData)) - .build(); - } - - @SuppressWarnings({"NanosTo_Seconds", "SecondsTo_Nanos"}) - private static Instant getInstant(long timestampNanos) { - var instant = new MutableInstant(); - // Don't use Duration here as (a) it allocates and (b) we can't allow error on overflow. - var epochSeconds = NANOSECONDS.toSeconds(timestampNanos); - @SuppressWarnings("NumericCastThatLosesPrecision") - var remainingNanos = (int) (timestampNanos - SECONDS.toNanos(epochSeconds)); - instant.initFromEpochSecond(epochSeconds, remainingNanos); - return instant; - } - - /** - * Converts {@code java.util.logging.Leve}l to {@code org.apache.log4j.Level}. - */ - static org.apache.logging.log4j.Level toLog4jLevel(Level level) { - var logLevel = level.getValue(); - if (logLevel < java.util.logging.Level.FINE.intValue()) { - return org.apache.logging.log4j.Level.TRACE; - } - if (logLevel < java.util.logging.Level.INFO.intValue()) { - return org.apache.logging.log4j.Level.DEBUG; - } - if (logLevel < java.util.logging.Level.WARNING.intValue()) { - return org.apache.logging.log4j.Level.INFO; - } - if (logLevel < java.util.logging.Level.SEVERE.intValue()) { - return org.apache.logging.log4j.Level.WARN; - } - return org.apache.logging.log4j.Level.ERROR; - } - - /** - * Formats the log message in response to an exception during a previous logging attempt. - * - *

A synthetic error message is generated from the original log data and the given - * exception is set as the cause. The level of this record is the maximum of WARNING or - * the original level. - */ - private static String formatBadLogData(RuntimeException error, LogData badLogData) { - var errorMsg = - new StringBuilder("LOGGING ERROR: ").append(error.getMessage()) - .append('\n'); - var length = errorMsg.length(); - try { - appendLogData(badLogData, errorMsg); - } catch (RuntimeException e) { - // Reset partially written buffer when an error occurs. - errorMsg.setLength(length); - errorMsg.append("Cannot append LogData: ") - .append(e); - } - return errorMsg.toString(); - } - - /** - * Appends the given {@link LogData} to the given {@link StringBuilder}. - */ - @SuppressWarnings("HardcodedLineSeparator") - private static void appendLogData(LogData data, StringBuilder out) { - out.append(" original message: "); - out.append(data.getLiteralArgument()); - var metadata = data.getMetadata(); - if (metadata.size() > 0) { - out.append("\n metadata:"); - for (var n = 0; n < metadata.size(); n++) { - out.append("\n "); - out.append(metadata.getKey(n) - .getLabel()) - .append(": ") - .append(metadata.getValue(n)); - } - } - out.append("\n level: ") - .append(data.getLevel()); - out.append("\n timestamp (nanos): ") - .append(data.getTimestampNanos()); - out.append("\n class: ") - .append(data.getLogSite() - .getClassName()); - out.append("\n method: ") - .append(data.getLogSite() - .getMethodName()); - out.append("\n line number: ") - .append(data.getLogSite() - .getLineNumber()); - } - - private static final MetadataHandler HANDLER = - MetadataHandler.builder(Log4j2LogEventUtil::handleMetadata) - .build(); - - private static void handleMetadata( - MetadataKey key, Object value, KeyValueHandler kvh) { - if (key.getClass() - .equals(LogContext.Key.TAGS.getClass())) { - processTags(key, value, kvh); - } else { - // In theory a user can define a custom tag and use it as a MetadataKey. Those - // keys shall be treated in the same way as LogContext.Key.TAGS when used as a - // MetadataKey. Might be removed if visibility of MetadataKey#clazz changes. - if (value instanceof Tags) { - processTags(key, value, kvh); - } else { - ValueQueue.appendValues(key.getLabel(), value, kvh); - } - } - } - - private static void processTags( - MetadataKey key, Object value, KeyValueHandler kvh) { - var valueQueue = ValueQueue.appendValueToNewQueue(value); - // Unlike single metadata (which is usually formatted as a single value), tags are always - // formatted as a list. - // Given the tags: tags -> foo=[bar], it will be formatted as tags=[foo=bar]. - ValueQueue.appendValues( - key.getLabel(), - valueQueue.size() == 1 - ? StreamSupport.stream(valueQueue.spliterator(), false) - .collect(Collectors.toList()) - : valueQueue, - kvh); - } - - /** - * We do not support {@code MDC.getContext()} and {@code NDC.getStack()} and we do not make any - * attempt to merge Log4j2 context data with Flogger's context data. Instead, users should use - * the {@link ScopedLoggingContext}. - * - *

Flogger's {@link ScopedLoggingContext} allows to include additional metadata and tags - * into logs which are written from current thread. - * This context data will be added to the log4j2 event. - */ - private static StringMap createContextMap(LogData logData) { - var metadataProcessor = forScopeAndLogSite(Platform.getInjectedMetadata(), - logData.getMetadata()); - - var contextData = ContextDataFactory.createContextData(metadataProcessor.keyCount()); - metadataProcessor.process( - HANDLER, - (key, value) -> { - requireNonNull(value); - contextData.putValue( - key, - ValueQueue.maybeWrap(value, contextData.getValue(key))); - } - ); - - contextData.freeze(); - - return contextData; - } -} diff --git a/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/Log4j2LoggerBackend.java b/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/Log4j2LoggerBackend.java deleted file mode 100644 index 7c95519e0..000000000 --- a/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/Log4j2LoggerBackend.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2023, The Flogger Authors; 2023, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.backend.log4j2; - -import io.spine.logging.Level; -import io.spine.logging.backend.LogData; -import io.spine.logging.backend.LoggerBackend; -import org.apache.logging.log4j.core.Logger; - -import static io.spine.logging.backend.log4j2.Log4j2LogEventUtil.toLog4jLevel; -import static io.spine.logging.backend.log4j2.Log4j2LogEventUtil.toLog4jLogEvent; - -/** - * A logging backend that uses Log4j2 to output log statements. - */ -final class Log4j2LoggerBackend extends LoggerBackend { - - private final Logger logger; - - // VisibleForTesting - Log4j2LoggerBackend(Logger logger) { - this.logger = logger; - } - - @Override - public String getLoggerName() { - return logger.getName(); - } - - @Override - public boolean isLoggable(Level level) { - return logger.isEnabled(toLog4jLevel(level)); - } - - @Override - public void log(LogData logData) { - // The caller is responsible to call `isLoggable()` before calling - // this method to ensure that only messages above the given - // threshold are logged. - logger.get().log(toLog4jLogEvent(logger.getName(), logData)); - } - - @Override - public void handleError(RuntimeException error, LogData badData) { - logger.get().log(toLog4jLogEvent(logger.getName(), error, badData)); - } -} diff --git a/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/ValueQueue.java b/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/ValueQueue.java deleted file mode 100644 index d0642d9f9..000000000 --- a/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/ValueQueue.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2021, The Flogger Authors; 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.backend.log4j2; - -import io.spine.logging.KeyValueHandler; -import io.spine.logging.context.Tags; -import org.jspecify.annotations.Nullable; - -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; - -import static io.spine.logging.util.Checks.checkNotNull; - -/** - * A simple FIFO queue linked-list implementation designed to store multiple - * metadata values in a {@link org.apache.logging.log4j.util.StringMap StringMap}. - * - *

There are two aspects worth pointing out: - * - *

First, it is expected that a value queue always contains at least - * a single item. You cannot add null references to the queue, and you cannot - * create an empty queue. - * - *

Second, it is expected to access the contents of the value queue via - * an iterator only. Hence, we do not provide a method for taking the first - * item in the value queue. - * - *

Metadata values in Flogger always have unique keys, but those keys can - * have the same label. Because Log4j2 uses a {@code String} keyed map, - * we would risk clashing of values if we just used the label to store each - * value directly. This class lets us store a list of values for a single - * label while being memory efficient in the common case where each label - * really does only have one value. - */ -@SuppressWarnings("UnnecessarilyQualifiedStaticUsage") -final class ValueQueue implements Iterable { - - // Since the number of elements is almost never above 1 or 2, a LinkedList saves space. - private final List values = new LinkedList<>(); - - private ValueQueue() { - } - - static ValueQueue newQueue(Object item) { - checkNotNull(item, "item"); - var valueQueue = new ValueQueue(); - valueQueue.put(item); - return valueQueue; - } - - static Object maybeWrap(Object value, @Nullable Object existingValue) { - checkNotNull(value, "value"); - if (existingValue == null) { - return value; - } else { - // This should only rarely happen, so a few small allocations seems acceptable. - var existingQueue = - existingValue instanceof ValueQueue - ? (ValueQueue) existingValue - : ValueQueue.newQueue(existingValue); - existingQueue.put(value); - return existingQueue; - } - } - - static void appendValues(String label, Object valueOrQueue, KeyValueHandler kvh) { - if (valueOrQueue instanceof ValueQueue) { - for (var value : (ValueQueue) valueOrQueue) { - emit(label, value, kvh); - } - } else { - emit(label, valueOrQueue, kvh); - } - } - - /** - * Helper method for creating and initializing a value queue with a non-nullable value. If value - * is an instance of Tags, each tag will be added to the value queue. - */ - static ValueQueue appendValueToNewQueue(Object value) { - var valueQueue = new ValueQueue(); - ValueQueue.emit(null, value, (k, v) -> valueQueue.put(v)); - return valueQueue; - } - - /** - * Emits a metadata label/value pair to a given {@code KeyValueHandler}, handling {@code Tags} - * values specially. - * - *

Tags are key-value mappings which cannot be modified or replaced. If you add the tag - * mapping - * {@code "foo" -> true} and later add {@code "foo" -> false}, you get "foo" mapped to both - * true - * and false. This is very deliberate since the key space for tags is global and the risk of - * two - * bits of code accidentally using the same tag name is real (e.g. you add "id=xyz" to a scope, - * but you see "id=abcd" because someone else added "id=abcd" in a context you weren't aware - * of). - * - *

Given three tag mappings: - *

    - *
  • {@code "baz"} (no value) - *
  • {@code "foo" -> true} - *
  • {@code "foo" -> false} - *
- * - * the value queue is going to store the mappings as: - *
{@code
-     * tags=[baz, foo=false, foo=true]
-     * }
- * - *

Reusing the label 'tags' is intentional as this allows us to store the flatten tags in - * Log4j2's ContextMap. - */ - static void emit(String label, Object value, KeyValueHandler kvh) { - if (value instanceof Tags) { - // Flatten tags to treat them as keys or key/value pairs, - // e.g., tags=[baz=bar, baz=bar2, foo] - ((Tags) value) - .asMap() - .forEach( - (k, v) -> { - if (v.isEmpty()) { - kvh.handle(label, k); - } else { - for (var obj : v) { - kvh.handle(label, k + '=' + obj); - } - } - }); - } else { - kvh.handle(label, value); - } - } - - @Override - public Iterator iterator() { - return values.iterator(); - } - - void put(Object item) { - checkNotNull(item, "item"); - values.add(item); - } - - int size() { - return values.size(); - } - - /** - * Returns a string representation of the contents of the specified value queue. - * - *
    - *
  • If the value queue is empty, the method returns an empty string. - *
  • If the value queue contains a single element {@code a}, this method returns {@code - * a.toString()}. - *
  • Otherwise, the contents of the queue are formatted like a {@code List}. - *
- */ - @Override - public String toString() { - // This case shouldn't actually happen unless you use the value queue - // for storing emitted values. - if (values.isEmpty()) { - return ""; - } - // Consider using MessageUtils.safeToString() here. - if (values.size() == 1) { - return values.get(0) - .toString(); - } - return values.toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - var that = (ValueQueue) o; - return values.equals(that.values); - } - - @Override - public int hashCode() { - return Objects.hashCode(values); - } -} diff --git a/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/package-info.java b/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/package-info.java deleted file mode 100644 index e7cea33bd..000000000 --- a/backends/log4j2-backend/src/main/java/io/spine/logging/backend/log4j2/package-info.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2023, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/** - * Contains implementation of Apache Log4j2 backend. - */ -@CheckReturnValue -package io.spine.logging.backend.log4j2; - -import com.google.errorprone.annotations.CheckReturnValue; diff --git a/backends/log4j2-backend/src/main/kotlin/io/spine/logging/backend/log4j2/Log4j2BackendFactory.kt b/backends/log4j2-backend/src/main/kotlin/io/spine/logging/backend/log4j2/Log4j2BackendFactory.kt new file mode 100644 index 000000000..b4b35f606 --- /dev/null +++ b/backends/log4j2-backend/src/main/kotlin/io/spine/logging/backend/log4j2/Log4j2BackendFactory.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2023, The Flogger Authors; 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.logging.backend.log4j2 + +import io.spine.logging.backend.BackendFactory +import io.spine.logging.backend.LoggerBackend +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.core.Logger + +/** + * Backend factory for Log4j2. + * + * When using `io.spine.logging.backend.system.DefaultPlatform`, this factory will + * automatically be used if it is included on the classpath, and no other implementation + * of `BackendFactory` (other than the default implementation) is present. + * + * To specify it more explicitly or to work around an issue where multiple backend implementations + * are on the classpath, you can set `spine.logging.backend_factory` system property to + * `io.spine.logging.backend.log4j2.Log4j2BackendFactory`. + */ +public class Log4j2BackendFactory : BackendFactory() { + + override fun create(loggingClass: String): LoggerBackend { + // Compute the logger name the same way as in SimpleBackendFactory. + val name = loggingClass.replace('$', '.') + + // There is log4j.Logger interface and log4j.core.Logger implementation. + // Implementation exposes more methods that are needed by the backend. + // So, we have to cast an interface back to its implementation. + val logger = LogManager.getLogger(name) as Logger + + return Log4j2LoggerBackend(logger) + } + + /** Returns a fully-qualified name of this class. */ + override fun toString(): String = javaClass.name +} diff --git a/platforms/jvm-default-platform/src/main/java/io/spine/logging/backend/system/package-info.java b/backends/log4j2-backend/src/main/kotlin/io/spine/logging/backend/log4j2/Log4j2LoggerBackend.kt similarity index 59% rename from platforms/jvm-default-platform/src/main/java/io/spine/logging/backend/system/package-info.java rename to backends/log4j2-backend/src/main/kotlin/io/spine/logging/backend/log4j2/Log4j2LoggerBackend.kt index 5150bbfc9..5dbb21c2a 100644 --- a/platforms/jvm-default-platform/src/main/java/io/spine/logging/backend/system/package-info.java +++ b/backends/log4j2-backend/src/main/kotlin/io/spine/logging/backend/log4j2/Log4j2LoggerBackend.kt @@ -24,16 +24,30 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package io.spine.logging.backend.log4j2 + +import io.spine.logging.Level +import io.spine.logging.backend.LogData +import io.spine.logging.backend.LoggerBackend +import org.apache.logging.log4j.core.Logger + /** - * Contains the default logger platform implementation for a server-side - * Java environment. - * - *

Although {@code system} is not the best name for this package, - * the better option, {@code default}, is reserved by Java and cannot be used here. - * - * @see Original Java code for historical context. + * A logging backend that uses Log4j2 to output log statements. */ -@CheckReturnValue -package io.spine.logging.backend.system; +internal class Log4j2LoggerBackend(private val logger: Logger) : LoggerBackend() { + + override val loggerName: String? + get() = logger.name + + override fun isLoggable(level: Level): Boolean = + logger.isEnabled(level.toLog4j()) + + override fun log(data: LogData) { + // The caller must ensure isLoggable() is checked before calling this method. + logger.get().log(toLog4jLogEvent(logger.name, data)) + } -import com.google.errorprone.annotations.CheckReturnValue; + override fun handleError(error: RuntimeException, badData: LogData) { + logger.get().log(toLog4jLogEvent(logger.name, error, badData)) + } +} diff --git a/backends/log4j2-backend/src/main/kotlin/io/spine/logging/backend/log4j2/LogEvents.kt b/backends/log4j2-backend/src/main/kotlin/io/spine/logging/backend/log4j2/LogEvents.kt new file mode 100644 index 000000000..eee726589 --- /dev/null +++ b/backends/log4j2-backend/src/main/kotlin/io/spine/logging/backend/log4j2/LogEvents.kt @@ -0,0 +1,232 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +@file:JvmName("LogEvents") + +package io.spine.logging.backend.log4j2 + +import io.spine.logging.KeyValueHandler +import io.spine.logging.Level +import io.spine.logging.LogContext +import io.spine.logging.MetadataKey +import io.spine.logging.backend.LogData +import io.spine.logging.backend.MetadataHandler +import io.spine.logging.backend.Platform +import io.spine.logging.backend.SimpleMessageFormatter +import io.spine.logging.context.ScopedLoggingContext +import io.spine.logging.context.Tags +import io.spine.logging.toLevel +import java.util.Objects.requireNonNull +import java.util.concurrent.TimeUnit.NANOSECONDS +import java.util.concurrent.TimeUnit.SECONDS +import java.util.logging.Level.FINE +import java.util.logging.Level.INFO +import java.util.logging.Level.SEVERE +import java.util.logging.Level.WARNING +import java.util.stream.Collectors +import java.util.stream.StreamSupport +import org.apache.logging.log4j.core.LogEvent +import org.apache.logging.log4j.core.LoggerContext +import org.apache.logging.log4j.core.config.DefaultConfiguration +import org.apache.logging.log4j.core.impl.ContextDataFactory +import org.apache.logging.log4j.core.impl.Log4jLogEvent +import org.apache.logging.log4j.core.time.Instant +import org.apache.logging.log4j.core.time.MutableInstant +import org.apache.logging.log4j.message.SimpleMessage +import org.apache.logging.log4j.util.StringMap +import org.apache.logging.log4j.Level as L4jLevel + +/** + * Helper to format [LogData]. + */ +public fun toLog4jLogEvent(loggerName: String, logData: LogData): LogEvent { + val metadata = io.spine.logging.backend.MetadataProcessor.forScopeAndLogSite( + Platform.getInjectedMetadata(), logData.metadata + ) + + // See JavaDoc in the original version for details about DefaultConfiguration handling. + val ctx = LoggerContext.getContext(false) + val config = ctx.configuration + val message: String = if (config is DefaultConfiguration) { + SimpleMessageFormatter.getDefaultFormatter().format(logData, metadata) + } else { + error("Unable to format a message for the configuration: `$config`.") + } + + val thrown = metadata.getSingleValue(LogContext.Key.LOG_CAUSE) + return toLog4jLogEvent( + loggerName, logData, message, logData.level.toLog4j(), thrown + ) +} + +/** + * Helper to format [LogData]. + */ +public fun toLog4jLogEvent( + loggerName: String, + error: RuntimeException, + badData: LogData +): LogEvent { + val message = formatBadLogData(error, badData) + val level = + if (badData.level.value < WARNING.intValue()) WARNING.toLevel() else badData.level + return toLog4jLogEvent(loggerName, badData, message, level.toLog4j(), error) +} + +private fun toLog4jLogEvent( + loggerName: String, + logData: LogData, + message: String, + level: L4jLevel, + thrown: Throwable? +): LogEvent { + val logSite = logData.logSite + val locationInfo = StackTraceElement( + logSite.className, + logSite.methodName, + logSite.fileName, + logSite.lineNumber + ) + + return Log4jLogEvent.newBuilder() + .setLoggerName(loggerName) + .setLoggerFqcn(logData.loggerName) + .setLevel(level) + .setMessage(SimpleMessage(message)) + .setThreadName(Thread.currentThread().name) + .setInstant(getInstant(logData.timestampNanos)) + .setThrown(thrown) + .setIncludeLocation(true) + .setSource(locationInfo) + .setContextData(createContextMap(logData)) + .build() +} + +@Suppress("NAME_SHADOWING") +private fun getInstant(timestampNanos: Long): Instant { + val instant = MutableInstant() + val epochSeconds = NANOSECONDS.toSeconds(timestampNanos) + val remainingNanos = (timestampNanos - SECONDS.toNanos(epochSeconds)).toInt() + instant.initFromEpochSecond(epochSeconds, remainingNanos) + return instant +} + +@Suppress("TooGenericExceptionCaught") +private fun formatBadLogData(error: RuntimeException, badLogData: LogData): String { + val errorMsg = StringBuilder("LOGGING ERROR: ").append(error.message).append('\n') + val length = errorMsg.length + return try { + appendLogData(badLogData, errorMsg) + errorMsg.toString() + } catch (e: RuntimeException) { + errorMsg.setLength(length) + errorMsg.append("Cannot append LogData: ").append(e) + errorMsg.toString() + } +} + +/** Appends the given [LogData] to the given [StringBuilder]. */ +@Suppress("HardcodedLineSeparator") +private fun appendLogData(data: LogData, out: StringBuilder) { + out.append(" original message: ") + out.append(data.literalArgument) + val metadata = data.metadata + if (metadata.size() > 0) { + out.append("\n metadata:") + for (n in 0 until metadata.size()) { + out.append("\n ") + out.append(metadata.getKey(n).label) + .append(": ") + .append(metadata.getValue(n)) + } + } + out.append("\n level: ").append(data.level) + out.append("\n timestamp (nanos): ").append(data.timestampNanos) + out.append("\n class: ").append(data.logSite.className) + out.append("\n method: ").append(data.logSite.methodName) + out.append("\n line number: ").append(data.logSite.lineNumber) +} + +private val HANDLER: MetadataHandler = + MetadataHandler.builder { key, value, kvh -> + handleMetadata(key, value, kvh) + }.build() + +private fun handleMetadata(key: MetadataKey, value: Any, kvh: KeyValueHandler) { + if (key.javaClass == LogContext.Key.TAGS.javaClass) { + processTags(key, value, kvh) + } else { + if (value is Tags) { + processTags(key, value, kvh) + } else { + ValueQueue.appendValues(key.label, value, kvh) + } + } +} + +private fun processTags(key: MetadataKey, value: Any, kvh: KeyValueHandler) { + val valueQueue = ValueQueue.appendValueToNewQueue(value) + ValueQueue.appendValues( + key.label, + if (valueQueue.size() == 1) StreamSupport.stream(valueQueue.spliterator(), false) + .collect(Collectors.toList()) else valueQueue, + kvh + ) +} + +/** + * We do not support MDC/NDC merging. Use [ScopedLoggingContext]. + */ +private fun createContextMap(logData: LogData): StringMap { + val metadataProcessor = io.spine.logging.backend.MetadataProcessor.forScopeAndLogSite( + Platform.getInjectedMetadata(), logData.metadata + ) + + val contextData = ContextDataFactory.createContextData(metadataProcessor.keyCount()) + val kvh = KeyValueHandler { key, value -> + requireNonNull(value) + contextData.putValue( + key, + ValueQueue.maybeWrap(value!!, contextData.getValue(key)) + ) + } + metadataProcessor.process(HANDLER, kvh) + contextData.freeze() + return contextData +} + +/** + * Converts this [java.util.logging.Level] to [org.apache.logging.log4j.Level]. + */ +public fun Level.toLog4j(): L4jLevel { + return when { + value < FINE.intValue() -> L4jLevel.TRACE + value < INFO.intValue() -> L4jLevel.DEBUG + value < WARNING.intValue() -> L4jLevel.INFO + value < SEVERE.intValue() -> L4jLevel.WARN + else -> L4jLevel.ERROR + } +} diff --git a/backends/log4j2-backend/src/main/kotlin/io/spine/logging/backend/log4j2/ValueQueue.kt b/backends/log4j2-backend/src/main/kotlin/io/spine/logging/backend/log4j2/ValueQueue.kt new file mode 100644 index 000000000..9494cfef2 --- /dev/null +++ b/backends/log4j2-backend/src/main/kotlin/io/spine/logging/backend/log4j2/ValueQueue.kt @@ -0,0 +1,174 @@ +/* + * Copyright 2021, The Flogger Authors; 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.logging.backend.log4j2 + +import io.spine.logging.KeyValueHandler +import io.spine.logging.context.Tags +import io.spine.logging.util.Checks.checkNotNull +import java.util.LinkedList + +/** + * A simple FIFO queue linked-list implementation designed to store multiple + * metadata values in a org.apache.logging.log4j.util.StringMap. + * + * There are two aspects worth pointing out: + * + * 1) It is expected that a value queue always contains at least a single item. + * You cannot add null references to the queue, and you cannot create an empty queue. + * + * 2) It is expected to access the contents of the value queue via an iterator only. + * Hence, we do not provide a method for taking the first item in the value queue. + * + * Metadata values in the Logging library always have unique keys, but those keys can + * have the same label. Because Log4j2 uses a String keyed map, we would risk clashing of + * values if we just used the label to store each value directly. This class lets us store + * a list of values for a single label while being memory efficient in the common case + * where each label really does only have one value. + */ +internal class ValueQueue private constructor() : Iterable { + + // Since the number of elements is almost never above 1 or 2, a LinkedList saves space. + private val values: MutableList = LinkedList() + + override fun iterator(): Iterator = values.iterator() + + fun put(item: Any?) { + val it = checkNotNull(item, "item") + values.add(it) + } + + fun size(): Int = values.size + + /** + * Returns a string representation of the contents of the specified value queue. + * - If the value queue is empty, returns an empty string. + * - If the value queue contains a single element `a`, returns `a.toString()`. + * - Otherwise, the contents of the queue are formatted like a List. + */ + @Suppress("ReturnCount") + override fun toString(): String { + // This case shouldn't actually happen unless you use the value queue + // for storing emitted values. + if (values.isEmpty()) return "" + // Consider using MessageUtils.safeToString() here. + if (values.size == 1) return values[0].toString() + return values.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + other as ValueQueue + return values == other.values + } + + override fun hashCode(): Int = values.hashCode() + + companion object { + + @JvmStatic + fun newQueue(item: Any): ValueQueue { + checkNotNull(item, "item") + val valueQueue = ValueQueue() + valueQueue.put(item) + return valueQueue + } + + @JvmStatic + fun maybeWrap(value: Any, existingValue: Any?): Any { + return if (existingValue == null) { + value + } else { + // This should only rarely happen, so a few small allocations seems acceptable. + val existingQueue = existingValue as? ValueQueue ?: newQueue(existingValue) + existingQueue.put(value) + existingQueue + } + } + + @JvmStatic + fun appendValues(label: String, valueOrQueue: Any, kvh: KeyValueHandler) { + if (valueOrQueue is ValueQueue) { + for (v in valueOrQueue) { + emit(label, v, kvh) + } + } else { + emit(label, valueOrQueue, kvh) + } + } + + /** + * Helper method for creating and initializing a value queue with a non-nullable value. + * If value is an instance of `Tags`, each tag will be added to the value queue. + */ + @JvmStatic + fun appendValueToNewQueue(value: Any): ValueQueue { + val valueQueue = ValueQueue() + emit("", value) { _, v -> valueQueue.put(v) } + return valueQueue + } + + /** + * Emits a metadata label/value pair to a given KeyValueHandler, + * handling Tags values specially. + * + * Tags are key-value mappings which cannot be modified or replaced. + * If you add the tag mapping "foo" -> true and later add "foo" -> false, + * you get "foo" mapped to both true and false. + * This is deliberate since the key space for tags is global and the risk + * of two bits of code accidentally using the same tag name is real. + * + * Given three tag mappings: + * - "baz" (no value) + * - "foo" -> true + * - "foo" -> false + * + * the value queue is going to store the mappings as: tags=[baz, foo=false, foo=true] + * Reusing the label 'tags' is intentional as this allows us to store the flattened tags in + * Log4j2's `ContextMap`. + */ + @JvmStatic + @Suppress("NestedBlockDepth", "ReturnCount") + fun emit(label: String, value: Any, kvh: KeyValueHandler) { + if (value is Tags) { + // Flatten tags to treat them as keys or key/value pairs, + // e.g., tags=[baz=bar, baz=bar2, foo] + value.asMap().forEach { k, v -> + if (v.isEmpty()) { + kvh.handle(label, k) + } else { + for (obj in v) { + kvh.handle(label, "$k=$obj") + } + } + } + } else { + kvh.handle(label, value) + } + } + } +} diff --git a/backends/log4j2-backend/src/test/kotlin/io/spine/logging/backend/log4j2/ValueQueueSpec.kt b/backends/log4j2-backend/src/test/kotlin/io/spine/logging/backend/log4j2/ValueQueueSpec.kt index 3225d1ddb..f99be1741 100644 --- a/backends/log4j2-backend/src/test/kotlin/io/spine/logging/backend/log4j2/ValueQueueSpec.kt +++ b/backends/log4j2-backend/src/test/kotlin/io/spine/logging/backend/log4j2/ValueQueueSpec.kt @@ -31,8 +31,8 @@ import io.kotest.matchers.collections.shouldContainInOrder import io.kotest.matchers.collections.shouldHaveSingleElement import io.kotest.matchers.types.shouldBeInstanceOf import io.kotest.matchers.types.shouldBeSameInstanceAs -import io.spine.logging.backend.log4j2.ValueQueue.appendValueToNewQueue -import io.spine.logging.backend.log4j2.ValueQueue.maybeWrap +import io.spine.logging.backend.log4j2.ValueQueue.Companion.appendValueToNewQueue +import io.spine.logging.backend.log4j2.ValueQueue.Companion.maybeWrap import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -94,13 +94,4 @@ internal class ValueQueueSpec { val queue = maybeWrap(value, existingValue) queue shouldBeSameInstanceAs value } - - @Test - fun `throw when given a 'null' value`() { - val value = null - val existingValue = null - shouldThrow { - maybeWrap(value, existingValue) - } - } } diff --git a/contexts/grpc-context/src/main/java/io/spine/logging/context/grpc/GrpcContextData.java b/contexts/grpc-context/src/main/java/io/spine/logging/context/grpc/GrpcContextData.java deleted file mode 100644 index ad5531701..000000000 --- a/contexts/grpc-context/src/main/java/io/spine/logging/context/grpc/GrpcContextData.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.context.grpc; - -import io.spine.logging.Level; -import io.spine.logging.LoggingScope; -import io.spine.logging.context.ContextMetadata; -import io.spine.logging.context.LogLevelMap; -import io.spine.logging.context.ScopeItem; -import io.spine.logging.context.ScopeType; -import io.spine.logging.context.Tags; -import org.jspecify.annotations.Nullable; - -import java.util.concurrent.atomic.AtomicReference; - -/** - * A mutable thread-safe holder for context-scoped logging information. - * - * @see Original Java code for historical context. - */ -final class GrpcContextData { - - static Tags getTagsFor(@Nullable GrpcContextData context) { - if (context != null) { - var tags = context.tagRef.get(); - if (tags != null) { - return tags; - } - } - return Tags.empty(); - } - - static ContextMetadata getMetadataFor(@Nullable GrpcContextData context) { - if (context != null) { - var metadata = context.metadataRef.get(); - if (metadata != null) { - return metadata; - } - } - return ContextMetadata.empty(); - } - - static boolean shouldForceLoggingFor( - @Nullable GrpcContextData context, String loggerName, Level level) { - if (context != null) { - var map = context.logLevelMapRef.get(); - if (map != null) { - return map.getLevel(loggerName).getValue() <= level.getValue(); - } - } - return false; - } - - /** - * Obtains a custom level set for the logger with the given name via - * a {@link LogLevelMap}, if it exists. - * - * @param loggerName the name of the logger - * @return the custom level or {@code null} if there is no map, or the map does not affect - * the level of the given logger - */ - @Nullable - Level getMappedLevel(String loggerName) { - var map = logLevelMapRef.get(); - if (map == null) { - return null; - } - var result = map.getLevel(loggerName); - return result; - } - - @Nullable - static LoggingScope lookupScopeFor(@Nullable GrpcContextData contextData, ScopeType type) { - return contextData != null ? ScopeItem.lookup(contextData.scopes, type) : null; - } - - private abstract static class ScopedReference { - private final AtomicReference value; - - ScopedReference(@Nullable T initialValue) { - this.value = new AtomicReference<>(initialValue); - } - - @Nullable - final T get() { - return value.get(); - } - - // Note: If we could use Java 1.8 runtime libraries, this would just be "accumulateAndGet()", - // but gRPC is Java 1.7 compatible: https://github.com/grpc/grpc-java/blob/master/README.md - final void mergeFrom(@Nullable T delta) { - if (delta != null) { - T current; - do { - current = get(); - } while (!value.compareAndSet(current, current != null ? merge(current, delta) : delta)); - } - } - - abstract T merge(T current, T delta); - } - - @Nullable private final ScopeItem scopes; - private final ScopedReference tagRef; - private final ScopedReference metadataRef; - private final ScopedReference logLevelMapRef; - // Only needed to register that log level maps are being used (as a performance optimization). - private final GrpcContextDataProvider provider; - - GrpcContextData( - @Nullable GrpcContextData parent, - @Nullable ScopeType scopeType, - GrpcContextDataProvider provider) { - this.scopes = ScopeItem.addScope(parent != null ? parent.scopes : null, scopeType); - this.tagRef = - new ScopedReference<>(parent != null ? parent.tagRef.get() : null) { - @Override - Tags merge(Tags current, Tags delta) { - return current.merge(delta); - } - }; - this.metadataRef = - new ScopedReference<>(parent != null ? parent.metadataRef.get() : null) { - @Override - ContextMetadata merge(ContextMetadata current, ContextMetadata delta) { - return current.concatenate(delta); - } - }; - this.logLevelMapRef = - new ScopedReference<>(parent != null ? parent.logLevelMapRef.get() : null) { - @Override - LogLevelMap merge(LogLevelMap current, LogLevelMap delta) { - return current.merge(delta); - } - }; - this.provider = provider; - } - - void addTags(@Nullable Tags tags) { - tagRef.mergeFrom(tags); - } - - void addMetadata(@Nullable ContextMetadata metadata) { - metadataRef.mergeFrom(metadata); - } - - void applyLogLevelMap(@Nullable LogLevelMap logLevelMap) { - if (logLevelMap != null) { - // Set the global flag to trigger testing of the log level map from now on (we only apply a - // log level map to an active context or one that's about to become active). - provider.setLogLevelMapFlag(); - logLevelMapRef.mergeFrom(logLevelMap); - } - } -} diff --git a/contexts/grpc-context/src/main/java/io/spine/logging/context/grpc/GrpcContextDataProvider.java b/contexts/grpc-context/src/main/java/io/spine/logging/context/grpc/GrpcContextDataProvider.java deleted file mode 100644 index 82888ab35..000000000 --- a/contexts/grpc-context/src/main/java/io/spine/logging/context/grpc/GrpcContextDataProvider.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.context.grpc; - -import io.grpc.Context; -import io.spine.logging.Level; -import io.spine.logging.LoggingScope; -import io.spine.logging.context.Tags; -import io.spine.logging.context.ContextDataProvider; -import io.spine.logging.context.ContextMetadata; -import io.spine.logging.context.ScopeType; -import io.spine.logging.context.ScopedLoggingContext; -import org.jspecify.annotations.Nullable; - -/** - * A {@link io.grpc.Context gRPC}-based implementation of {@link ContextDataProvider}. - * - *

When using {@code DefaultPlatform}, this provider will automatically - * be used if it is included on the classpath, and no other implementation - * of {@code ContextDataProvider} other than the default implementation is. - * - *

To specify it more explicitly or to work around an issue where multiple - * {@code ContextDataProvider} implementations are on the classpath, one can set - * the {@code flogger.logging_context} system property to the fully-qualified name - * of this class: - * - *

    - *
  • {@code flogger.logging_context=io.spine.logging.context.grpc.GrpcContextDataProvider} - *
- * - * @see Original Java code for historical context. - */ -public final class GrpcContextDataProvider extends ContextDataProvider { - - // For use by GrpcScopedLoggingContext (same package). We cannot define the keys in there because - // this class must not depend on GrpcScopedLoggingContext during static initialization. We must - // also delay initializing this value (via a lazy-holder) to avoid any risks during logger - // initialization. - static Context.Key getContextKey() { - return KeyHolder.GRPC_SCOPE; - } - - /** Returns the current context data, or {@code null} if we are not in a context. */ - @Nullable - static GrpcContextData currentContext() { - return getContextKey().get(); - } - - // This is created lazily to avoid requiring it to be initiated at the same time as - // GrpcContextDataProvider (which is created as the Platform instance is initialized). By doing - // this we break any initialization cycles and allow the config API perform its own logging if - // necessary. - private volatile GrpcScopedLoggingContext configInstance = null; - - // When this is false we can skip some work for every log statement. This is set to true if _any_ - // context adds a log level map at any point (this is generally rare and only used for targeted - // debugging so will often never occur during normal application use). This is never reset. - private volatile boolean hasLogLevelMap = false; - - // A public no-arg constructor is necessary for use by ServiceLoader - public GrpcContextDataProvider() {} - - /** Sets the flag to enable checking for a log level map after one is set for the first time. */ - void setLogLevelMapFlag() { - hasLogLevelMap = true; - } - - @Override - public ScopedLoggingContext getContextApiSingleton() { - var result = configInstance; - if (result == null) { - // GrpcScopedLoggingContext is stateless, so we shouldn't need double-checked locking here to - // ensure we don't make more than one. - result = new GrpcScopedLoggingContext(this); - configInstance = result; - } - return result; - } - - @Override - public Tags getTags() { - return GrpcContextData.getTagsFor(currentContext()); - } - - @Override - public ContextMetadata getMetadata() { - return GrpcContextData.getMetadataFor(currentContext()); - } - - @Nullable - @Override - public LoggingScope getScope(ScopeType type) { - return GrpcContextData.lookupScopeFor(currentContext(), type); - } - - @Override - public boolean shouldForceLogging(String loggerName, Level level, boolean isEnabledByLevel) { - // Shortcutting boolean saves doing any work in the commonest case (this code is called for - // every log statement, which is 100-1000 times more than just the enabled log statements). - return hasLogLevelMap - && GrpcContextData.shouldForceLoggingFor(currentContext(), loggerName, level); - } - - @Override - public @Nullable Level getMappedLevel(String loggerName) { - if (!hasLogLevelMap) { - return null; - } - var context = currentContext(); - if (context == null) { - return null; - } - var result = context.getMappedLevel(loggerName); - return result; - } - - // Static lazy-holder to avoid needing to call unknown code during Flogger initialization. While - // gRPC context keys don't trigger any logging now, it's not certain that this is guaranteed. - private static final class KeyHolder { - private static final Context.Key GRPC_SCOPE = - Context.key("Flogger gRPC scope"); - - private KeyHolder() {} - } -} diff --git a/contexts/grpc-context/src/main/java/io/spine/logging/context/grpc/GrpcScopedLoggingContext.java b/contexts/grpc-context/src/main/java/io/spine/logging/context/grpc/GrpcScopedLoggingContext.java deleted file mode 100644 index d289b2aeb..000000000 --- a/contexts/grpc-context/src/main/java/io/spine/logging/context/grpc/GrpcScopedLoggingContext.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.context.grpc; - -import io.grpc.Context; -import io.spine.logging.MetadataKey; -import io.spine.logging.context.Tags; -import io.spine.logging.context.ContextMetadata; -import io.spine.logging.context.LogLevelMap; -import io.spine.logging.context.ScopeType; -import io.spine.logging.context.ScopedLoggingContext; -import org.jspecify.annotations.Nullable; - -import static io.spine.logging.util.Checks.checkNotNull; - -/** - * A {@link io.grpc.Context gRPC}-based implementation of {@link ScopedLoggingContext}. - * - *

This is a lazily loaded singleton instance returned from - * {@link GrpcContextDataProvider#getContextApiSingleton()}, which provides - * application code with a mechanism for controlling logging contexts. - * - * @see Original Java code - * for historical context. - */ -final class GrpcScopedLoggingContext extends ScopedLoggingContext { - - private final GrpcContextDataProvider provider; - - GrpcScopedLoggingContext(GrpcContextDataProvider provider) { - this.provider = provider; - } - - @Override - public ScopedLoggingContext.Builder newContext() { - return newBuilder(null); - } - - @Override - public ScopedLoggingContext.Builder newContext(ScopeType scopeType) { - return newBuilder(scopeType); - } - - private ScopedLoggingContext.Builder newBuilder(@Nullable ScopeType scopeType) { - return new ScopedLoggingContext.Builder() { - @Override - public AutoCloseable install() { - var newContextData = - new GrpcContextData(GrpcContextDataProvider.currentContext(), scopeType, provider); - newContextData.addTags(getTags()); - newContextData.addMetadata(getMetadata()); - newContextData.applyLogLevelMap(getLogLevelMap()); - return installContextData(newContextData); - } - }; - } - - private static AutoCloseable installContextData(GrpcContextData newContextData) { - // Capture these variables outside the lambda. - var newGrpcContext = - Context.current().withValue(GrpcContextDataProvider.getContextKey(), newContextData); - @SuppressWarnings("MustBeClosedChecker") - var prev = newGrpcContext.attach(); - return () -> newGrpcContext.detach(prev); - } - - @Override - public boolean addTags(Tags tags) { - checkNotNull(tags, "tags"); - var context = GrpcContextDataProvider.currentContext(); - if (context != null) { - context.addTags(tags); - return true; - } - return false; - } - - @Override - public boolean addMetadata(MetadataKey key, T value) { - // Serves as the null pointer check, and we don't care much about the extra allocation in the - // case where there's no context, because that should be very rare (and the singleton is small). - var metadata = ContextMetadata.singleton(key, value); - var context = GrpcContextDataProvider.currentContext(); - if (context != null) { - context.addMetadata(metadata); - return true; - } - return false; - } - - @Override - public boolean applyLogLevelMap(LogLevelMap logLevelMap) { - checkNotNull(logLevelMap, "log level map"); - var context = GrpcContextDataProvider.currentContext(); - if (context != null) { - context.applyLogLevelMap(logLevelMap); - return true; - } - return false; - } -} diff --git a/contexts/grpc-context/src/main/kotlin/io/spine/logging/context/grpc/GrpcContextData.kt b/contexts/grpc-context/src/main/kotlin/io/spine/logging/context/grpc/GrpcContextData.kt new file mode 100644 index 000000000..1d421efc1 --- /dev/null +++ b/contexts/grpc-context/src/main/kotlin/io/spine/logging/context/grpc/GrpcContextData.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.logging.context.grpc + +import io.spine.logging.Level +import io.spine.logging.LoggingScope +import io.spine.logging.context.ContextMetadata +import io.spine.logging.context.LogLevelMap +import io.spine.logging.context.ScopeItem +import io.spine.logging.context.ScopeType +import io.spine.logging.context.Tags +import java.util.concurrent.atomic.AtomicReference + +/** + * A mutable thread-safe holder for context-scoped logging information. + * + * See original Java for historical context. + */ +internal class GrpcContextData( + parent: GrpcContextData?, + scopeType: ScopeType?, + private val provider: GrpcContextDataProvider, +) { + + private val scopes: ScopeItem? = ScopeItem.addScope(parent?.scopes, scopeType) + + private abstract class ScopedReference(initialValue: T?) { + private val value = AtomicReference(initialValue) + fun get(): T? = value.get() + fun mergeFrom(delta: T?) { + if (delta != null) { + var current: T? + do { + current = get() + } while (!value.compareAndSet(current, current?.let { merge(it, delta) } ?: delta)) + } + } + protected abstract fun merge(current: T, delta: T): T + } + + private val parentTags: Tags? = parent?.tagRef?.get() + private val tagRef = object : ScopedReference(parentTags) { + override fun merge(current: Tags, delta: Tags): Tags = current.merge(delta) + } + + private val parentMetadata: ContextMetadata? = parent?.metadataRef?.get() + private val metadataRef = object : ScopedReference(parentMetadata) { + override fun merge(current: ContextMetadata, delta: ContextMetadata): ContextMetadata = + current.concatenate(delta) + } + + private val parentLogLevelMap: LogLevelMap? = parent?.logLevelMapRef?.get() + + private val logLevelMapRef = object : ScopedReference(parentLogLevelMap) { + override fun merge(current: LogLevelMap, delta: LogLevelMap): LogLevelMap = + current.merge(delta) + } + + fun addTags(tags: Tags?) { + tagRef.mergeFrom(tags) + } + + fun addMetadata(metadata: ContextMetadata?) { + metadataRef.mergeFrom(metadata) + } + + fun applyLogLevelMap(logLevelMap: LogLevelMap?) { + if (logLevelMap != null) { + provider.setLogLevelMapFlag() + logLevelMapRef.mergeFrom(logLevelMap) + } + } + + fun getMappedLevel(loggerName: String): Level? { + val map = logLevelMapRef.get() ?: return null + return map.getLevel(loggerName) + } + + companion object { + fun getTagsFor(context: GrpcContextData?): Tags { + if (context != null) { + val tags = context.tagRef.get() + if (tags != null) return tags + } + return Tags.empty() + } + + fun getMetadataFor(context: GrpcContextData?): ContextMetadata { + if (context != null) { + val metadata = context.metadataRef.get() + if (metadata != null) return metadata + } + return ContextMetadata.empty() + } + + fun shouldForceLoggingFor( + context: GrpcContextData?, + loggerName: String, + level: Level + ): Boolean { + if (context != null) { + val map = context.logLevelMapRef.get() + if (map != null) { + return map.getLevel(loggerName).value <= level.value + } + } + return false + } + + fun lookupScopeFor(contextData: GrpcContextData?, type: ScopeType): LoggingScope? = + if (contextData != null) ScopeItem.lookup(contextData.scopes, type) else null + } +} diff --git a/contexts/grpc-context/src/main/kotlin/io/spine/logging/context/grpc/GrpcContextDataProvider.kt b/contexts/grpc-context/src/main/kotlin/io/spine/logging/context/grpc/GrpcContextDataProvider.kt new file mode 100644 index 000000000..74932d629 --- /dev/null +++ b/contexts/grpc-context/src/main/kotlin/io/spine/logging/context/grpc/GrpcContextDataProvider.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.logging.context.grpc + +import io.grpc.Context +import io.spine.logging.Level +import io.spine.logging.LoggingScope +import io.spine.logging.context.ContextDataProvider +import io.spine.logging.context.ContextMetadata +import io.spine.logging.context.ScopeType +import io.spine.logging.context.ScopedLoggingContext +import io.spine.logging.context.Tags + +/** + * A gRPC-based implementation of ContextDataProvider. + */ +public class GrpcContextDataProvider : ContextDataProvider() { + + public companion object { + // For use by GrpcScopedLoggingContext (same package). + internal fun getContextKey(): Context.Key = KeyHolder.GRPC_SCOPE + internal fun currentContext(): GrpcContextData? = getContextKey().get() + } + + // Lazily created API singleton + @Volatile private var configInstance: GrpcScopedLoggingContext? = null + + // Flag to know if any context applied a log level map + @Volatile private var hasLogLevelMap: Boolean = false + + internal fun setLogLevelMapFlag() { hasLogLevelMap = true } + + public override fun getContextApiSingleton(): ScopedLoggingContext { + var result = configInstance + if (result == null) { + result = GrpcScopedLoggingContext(this) + configInstance = result + } + return result + } + + public override fun getTags(): Tags = GrpcContextData.getTagsFor(currentContext()) + + public override fun getMetadata(): ContextMetadata = + GrpcContextData.getMetadataFor(currentContext()) + + public override fun getScope(type: ScopeType): LoggingScope? = + GrpcContextData.lookupScopeFor(currentContext(), type) + + public override fun shouldForceLogging( + loggerName: String, + level: Level, + isEnabledByLevel: Boolean + ): Boolean = + hasLogLevelMap && GrpcContextData.shouldForceLoggingFor(currentContext(), loggerName, level) + + @Suppress("ReturnCount") + public override fun getMappedLevel(loggerName: String): Level? { + if (!hasLogLevelMap) return null + val context = currentContext() ?: return null + return context.getMappedLevel(loggerName) + } + + /** + * Lazy holder for `Context.Key`. + */ + private object KeyHolder { + val GRPC_SCOPE: Context.Key = Context.key("Logging gRPC scope") + } +} diff --git a/contexts/grpc-context/src/main/kotlin/io/spine/logging/context/grpc/GrpcScopedLoggingContext.kt b/contexts/grpc-context/src/main/kotlin/io/spine/logging/context/grpc/GrpcScopedLoggingContext.kt new file mode 100644 index 000000000..2aea83921 --- /dev/null +++ b/contexts/grpc-context/src/main/kotlin/io/spine/logging/context/grpc/GrpcScopedLoggingContext.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.logging.context.grpc + +import io.grpc.Context +import io.spine.logging.MetadataKey +import io.spine.logging.context.ContextMetadata +import io.spine.logging.context.LogLevelMap +import io.spine.logging.context.ScopeType +import io.spine.logging.context.ScopedLoggingContext +import io.spine.logging.context.Tags +import io.spine.logging.util.Checks.checkNotNull + +/** + * A gRPC-based implementation of ScopedLoggingContext. + */ +internal class GrpcScopedLoggingContext(private val provider: GrpcContextDataProvider) : + ScopedLoggingContext() { + + override fun newContext(): Builder = newBuilder(null) + + public override fun newContext(scopeType: ScopeType?): Builder = newBuilder(scopeType) + + private fun newBuilder(scopeType: ScopeType?): Builder = object : Builder() { + override fun install(): AutoCloseable { + val newContextData = + GrpcContextData(GrpcContextDataProvider.currentContext(), scopeType, provider) + newContextData.run { + addTags(getTags()) + addMetadata(getMetadata()) + applyLogLevelMap(getLogLevelMap()) + } + return installContextData(newContextData) + } + } + + private companion object { + + private fun installContextData(newContextData: GrpcContextData): AutoCloseable { + val newGrpcContext = + Context.current().withValue(GrpcContextDataProvider.getContextKey(), newContextData) + @Suppress("MustBeClosedChecker") + val prev = newGrpcContext.attach() + return AutoCloseable { newGrpcContext.detach(prev) } + } + } + + override fun addTags(tags: Tags): Boolean { + checkNotNull(tags, "tags") + val context = GrpcContextDataProvider.currentContext() + if (context != null) { + context.addTags(tags) + return true + } + return false + } + + override fun addMetadata(key: MetadataKey, value: T): Boolean { + val metadata = ContextMetadata.singleton(key, value) + val context = GrpcContextDataProvider.currentContext() + if (context != null) { + context.addMetadata(metadata) + return true + } + return false + } + + override fun applyLogLevelMap(logLevelMap: LogLevelMap): Boolean { + checkNotNull(logLevelMap, "log level map") + val context = GrpcContextDataProvider.currentContext() + if (context != null) { + context.applyLogLevelMap(logLevelMap) + return true + } + return false + } +} diff --git a/dependencies.md b/dependencies.md index f71a5ed75..104bdf0ec 100644 --- a/dependencies.md +++ b/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine:spine-logging-context-tests:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-context-tests:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.0.2. @@ -413,14 +413,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:33 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-fixtures:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-fixtures:2.0.0-SNAPSHOT.410` ## Runtime ## Compile, tests, and tooling @@ -1148,14 +1148,14 @@ This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:54 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:35 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-grpc-context:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-grpc-context:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1982,14 +1982,14 @@ This report was generated on **Mon Sep 15 16:45:54 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:33 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jul-backend:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-jul-backend:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -2800,14 +2800,14 @@ This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:33 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-default-platform:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-jvm-default-platform:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -3662,14 +3662,14 @@ This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:33 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-jul-backend-grpc-context:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-jvm-jul-backend-grpc-context:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -4520,14 +4520,14 @@ This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:34 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-jul-backend-std-context:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-jvm-jul-backend-std-context:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -5370,14 +5370,14 @@ This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:34 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-log4j2-backend-std-context:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-jvm-log4j2-backend-std-context:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -6220,14 +6220,14 @@ This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:34 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-slf4j-jdk14-backend-std-context:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-jvm-slf4j-jdk14-backend-std-context:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -7078,14 +7078,14 @@ This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:34 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-slf4j-reload4j-backend-std-context:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-jvm-slf4j-reload4j-backend-std-context:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -7940,14 +7940,14 @@ This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:34 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-log4j2-backend:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-log4j2-backend:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -8782,14 +8782,14 @@ This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:33 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging:2.0.0-SNAPSHOT.410` ## Runtime ## Compile, tests, and tooling @@ -9521,14 +9521,14 @@ This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:33 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-logging-testlib:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine.tools:spine-logging-testlib:2.0.0-SNAPSHOT.410` ## Runtime ## Compile, tests, and tooling @@ -10256,14 +10256,14 @@ This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:54 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:34 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-probe-backend:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-probe-backend:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.google.auto.service. **Name** : auto-service-annotations. **Version** : 1.1.1. @@ -11122,14 +11122,14 @@ This report was generated on **Mon Sep 15 16:45:54 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:33 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-smoke-test:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-smoke-test:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.18.3. @@ -12008,14 +12008,14 @@ This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:33 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-std-context:2.0.0-SNAPSHOT.400` +# Dependencies of `io.spine:spine-logging-std-context:2.0.0-SNAPSHOT.410` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -12826,6 +12826,6 @@ This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Sep 15 16:45:53 WEST 2025** using +This report was generated on **Tue Sep 16 15:54:33 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/logging/src/commonMain/kotlin/io/spine/logging/LoggingFactory.kt b/logging/src/commonMain/kotlin/io/spine/logging/LoggingFactory.kt index 1b829273e..c4a1a3622 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/LoggingFactory.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/LoggingFactory.kt @@ -31,6 +31,7 @@ import kotlin.reflect.KClass /** * A factory for logging objects. */ +@Suppress("unused") // parameters are used in actual classes, but not here. public expect object LoggingFactory { /** diff --git a/platforms/jvm-default-platform/src/main/java/io/spine/logging/backend/system/DefaultPlatform.java b/platforms/jvm-default-platform/src/main/java/io/spine/logging/backend/system/DefaultPlatform.java deleted file mode 100644 index 3a0d45122..000000000 --- a/platforms/jvm-default-platform/src/main/java/io/spine/logging/backend/system/DefaultPlatform.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.backend.system; - -import io.spine.annotation.VisibleForTesting; -import io.spine.logging.backend.jul.JulBackendFactory; -import io.spine.logging.backend.BackendFactory; -import io.spine.logging.backend.Clock; -import io.spine.logging.backend.LoggerBackend; -import io.spine.logging.backend.Platform; -import io.spine.logging.backend.LogCallerFinder; -import io.spine.logging.context.ContextDataProvider; -import org.jspecify.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.ServiceLoader; - -import static io.spine.logging.backend.system.StaticMethodCaller.getInstanceFromSystemProperty; - -/** - * The default logger platform for a server-side Java environment. - * - *

This class allows configuration via a number of service types. A single instance of each - * service type may be provided, either via the classpath using service providers (see {@link - * ServiceLoader}) or by system property. For most users, configuring one of these should just - * require including the appropriate dependency. - * - *

If set, the system property for each service type takes precedence over any implementations - * that may be found on the classpath. The value of the system property is expected to be of one of - * two forms: - * - *

    - *
  • A fully-qualified class name: In this case, the platform will attempt to get an - * instance of that class by invoking the public no-arg constructor. If the class defines a - * public static no-arg {@code getInstance} method, the platform will call that instead. - * Note: Support for {@code getInstance} is only provided to facilitate transition - * from older service implementations that include a {@code getInstance} method and will - * likely be removed in the future. - *
  • A fully-qualified class name followed by "#" and the name of a static method: - * In this case, the platform will attempt to get an instance of that class by invoking - * either the named no-arg static method or the public no-arg constructor. - * Note: This option exists only for compatibility with previous Flogger behavior and - * may be removed in the future; service implementations should prefer providing a no-arg - * public constructor rather than a static method, and system properties should prefer - * only including the class name. - *
- * - *

The services used by this platform are the following: - * - *

- * | Service Type            | System Property            | Default                                    |
- * |------------------------|---------------------------|---------------------------------------------|
- * | {@link BackendFactory} | {@code flogger.backend_factory} | {@link JulBackendFactory}                   |
- * | {@link ContextDataProvider} | {@code flogger.logging_context} | A no-op {@code ContextDataProvider}         |
- * | {@link Clock}          | {@code flogger.clock}           | {@link SystemClock}, a millisecond-precision clock |
- * 
- * - * @see Original Java code for historical context. - */ -// Non-final for testing. -public class DefaultPlatform extends Platform { - - // System property names for properties expected to define "getters" for platform attributes. - private static final String BACKEND_FACTORY = "flogger.backend_factory"; - private static final String CONTEXT_DATA_PROVIDER = "flogger.logging_context"; - private static final String CLOCK = "flogger.clock"; - - private final BackendFactory backendFactory; - private final ContextDataProvider context; - private final Clock clock; - private final LogCallerFinder callerFinder; - - public DefaultPlatform() { - // To avoid eagerly loading the default implementations of each service when they might not - // be required, we return null from the loadService() method rather than accepting a default - // instance. This avoids a bunch of potentially unnecessary static initialization. - var backendFactory = loadService(BackendFactory.class, BACKEND_FACTORY); - this.backendFactory = backendFactory != null ? backendFactory : new JulBackendFactory(); - - var contextDataProvider = - loadService(ContextDataProvider.class, CONTEXT_DATA_PROVIDER); - this.context = - contextDataProvider != - null ? contextDataProvider : ContextDataProvider.getNoOpProvider(); - - var clock = loadService(Clock.class, CLOCK); - this.clock = clock != null ? clock : SystemClock.getInstance(); - - this.callerFinder = StackBasedCallerFinder.getInstance(); - } - - /** - * Attempts to load an implementation of the given {@code serviceType}: - * - *
    - *
  1. First looks for an implementation specified by the value of the given {@code - * systemProperty}, if that system property is set correctly. If the property is set but - * can't be used to get an instance of the service type, prints an error and returns {@code - * null}. - *
  2. Then attempts to load an implementation from the classpath via {@code ServiceLoader}, if - * there is exactly one. If there is more than one, prints an error and returns {@code - * null}. - *
  3. If neither is present, returns {@code null}. - *
- */ - @SuppressWarnings("UseOfSystemOutOrSystemErr") - @Nullable - private static S loadService(Class serviceType, String systemProperty) { - // TODO(cgdecker): Throw an exception if configuration is present but invalid? - // - If the system property is set but using it to get the service fails. - // - If the system property is not set and more than one service is loaded by ServiceLoader - // If no configuration is present, falling back to the default makes sense, but when invalid - // configuration is present it may be best to attempt to fail fast. - var service = getInstanceFromSystemProperty(systemProperty, serviceType); - if (service != null) { - // Service was loaded successfully via an explicitly overridden system property. - return service; - } - - List loadedServices = new ArrayList<>(); - for (var loaded : ServiceLoader.load(serviceType)) { - loadedServices.add(loaded); - } - - return switch (loadedServices.size()) { - // Normal use of the default service when nothing else exists. - case 0 -> null; - // A single service implementation was found and loaded automatically. - case 1 -> loadedServices.get(0); - default -> { System.err.printf( - "Multiple implementations of service %s found on the classpath: %s%n" - + "Ensure only the service implementation you want to use is included on the " - + "classpath or else specify the service class at startup with the '%s' system " - + "property. The default implementation will be used instead.%n", - serviceType.getName(), loadedServices, systemProperty - ); - yield null; - } - }; - } - - @VisibleForTesting - DefaultPlatform( - BackendFactory factory, - ContextDataProvider context, - Clock clock, - LogCallerFinder callerFinder) { - this.backendFactory = factory; - this.context = context; - this.clock = clock; - this.callerFinder = callerFinder; - } - - @Override - protected LogCallerFinder getCallerFinderImpl() { - return callerFinder; - } - - @Override - protected LoggerBackend getBackendImpl(String className) { - return backendFactory.create(className); - } - - @Override - protected ContextDataProvider getContextDataProviderImpl() { - return context; - } - - @Override - protected long getCurrentTimeNanosImpl() { - return clock.getCurrentTimeNanos(); - } - - @SuppressWarnings("HardcodedLineSeparator") - @Override - protected String getConfigInfoImpl() { - return "Platform: " + getClass().getName() + '\n' - + "BackendFactory: " + backendFactory + '\n' - + "Clock: " + clock + '\n' - + "ContextDataProvider: " + context + '\n' - + "LogCallerFinder: " + callerFinder + '\n'; - } -} diff --git a/platforms/jvm-default-platform/src/main/java/io/spine/logging/backend/system/StackBasedCallerFinder.java b/platforms/jvm-default-platform/src/main/java/io/spine/logging/backend/system/StackBasedCallerFinder.java deleted file mode 100644 index 4dd0f3e9e..000000000 --- a/platforms/jvm-default-platform/src/main/java/io/spine/logging/backend/system/StackBasedCallerFinder.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2018, The Flogger Authors; 2023, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.backend.system; - -import com.google.errorprone.annotations.Immutable; -import com.google.errorprone.annotations.ThreadSafe; -import io.spine.logging.AbstractLogger; -import io.spine.logging.LogSite; -import io.spine.logging.LogSiteInjector; -import io.spine.logging.backend.LogCallerFinder; -import kotlin.jvm.JvmClassMappingKt; -import kotlin.reflect.KClass; - -import static io.spine.logging.InjectedLogSiteKt.injectedLogSite; -import static io.spine.reflect.CallerFinder.findCallerOf; - -/** - * The default caller finder implementation for Java 9+. - * - *

See class documentation in {@link LogCallerFinder} for important - * implementation restrictions. - * - * @see Original Java code for historical context. - */ -@Immutable -@ThreadSafe -public final class StackBasedCallerFinder extends LogCallerFinder { - private static final LogCallerFinder INSTANCE = new StackBasedCallerFinder(); - - // Called during logging platform initialization; MUST NOT call any code that might log. - public static LogCallerFinder getInstance() { - return INSTANCE; - } - - @Override - public String findLoggingClass(KClass> loggerClass) { - // Convert KClass to Java Class for compatibility with existing findCallerOf method - var javaClass = JvmClassMappingKt.getJavaClass(loggerClass); - // We can skip at most only 1 method from the analysis, the inferLoggingClass() method itself. - var caller = findCallerOf(javaClass, 1); - if (caller != null) { - // This might contain '$' for inner/nested classes, but that's okay. - return caller.getClassName(); - } - throw new IllegalStateException("no caller found on the stack for: " + javaClass.getName()); - } - - @LogSiteInjector - @Override - public LogSite findLogSite(KClass loggerApi, int stackFramesToSkip) { - // Convert KClass to Java Class for compatibility with existing findCallerOf method - var javaClass = JvmClassMappingKt.getJavaClass(loggerApi); - // Skip an additional stack frame because we create the Throwable inside this method, not at - // the point that this method was invoked (which allows completely alternate implementations - // to avoid even constructing the Throwable instance). - var caller = findCallerOf(javaClass, stackFramesToSkip + 1); - // Returns INVALID if "caller" is null (no caller found for given API class). - if (caller == null) { - return LogSite.Invalid.INSTANCE; - } - return injectedLogSite( - caller.getClassName().replace('.', '/'), - caller.getMethodName(), - caller.getLineNumber(), - caller.getFileName() - ); - } - - @Override - public String toString() { - return "Default stack-based caller finder"; - } - - private StackBasedCallerFinder() {} -} diff --git a/platforms/jvm-default-platform/src/main/java/io/spine/logging/backend/system/SystemClock.java b/platforms/jvm-default-platform/src/main/java/io/spine/logging/backend/system/SystemClock.java deleted file mode 100644 index f1ef468a3..000000000 --- a/platforms/jvm-default-platform/src/main/java/io/spine/logging/backend/system/SystemClock.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2018, The Flogger Authors; 2023, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.backend.system; - -import io.spine.logging.backend.Clock; - -import static java.util.concurrent.TimeUnit.MILLISECONDS; - -/** - * Default millisecond precision clock. - * - *

See class documentation in {@link Clock} for important - * implementation restrictions. - * - * @see Original Java code for historical context. - */ -public final class SystemClock extends Clock { - private static final SystemClock INSTANCE = new SystemClock(); - - // Called during logging platform initialization; MUST NOT call any code that might log. - public static SystemClock getInstance() { - return INSTANCE; - } - - private SystemClock() { } - - @Override - public long getCurrentTimeNanos() { - return MILLISECONDS.toNanos(System.currentTimeMillis()); - } - - @Override - public String toString() { - return "Default millisecond precision clock"; - } -} diff --git a/platforms/jvm-default-platform/src/main/kotlin/io/spine/logging/backend/system/DefaultPlatform.kt b/platforms/jvm-default-platform/src/main/kotlin/io/spine/logging/backend/system/DefaultPlatform.kt new file mode 100644 index 000000000..703a3ca1b --- /dev/null +++ b/platforms/jvm-default-platform/src/main/kotlin/io/spine/logging/backend/system/DefaultPlatform.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.logging.backend.system + +import io.spine.annotation.TestOnly +import io.spine.annotation.VisibleForTesting +import io.spine.logging.backend.BackendFactory +import io.spine.logging.backend.Clock +import io.spine.logging.backend.LogCallerFinder +import io.spine.logging.backend.LoggerBackend +import io.spine.logging.backend.Platform +import io.spine.logging.backend.jul.JulBackendFactory +import io.spine.logging.context.ContextDataProvider +import java.util.ServiceLoader + +/** + * The default logger platform for a server-side Java environment. + * + * This class allows configuration via a number of service types. + * A single instance of each service type may be provided, either via the classpath + * using service providers (see [ServiceLoader]) or by system property. For most users, + * configuring one of these should just require including the appropriate dependency. + * + * If set, the system property for each service type takes precedence over any + * implementations that may be found on the classpath. + * The value of the system property is expected to be of one of two forms: + * + * - A fully-qualified class name: In this case, the platform will attempt to get + * an instance of that class by invoking the public no-arg constructor. + * If the class defines a public static no-arg `getInstance` method, + * the platform will call that instead. + * + * - A fully-qualified class name followed by "#" and the name of a static method: + * In this case, the platform will attempt to get an instance of that class by + * invoking either the named no-arg static method or the public no-arg constructor. + * + * The services used by this platform are the following: + * - [BackendFactory] (system property `spine.logging.backend_factory`) — [JulBackendFactory] by default. + * - [ContextDataProvider] (system property `spine.logging.logging_context`) — No-op provider by default. + * - [Clock] (system property `spine.logging.clock`) — [SystemClock], + * a millisecond-precision clock by default. + */ +public open class DefaultPlatform : Platform { + + private val backendFactory: BackendFactory + private val context: ContextDataProvider + private val clock: Clock + private val callerFinder: LogCallerFinder + + public constructor() { + // Avoid eager loading of default implementations when not required. + val backendFactory = loadService(BackendFactory::class.java, BACKEND_FACTORY) + this.backendFactory = backendFactory ?: JulBackendFactory() + + val contextDataProvider = + loadService(ContextDataProvider::class.java, CONTEXT_DATA_PROVIDER) + this.context = contextDataProvider ?: ContextDataProvider.getNoOpProvider() + + val clock = loadService(Clock::class.java, CLOCK) + this.clock = clock ?: SystemClock.getInstance() + + this.callerFinder = StackBasedCallerFinder.getInstance() + } + + @VisibleForTesting + public constructor( + factory: BackendFactory, + context: ContextDataProvider, + clock: Clock, + callerFinder: LogCallerFinder + ) { + this.backendFactory = factory + this.context = context + this.clock = clock + this.callerFinder = callerFinder + } + + override fun getCallerFinderImpl(): LogCallerFinder = callerFinder + + @TestOnly + internal fun doGetCallerFinderImpl(): LogCallerFinder = + getCallerFinderImpl() + + override fun getBackendImpl(className: String): LoggerBackend = backendFactory.create(className) + + @TestOnly + internal fun doGetBackendImpl(className: String): LoggerBackend = + getBackendImpl(className) + + override fun getContextDataProviderImpl(): ContextDataProvider = context + + @TestOnly + internal fun doGetContextDataProviderImpl(): ContextDataProvider = + getContextDataProviderImpl() + + override fun getCurrentTimeNanosImpl(): Long = clock.getCurrentTimeNanos() + + @TestOnly + internal fun doGetCurrentTimeNanosImpl(): Long = + getCurrentTimeNanosImpl() + + override fun getConfigInfoImpl(): String = buildString { + append("Platform: ").append(this@DefaultPlatform.javaClass.name).append('\n') + append("BackendFactory: ").append(backendFactory).append('\n') + append("Clock: ").append(clock).append('\n') + append("ContextDataProvider: ").append(context).append('\n') + append("LogCallerFinder: ").append(callerFinder).append('\n') + } + + @TestOnly + internal fun doGetConfigInfoImpl(): String = + getConfigInfoImpl() + + public companion object { + private const val BACKEND_FACTORY = "spine.logging.backend_factory" + private const val CONTEXT_DATA_PROVIDER = "spine.logging.logging_context" + private const val CLOCK = "spine.logging.clock" + + @JvmStatic + private fun loadService(serviceType: Class, systemProperty: String): S? { + // First, try system property-specified implementation. + val fromProp = + StaticMethodCaller.getInstanceFromSystemProperty(systemProperty, serviceType) + if (fromProp != null) return fromProp + + // Next, attempt to load via ServiceLoader. If exactly one, return it; + // if many, warn; else null. + val loaded = ServiceLoader.load(serviceType).toList() + return when (loaded.size) { + 0 -> null + 1 -> loaded[0] + else -> { + System.err.printf( + "Multiple implementations of service %s found on the classpath: %s%n" + + "Ensure only the service implementation you want to use is included" + + " on the classpath or else specify the service class at startup" + + " with the '%s' system property." + + " The default implementation will be used instead.%n", + serviceType.name, loaded, systemProperty + ) + null + } + } + } + } +} diff --git a/platforms/jvm-default-platform/src/main/kotlin/io/spine/logging/backend/system/StackBasedCallerFinder.kt b/platforms/jvm-default-platform/src/main/kotlin/io/spine/logging/backend/system/StackBasedCallerFinder.kt new file mode 100644 index 000000000..2d04e0d02 --- /dev/null +++ b/platforms/jvm-default-platform/src/main/kotlin/io/spine/logging/backend/system/StackBasedCallerFinder.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.logging.backend.system + +import com.google.errorprone.annotations.Immutable +import com.google.errorprone.annotations.ThreadSafe +import io.spine.logging.AbstractLogger +import io.spine.logging.LogSite +import io.spine.logging.backend.LogCallerFinder +import io.spine.logging.injectedLogSite +import io.spine.reflect.CallerFinder +import kotlin.reflect.KClass + +/** + * The default caller finder implementation for Java 9+. + * + * See class documentation in [LogCallerFinder] for important implementation restrictions. + */ +@Immutable +@ThreadSafe +public class StackBasedCallerFinder private constructor() : LogCallerFinder() { + + public override fun findLoggingClass(loggerClass: KClass>): String { + val javaClass = loggerClass.java + // We can skip at most only 1 method from the analysis, the inferLoggingClass() method itself. + val caller = CallerFinder.findCallerOf(javaClass, 1) + if (caller != null) { + return caller.className + } + error("No caller found on the stack for: ${javaClass.name}") + } + + public override fun findLogSite(loggerApi: KClass<*>, stackFramesToSkip: Int): LogSite { + val javaClass = loggerApi.java + // Skip an additional frame due to Throwable instantiation in this method. + val caller = CallerFinder.findCallerOf(javaClass, stackFramesToSkip + 1) + return if (caller == null) { + LogSite.Invalid + } else { + injectedLogSite( + caller.className.replace('.', '/'), + caller.methodName, + caller.lineNumber, + caller.fileName + ) + } + } + + override fun toString(): String = "Default stack-based caller finder" + + public companion object { + private val INSTANCE: LogCallerFinder = StackBasedCallerFinder() + /** Called during logging platform initialization; MUST NOT call any code that might log. */ + @JvmStatic + public fun getInstance(): LogCallerFinder = INSTANCE + } +} diff --git a/contexts/grpc-context/src/main/java/io/spine/logging/context/grpc/package-info.java b/platforms/jvm-default-platform/src/main/kotlin/io/spine/logging/backend/system/SystemClock.kt similarity index 56% rename from contexts/grpc-context/src/main/java/io/spine/logging/context/grpc/package-info.java rename to platforms/jvm-default-platform/src/main/kotlin/io/spine/logging/backend/system/SystemClock.kt index ece041213..261fdb3ed 100644 --- a/contexts/grpc-context/src/main/java/io/spine/logging/context/grpc/package-info.java +++ b/platforms/jvm-default-platform/src/main/kotlin/io/spine/logging/backend/system/SystemClock.kt @@ -24,13 +24,30 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package io.spine.logging.backend.system + +import io.spine.logging.backend.Clock +import java.util.concurrent.TimeUnit.MILLISECONDS + /** - * Contains {@link io.grpc.Context gRPC}-based implementation of - * {@link io.spine.logging.jvm.context.ScopedLoggingContext ScopedLoggingContext}. + * Default millisecond precision clock. + * + * See class documentation in [Clock] for important implementation restrictions. * - * @see Original Java code for historical context. + * @see + * Original Java code for historical context. */ -@CheckReturnValue -package io.spine.logging.context.grpc; +public class SystemClock private constructor() : Clock() { + + public override fun getCurrentTimeNanos(): Long = + MILLISECONDS.toNanos(System.currentTimeMillis()) + + override fun toString(): String = "Default millisecond precision clock" -import com.google.errorprone.annotations.CheckReturnValue; + public companion object { + private val INSTANCE: SystemClock = SystemClock() + /** Called during logging platform initialization; MUST NOT call any code that might log. */ + @JvmStatic + public fun getInstance(): SystemClock = INSTANCE + } +} diff --git a/platforms/jvm-default-platform/src/test/kotlin/io/spine/logging/backend/system/DefaultPlatformSpec.kt b/platforms/jvm-default-platform/src/test/kotlin/io/spine/logging/backend/system/DefaultPlatformSpec.kt index 5550d2a9c..49e6ff4a9 100644 --- a/platforms/jvm-default-platform/src/test/kotlin/io/spine/logging/backend/system/DefaultPlatformSpec.kt +++ b/platforms/jvm-default-platform/src/test/kotlin/io/spine/logging/backend/system/DefaultPlatformSpec.kt @@ -62,14 +62,14 @@ internal class DefaultPlatformSpec { @Test fun `use the given factory to create backend instances`() { val loggerName = "logger.name" - val backend = platform.getBackendImpl(loggerName) + val backend = platform.doGetBackendImpl(loggerName) backend.loggerName shouldContain loggerName backend::class shouldBe StubLoggerBackend::class } @Test fun `return the configured context provider`() { - val contextProvider = platform.getContextDataProviderImpl() + val contextProvider = platform.doGetContextDataProviderImpl() contextProvider shouldBeSameInstanceAs context } @@ -77,19 +77,19 @@ internal class DefaultPlatformSpec { fun `use the given clock to provide the current time`() { val randomTimestamp = Math.random().toLong() clock.returnedTimestamp = randomTimestamp - val timestamp = platform.getCurrentTimeNanosImpl() + val timestamp = platform.doGetCurrentTimeNanosImpl() timestamp shouldBe randomTimestamp } @Test fun `return the configured caller finder`() { - val callerFinder = platform.getCallerFinderImpl() + val callerFinder = platform.doGetCallerFinderImpl() callerFinder shouldBeSameInstanceAs caller } @Test fun `return a human-readable string describing the platform configuration`() { - val configInfo = platform.getConfigInfoImpl().trimEnd() + val configInfo = platform.doGetConfigInfoImpl().trimEnd() val expectedConfig = """ Platform: ${platform.javaClass.name} BackendFactory: $factory @@ -103,7 +103,7 @@ internal class DefaultPlatformSpec { @Test fun `load services from the classpath`() { val platform = DefaultPlatform() - val configInfo = platform.getConfigInfoImpl().trimEnd() + val configInfo = platform.doGetConfigInfoImpl().trimEnd() val expectedServices = setOf( "BackendFactory: ${StubBackendFactoryService::class.java.name}", "Clock: ${StubClockService::class.java.name}", diff --git a/pom.xml b/pom.xml index 4c0958059..0969c6788 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine spine-logging -2.0.0-SNAPSHOT.400 +2.0.0-SNAPSHOT.410 2015 diff --git a/version.gradle.kts b/version.gradle.kts index 3047396fd..2a2a38a5a 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -24,4 +24,4 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -val versionToPublish: String by extra("2.0.0-SNAPSHOT.400") +val versionToPublish: String by extra("2.0.0-SNAPSHOT.410")