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}.
- *
- *
- *
Since the "message" field being {@code null} indicates a private state, calling {@code
- * setMessage(null)} from outside this class is equivalent to calling {@code setMessage("")},
- * and will not reset the instance to its initial "unformatted" state. This is within
- * specification for {@code LogRecord} since the documentation for {@link #getMessage()} says
- * that a return value of {@code null} is equivalent to the empty string.
- *
Setting the parameters to {@code null} from outside this class will reset the parameters to
- * a static singleton empty array. From outside this class, {@link #getParameters} is never
- * observed to contain {@code null}. This is also within specification for {@code 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 {@code null}.
- *
{@code ResourceBundles} are not supported by {@code AbstractLogRecord} and any attempt to
- * set them is ignored.
- *
- *
- * @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