diff --git a/src/main/java/net/logstash/log4j/JSONEventLayoutV0.java b/src/main/java/net/logstash/log4j/JSONEventLayoutV0.java index b29e3fe..48d24f4 100644 --- a/src/main/java/net/logstash/log4j/JSONEventLayoutV0.java +++ b/src/main/java/net/logstash/log4j/JSONEventLayoutV0.java @@ -9,12 +9,17 @@ import org.apache.log4j.spi.LoggingEvent; import org.apache.log4j.spi.ThrowableInformation; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; public class JSONEventLayoutV0 extends Layout { + private boolean renderObjectFields = false; private boolean locationInfo = false; private String tags; @@ -44,16 +49,19 @@ public static String dateFormat(long timestamp) { * in the log messages. */ public JSONEventLayoutV0() { - this(true); + this(true, false); } /** * Creates a layout that optionally inserts location information into log messages. * * @param locationInfo whether or not to include location information in the log messages. + * @param renderObjectFields whether or not to render the fields of the message object into the json when an object is logged to log4j. + * Rendering the fields is done using reflection and incurs a performance cost */ - public JSONEventLayoutV0(boolean locationInfo) { + public JSONEventLayoutV0(boolean locationInfo, boolean renderObjectFields) { this.locationInfo = locationInfo; + this.renderObjectFields = renderObjectFields; } public String format(LoggingEvent loggingEvent) { @@ -70,6 +78,13 @@ public String format(LoggingEvent loggingEvent) { logstashEvent.put("@message", loggingEvent.getRenderedMessage()); logstashEvent.put("@timestamp", dateFormat(timestamp)); + if (renderObjectFields) { + Object messageObj = loggingEvent.getMessage(); + if (messageObj instanceof Serializable && !(messageObj instanceof String)) { + addObjectFieldData(messageObj); + } + } + if (loggingEvent.getThrowableInformation() != null) { final ThrowableInformation throwableInformation = loggingEvent.getThrowableInformation(); if (throwableInformation.getThrowable().getClass().getCanonicalName() != null) { @@ -103,6 +118,32 @@ public String format(LoggingEvent loggingEvent) { return logstashEvent.toString() + "\n"; } + private void addObjectFieldData(Object messageObj) { + Field[] fields = messageObj.getClass().getDeclaredFields(); + Object value; + + for(Field f : fields) { + try { + value = f.get(messageObj); + if (value != null) fieldData.put(f.getName(), value); + } catch (IllegalAccessException e) { + } + } + Method[] methods = messageObj.getClass().getDeclaredMethods(); + for(Method m : methods) + { + if(m.getName().startsWith("get")) + { + try { + value = m.invoke(messageObj); + if (value != null) fieldData.put(m.getName().substring(3), value); + } catch (IllegalAccessException e) { + } catch (InvocationTargetException e) { + } + } + } + } + public boolean ignoresThrowable() { return ignoreThrowable; } @@ -125,6 +166,19 @@ public void setLocationInfo(boolean locationInfo) { this.locationInfo = locationInfo; } + /** + * Set whether or not to render the fields of the message object into the json when an object is logged to log4j. + * Rendering the fields is done using reflection and incurs a performance cost + * @param renderObjectFields + */ + public void setRenderObjectFields(boolean renderObjectFields) { + this.renderObjectFields = renderObjectFields; + } + + public boolean getRenderObjectFields() { + return renderObjectFields; + } + public void activateOptions() { activeIgnoreThrowable = ignoreThrowable; } diff --git a/src/main/java/net/logstash/log4j/JSONEventLayoutV1.java b/src/main/java/net/logstash/log4j/JSONEventLayoutV1.java index aaf3228..4f7e806 100644 --- a/src/main/java/net/logstash/log4j/JSONEventLayoutV1.java +++ b/src/main/java/net/logstash/log4j/JSONEventLayoutV1.java @@ -10,12 +10,17 @@ import org.apache.log4j.spi.LoggingEvent; import org.apache.log4j.spi.ThrowableInformation; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; public class JSONEventLayoutV1 extends Layout { + private boolean renderObjectFields = false; private boolean locationInfo = false; private String customUserFields; @@ -47,15 +52,19 @@ public static String dateFormat(long timestamp) { * in the log messages. */ public JSONEventLayoutV1() { - this(true); + this(true, false); } /** * Creates a layout that optionally inserts location information into log messages. * * @param locationInfo whether or not to include location information in the log messages. + * @param renderObjectFields whether or not to render the fields of the message object into the json when an object is logged to log4j. + * Rendering the fields is done using reflection and incurs a performance cost + */ - public JSONEventLayoutV1(boolean locationInfo) { + public JSONEventLayoutV1(boolean locationInfo, boolean renderObjectFields) { + this.renderObjectFields = renderObjectFields; this.locationInfo = locationInfo; } @@ -105,6 +114,13 @@ public String format(LoggingEvent loggingEvent) { logstashEvent.put("source_host", hostname); logstashEvent.put("message", loggingEvent.getRenderedMessage()); + if (renderObjectFields) { + Object messageObject = loggingEvent.getMessage(); + if (messageObject instanceof Serializable && ! (messageObject instanceof String)) { + addObjectFieldData(messageObject); + } + } + if (loggingEvent.getThrowableInformation() != null) { final ThrowableInformation throwableInformation = loggingEvent.getThrowableInformation(); if (throwableInformation.getThrowable().getClass().getCanonicalName() != null) { @@ -159,6 +175,19 @@ public void setLocationInfo(boolean locationInfo) { this.locationInfo = locationInfo; } + /** + * Set whether or not to render the fields of the message object into the json when an object is logged to log4j. + * Rendering the fields is done using reflection and incurs a performance cost + * @param renderObjectFields + */ + public void setRenderObjectFields(boolean renderObjectFields) { + this.renderObjectFields = renderObjectFields; + } + + public boolean getRenderObjectFields() { + return renderObjectFields; + } + public String getUserFields() { return customUserFields; } public void setUserFields(String userFields) { this.customUserFields = userFields; } @@ -184,4 +213,27 @@ private void addEventData(String keyname, Object keyval) { logstashEvent.put(keyname, keyval); } } + + private void addObjectFieldData(Object messageObj) { + Field[] fields = messageObj.getClass().getDeclaredFields(); + + for(Field f : fields) { + try { + addEventData(f.getName(), f.get(messageObj)); + } catch (IllegalAccessException e) { + } + } + Method[] methods = messageObj.getClass().getDeclaredMethods(); + for(Method m : methods) + { + if(m.getName().startsWith("get")) + { + try { + addEventData(m.getName().substring(3), m.invoke(messageObj)); + } catch (IllegalAccessException e) { + } catch (InvocationTargetException e) { + } + } + } + } } diff --git a/src/test/java/net/logstash/log4j/JSONEventLayoutV0Test.java b/src/test/java/net/logstash/log4j/JSONEventLayoutV0Test.java index 969ccc7..96a6d07 100644 --- a/src/test/java/net/logstash/log4j/JSONEventLayoutV0Test.java +++ b/src/test/java/net/logstash/log4j/JSONEventLayoutV0Test.java @@ -12,6 +12,8 @@ import org.junit.Ignore; import org.junit.Test; +import java.io.Serializable; + /** * Created with IntelliJ IDEA. * User: jvincent @@ -158,6 +160,25 @@ public void testJSONEventHasThreadName() { Assert.assertNotNull("ThreadName value is missing", atFields.get("threadName")); } + @Test + public void testMessageObjectRendering() { + JSONEventLayoutV0 layout = (JSONEventLayoutV0) appender.getLayout(); + boolean prevRenderObjectFields = layout.getRenderObjectFields(); + layout.setRenderObjectFields(true); + logger.info(new Serializable() { + String test = "TEST"; + int testNum = 1123; + }); + String message = appender.getMessages()[0]; + Object obj = JSONValue.parse(message); + JSONObject jsonObject = (JSONObject) obj; + JSONObject atFields = (JSONObject) jsonObject.get("@fields"); + Assert.assertTrue(atFields.containsKey("testNum")); + Assert.assertTrue(atFields.get("testNum") instanceof Integer); + Assert.assertEquals(atFields.get("testNum"), 1123); + layout.setRenderObjectFields(prevRenderObjectFields); + } + @Test public void testJSONEventLayoutNoLocationInfo() { JSONEventLayoutV0 layout = (JSONEventLayoutV0) appender.getLayout(); diff --git a/src/test/java/net/logstash/log4j/JSONEventLayoutV1Test.java b/src/test/java/net/logstash/log4j/JSONEventLayoutV1Test.java index 96ad821..be92e07 100644 --- a/src/test/java/net/logstash/log4j/JSONEventLayoutV1Test.java +++ b/src/test/java/net/logstash/log4j/JSONEventLayoutV1Test.java @@ -3,15 +3,16 @@ import junit.framework.Assert; import net.minidev.json.JSONObject; import net.minidev.json.JSONValue; -import org.apache.log4j.*; -import org.apache.log4j.or.ObjectRenderer; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.apache.log4j.MDC; +import org.apache.log4j.NDC; import org.junit.After; -import org.junit.Before; -import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; +import java.io.Serializable; import java.util.HashMap; /** @@ -69,7 +70,7 @@ public void testJSONEventLayoutHasUserFieldsFromProps() { Assert.assertTrue("Event is not valid JSON", JSONValue.isValidJsonStrict(message)); Object obj = JSONValue.parse(message); JSONObject jsonObject = (JSONObject) obj; - Assert.assertTrue("Event does not contain field 'field1'" , jsonObject.containsKey("field1")); + Assert.assertTrue("Event does not contain field 'field1'", jsonObject.containsKey("field1")); Assert.assertEquals("Event does not contain value 'value1'", "propval1", jsonObject.get("field1")); System.clearProperty(JSONEventLayoutV1.ADDITIONAL_DATA_PROPERTY); } @@ -125,7 +126,7 @@ public void testJSONEventLayoutUserFieldsPropOverride() { Assert.assertTrue("Event is not valid JSON", JSONValue.isValidJsonStrict(message)); Object obj = JSONValue.parse(message); JSONObject jsonObject = (JSONObject) obj; - Assert.assertTrue("Event does not contain field 'field1'" , jsonObject.containsKey("field1")); + Assert.assertTrue("Event does not contain field 'field1'", jsonObject.containsKey("field1")); Assert.assertEquals("Event does not contain value 'propval1'", "propval1", jsonObject.get("field1")); layout.setUserFields(prevUserData); @@ -144,6 +145,24 @@ public void testJSONEventLayoutHasKeys() { } } + @Test + public void testMessageObjectRendering() { + JSONEventLayoutV1 layout = (JSONEventLayoutV1) appender.getLayout(); + boolean prevRenderObjectFields = layout.getRenderObjectFields(); + layout.setRenderObjectFields(true); + logger.info(new Serializable() { + String test = "TEST"; + int testNum = 1123; + }); + String message = appender.getMessages()[0]; + Object obj = JSONValue.parse(message); + JSONObject jsonObject = (JSONObject) obj; + Assert.assertTrue(jsonObject.containsKey("testNum")); + Assert.assertTrue(jsonObject.get("testNum") instanceof Integer); + Assert.assertEquals(jsonObject.get("testNum"), 1123); + layout.setRenderObjectFields(prevRenderObjectFields); + } + @Test public void testJSONEventLayoutHasNDC() { String ndcData = new String("json-layout-test"); @@ -165,14 +184,14 @@ public void testJSONEventLayoutHasMDC() { JSONObject jsonObject = (JSONObject) obj; JSONObject mdc = (JSONObject) jsonObject.get("mdc"); - Assert.assertEquals("MDC is wrong","bar", mdc.get("foo")); + Assert.assertEquals("MDC is wrong", "bar", mdc.get("foo")); } @Test public void testJSONEventLayoutHasNestedMDC() { HashMap nestedMdc = new HashMap(); - nestedMdc.put("bar","baz"); - MDC.put("foo",nestedMdc); + nestedMdc.put("bar", "baz"); + MDC.put("foo", nestedMdc); logger.warn("I should have nested MDC data in my log"); String message = appender.getMessages()[0]; Object obj = JSONValue.parse(message);