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