Skip to content

Commit 8e33673

Browse files
committed
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 (cherry picked from commit c6c58e5)
1 parent 6a6950e commit 8e33673

File tree

4 files changed

+262
-10
lines changed

4 files changed

+262
-10
lines changed

common/src/main/java/com/genexus/diagnostics/UserLog.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,17 @@ public static void debug(String message, String topic) {
120120
public static void debug(String message, String topic, Throwable ex) {
121121
getLogger(topic).debug(message, ex);
122122
}
123+
124+
125+
/******* Log Improvements *****/
126+
127+
public static void setContext(String key, Object value) {
128+
// Topic is ignored, also if you put something
129+
getLogger("$").setContext(key, value);
130+
}
131+
132+
public static void write(String message, String topic, int logLevel, Object data, boolean stackTrace) {
133+
getLogger(topic).write(message, logLevel, data, stackTrace);
134+
}
135+
123136
}

common/src/main/java/com/genexus/diagnostics/core/ILogger.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,15 @@ public interface ILogger {
6565
* msg); } }
6666
*/
6767

68-
68+
69+
/******* START - Log Improvements *****/
70+
// A default implementation is added for AndroidLogger because it fails, it needs an
71+
// implementation, another solution is to declare the class as abstract, but it is
72+
// not possible because of the way the class is made.
73+
74+
default void setContext(String key, Object value) {}
75+
76+
default void write(String message, int logLevel, Object data, boolean stackTrace) {}
77+
78+
/******* END - Log Improvements *****/
6979
}

wrappercommon/pom.xml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,22 @@
5656
</exclusion>
5757
</exclusions>
5858
</dependency>
59-
60-
61-
59+
<dependency>
60+
<groupId>com.google.code.gson</groupId>
61+
<artifactId>gson</artifactId>
62+
<version>2.12.1</version>
63+
</dependency>
64+
<dependency>
65+
<groupId>com.genexus</groupId>
66+
<artifactId>gxclassR</artifactId>
67+
<version>104.6-trunk.20240524121701-SNAPSHOT</version>
68+
<scope>compile</scope>
69+
</dependency>
70+
<dependency>
71+
<groupId>org.apache.logging.log4j</groupId>
72+
<artifactId>log4j-layout-template-json</artifactId>
73+
<version>2.24.3</version>
74+
</dependency>
6275
</dependencies>
6376

6477
<build>
@@ -86,4 +99,4 @@
8699
</plugin>
87100
</plugins>
88101
</build>
89-
</project>
102+
</project>

wrappercommon/src/main/java/com/genexus/diagnostics/core/provider/Log4J2Logger.java

Lines changed: 221 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
package com.genexus.diagnostics.core.provider;
22

3+
import com.genexus.diagnostics.LogLevel;
34
import com.genexus.diagnostics.core.ILogger;
5+
import com.genexus.GxUserType;
6+
import com.google.gson.*;
7+
import com.google.gson.reflect.TypeToken;
8+
import org.apache.logging.log4j.Level;
9+
import org.apache.logging.log4j.LogManager;
10+
import org.apache.logging.log4j.ThreadContext;
11+
import org.apache.logging.log4j.core.Appender;
12+
import org.apache.logging.log4j.core.LoggerContext;
13+
import org.apache.logging.log4j.core.appender.AbstractAppender;
14+
import org.apache.logging.log4j.core.config.Configuration;
15+
import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout;
16+
import org.apache.logging.log4j.message.ObjectMessage;
17+
18+
import java.lang.reflect.Type;
19+
import java.util.*;
420

521
public class Log4J2Logger implements ILogger {
622
private org.apache.logging.log4j.Logger log;
@@ -37,7 +53,7 @@ public void fatal(Throwable ex, String[] list) {
3753
}
3854

3955
public void fatal(String[] list) {
40-
fatal(null, list);
56+
fatal((Throwable) null, list);
4157
}
4258

4359
public void error(String msg, Throwable ex) {
@@ -59,7 +75,7 @@ public void error(Throwable ex, String[] list) {
5975
}
6076

6177
public void error(String[] list) {
62-
error(null, list);
78+
error((Throwable) null, list);
6379
}
6480

6581
public void error(String msg) {
@@ -80,7 +96,7 @@ public void warn(Throwable ex, String[] list) {
8096
}
8197

8298
public void warn(String[] list) {
83-
warn(null, list);
99+
warn((Throwable) null, list);
84100
}
85101

86102
public void warn(String msg, Throwable ex) {
@@ -106,7 +122,7 @@ public void debug(Throwable ex, String[] list) {
106122
}
107123

108124
public void debug(String[] list) {
109-
debug(null, list);
125+
debug((Throwable) null, list);
110126
}
111127

112128
// Lambda Functions not supported JAVA 7. Only Java 8.
@@ -162,7 +178,7 @@ public void trace(Throwable ex, String[] list) {
162178
}
163179

164180
public void trace(String[] list) {
165-
trace(null, list);
181+
trace((Throwable) null, list);
166182
}
167183

168184
// Lambda Functions not supported JAVA 7. Only Java 8.
@@ -190,4 +206,204 @@ public boolean isErrorEnabled() {
190206
return log.isErrorEnabled();
191207
}
192208

209+
210+
211+
212+
213+
214+
215+
/******** NEW methods to improve logging ********/
216+
217+
public void setContext(String key, Object value) {
218+
// Add entry to the MDC (only works for JSON log format)
219+
ThreadContext.put(key, fromObjectToString(value));
220+
}
221+
222+
private ObjectMessage buildLogMessage(String messageKey, Object messageValue, boolean stackTrace) {
223+
Map<String, Object> messageMap;
224+
String stacktraceLabel = "stackTrace";
225+
String msgLabel = "message";
226+
227+
if (isNullOrBlank(messageValue)) {
228+
if (stackTrace) {
229+
messageMap = new LinkedHashMap<>();
230+
messageMap.put(msgLabel, messageKey);
231+
messageMap.put(stacktraceLabel, getStackTraceAsList());
232+
if(isJsonLogFormat())
233+
return new ObjectMessage(messageMap);
234+
else
235+
return new ObjectMessage(new Gson().toJson(messageMap));
236+
}
237+
return new ObjectMessage(messageKey);
238+
} else {
239+
messageMap = objectToMap(messageKey, messageValue);
240+
if (stackTrace) {
241+
messageMap.put(stacktraceLabel, getStackTraceAsList());
242+
}
243+
if(isJsonLogFormat())
244+
return new ObjectMessage(messageMap);
245+
else
246+
return new ObjectMessage(new Gson().toJson(messageMap));
247+
}
248+
}
249+
250+
public void write(String message, int logLevel, Object data, boolean stackTrace) {
251+
printLog("data", data, stackTrace, Level.DEBUG);
252+
}
253+
254+
private void printLog(final String messageKey, final Object messageValue, final boolean stackTrace,
255+
final Level logLevel) {
256+
257+
/* Generate the message JSON in this format:
258+
* { "message" :
259+
* {
260+
* "messageKey": "USER messageValue",
261+
* }
262+
* }
263+
* */
264+
ObjectMessage om = buildLogMessage(messageKey, messageValue, stackTrace);
265+
266+
// Log the message received or the crafted msg
267+
if (logLevel.equals(Level.FATAL)) log.fatal(om);
268+
else if (logLevel.equals(Level.ERROR)) log.error(om);
269+
else if (logLevel.equals(Level.WARN)) log.warn(om);
270+
else if (logLevel.equals(Level.INFO)) log.info(om);
271+
else if (logLevel.equals(Level.DEBUG)) log.debug(om);
272+
else if (logLevel.equals(Level.TRACE)) log.trace(om);
273+
}
274+
275+
276+
277+
private static String fromObjectToString(Object value) {
278+
String res = "";
279+
if (value == null) {
280+
res = "null";
281+
} else if (value instanceof String && isJson((String) value)) {
282+
// Avoid double serialization
283+
res = (String) value;
284+
} else if (value instanceof String) {
285+
res = (String) value;
286+
} else if (value instanceof Number || value instanceof Boolean) {
287+
res = value.toString();
288+
} else if (value instanceof Map || value instanceof List) {
289+
res = new Gson().toJson(value);
290+
} else if (value instanceof GxUserType) {
291+
res = ((GxUserType) value).toJSonString();
292+
} else {
293+
// Any other object → serialize as JSON
294+
res = new Gson().toJson(value);
295+
}
296+
return res;
297+
}
298+
299+
private static boolean isJson(String input) {
300+
try {
301+
JsonElement json = JsonParser.parseString(input);
302+
return json.isJsonObject() || json.isJsonArray();
303+
} catch (Exception e) {
304+
return false;
305+
}
306+
}
307+
308+
private Map<String, Object> objectToMap(String key, Object value) {
309+
Map<String, Object> result = new LinkedHashMap<>();
310+
if (value == null) {
311+
result.put(key, null);
312+
} else if (value instanceof Number || value instanceof Boolean
313+
|| value instanceof Map || value instanceof List) {
314+
result.put(key, value);
315+
} else if (value instanceof GxUserType) {
316+
result.put(key, jsonStringToMap(((GxUserType) value).toJSonString()));
317+
} else if (value instanceof String) {
318+
String str = (String) value;
319+
320+
// Try to parse as JSON
321+
try {
322+
JsonElement parsed = JsonParser.parseString(str);
323+
Gson gson = new Gson();
324+
if (parsed.isJsonObject()) {
325+
result.put(key, gson.fromJson(parsed, Map.class));
326+
} else if (parsed.isJsonArray()) {
327+
result.put(key, gson.fromJson(parsed, List.class));
328+
} else if (parsed.isJsonPrimitive()) {
329+
JsonPrimitive primitive = parsed.getAsJsonPrimitive();
330+
if (primitive.isBoolean()) {
331+
result.put(key, primitive.getAsBoolean());
332+
} else if (primitive.isNumber()) {
333+
result.put(key, primitive.getAsNumber());
334+
} else if (primitive.isString()) {
335+
result.put(key, primitive.getAsString());
336+
}
337+
}
338+
} catch (JsonSyntaxException e) {
339+
// Invalid JSON: it is left as string
340+
result.put(key, str);
341+
}
342+
} else {
343+
// Any other object: convert to string
344+
result.put(key, value.toString());
345+
}
346+
return result;
347+
}
348+
349+
private static String getStackTrace() {
350+
StringBuilder stackTrace;
351+
stackTrace = new StringBuilder();
352+
for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
353+
stackTrace.append(ste).append(System.lineSeparator());
354+
}
355+
return stackTrace.toString();
356+
}
357+
358+
private static List<String> getStackTraceAsList() {
359+
List<String> stackTraceLines = new ArrayList<>();
360+
for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
361+
stackTraceLines.add(ste.toString());
362+
}
363+
return stackTraceLines;
364+
}
365+
366+
private static String stackTraceListToString(List<String> stackTraceLines) {
367+
return String.join(System.lineSeparator(), stackTraceLines);
368+
}
369+
370+
// Convert a JSON String to Map<String, Object>
371+
private static Map<String, Object> jsonStringToMap(String jsonString) {
372+
Gson gson = new Gson();
373+
Type type = new TypeToken<Map<String, Object>>(){}.getType();
374+
return gson.fromJson(jsonString, type);
375+
}
376+
377+
private String toJson(String key, Object value) {
378+
Map<String, Object> map = new HashMap<>();
379+
map.put(key, value);
380+
return new Gson().toJson(map);
381+
}
382+
383+
private static boolean isJsonLogFormat() {
384+
LoggerContext context = (LoggerContext) LogManager.getContext(false);
385+
Configuration config = context.getConfiguration();
386+
387+
for (Appender appender : config.getAppenders().values()) {
388+
if (appender instanceof AbstractAppender) {
389+
Object layout = ((AbstractAppender) appender).getLayout();
390+
if (layout instanceof JsonTemplateLayout) {
391+
return true;
392+
}
393+
}
394+
}
395+
396+
return false;
397+
}
398+
399+
public static boolean isNullOrBlank(Object obj) {
400+
if (obj == null) {
401+
return true;
402+
}
403+
if (obj instanceof String) {
404+
return ((String) obj).trim().isEmpty();
405+
}
406+
return false; // It is not null, and it isn't an empty string
407+
}
408+
193409
}

0 commit comments

Comments
 (0)