From c6c58e5b199f0db54216800125736b34a418433a Mon Sep 17 00:00:00 2001 From: mrodriguez Date: Thu, 5 Jun 2025 10:53:28 -0300 Subject: [PATCH 1/6] Add custom JSON logging support with unescaped `data` and plain `message` fields - Ensured `data` field is unescaped JSON, while `message` remains a simple String - Updated `EcsLayout.json` to include `message`, `data`, and `context` fields (or add a new template) - `context` is populated via MDC (`ThreadContext.put`) and supports simple key-value pairs --- .../java/com/genexus/diagnostics/UserLog.java | 13 + .../com/genexus/diagnostics/core/ILogger.java | 12 +- wrappercommon/pom.xml | 20 +- .../core/provider/Log4J2Logger.java | 226 +++++++++++++++++- 4 files changed, 263 insertions(+), 8 deletions(-) diff --git a/common/src/main/java/com/genexus/diagnostics/UserLog.java b/common/src/main/java/com/genexus/diagnostics/UserLog.java index 210041433..45dce93b8 100644 --- a/common/src/main/java/com/genexus/diagnostics/UserLog.java +++ b/common/src/main/java/com/genexus/diagnostics/UserLog.java @@ -120,4 +120,17 @@ public static void debug(String message, String topic) { public static void debug(String message, String topic, Throwable ex) { getLogger(topic).debug(message, ex); } + + + /******* Log Improvements *****/ + + 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); + } + } 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..dd38624e2 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,15 @@ public interface ILogger { * msg); } } */ - + + /******* START - Log Improvements *****/ + // A default implementation is added for AndroidLogger because it fails, it needs an + // implementation, another solution is to declare the class as abstract, but it is + // not possible because of the way the class is made. + + default void setContext(String key, Object value) {} + + default void write(String message, int logLevel, Object data, boolean stackTrace) {} + + /******* END - Log Improvements *****/ } 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/Log4J2Logger.java b/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/Log4J2Logger.java index 9db36cb9a..445b97353 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,22 @@ package com.genexus.diagnostics.core.provider; +import com.genexus.diagnostics.LogLevel; import com.genexus.diagnostics.core.ILogger; +import com.genexus.GxUserType; +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +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.ObjectMessage; + +import java.lang.reflect.Type; +import java.util.*; public class Log4J2Logger implements ILogger { private org.apache.logging.log4j.Logger log; @@ -37,7 +53,7 @@ public void fatal(Throwable ex, String[] list) { } public void fatal(String[] list) { - fatal(null, list); + fatal((Throwable) null, list); } public void error(String msg, Throwable ex) { @@ -59,7 +75,7 @@ public void error(Throwable ex, String[] list) { } public void error(String[] list) { - error(null, list); + error((Throwable) null, list); } public void error(String msg) { @@ -80,7 +96,7 @@ public void warn(Throwable ex, String[] list) { } public void warn(String[] list) { - warn(null, list); + warn((Throwable) null, list); } public void warn(String msg, Throwable ex) { @@ -106,7 +122,7 @@ public void debug(Throwable ex, String[] list) { } public void debug(String[] list) { - debug(null, list); + debug((Throwable) null, list); } // Lambda Functions not supported JAVA 7. Only Java 8. @@ -162,7 +178,7 @@ public void trace(Throwable ex, String[] list) { } public void trace(String[] list) { - trace(null, list); + trace((Throwable) null, list); } // Lambda Functions not supported JAVA 7. Only Java 8. @@ -190,4 +206,204 @@ public boolean isErrorEnabled() { return log.isErrorEnabled(); } + + + + + + + /******** NEW methods to improve logging ********/ + + public void setContext(String key, Object value) { + // Add entry to the MDC (only works for JSON log format) + ThreadContext.put(key, fromObjectToString(value)); + } + + private ObjectMessage buildLogMessage(String messageKey, Object messageValue, boolean stackTrace) { + Map messageMap; + String stacktraceLabel = "stackTrace"; + String msgLabel = "message"; + + if (isNullOrBlank(messageValue)) { + if (stackTrace) { + messageMap = new LinkedHashMap<>(); + messageMap.put(msgLabel, messageKey); + messageMap.put(stacktraceLabel, getStackTraceAsList()); + if(isJsonLogFormat()) + return new ObjectMessage(messageMap); + else + return new ObjectMessage(new Gson().toJson(messageMap)); + } + return new ObjectMessage(messageKey); + } else { + messageMap = objectToMap(messageKey, messageValue); + if (stackTrace) { + messageMap.put(stacktraceLabel, getStackTraceAsList()); + } + if(isJsonLogFormat()) + return new ObjectMessage(messageMap); + else + return new ObjectMessage(new Gson().toJson(messageMap)); + } + } + + public void write(String message, int logLevel, Object data, boolean stackTrace) { + printLog("data", data, stackTrace, Level.DEBUG); + } + + private void printLog(final String messageKey, final Object messageValue, final boolean stackTrace, + final Level logLevel) { + + /* Generate the message JSON in this format: + * { "message" : + * { + * "messageKey": "USER messageValue", + * } + * } + * */ + ObjectMessage om = buildLogMessage(messageKey, messageValue, stackTrace); + + // Log the message received or the crafted msg + if (logLevel.equals(Level.FATAL)) log.fatal(om); + else if (logLevel.equals(Level.ERROR)) log.error(om); + else if (logLevel.equals(Level.WARN)) log.warn(om); + else if (logLevel.equals(Level.INFO)) log.info(om); + else if (logLevel.equals(Level.DEBUG)) log.debug(om); + else if (logLevel.equals(Level.TRACE)) log.trace(om); + } + + + + 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 Map objectToMap(String key, Object value) { + Map result = new LinkedHashMap<>(); + if (value == null) { + result.put(key, null); + } else if (value instanceof Number || value instanceof Boolean + || value instanceof Map || value instanceof List) { + result.put(key, value); + } else if (value instanceof GxUserType) { + result.put(key, jsonStringToMap(((GxUserType) value).toJSonString())); + } else if (value instanceof String) { + String str = (String) value; + + // Try to parse as JSON + try { + JsonElement parsed = JsonParser.parseString(str); + Gson gson = new Gson(); + if (parsed.isJsonObject()) { + result.put(key, gson.fromJson(parsed, Map.class)); + } else if (parsed.isJsonArray()) { + result.put(key, gson.fromJson(parsed, List.class)); + } else if (parsed.isJsonPrimitive()) { + JsonPrimitive primitive = parsed.getAsJsonPrimitive(); + if (primitive.isBoolean()) { + result.put(key, primitive.getAsBoolean()); + } else if (primitive.isNumber()) { + result.put(key, primitive.getAsNumber()); + } else if (primitive.isString()) { + result.put(key, primitive.getAsString()); + } + } + } catch (JsonSyntaxException e) { + // Invalid JSON: it is left as string + result.put(key, str); + } + } else { + // Any other object: convert to string + result.put(key, value.toString()); + } + return result; + } + + 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 String toJson(String key, Object value) { + Map map = new HashMap<>(); + map.put(key, value); + return new Gson().toJson(map); + } + + 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; + } + + public static boolean isNullOrBlank(Object obj) { + if (obj == null) { + return true; + } + if (obj instanceof String) { + return ((String) obj).trim().isEmpty(); + } + return false; // It is not null, and it isn't an empty string + } + } From 0390affd312401a2e6fe69d4b5b413ca99844787 Mon Sep 17 00:00:00 2001 From: mrodbratschi Date: Fri, 6 Jun 2025 12:00:53 -0300 Subject: [PATCH 2/6] Restore some changes --- .../core/provider/Log4J2Logger.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) 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 445b97353..0659158a3 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 @@ -53,7 +53,7 @@ public void fatal(Throwable ex, String[] list) { } public void fatal(String[] list) { - fatal((Throwable) null, list); + fatal(null, list); } public void error(String msg, Throwable ex) { @@ -75,7 +75,7 @@ public void error(Throwable ex, String[] list) { } public void error(String[] list) { - error((Throwable) null, list); + error(null, list); } public void error(String msg) { @@ -96,7 +96,7 @@ public void warn(Throwable ex, String[] list) { } public void warn(String[] list) { - warn((Throwable) null, list); + warn(null, list); } public void warn(String msg, Throwable ex) { @@ -122,7 +122,7 @@ public void debug(Throwable ex, String[] list) { } public void debug(String[] list) { - debug((Throwable) null, list); + debug(null, list); } // Lambda Functions not supported JAVA 7. Only Java 8. @@ -178,7 +178,7 @@ public void trace(Throwable ex, String[] list) { } public void trace(String[] list) { - trace((Throwable) null, list); + trace(null, list); } // Lambda Functions not supported JAVA 7. Only Java 8. @@ -219,6 +219,14 @@ public void setContext(String key, Object value) { ThreadContext.put(key, fromObjectToString(value)); } + public void write(String message, int logLevel, Object data, boolean stackTrace) { + printLog("data", data, stackTrace, Level.DEBUG); + } + + public void write(String message, int logLevel, Object data) { + printLog("data", data, false, Level.DEBUG); + } + private ObjectMessage buildLogMessage(String messageKey, Object messageValue, boolean stackTrace) { Map messageMap; String stacktraceLabel = "stackTrace"; @@ -247,9 +255,7 @@ private ObjectMessage buildLogMessage(String messageKey, Object messageValue, bo } } - public void write(String message, int logLevel, Object data, boolean stackTrace) { - printLog("data", data, stackTrace, Level.DEBUG); - } + private void printLog(final String messageKey, final Object messageValue, final boolean stackTrace, final Level logLevel) { From 9fe1f5de04995cc692281ead4b476f505e2e9257 Mon Sep 17 00:00:00 2001 From: mrodbratschi Date: Thu, 12 Jun 2025 16:00:43 -0300 Subject: [PATCH 3/6] New methods added --- .../com/genexus/diagnostics/LogLevel.java | 15 +- .../java/com/genexus/diagnostics/UserLog.java | 40 +++- .../com/genexus/diagnostics/core/ILogger.java | 19 +- .../core/provider/Log4J2Logger.java | 185 +++++++----------- 4 files changed, 130 insertions(+), 129 deletions(-) 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 45dce93b8..4c1edd5b6 100644 --- a/common/src/main/java/com/genexus/diagnostics/UserLog.java +++ b/common/src/main/java/com/genexus/diagnostics/UserLog.java @@ -121,9 +121,6 @@ public static void debug(String message, String topic, Throwable ex) { getLogger(topic).debug(message, ex); } - - /******* Log Improvements *****/ - public static void setContext(String key, Object value) { // Topic is ignored, also if you put something getLogger("$").setContext(key, value); @@ -133,4 +130,41 @@ public static void write(String message, String topic, int logLevel, Object data 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 dd38624e2..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,15 +65,20 @@ public interface ILogger { * msg); } } */ + void setContext(String key, Object value); - /******* START - Log Improvements *****/ - // A default implementation is added for AndroidLogger because it fails, it needs an - // implementation, another solution is to declare the class as abstract, but it is - // not possible because of the way the class is made. + void write(String message, int logLevel, Object data, boolean stackTrace); - default void setContext(String key, Object value) {} + boolean isFatalEnabled(); - default void write(String message, int logLevel, Object data, boolean stackTrace) {} + boolean isWarnEnabled(); + + boolean isInfoEnabled(); + + boolean isTraceEnabled(); + + boolean isEnabled(int logLevel); + + //boolean isEnabled(int logLevel, String topic); - /******* END - Log Improvements *****/ } 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 0659158a3..ad2f44082 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 @@ -7,13 +7,14 @@ 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.ObjectMessage; +import org.apache.logging.log4j.message.MapMessage; import java.lang.reflect.Type; import java.util.*; @@ -206,13 +207,29 @@ 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)); + } - - /******** NEW methods to improve logging ********/ + 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) @@ -220,68 +237,72 @@ public void setContext(String key, Object value) { } public void write(String message, int logLevel, Object data, boolean stackTrace) { - printLog("data", data, stackTrace, Level.DEBUG); - } + if (isJsonLogFormat()) + writeJsonFormat(message, logLevel, data, stackTrace); + else + writeTextFormat(message, logLevel, data, stackTrace); + } + + 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); + } - public void write(String message, int logLevel, Object data) { - printLog("data", data, false, Level.DEBUG); - } + if (stackTrace) { + mapMessage.put("stackTrace", getStackTraceAsList()); + } - private ObjectMessage buildLogMessage(String messageKey, Object messageValue, boolean stackTrace) { - Map messageMap; - String stacktraceLabel = "stackTrace"; - String msgLabel = "message"; + String json = new Gson().newBuilder().serializeNulls().create().toJson(mapMessage); + String format = "{} - {}"; - if (isNullOrBlank(messageValue)) { - if (stackTrace) { - messageMap = new LinkedHashMap<>(); - messageMap.put(msgLabel, messageKey); - messageMap.put(stacktraceLabel, getStackTraceAsList()); - if(isJsonLogFormat()) - return new ObjectMessage(messageMap); - else - return new ObjectMessage(new Gson().toJson(messageMap)); - } - return new ObjectMessage(messageKey); - } else { - messageMap = objectToMap(messageKey, messageValue); - if (stackTrace) { - messageMap.put(stacktraceLabel, getStackTraceAsList()); - } - if(isJsonLogFormat()) - return new ObjectMessage(messageMap); - else - return new ObjectMessage(new Gson().toJson(messageMap)); - } - } + 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", message); - private void printLog(final String messageKey, final Object messageValue, final boolean stackTrace, - final Level logLevel) { + 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); + } - /* Generate the message JSON in this format: - * { "message" : - * { - * "messageKey": "USER messageValue", - * } - * } - * */ - ObjectMessage om = buildLogMessage(messageKey, messageValue, stackTrace); + if (stackTrace) { + mapMessage.with("stackTrace", getStackTraceAsList()); + } - // Log the message received or the crafted msg - if (logLevel.equals(Level.FATAL)) log.fatal(om); - else if (logLevel.equals(Level.ERROR)) log.error(om); - else if (logLevel.equals(Level.WARN)) log.warn(om); - else if (logLevel.equals(Level.INFO)) log.info(om); - else if (logLevel.equals(Level.DEBUG)) log.debug(om); - else if (logLevel.equals(Level.TRACE)) log.trace(om); + 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 = ""; + String res; if (value == null) { res = "null"; } else if (value instanceof String && isJson((String) value)) { @@ -311,47 +332,6 @@ private static boolean isJson(String input) { } } - private Map objectToMap(String key, Object value) { - Map result = new LinkedHashMap<>(); - if (value == null) { - result.put(key, null); - } else if (value instanceof Number || value instanceof Boolean - || value instanceof Map || value instanceof List) { - result.put(key, value); - } else if (value instanceof GxUserType) { - result.put(key, jsonStringToMap(((GxUserType) value).toJSonString())); - } else if (value instanceof String) { - String str = (String) value; - - // Try to parse as JSON - try { - JsonElement parsed = JsonParser.parseString(str); - Gson gson = new Gson(); - if (parsed.isJsonObject()) { - result.put(key, gson.fromJson(parsed, Map.class)); - } else if (parsed.isJsonArray()) { - result.put(key, gson.fromJson(parsed, List.class)); - } else if (parsed.isJsonPrimitive()) { - JsonPrimitive primitive = parsed.getAsJsonPrimitive(); - if (primitive.isBoolean()) { - result.put(key, primitive.getAsBoolean()); - } else if (primitive.isNumber()) { - result.put(key, primitive.getAsNumber()); - } else if (primitive.isString()) { - result.put(key, primitive.getAsString()); - } - } - } catch (JsonSyntaxException e) { - // Invalid JSON: it is left as string - result.put(key, str); - } - } else { - // Any other object: convert to string - result.put(key, value.toString()); - } - return result; - } - private static String getStackTrace() { StringBuilder stackTrace; stackTrace = new StringBuilder(); @@ -380,12 +360,6 @@ private static Map jsonStringToMap(String jsonString) { return gson.fromJson(jsonString, type); } - private String toJson(String key, Object value) { - Map map = new HashMap<>(); - map.put(key, value); - return new Gson().toJson(map); - } - private static boolean isJsonLogFormat() { LoggerContext context = (LoggerContext) LogManager.getContext(false); Configuration config = context.getConfiguration(); @@ -398,18 +372,7 @@ private static boolean isJsonLogFormat() { } } } - return false; } - public static boolean isNullOrBlank(Object obj) { - if (obj == null) { - return true; - } - if (obj instanceof String) { - return ((String) obj).trim().isEmpty(); - } - return false; // It is not null, and it isn't an empty string - } - } From 5757ac42ba80727c892c178f1117b8eae8c0b3b5 Mon Sep 17 00:00:00 2001 From: mrodbratschi Date: Wed, 18 Jun 2025 14:39:09 -0300 Subject: [PATCH 4/6] A custom resolver is added to handle messages other than MapMessages to be compatible with the existing implementation of the log (the old implementation remains unchanged). --- .../core/provider/CustomMessageFactory.java | 29 +++++++++++++++ .../core/provider/CustomMessageResolver.java | 36 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/CustomMessageFactory.java create mode 100644 wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/CustomMessageResolver.java 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()); + } +} + From 14345036504ee2705789380e1a03a4cf3723fe17 Mon Sep 17 00:00:00 2001 From: mrodbratschi Date: Wed, 18 Jun 2025 14:41:06 -0300 Subject: [PATCH 5/6] Add a custom .json template that uses the custom resolver --- .../core/provider/CustomEcsLayout.json | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/CustomEcsLayout.json 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" + } +} From 019dd90155f0f5c961cc804faa941e02c1819c14 Mon Sep 17 00:00:00 2001 From: mrodbratschi Date: Wed, 18 Jun 2025 14:51:18 -0300 Subject: [PATCH 6/6] Refactoring --- .../core/provider/Log4J2Logger.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) 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 ad2f44082..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,9 +1,11 @@ package com.genexus.diagnostics.core.provider; +import com.genexus.GxUserType; import com.genexus.diagnostics.LogLevel; import com.genexus.diagnostics.core.ILogger; -import com.genexus.GxUserType; -import com.google.gson.*; +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; @@ -17,7 +19,10 @@ import org.apache.logging.log4j.message.MapMessage; import java.lang.reflect.Type; -import java.util.*; +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; @@ -243,6 +248,9 @@ public void write(String message, int logLevel, Object data, boolean stackTrace) 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<>(); @@ -258,19 +266,17 @@ private void writeTextFormat(String message, int logLevel, Object data, boolean } if (stackTrace) { - mapMessage.put("stackTrace", getStackTraceAsList()); + 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", message); + MapMessage mapMessage = new MapMessage<>().with(MESSAGE_KEY, message); if (data == null || (data instanceof String && "null".equals(data.toString()))) { mapMessage.with(dataKey, (Object) null); @@ -283,7 +289,7 @@ private void writeJsonFormat(String message, int logLevel, Object data, boolean } if (stackTrace) { - mapMessage.with("stackTrace", getStackTraceAsList()); + mapMessage.with(STACKTRACE_KEY, getStackTraceAsList()); } log.log(getLogLevel(logLevel), mapMessage);