diff --git a/common/src/main/java/com/genexus/diagnostics/Log.java b/common/src/main/java/com/genexus/diagnostics/Log.java index 4f3476bc0..28187101f 100644 --- a/common/src/main/java/com/genexus/diagnostics/Log.java +++ b/common/src/main/java/com/genexus/diagnostics/Log.java @@ -7,63 +7,60 @@ public class Log { private static ILogger getLogger() { return getLogger(""); } - + public static ILogger getMainLogger() { return LogManager.getLogger("com.genexus.logging"); } - + private static ILogger getLogger(String topic) { ILogger log; if (topic != null && topic.length() > 0) { log = LogManager.getLogger(topic); - } - else { + } else { log = getMainLogger(); } return log; } - + public static void write(int logLevel, String message, String topic) { write(message, topic, logLevel); } - + public static void write(String message, String topic, int logLevel) { ILogger log = getLogger(topic); - - switch (logLevel) { - case LogLevel.OFF: //LogLevel off + LogLevel level = LogLevel.fromInt(logLevel); + + switch (level) { + case OFF: //LogLevel off break; - case LogLevel.TRACE: + case TRACE: log.trace(message); break; - case LogLevel.DEBUG: - log.debug(message); - break; - case LogLevel.INFO: + case INFO: log.info(message); break; - case LogLevel.WARNING: + case WARN: log.warn(message); break; - case LogLevel.ERROR: + case ERROR: log.error(message); break; - case LogLevel.FATAL: + case FATAL: log.fatal(message); break; default: - log.debug(message); - } + log.debug(message); + } } - + public static void write(String message) { getLogger().debug(message); } - + public static void write(String message, String topic) { getLogger(topic).debug(message); } - + public static void error(String message) { getLogger().error(message); } @@ -87,7 +84,7 @@ public static void fatal(String message, String topic) { public static void fatal(String message, String topic, Throwable ex) { getLogger(topic).fatal(message, ex); } - + public static void warning(String message) { getLogger().warn(message); } @@ -115,7 +112,7 @@ public static void debug(String message) { public static void debug(String message, String topic) { getLogger(topic).debug(message); } - + public static void debug(String message, String topic, Throwable ex) { getLogger(topic).debug(message, ex); } diff --git a/common/src/main/java/com/genexus/diagnostics/LogLevel.java b/common/src/main/java/com/genexus/diagnostics/LogLevel.java index 8279a7835..2b7caf9a1 100644 --- a/common/src/main/java/com/genexus/diagnostics/LogLevel.java +++ b/common/src/main/java/com/genexus/diagnostics/LogLevel.java @@ -1,14 +1,24 @@ package com.genexus.diagnostics; -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 enum LogLevel { + OFF(0), + TRACE(1), + DEBUG(5), + INFO(10), + WARN(15), + ERROR(20), + FATAL(30); + + private final int lvl; + LogLevel(int lvl) { this.lvl = lvl; } + public int intValue() { return lvl; } + + public static LogLevel fromInt(int lvl) { + for (LogLevel level : LogLevel.values()) { + if (level.intValue() == lvl) { + return level; + } + } + return LogLevel.OFF; + } } diff --git a/common/src/main/java/com/genexus/diagnostics/UserLog.java b/common/src/main/java/com/genexus/diagnostics/UserLog.java index 210041433..dc3f90db3 100644 --- a/common/src/main/java/com/genexus/diagnostics/UserLog.java +++ b/common/src/main/java/com/genexus/diagnostics/UserLog.java @@ -19,37 +19,34 @@ public static ILogger getMainLogger() { private static ILogger getLogger(String topic) { ILogger log; if (topic != null && topic.length() > 0) { - String loggerName = topic.startsWith("$") ? topic.substring(1): String.format("%s.%s", defaultUserLogNamespace, topic.trim()); + String loggerName = topic.startsWith("$") ? topic.substring(1) : String.format("%s.%s", defaultUserLogNamespace, topic.trim()); log = LogManager.getLogger(loggerName); - } - else { + } else { log = getMainLogger(); } return log; } - public static void write( int logLevel, String message, String topic) { + public static void write(int logLevel, String message, String topic) { ILogger log = getLogger(topic); + LogLevel level = LogLevel.fromInt(logLevel); - switch (logLevel) { - case LogLevel.OFF: //LogLevel off + switch (level) { + case OFF: //LogLevel off break; - case LogLevel.TRACE: + case TRACE: log.trace(message); break; - case LogLevel.DEBUG: - log.debug(message); - break; - case LogLevel.INFO: + case INFO: log.info(message); break; - case LogLevel.WARNING: + case WARN: log.warn(message); break; - case LogLevel.ERROR: + case ERROR: log.error(message); break; - case LogLevel.FATAL: + case FATAL: log.fatal(message); break; default: @@ -120,4 +117,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) { + write(message, topic, 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..cc4b53a75 100644 --- a/common/src/main/java/com/genexus/diagnostics/core/ILogger.java +++ b/common/src/main/java/com/genexus/diagnostics/core/ILogger.java @@ -1,7 +1,7 @@ package com.genexus.diagnostics.core; public interface ILogger { - + void fatal(String msg, Throwable ex); void fatal(String msg1, String msg2, Throwable ex); @@ -9,9 +9,9 @@ public interface ILogger { void fatal(Throwable ex, String[] list); void fatal(String[] list); - + void fatal(String msg); - + void error(String msg, Throwable ex); void error(String msg1, String msg2, Throwable ex); @@ -19,11 +19,11 @@ public interface ILogger { void error(Throwable ex, String[] list); void error(String[] list); - + void error(String msg); void warn(String msg); - + void warn(Throwable ex, String[] list); void warn(String[] list); @@ -31,7 +31,7 @@ public interface ILogger { void warn(String msg, Throwable ex); void debug(String msg); - + void debug(Throwable ex, String[] list); void debug(String[] list); @@ -41,11 +41,11 @@ public interface ILogger { void debug(String msg, Throwable ex); void info(String[] list); - + void info(String msg); void trace(String msg); - + void trace(Throwable ex, String[] list); void trace(String[] list); @@ -53,7 +53,7 @@ public interface ILogger { void trace(String msg1, String msg2, Throwable ex); void trace(String msg, Throwable ex); - + boolean isDebugEnabled(); boolean isErrorEnabled(); @@ -65,5 +65,18 @@ public interface ILogger { * msg); } } */ - + default void setContext(String key, Object value) {} + + default void write(String message, int logLevel, Object data, boolean stackTrace) {} + + default boolean isFatalEnabled() { return false; } + + default boolean isWarnEnabled() { return false; } + + default boolean isInfoEnabled() { return false; } + + default boolean isTraceEnabled() { return false; } + + default boolean isEnabled(int logLevel) { return false; } + } diff --git a/wrappercommon/pom.xml b/wrappercommon/pom.xml index c94606440..ad97e9a3d 100644 --- a/wrappercommon/pom.xml +++ b/wrappercommon/pom.xml @@ -33,8 +33,13 @@ org.apache.ws.security wss4j 1.6.19 - - + + + org.apache.logging.log4j + log4j-layout-template-json + 2.24.3 + + gxwrappercommon 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..94b2e6138 --- /dev/null +++ b/wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/CustomMessageFactory.java @@ -0,0 +1,30 @@ +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 = "CustomMessage", category = TemplateResolverFactory.CATEGORY) +public final class CustomMessageFactory implements EventResolverFactory { + private static final CustomMessageFactory INSTANCE = new CustomMessageFactory(); + private CustomMessageFactory() { /* no instances */ } + + @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..886ca0911 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,29 @@ package com.genexus.diagnostics.core.provider; +import com.genexus.diagnostics.LogLevel; import com.genexus.diagnostics.core.ILogger; +import com.genexus.json.JSONObjectWrapper; +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.MapMessage; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.*; public class Log4J2Logger implements ILogger { - private org.apache.logging.log4j.Logger log; + private static final String STACKTRACE_KEY = "stackTrace"; + private static final String MESSAGE_KEY = "message"; + private static final String DATA_KEY = "data"; + private static final boolean IS_JSON_FORMAT = isJsonLogFormat(); + + private final org.apache.logging.log4j.Logger log; public Log4J2Logger(final Class clazz) { log = org.apache.logging.log4j.LogManager.getLogger(clazz); @@ -147,7 +167,7 @@ public void warn(String msg) { public void trace(String msg) { log.trace(msg); } - + public void trace(Throwable ex, String[] list) { if (log.isTraceEnabled()) { StringBuilder msg = new StringBuilder(); @@ -190,4 +210,272 @@ public boolean isErrorEnabled() { return log.isErrorEnabled(); } + @Override + public boolean isFatalEnabled() { + return log.isFatalEnabled(); + } + + @Override + public boolean isWarnEnabled() { + return log.isWarnEnabled(); + } + + @Override + public boolean isInfoEnabled() { + return log.isInfoEnabled(); + } + + @Override + public boolean isTraceEnabled() { + return log.isTraceEnabled(); + } + + @Override + public boolean isEnabled(int logLevel) { + return log.isEnabled(getLogLevel(logLevel)); + } + + @Override + public void setContext(String key, Object value) { + // Add entry to the MDC (only works for JSON log format) + ThreadContext.put(key, fromObjectToString(value)); + } + + @Override + public void write(String message, int logLevel, Object data, boolean stackTrace) { + if (isEnabled(logLevel)) { + if (IS_JSON_FORMAT) + writeJsonFormat(message, logLevel, data, stackTrace); + else + writeTextFormat(message, logLevel, data, stackTrace); + } + } + + private void writeTextFormat(String message, int logLevel, Object data, boolean stackTrace) { + Map mapMessage = new LinkedHashMap<>(); + + if (data == null) { + mapMessage.put(DATA_KEY, JSONObject.NULL); + } else if (data instanceof String && isJson((String) data)) { // JSON Strings + mapMessage.put(DATA_KEY, jsonStringToMap((String) data)); + } else { + mapMessage.put(DATA_KEY, data); + } + + if (stackTrace) { + mapMessage.put(STACKTRACE_KEY, getStackTraceAsList()); + } + + String json = mapToJsonString(mapMessage); + String format = "{} - {}"; + log.log(getLogLevel(logLevel), format, message, json); + } + + private void writeJsonFormat(String message, int logLevel, Object data, boolean stackTrace) { + MapMessage mapMessage = new MapMessage<>().with(MESSAGE_KEY, message); + + if (data == null) { + mapMessage.with(DATA_KEY, JSONObject.NULL); + } else if (data instanceof String && isJson((String) data)) { // JSON Strings + mapMessage.with(DATA_KEY, jsonStringToMap((String) data)); + } else { + mapMessage.with(DATA_KEY, data); + } + + if (stackTrace) { + mapMessage.with(STACKTRACE_KEY, getStackTraceAsList()); + } + + log.log(getLogLevel(logLevel), mapMessage); + } + + private Level getLogLevel(int logLevel) { + LogLevel level = LogLevel.fromInt(logLevel); + switch (level) { + case OFF: + return Level.OFF; + case TRACE: + return Level.TRACE; + case INFO: + return Level.INFO; + case WARN: + return Level.WARN; + case ERROR: + return Level.ERROR; + case 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) { + res = new JSONObject((Map) value).toString(); + } else if (value instanceof List) { + res = new JSONArray((List) value).toString(); + } else { + // Any other object → serialize as JSON + // You never enter here from GX + res = JSONObject.quote(value.toString()); + } + return res; + } + + private static boolean isJson(String str) { + try { + new JSONObject(str); + return true; + } catch (Exception e1) { + try { + new JSONArray(str); + return true; + } catch (Exception e2) { + return false; + } + } + } + + private static List getStackTraceAsList() { + List stackTraceLines = new ArrayList<>(); + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + + boolean skipping = true; + for (StackTraceElement ste : stackTrace) { + String className = ste.getClassName(); + + // Skip lines from this package + if (skipping && (className.startsWith("com.genexus.diagnostics") || + className.startsWith("java.lang.Thread"))) { + continue; + } + + skipping = false; + stackTraceLines.add(ste.toString()); + } + return stackTraceLines; + } + + 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 = appender.getLayout(); + if (layout instanceof JsonTemplateLayout) { + return true; + } + } + } + return false; + } + + public static Map jsonStringToMap(String jsonString) { + JSONObjectWrapper jsonObject = new JSONObjectWrapper(jsonString); + return toMap(jsonObject); + } + + private static Map toMap(JSONObject jsonObject) { + Map map = new LinkedHashMap<>(); + + Set> entries = (jsonObject instanceof JSONObjectWrapper) + ? ((JSONObjectWrapper) jsonObject).entrySet() + : jsonObject.toMap().entrySet(); // fallback for other JSONObject + + for (Map.Entry entry : entries) { + String key = entry.getKey(); + Object value = entry.getValue(); + map.put(key, convert(value)); + } + + return map; + } + + private static List toList(JSONArray array) { + List list = new ArrayList<>(); + for (int i = 0; i < array.length(); i++) { + Object value = array.get(i); + list.add(convert(value)); + } + return list; + } + + private static Object convert(Object value) { + if (value instanceof JSONObject) { + return toMap((JSONObject) value); + } else if (value instanceof JSONArray) { + return toList((JSONArray) value); + } else if (value.equals(JSONObject.NULL)) { + return null; + } else { + return value; + } + } + + public static String mapToJsonString(Map map) { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + + Iterator> iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + + sb.append("\"").append(entry.getKey()).append("\":"); + sb.append(toJsonValue(entry.getValue())); + + if (iterator.hasNext()) { + sb.append(","); + } + } + + sb.append("}"); + return sb.toString(); + } + + private static String toJsonValue(Object value) { + if (value == null || value == JSONObject.NULL) { + return "null"; + } else if (value instanceof String) { + return "\"" + value + "\""; + } else if (value instanceof Number || value instanceof Boolean) { + return value.toString(); + } else if (value instanceof JSONObject) { + return mapToJsonString(((JSONObject) value).toMap()); + } else if (value instanceof Map) { + return mapToJsonString((Map) value); + } else if (value instanceof JSONArray) { + return listToJsonString(((JSONArray) value).toList()); + } else if (value instanceof Collection) { + return listToJsonString((Collection) value); + } else { + return "\"" + value.toString() + "\""; // fallback: string + } + } + + private static String listToJsonString(Collection list) { + StringBuilder sb = new StringBuilder(); + sb.append("["); + + Iterator it = list.iterator(); + while (it.hasNext()) { + sb.append(toJsonValue(it.next())); + if (it.hasNext()) { + sb.append(","); + } + } + + sb.append("]"); + return sb.toString(); + } } diff --git a/wrappercommon/src/main/resources/CustomEcsLayout.json b/wrappercommon/src/main/resources/CustomEcsLayout.json new file mode 100644 index 000000000..bf77e34d6 --- /dev/null +++ b/wrappercommon/src/main/resources/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" + } +}