diff --git a/common/src/main/java/com/genexus/diagnostics/LogLevel.java b/common/src/main/java/com/genexus/diagnostics/LogLevel.java index 8279a7835..59890af7c 100644 --- a/common/src/main/java/com/genexus/diagnostics/LogLevel.java +++ b/common/src/main/java/com/genexus/diagnostics/LogLevel.java @@ -2,13 +2,12 @@ public class LogLevel { - static final int OFF = 0; - static final int TRACE = 1; - static final int DEBUG = 5; - static final int INFO = 10; - static final int WARNING = 15; - static final int ERROR = 20; - static final int FATAL = 30; - + public static final int OFF = 0; + public static final int TRACE = 1; + public static final int DEBUG = 5; + public static final int INFO = 10; + public static final int WARNING = 15; + public static final int ERROR = 20; + public static final int FATAL = 30; } diff --git a/common/src/main/java/com/genexus/diagnostics/UserLog.java b/common/src/main/java/com/genexus/diagnostics/UserLog.java index 210041433..4c1edd5b6 100644 --- a/common/src/main/java/com/genexus/diagnostics/UserLog.java +++ b/common/src/main/java/com/genexus/diagnostics/UserLog.java @@ -120,4 +120,51 @@ public static void debug(String message, String topic) { public static void debug(String message, String topic, Throwable ex) { getLogger(topic).debug(message, ex); } + + public static void setContext(String key, Object value) { + // Topic is ignored, also if you put something + getLogger("$").setContext(key, value); + } + + public static void write(String message, String topic, int logLevel, Object data, boolean stackTrace) { + getLogger(topic).write(message, logLevel, data, stackTrace); + } + + public static void write(String message, String topic, int logLevel, Object data) { + getLogger(topic).write(message, logLevel, data, false); + } + + public static boolean isDebugEnabled() { + return getLogger().isDebugEnabled(); + } + + public static boolean isErrorEnabled() { + return getLogger().isErrorEnabled(); + } + + public static boolean isFatalEnabled() { + return getLogger().isFatalEnabled(); + } + + public static boolean isInfoEnabled() { + return getLogger().isInfoEnabled(); + } + + public static boolean isWarnEnabled() { + return getLogger().isWarnEnabled(); + } + + public static boolean isTraceEnabled() { + return getLogger().isTraceEnabled(); + } + + public static boolean isEnabled(int logLevel) { + return getLogger().isEnabled(logLevel); + } + + public static boolean isEnabled(int logLevel, String topic) { + return getLogger(topic).isEnabled(logLevel); + } + + } diff --git a/common/src/main/java/com/genexus/diagnostics/core/ILogger.java b/common/src/main/java/com/genexus/diagnostics/core/ILogger.java index 79424c5be..478fbfa14 100644 --- a/common/src/main/java/com/genexus/diagnostics/core/ILogger.java +++ b/common/src/main/java/com/genexus/diagnostics/core/ILogger.java @@ -65,5 +65,20 @@ public interface ILogger { * msg); } } */ - + void setContext(String key, Object value); + + void write(String message, int logLevel, Object data, boolean stackTrace); + + boolean isFatalEnabled(); + + boolean isWarnEnabled(); + + boolean isInfoEnabled(); + + boolean isTraceEnabled(); + + boolean isEnabled(int logLevel); + + //boolean isEnabled(int logLevel, String topic); + } diff --git a/wrappercommon/pom.xml b/wrappercommon/pom.xml index b52622955..4f1be37d2 100644 --- a/wrappercommon/pom.xml +++ b/wrappercommon/pom.xml @@ -38,8 +38,24 @@ org.apache.ws.security wss4j 1.6.19 - - + + + com.google.code.gson + gson + 2.12.1 + + + com.genexus + gxclassR + 104.6-trunk.20240524121701-SNAPSHOT + compile + + + org.apache.logging.log4j + log4j-layout-template-json + 2.24.3 + + gxwrappercommon diff --git a/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/CustomEcsLayout.json b/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/CustomEcsLayout.json new file mode 100644 index 000000000..bf77e34d6 --- /dev/null +++ b/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/CustomEcsLayout.json @@ -0,0 +1,57 @@ +{ + "@timestamp": { + "$resolver": "timestamp", + "pattern": { + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "timeZone": "UTC" + } + }, + "ecs.version": "1.2.0", + "log.level": { + "$resolver": "level", + "field": "name" + }, + "message": { + "$resolver": "customMessage", + "stringified": true + }, + "data": { + "$resolver": "map", + "key": "data" + }, + "process.thread.name": { + "$resolver": "thread", + "field": "name" + }, + "log.logger": { + "$resolver": "logger", + "field": "name" + }, + "tags": { + "$resolver": "ndc" + }, + "error.type": { + "$resolver": "exception", + "field": "className" + }, + "error.message": { + "$resolver": "exception", + "field": "message" + }, + "error.stack_trace": { + "$resolver": "exception", + "field": "stackTrace", + "stackTrace": { + "stringified": true + } + }, + "context": { + "$resolver": "mdc", + "field": "context", + "stringified": false + }, + "stackTrace": { + "$resolver": "map", + "key": "stackTrace" + } +} diff --git a/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/CustomMessageFactory.java b/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/CustomMessageFactory.java new file mode 100644 index 000000000..a1998ebd5 --- /dev/null +++ b/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/CustomMessageFactory.java @@ -0,0 +1,29 @@ +package com.genexus.diagnostics.core.provider; + +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext; +import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory; +import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig; +import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory; + + +@Plugin(name = "CustomMessageFactory", category = TemplateResolverFactory.CATEGORY) +public final class CustomMessageFactory implements EventResolverFactory { + private static final CustomMessageFactory INSTANCE = new CustomMessageFactory(); + + @PluginFactory + public static CustomMessageFactory getInstance() { + return INSTANCE; + } + + @Override + public String getName() { + return CustomMessageResolver.getName(); + } + + @Override + public CustomMessageResolver create(EventResolverContext context, TemplateResolverConfig config) { + return new CustomMessageResolver(config); + } +} diff --git a/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/CustomMessageResolver.java b/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/CustomMessageResolver.java new file mode 100644 index 000000000..d8541284e --- /dev/null +++ b/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/CustomMessageResolver.java @@ -0,0 +1,36 @@ +package com.genexus.diagnostics.core.provider; + +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.layout.template.json.resolver.EventResolver; +import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig; +import org.apache.logging.log4j.layout.template.json.util.JsonWriter; +import org.apache.logging.log4j.message.MapMessage; +import org.apache.logging.log4j.message.Message; + + +public class CustomMessageResolver implements EventResolver { + private static final String RESOLVER_NAME = "customMessage"; + + CustomMessageResolver(TemplateResolverConfig config) { + } + + static String getName() { + return RESOLVER_NAME; + } + + @Override + public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { + Message message = logEvent.getMessage(); + if (message instanceof MapMessage) { + MapMessage mapMessage = (MapMessage) message; + Object msgValue = mapMessage.get("message"); + if (msgValue != null) { + jsonWriter.writeString(msgValue.toString()); + return; + } + } + // fallback + jsonWriter.writeString(message.getFormattedMessage()); + } +} + diff --git a/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/Log4J2Logger.java b/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/Log4J2Logger.java index 9db36cb9a..4324fc55b 100644 --- a/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/Log4J2Logger.java +++ b/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/Log4J2Logger.java @@ -1,6 +1,28 @@ package com.genexus.diagnostics.core.provider; +import com.genexus.GxUserType; +import com.genexus.diagnostics.LogLevel; import com.genexus.diagnostics.core.ILogger; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.MarkerManager; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout; +import org.apache.logging.log4j.message.MapMessage; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; public class Log4J2Logger implements ILogger { private org.apache.logging.log4j.Logger log; @@ -190,4 +212,173 @@ public boolean isErrorEnabled() { return log.isErrorEnabled(); } + public boolean isFatalEnabled() { + return log.isFatalEnabled(); + } + + public boolean isWarnEnabled() { + return log.isWarnEnabled(); + } + + public boolean isInfoEnabled() { + return log.isInfoEnabled(); + } + + public boolean isTraceEnabled() { + return log.isTraceEnabled(); + } + + public boolean isEnabled(int logLevel) { + return log.isEnabled(getLogLevel(logLevel)); + } + + public boolean isEnabled(int logLevel, String marker) { + return log.isEnabled(getLogLevel(logLevel), MarkerManager.getMarker(marker)); + } + + public void setContext(String key, Object value) { + // Add entry to the MDC (only works for JSON log format) + ThreadContext.put(key, fromObjectToString(value)); + } + + public void write(String message, int logLevel, Object data, boolean stackTrace) { + if (isJsonLogFormat()) + writeJsonFormat(message, logLevel, data, stackTrace); + else + writeTextFormat(message, logLevel, data, stackTrace); + } + + private static final String STACKTRACE_KEY = "stackTrace"; + private static final String MESSAGE_KEY = "message"; + + private void writeTextFormat(String message, int logLevel, Object data, boolean stackTrace) { + String dataKey = "data"; + Map mapMessage = new LinkedHashMap<>(); + + if (data == null || (data instanceof String && "null".equals(data.toString()))) { + mapMessage.put(dataKey, (Object) null); + } else if (data instanceof GxUserType) { // SDT + mapMessage.put(dataKey, jsonStringToMap(fromObjectToString(data))); + } else if (data instanceof String && isJson((String) data)) { // JSON Strings + mapMessage.put(dataKey, jsonStringToMap(fromObjectToString(data))); + } else { + mapMessage.put(dataKey, data); + } + + if (stackTrace) { + mapMessage.put(STACKTRACE_KEY, getStackTraceAsList()); + } + + String json = new Gson().newBuilder().serializeNulls().create().toJson(mapMessage); + String format = "{} - {}"; + log.log(getLogLevel(logLevel), format, message, json); + } + + private void writeJsonFormat(String message, int logLevel, Object data, boolean stackTrace) { + String dataKey = "data"; + MapMessage mapMessage = new MapMessage<>().with(MESSAGE_KEY, message); + + if (data == null || (data instanceof String && "null".equals(data.toString()))) { + mapMessage.with(dataKey, (Object) null); + } else if (data instanceof GxUserType) { // SDT + mapMessage.with(dataKey, jsonStringToMap(fromObjectToString(data))); + } else if (data instanceof String && isJson((String) data)) { // JSON Strings + mapMessage.with(dataKey, jsonStringToMap(fromObjectToString(data))); + } else { + mapMessage.with(dataKey, data); + } + + if (stackTrace) { + mapMessage.with(STACKTRACE_KEY, getStackTraceAsList()); + } + + log.log(getLogLevel(logLevel), mapMessage); + } + + private Level getLogLevel(int logLevel) { + switch (logLevel) { + case LogLevel.OFF: return Level.OFF; + case LogLevel.TRACE: return Level.TRACE; + case LogLevel.INFO: return Level.INFO; + case LogLevel.WARNING: return Level.WARN; + case LogLevel.ERROR: return Level.ERROR; + case LogLevel.FATAL: return Level.FATAL; + default: return Level.DEBUG; + } + } + + private static String fromObjectToString(Object value) { + String res; + if (value == null) { + res = "null"; + } else if (value instanceof String && isJson((String) value)) { + // Avoid double serialization + res = (String) value; + } else if (value instanceof String) { + res = (String) value; + } else if (value instanceof Number || value instanceof Boolean) { + res = value.toString(); + } else if (value instanceof Map || value instanceof List) { + res = new Gson().toJson(value); + } else if (value instanceof GxUserType) { + res = ((GxUserType) value).toJSonString(); + } else { + // Any other object → serialize as JSON + res = new Gson().toJson(value); + } + return res; + } + + private static boolean isJson(String input) { + try { + JsonElement json = JsonParser.parseString(input); + return json.isJsonObject() || json.isJsonArray(); + } catch (Exception e) { + return false; + } + } + + private static String getStackTrace() { + StringBuilder stackTrace; + stackTrace = new StringBuilder(); + for (StackTraceElement ste : Thread.currentThread().getStackTrace()) { + stackTrace.append(ste).append(System.lineSeparator()); + } + return stackTrace.toString(); + } + + private static List getStackTraceAsList() { + List stackTraceLines = new ArrayList<>(); + for (StackTraceElement ste : Thread.currentThread().getStackTrace()) { + stackTraceLines.add(ste.toString()); + } + return stackTraceLines; + } + + private static String stackTraceListToString(List stackTraceLines) { + return String.join(System.lineSeparator(), stackTraceLines); + } + + // Convert a JSON String to Map + private static Map jsonStringToMap(String jsonString) { + Gson gson = new Gson(); + Type type = new TypeToken>(){}.getType(); + return gson.fromJson(jsonString, type); + } + + private static boolean isJsonLogFormat() { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + Configuration config = context.getConfiguration(); + + for (Appender appender : config.getAppenders().values()) { + if (appender instanceof AbstractAppender) { + Object layout = ((AbstractAppender) appender).getLayout(); + if (layout instanceof JsonTemplateLayout) { + return true; + } + } + } + return false; + } + }