elastic / ecs-logging-java

https://www.elastic.co/guide/en/ecs-logging/java/current/intro.html
Apache License 2.0
139 stars 74 forks source link

feature request: objectMessageAsJsonObject #187

Open hdevalke opened 2 years ago

hdevalke commented 2 years ago

Log4j2 ObjectMessages are automatically converted to json objects. This makes the log statements fail, if the object cannot be converted to json or it generates invalid json.

I would propose a objectMessageAsJsonObject config. Putting it to false would just put the objects toString() result into the message field, as described in the documentation of log4j2.

e.g.

public class EcsLogTest {

    static class A {
        String getA() {
            Objects.requireNonNull(null);
            return null;
        }
    }

    @Test
    void test() {
        LogManager.getLogger().info(new A());
    }
}

produces:

{"@timestamp":"2022-05-16T12:06:08.284Z", "log.level": "INFO",   , "ecs.version": "1.2.0","process.thread.name":"main","log.logger":"EcsLogTest"}

This patch would probably enough to implement this feature:

diff --git a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java
index 1a99742..41937c7 100644
--- a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java
+++ b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java
@@ -79,9 +79,10 @@ public class EcsLayout extends AbstractStringLayout {
     private final boolean includeOrigin;
     private final PatternFormatter[] exceptionPatternFormatter;
     private final ConcurrentMap<Class<? extends MultiformatMessage>, Boolean> supportsJson = new ConcurrentHashMap<Class<? extends MultiformatMessage>, Boolean>();
+    private final boolean objectMessageAsJsonObject;

     private EcsLayout(Configuration config, String serviceName, String serviceVersion, String serviceEnvironment, String serviceNodeName, String eventDataset, boolean includeMarkers,
-                      KeyValuePair[] additionalFields, boolean includeOrigin, String exceptionPattern, boolean stackTraceAsArray) {
+                      KeyValuePair[] additionalFields, boolean includeOrigin, String exceptionPattern, boolean stackTraceAsArray, boolean objectMessageAsJsonObject) {
         super(config, UTF_8, null, null);
         this.serviceName = serviceName;
         this.serviceVersion = serviceVersion;
@@ -92,6 +93,7 @@ public class EcsLayout extends AbstractStringLayout {
         this.includeOrigin = includeOrigin;
         this.stackTraceAsArray = stackTraceAsArray;
         this.additionalFields = additionalFields;
+        this.objectMessageAsJsonObject = objectMessageAsJsonObject;
         fieldValuePatternFormatter = new PatternFormatter[additionalFields.length][];
         for (int i = 0; i < additionalFields.length; i++) {
             KeyValuePair additionalField = additionalFields[i];
@@ -253,7 +255,7 @@ public class EcsLayout extends AbstractStringLayout {
             } else {
                 serializeSimpleMessage(builder, gcFree, message, thrown);
             }
-        } else if (JACKSON_SERIALIZER != null && message instanceof ObjectMessage) {
+        } else if (JACKSON_SERIALIZER != null && message instanceof ObjectMessage && objectMessageAsJsonObject) {
             final StringBuilder jsonBuffer = EcsJsonSerializer.getMessageStringBuilder();
             JACKSON_SERIALIZER.formatTo(jsonBuffer, (ObjectMessage) message);
             addJson(builder, jsonBuffer);
@@ -377,6 +379,8 @@ public class EcsLayout extends AbstractStringLayout {
         private KeyValuePair[] additionalFields = new KeyValuePair[]{};
         @PluginBuilderAttribute("includeOrigin")
         private boolean includeOrigin = false;
+        @PluginBuilderAttribute("objectMessageAsJsonObject")
+        private boolean objectMessageAsJsonObject = true;

         Builder() {
         }
@@ -428,6 +432,10 @@ public class EcsLayout extends AbstractStringLayout {
             return exceptionPattern;
         }

+        public boolean isObjectMessageAsJsonObject() {
+            return objectMessageAsJsonObject;
+        }
+
         /**
          * Additional fields to set on each log event.
          *
@@ -483,11 +491,16 @@ public class EcsLayout extends AbstractStringLayout {
             return this;
         }

+        public EcsLayout.Builder setObjectMessageAsJsonObject(boolean objectMessageAsJsonObject) {
+            this.objectMessageAsJsonObject = objectMessageAsJsonObject;
+            return this;
+        }
+
         @Override
         public EcsLayout build() {
             return new EcsLayout(getConfiguration(), serviceName, serviceVersion, serviceEnvironment, serviceNodeName,
                     EcsJsonSerializer.computeEventDataset(eventDataset, serviceName),
-                    includeMarkers, additionalFields, includeOrigin, exceptionPattern, stackTraceAsArray);
+                    includeMarkers, additionalFields, includeOrigin, exceptionPattern, stackTraceAsArray, objectMessageAsJsonObject);
         }
     }
 }
hdevalke commented 2 years ago

Another option is to bypass log4j to create object messages using a custom message factory and use -Dlog4j2.messageFactory=CustomMessageFactory. But I think a configuration property is a nicer solution.

public class CustomMessageFactory extends AbstractMessageFactory {
    @Override
    public Message newMessage(String message, Object... params) {
        return new ParameterizedMessage(message, params);
    }

    @Override
    public Message newMessage(Object message) {
        return new ParameterizedMessage("{}", message);
    }
}