From bb0e078a2806a191d9a2a2365a985e679c8eb2db Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Wed, 20 Sep 2023 13:15:41 +0200 Subject: [PATCH] Emergency logger + needed dependencies added (#8680) Signed-off-by: Hendrik Ebbers Co-authored-by: Cody Littley <56973212+cody-littley@users.noreply.github.com> --- .../java/com/swirlds/logging/v2/Level.java | 64 +++++ .../java/com/swirlds/logging/v2/Marker.java | 39 +++ .../extensions/emergency/EmergencyLogger.java | 63 +++++ .../emergency/EmergencyLoggerProvider.java | 41 +++ .../v2/extensions/emergency/package-info.java | 6 + .../logging/v2/extensions/event/LogEvent.java | 122 ++++++++ .../v2/extensions/event/LogEventConsumer.java | 36 +++ .../v2/extensions/event/LogMessage.java | 37 +++ .../event/ParameterizedLogMessage.java | 129 +++++++++ .../v2/extensions/event/SimpleLogMessage.java | 33 +++ .../v2/extensions/event/package-info.java | 5 + .../logging/v2/extensions/package-info.java | 5 + .../emergency/EmergencyLoggerImpl.java | 263 ++++++++++++++++++ .../v2/internal/format/LineBasedFormat.java | 196 +++++++++++++ .../com/swirlds/logging/v2/package-info.java | 4 + 15 files changed, 1043 insertions(+) create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/Level.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/Marker.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/emergency/EmergencyLogger.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/emergency/EmergencyLoggerProvider.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/emergency/package-info.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/LogEvent.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/LogEventConsumer.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/LogMessage.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/ParameterizedLogMessage.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/SimpleLogMessage.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/package-info.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/package-info.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/internal/emergency/EmergencyLoggerImpl.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/internal/format/LineBasedFormat.java create mode 100644 platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/package-info.java diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/Level.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/Level.java new file mode 100644 index 000000000000..f594916885de --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/Level.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.logging.v2; + +import com.swirlds.logging.v2.extensions.emergency.EmergencyLogger; +import com.swirlds.logging.v2.extensions.emergency.EmergencyLoggerProvider; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * The level of a log message + */ +public enum Level { + ERROR(10), + WARN(20), + INFO(30), + DEBUG(40), + TRACE(100); + + /** + * The emergency logger + */ + private static final EmergencyLogger EMERGENCY_LOGGER = EmergencyLoggerProvider.getEmergencyLogger(); + + /** + * The ordinal of the level + */ + private final int levelOrdinal; + + /** + * Constructs a level + * + * @param levelOrdinal the ordinal of the level + */ + Level(final int levelOrdinal) { + this.levelOrdinal = levelOrdinal; + } + + /** + * Returns true if the logging of the given level is enabled + * @param level the level + * @return true if the logging of the given level is enabled + */ + public boolean enabledLoggingOfLevel(@NonNull final Level level) { + if (level == null) { + EMERGENCY_LOGGER.logNPE("level"); + return true; + } + return this.levelOrdinal >= level.levelOrdinal; + } +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/Marker.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/Marker.java new file mode 100644 index 000000000000..3217364bd982 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/Marker.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.logging.v2; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; + +/** + * A marker is a named reference to a location / package / context in the code. It can be used to filter log messages at + * runtime + * + * @param name the name of the marker + * @param parent the parent marker (if present) + */ +public record Marker(@NonNull String name, @Nullable Marker parent) { + + public Marker { + Objects.requireNonNull(name, "name must not be null"); + } + + public Marker(@NonNull final String name) { + this(name, null); + } +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/emergency/EmergencyLogger.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/emergency/EmergencyLogger.java new file mode 100644 index 000000000000..f84ceb4dec36 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/emergency/EmergencyLogger.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.logging.v2.extensions.emergency; + +import com.swirlds.logging.v2.Level; +import com.swirlds.logging.v2.extensions.event.LogEvent; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * This interface is used to log emergency events. It is used in the logger api to log events. An implementation must + * not be dependend on any other logging system and must be directly usable in the logger api. Next to that an + * implementation must be bullet proof and must not throw any exceptions. + */ +public interface EmergencyLogger { + + /** + * Logs a null pointer exception. + * + * @param nameOfNullParam the name of the null parameter + */ + void logNPE(@NonNull String nameOfNullParam); + + /** + * Logs an event. + * + * @param event the event to log + */ + void log(@NonNull LogEvent event); + + /** + * Logs a message. + * + * @param level the level of the message + * @param message the message to log + */ + default void log(@NonNull final Level level, @NonNull final String message) { + log(level, message, null); + } + + /** + * Logs a message with a throwable. + * + * @param level the level of the message + * @param message the message to log + * @param thrown the throwable to log + */ + void log(@NonNull Level level, @NonNull String message, @Nullable Throwable thrown); +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/emergency/EmergencyLoggerProvider.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/emergency/EmergencyLoggerProvider.java new file mode 100644 index 000000000000..4fbc1e7254e2 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/emergency/EmergencyLoggerProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.logging.v2.extensions.emergency; + +import com.swirlds.logging.v2.internal.emergency.EmergencyLoggerImpl; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * This class is used to get an instance of the emergency logger. + */ +public final class EmergencyLoggerProvider { + + /** + * Private constructor to prevent instantiation. + */ + private EmergencyLoggerProvider() {} + + /** + * Gets an instance of the emergency logger. + * + * @return an instance of the emergency logger + */ + @NonNull + public static EmergencyLogger getEmergencyLogger() { + return EmergencyLoggerImpl.getInstance(); + } +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/emergency/package-info.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/emergency/package-info.java new file mode 100644 index 000000000000..578e653a49b9 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/emergency/package-info.java @@ -0,0 +1,6 @@ +/** + * This package contains the API of the emergency logger. The emergency logger should be used in the logging api (and + * extensions of the logging api) for logging. The emergency logger does not depend on the logging API and is defined in + * a bulletproof way. + */ +package com.swirlds.logging.v2.extensions.emergency; diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/LogEvent.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/LogEvent.java new file mode 100644 index 000000000000..7f544a8dbe16 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/LogEvent.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.logging.v2.extensions.event; + +import com.swirlds.logging.v2.Level; +import com.swirlds.logging.v2.Marker; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Instant; +import java.util.Map; + +/** + * A log event that is passed to the {@link LogEventConsumer} for processing. + * + * @param level The log level + * @param loggerName The name of the logger + * @param threadName The name of the thread + * @param timestamp The timestamp of the log event + * @param message The log message (this is not a String since the message can be parameterized. See + * {@link LogMessage} for more details). + * @param throwable The throwable + * @param marker The marker + * @param context The context + */ +public record LogEvent( + @NonNull Level level, + @NonNull String loggerName, + @NonNull String threadName, + @NonNull Instant timestamp, + @NonNull LogMessage message, + @Nullable Throwable throwable, + @Nullable Marker marker, + @NonNull Map context) { + + public LogEvent( + @NonNull final Level level, + @NonNull final String loggerName, + @NonNull final String threadName, + @NonNull final Instant timestamp, + @NonNull final String message, + @Nullable final Throwable throwable, + @Nullable final Marker marker, + @NonNull final Map context) { + this(level, loggerName, threadName, timestamp, new SimpleLogMessage(message), throwable, marker, context); + } + + public LogEvent(@NonNull final Level level, @NonNull final String loggerName, @NonNull final String message) { + this(level, loggerName, message, null); + } + + public LogEvent( + @NonNull final Level level, + @NonNull final String loggerName, + @NonNull final String message, + @Nullable final Throwable throwable) { + this( + level, + loggerName, + Thread.currentThread().getName(), + Instant.now(), + new SimpleLogMessage(message), + throwable, + null, + Map.of()); + } + + /** + * Creates a new {@link LogEvent} that has all parameters of the given logEvent but a different context. + * + * @param logEvent the logEvent that should be copied (excluding the context) + * @param context the new context + * @return the new copy of the event + */ + @NonNull + public static LogEvent createCopyWithDifferentContext( + @NonNull final LogEvent logEvent, @NonNull final Map context) { + return new LogEvent( + logEvent.level, + logEvent.loggerName, + logEvent.threadName, + logEvent.timestamp, + logEvent.message, + logEvent.throwable, + logEvent.marker, + context); + } + + /** + * Creates a new {@link LogEvent} that has all parameters of the given logEvent but a different loggerName. + * + * @param logEvent the logEvent that should be copied (excluding the loggerName) + * @param loggerName the new logger name + * @return the new copy of the event + */ + @NonNull + public static LogEvent createCopyWithDifferentName( + @NonNull final LogEvent logEvent, @NonNull final String loggerName) { + return new LogEvent( + logEvent.level, + loggerName, + logEvent.threadName, + logEvent.timestamp, + logEvent.message, + logEvent.throwable, + logEvent.marker, + logEvent.context); + } +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/LogEventConsumer.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/LogEventConsumer.java new file mode 100644 index 000000000000..3adce42fea7a --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/LogEventConsumer.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.logging.v2.extensions.event; + +import com.swirlds.logging.v2.Level; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.function.Consumer; + +/** + * A consumer that consumes log events. + */ +public interface LogEventConsumer extends Consumer { + + /** + * Checks if the consumer is enabled for the given name and level. + * + * @param name the name + * @param level the level + * @return true if the consumer is enabled, false otherwise + */ + boolean isEnabled(@NonNull String name, @NonNull Level level); +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/LogMessage.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/LogMessage.java new file mode 100644 index 000000000000..fd9b7ad4d1db --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/LogMessage.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.logging.v2.extensions.event; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A log message that is part of a {@link LogEvent}. A message can be a simple String (see {@link SimpleLogMessage}) or + * a parameterized String (see {@link ParameterizedLogMessage}). + * + * @see SimpleLogMessage + * @see ParameterizedLogMessage + */ +public sealed interface LogMessage permits SimpleLogMessage, ParameterizedLogMessage { + + /** + * Returns the message as a String. If the message is parameterized, the parameters are resolved. + * + * @return the message as a String + */ + @NonNull + String getMessage(); +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/ParameterizedLogMessage.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/ParameterizedLogMessage.java new file mode 100644 index 000000000000..68178621f798 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/ParameterizedLogMessage.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.logging.v2.extensions.event; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * A log message that can be parameterized with arguments. The arguments are inserted into the message at the positions + * of the {} placeholders. + *

+ * The implementation is copied from slf4j for first tests. need to be replaced in future. SLF4J is using MIT license + * (https://github.com/qos-ch/slf4j/blob/master/LICENSE.txt). Based on that we can use it in our project for now + * + * @param messagePattern the message pattern + * @param args the arguments + * @see LogMessage + */ +public record ParameterizedLogMessage(@NonNull String messagePattern, @NonNull Object... args) implements LogMessage { + + static final char DELIM_START = '{'; + static final String DELIM_STR = "{}"; + private static final char ESCAPE_CHAR = '\\'; + + @Override + public String getMessage() { + if (messagePattern == null) { + return ""; + } + + if (args == null) { + return messagePattern; + } + + int i = 0; + int j; + // use string builder for better multicore performance + final StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); + + int L; + for (L = 0; L < args.length; L++) { + + j = messagePattern.indexOf(DELIM_STR, i); + + if (j == -1) { + // no more variables + if (i == 0) { // this is a simple string + return messagePattern; + } else { // add the tail string which contains no variables and return + // the result. + sbuf.append(messagePattern, i, messagePattern.length()); + return sbuf.toString(); + } + } else { + if (isEscapedDelimeter(messagePattern, j)) { + if (!isDoubleEscaped(messagePattern, j)) { + L--; // DELIM_START was escaped, thus should not be incremented + sbuf.append(messagePattern, i, j - 1); + sbuf.append(DELIM_START); + i = j + 1; + } else { + // The escape character preceding the delimiter start is + // itself escaped: "abc x:\\{}" + // we have to consume one backward slash + sbuf.append(messagePattern, i, j - 1); + deeplyAppendParameter(sbuf, args[L]); + i = j + 2; + } + } else { + // normal case + sbuf.append(messagePattern, i, j); + deeplyAppendParameter(sbuf, args[L]); + i = j + 2; + } + } + } + // append the characters following the last {} pair. + sbuf.append(messagePattern, i, messagePattern.length()); + return sbuf.toString(); + } + + private static boolean isEscapedDelimeter(@NonNull final String messagePattern, final int delimeterStartIndex) { + + if (delimeterStartIndex == 0) { + return false; + } + final char potentialEscape = messagePattern.charAt(delimeterStartIndex - 1); + if (potentialEscape == ESCAPE_CHAR) { + return true; + } else { + return false; + } + } + + private static boolean isDoubleEscaped(@NonNull final String messagePattern, final int delimeterStartIndex) { + if (delimeterStartIndex >= 2 && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR) { + return true; + } else { + return false; + } + } + + private static void deeplyAppendParameter(@NonNull final StringBuilder sbuf, @Nullable final Object o) { + if (o == null) { + sbuf.append("null"); + return; + } + try { + final String oAsString = o.toString(); + sbuf.append(oAsString); + } catch (Throwable t) { + sbuf.append("[FAILED toString()]"); + } + } +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/SimpleLogMessage.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/SimpleLogMessage.java new file mode 100644 index 000000000000..95d98710e00e --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/SimpleLogMessage.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.logging.v2.extensions.event; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A simple log message that is just a String that does not need to be handled / parsed / ... in any specific way. + * + * @param message The message + * @see LogMessage + */ +public record SimpleLogMessage(@NonNull String message) implements LogMessage { + + @Override + public String getMessage() { + return message; + } +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/package-info.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/package-info.java new file mode 100644 index 000000000000..7eb629a8dd98 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/event/package-info.java @@ -0,0 +1,5 @@ +/** + * This package contains the API for the logging events. This is the used as the basic data model of the logging API and + * must be used by any extension to handle or provide logging events. + */ +package com.swirlds.logging.v2.extensions.event; diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/package-info.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/package-info.java new file mode 100644 index 000000000000..54bd0a0ae5a0 --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/extensions/package-info.java @@ -0,0 +1,5 @@ +/** + * This package (and all subpackages) contains the extension API of the logging framework. That API can be used to + * create custom providers or handlers. + */ +package com.swirlds.logging.v2.extensions; diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/internal/emergency/EmergencyLoggerImpl.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/internal/emergency/EmergencyLoggerImpl.java new file mode 100644 index 000000000000..c1dbfd2b95ca --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/internal/emergency/EmergencyLoggerImpl.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.logging.v2.internal.emergency; + +import com.swirlds.logging.v2.Level; +import com.swirlds.logging.v2.extensions.emergency.EmergencyLogger; +import com.swirlds.logging.v2.extensions.event.LogEvent; +import com.swirlds.logging.v2.internal.format.LineBasedFormat; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.function.Supplier; + +/** + * A {@link EmergencyLogger} implementation that is used as a LAST RESORT when no other logger is available. It is + * important that this logger does not depend on any other logger implementation and that it does not throw exceptions. + * Next to that the logger should try to somehow log the message even in a broken system. + *

+ * The logger is defined as a singleton. + */ +public class EmergencyLoggerImpl implements EmergencyLogger { + + /** + * The name of the emergency logger. + */ + private static final String EMERGENCY_LOGGER_NAME = "EMERGENCY-LOGGER"; + + /** + * The message that is used when the message is undefined. + */ + private static final String UNDEFINED_MESSAGE = "UNDEFINED-MESSAGE"; + + /** + * The size of the queue that is used to store the log events. + */ + private static final int LOG_EVENT_QUEUE_SIZE = 1000; + + /** + * The name of the system property that defines the level of the logger. + */ + private static final String LEVEL_PROPERTY_NAME = "com.swirlds.logging.emergency.level"; + + /** + * The singleton instance of the logger. + */ + private static final EmergencyLoggerImpl INSTANCE = new EmergencyLoggerImpl(); + + /** + * The level that is supported by the logger. + */ + private final Level supportedLevel; + + /** + * The queue that is used to store the log events. Once the real logging system is available the events can be taken + * by the logging system and logged. + */ + private final ArrayBlockingQueue logEvents; + + /** + * A thread local that is used to prevent recursion. This can happen when the logger is used in a broken system. + */ + private final ThreadLocal recursionGuard; + + /** + * Creates the singleton instance of the logger. + */ + private EmergencyLoggerImpl() { + this.logEvents = new ArrayBlockingQueue<>(LOG_EVENT_QUEUE_SIZE); + recursionGuard = new ThreadLocal<>(); + supportedLevel = getSupportedLevelFromSystemProperties(); + } + + /** + * Returns the level based on a possible system property. + * + * @return the level based on a possible system property + */ + @NonNull + private static Level getSupportedLevelFromSystemProperties() { + final String property = System.getProperty(LEVEL_PROPERTY_NAME); + if (property == null) { + return Level.DEBUG; // DEFAULT LEVEL + } else if (Objects.equals(property.toUpperCase(), Level.TRACE.name())) { + return Level.TRACE; + } else if (Objects.equals(property.toUpperCase(), Level.DEBUG.name())) { + return Level.DEBUG; + } else if (Objects.equals(property.toUpperCase(), Level.INFO.name())) { + return Level.INFO; + } else if (Objects.equals(property.toUpperCase(), Level.WARN.name())) { + return Level.WARN; + } else if (Objects.equals(property.toUpperCase(), Level.ERROR.name())) { + return Level.ERROR; + } else { + return Level.TRACE; + } + } + + @Override + public void logNPE(@NonNull final String nameOfNullParam) { + log( + Level.ERROR, + "Null parameter: " + nameOfNullParam, + new NullPointerException("Null parameter: " + nameOfNullParam)); + } + + @Override + public void log(@NonNull final Level level, @NonNull final String message, @Nullable final Throwable thrown) { + if (level == null && message == null) { + log(new LogEvent(Level.ERROR, EMERGENCY_LOGGER_NAME, UNDEFINED_MESSAGE, thrown)); + } else if (level == null) { + log(new LogEvent(Level.ERROR, EMERGENCY_LOGGER_NAME, message, thrown)); + } else if (message == null) { + log(new LogEvent(level, EMERGENCY_LOGGER_NAME, UNDEFINED_MESSAGE, thrown)); + } else { + log(new LogEvent(level, EMERGENCY_LOGGER_NAME, message, thrown)); + } + } + + @Override + public void log(@NonNull final LogEvent event) { + if (event == null) { + logNPE("event"); + return; + } + if (isLoggable(event.level())) { + callGuarded(event, () -> handle(event)); + } + } + + /** + * Checks if the given level is loggable by the logger. + * + * @param level the level to check + * @return true if the level is loggable, false otherwise + */ + private boolean isLoggable(@NonNull final Level level) { + if (level == null) { + logNPE("level"); + return true; + } + return supportedLevel.enabledLoggingOfLevel(level); + } + + /** + * A method that is used to call any given {@link Runnable} in a guarded way. This includes exception handling and a + * recursion check. In case of a problem the method tries to at least log the given fallback log event. + * + * @param fallbackLogEvent the fallback log event that should be logged when the logger is broken. + * @param task the task that should be called + */ + private void callGuarded(@NonNull final LogEvent fallbackLogEvent, @NonNull final Runnable task) { + callGuarded(fallbackLogEvent, null, () -> { + task.run(); + return null; + }); + } + + /** + * A method that is used to call any given {@link Supplier} in a guarded way. This includes exception handling and a + * recursion check. In case of a problem the method tries to at least log the given fallback log event. + * + * @param fallbackLogEvent the fallback log event that should be logged when the logger is broken. + * @param fallbackValue the fallback value that should be returned when the logger is broken. + * @param supplier the supplier that should be called + * @param the type of the result + * @return the result of the supplier + */ + @Nullable + private T callGuarded( + @Nullable final LogEvent fallbackLogEvent, + @Nullable final T fallbackValue, + @NonNull final Supplier supplier) { + final Boolean guard = recursionGuard.get(); + if (guard != null && guard) { + final LogEvent logEvent = new LogEvent( + Level.ERROR, + EMERGENCY_LOGGER_NAME, + "Recursion in Emergency logger", + new IllegalStateException("Recursion in Emergency logger")); + handle(logEvent); + if (fallbackLogEvent != null) { + handle(fallbackLogEvent); + } + return fallbackValue; + } else { + recursionGuard.set(true); + try { + return supplier.get(); + } catch (final Throwable t) { + final LogEvent logEvent = + new LogEvent(Level.ERROR, EMERGENCY_LOGGER_NAME, "Error in Emergency logger", t); + handle(logEvent); + if (fallbackLogEvent != null) { + handle(fallbackLogEvent); + } + return fallbackValue; + } finally { + recursionGuard.set(false); + } + } + } + + /** + * Handles the given log event by trying to print the message to the console and adding it to the queue. + * + * @param logEvent the log event that should be handled + */ + private void handle(@NonNull final LogEvent logEvent) { + if (logEvent == null) { + logNPE("logEvent"); + return; + } + final PrintStream printStream = Optional.ofNullable(System.err).orElse(System.out); + if (printStream != null) { + new LineBasedFormat(new PrintWriter(printStream, true)).print(logEvent); + } + if (logEvents.remainingCapacity() == 0) { + logEvents.remove(); + } + logEvents.add(logEvent); + } + + /** + * Returns the list of logged events and clears the list. + * + * @return the list of logged events + */ + @NonNull + public List publishLoggedEvents() { + final List result = List.copyOf(logEvents); + logEvents.clear(); + return result; + } + + /** + * Returns the instance of the logger. + * + * @return the instance of the logger + */ + @NonNull + public static EmergencyLoggerImpl getInstance() { + return INSTANCE; + } +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/internal/format/LineBasedFormat.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/internal/format/LineBasedFormat.java new file mode 100644 index 000000000000..b8cd7827bd6e --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/internal/format/LineBasedFormat.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.logging.v2.internal.format; + +import com.swirlds.logging.v2.Level; +import com.swirlds.logging.v2.Marker; +import com.swirlds.logging.v2.extensions.emergency.EmergencyLogger; +import com.swirlds.logging.v2.extensions.emergency.EmergencyLoggerProvider; +import com.swirlds.logging.v2.extensions.event.LogEvent; +import com.swirlds.logging.v2.extensions.event.LogMessage; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.PrintWriter; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import java.util.Objects; + +/** + * This class is used to format a {@link LogEvent} as a line of text and write it to a {@link PrintWriter}. + */ +public class LineBasedFormat { + + /** + * The emergency logger. + */ + private static final EmergencyLogger EMERGENCY_LOGGER = EmergencyLoggerProvider.getEmergencyLogger(); + + /** + * The formatter used to format the timestamp. + */ + private final DateTimeFormatter formatter; + + /** + * The underlying {@link PrintWriter}. + */ + private final PrintWriter printWriter; + + /** + * Constructs a new instance of this class. + * + * @param printWriter + */ + public LineBasedFormat(@NonNull final PrintWriter printWriter) { + this.formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.systemDefault()); + this.printWriter = Objects.requireNonNull(printWriter, "printWriter must not be null"); + } + + /** + * Prints the given event to the underlying {@link PrintWriter}. + * + * @param event the event to print + */ + public void print(@NonNull final LogEvent event) { + if (event == null) { + EMERGENCY_LOGGER.logNPE("event"); + } + printWriter.print(asString(event.timestamp())); + printWriter.print(' '); + printWriter.print(asString(event.level())); + printWriter.print(' '); + printWriter.print('['); + printWriter.print(asString(event.threadName(), "THREAD")); + printWriter.print(']'); + printWriter.print(' '); + printWriter.print(asString(event.loggerName(), "LOGGER")); + printWriter.print(" - "); + printWriter.print(asString(event.message())); + + final Marker marker = event.marker(); + if (marker != null) { + printWriter.print(" - M:"); + printWriter.print(asString(marker)); + } + + final Map context = event.context(); + if (context != null && !context.isEmpty()) { + printWriter.print(" - C:"); + printWriter.print(context); + } + printWriter.println(); + + final Throwable throwable = event.throwable(); + if (throwable != null) { + throwable.printStackTrace(printWriter); + } + } + + /** + * Returns the given string or a default value if the given string is {@code null}. + * + * @param str the string to return or a default value if the given string is {@code null} + * @param suffix the suffix to append to the default value + * @return the given string or a default value if the given string is {@code null} + */ + @NonNull + private String asString(@Nullable final String str, @NonNull final String suffix) { + if (str == null) { + return "UNDEFINED-" + suffix; + } else { + return str; + } + } + + /** + * Returns the given level as a string or {@code UNDEFINED} if the given level is {@code null}. + * + * @param level the level to return as a string or {@code UNDEFINED} if the given level is {@code null} + * @return the given level as a string or {@code UNDEFINED} if the given level is {@code null} + */ + @NonNull + private String asString(@Nullable final Level level) { + if (level == null) { + return "UNDEFINED"; + } else { + return level.name(); + } + } + + /** + * Returns the given message as a string or {@code UNDEFINED-MESSAGE} if the given message is {@code null}. + * + * @param message the message to return as a string or {@code UNDEFINED-MESSAGE} if the given message is + * {@code null} + * @return the given message as a string or {@code UNDEFINED-MESSAGE} if the given message is {@code null} + */ + @NonNull + private String asString(@Nullable final LogMessage message) { + if (message == null) { + return "UNDEFINED-MESSAGE"; + } else { + try { + return message.getMessage(); + } catch (final Throwable e) { + EMERGENCY_LOGGER.log(Level.ERROR, "Failed to format message", e); + return "BROKEN-MESSAGE"; + } + } + } + + /** + * Returns the given instant as a string or {@code UNDEFINED-TIMESTAMP} if the given instant is {@code null}. + * + * @param instant the instant to return as a string or {@code UNDEFINED-TIMESTAMP} if the given instant is + * {@code null} + * @return the given instant as a string or {@code UNDEFINED-TIMESTAMP} if the given instant is {@code null} + */ + @NonNull + private String asString(@Nullable final Instant instant) { + if (instant == null) { + return "UNDEFINED-TIMESTAMP "; + } else { + try { + return formatter.format(instant); + } catch (final Throwable e) { + EMERGENCY_LOGGER.log(Level.ERROR, "Failed to format instant", e); + return "BROKEN-TIMESTAMP "; + } + } + } + + /** + * Returns the given marker as a string or {@code null} if the given marker is {@code null}. + * + * @param marker the marker to return as a string or {@code null} if the given marker is {@code null} + * @return the given marker as a string or {@code null} if the given marker is {@code null} + */ + @NonNull + private String asString(@Nullable final Marker marker) { + if (marker == null) { + return "null"; + } else { + final Marker parent = marker.parent(); + if (parent == null) { + return "Marker{name='" + marker.name() + "'}"; + } else { + return "Marker{name='" + marker.name() + "', parent='" + asString(parent) + "'}"; + } + } + } +} diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/package-info.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/package-info.java new file mode 100644 index 000000000000..e7255950f0fa --- /dev/null +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/v2/package-info.java @@ -0,0 +1,4 @@ +/** + * This package contains the private API of the logging framework. + */ +package com.swirlds.logging.v2;