diff --git a/FORK.md b/FORK.md new file mode 100644 index 0000000..a07c9b0 --- /dev/null +++ b/FORK.md @@ -0,0 +1,5 @@ +Simple refactoring of the JSONEventLayoutV1 format method to separate the creation of the Logstash event and the formatting operation. +The benefit is subclasses of JSONEventLayoutV1 can create the Logstash event, add any specific details to the event, and +then get the formatted string. + +Also, added the ability to customize the output field names. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 1dc589b..f8b4ca4 100644 --- a/pom.xml +++ b/pom.xml @@ -66,8 +66,8 @@ maven-compiler-plugin 2.3.2 - 1.5 - 1.5 + 1.7 + 1.7 diff --git a/src/main/java/net/logstash/log4j/IJSONEventLayout.java b/src/main/java/net/logstash/log4j/IJSONEventLayout.java new file mode 100644 index 0000000..74554ab --- /dev/null +++ b/src/main/java/net/logstash/log4j/IJSONEventLayout.java @@ -0,0 +1,10 @@ +package net.logstash.log4j; + + +public interface IJSONEventLayout { + + public abstract String getUserFields(); + public abstract void setUserFields(String userFields); + public abstract boolean getLocationInfo(); + public abstract void setLocationInfo(boolean locationInfo); +} diff --git a/src/main/java/net/logstash/log4j/JSONEventLayoutV1.java b/src/main/java/net/logstash/log4j/JSONEventLayoutV1.java index aaf3228..c2c7cdc 100644 --- a/src/main/java/net/logstash/log4j/JSONEventLayoutV1.java +++ b/src/main/java/net/logstash/log4j/JSONEventLayoutV1.java @@ -14,7 +14,7 @@ import java.util.Map; import java.util.TimeZone; -public class JSONEventLayoutV1 extends Layout { +public class JSONEventLayoutV1 extends Layout implements IJSONEventLayout { private boolean locationInfo = false; private String customUserFields; @@ -60,6 +60,12 @@ public JSONEventLayoutV1(boolean locationInfo) { } public String format(LoggingEvent loggingEvent) { + JSONObject lsEvent = createLogstashEvent(loggingEvent); + + return lsEvent.toString() + "\n"; + } + + protected JSONObject createLogstashEvent(LoggingEvent loggingEvent) { threadName = loggingEvent.getThreadName(); timestamp = loggingEvent.getTimeStamp(); exceptionInformation = new HashMap(); @@ -82,7 +88,7 @@ public String format(LoggingEvent loggingEvent) { */ if (getUserFields() != null) { String userFlds = getUserFields(); - LogLog.debug("["+whoami+"] Got user data from log4j property: "+ userFlds); + LogLog.debug("[" + whoami + "] Got user data from log4j property: " + userFlds); addUserFields(userFlds); } @@ -134,7 +140,7 @@ public String format(LoggingEvent loggingEvent) { addEventData("level", loggingEvent.getLevel().toString()); addEventData("thread_name", threadName); - return logstashEvent.toString() + "\n"; + return logstashEvent; } public boolean ignoresThrowable() { @@ -146,6 +152,7 @@ public boolean ignoresThrowable() { * * @return true if location information is included in log messages, false otherwise. */ + @Override public boolean getLocationInfo() { return locationInfo; } @@ -155,11 +162,15 @@ public boolean getLocationInfo() { * * @param locationInfo true if location information should be included, false otherwise. */ + @Override public void setLocationInfo(boolean locationInfo) { this.locationInfo = locationInfo; } + @Override public String getUserFields() { return customUserFields; } + + @Override public void setUserFields(String userFields) { this.customUserFields = userFields; } public void activateOptions() { @@ -179,6 +190,7 @@ private void addUserFields(String data) { } } } + private void addEventData(String keyname, Object keyval) { if (null != keyval) { logstashEvent.put(keyname, keyval); diff --git a/src/main/java/net/logstash/log4j/JSONEventLayoutV2.java b/src/main/java/net/logstash/log4j/JSONEventLayoutV2.java new file mode 100644 index 0000000..14acce2 --- /dev/null +++ b/src/main/java/net/logstash/log4j/JSONEventLayoutV2.java @@ -0,0 +1,271 @@ +package net.logstash.log4j; + + +import net.logstash.log4j.data.HostData; +import net.logstash.log4j.fieldnames.LogstashFieldNames; +import net.minidev.json.JSONObject; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.time.FastDateFormat; +import org.apache.log4j.Layout; +import org.apache.log4j.helpers.LogLog; +import org.apache.log4j.helpers.OnlyOnceErrorHandler; +import org.apache.log4j.spi.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; + + +/** + * Log4j JSON Layout providing mutable output field names + *

+ * Based upon the similar field name solution for logback found here + * https://github.com/logstash/logstash-logback-encoder + *

+ * Also allows for "flattening" of the output structure, removing any nested structures + */ +public class JSONEventLayoutV2 extends Layout implements IJSONEventLayout { + + protected ErrorHandler errorHandler = new OnlyOnceErrorHandler(); + + private LogstashFieldNames fieldNames = new LogstashFieldNames(); + private boolean locationInfo = true; + private String customUserFields; + private boolean ignoreThrowable = false; + private String hostname = new HostData().getHostName(); + private static Integer version = 1; + + public static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + public static final FastDateFormat ISO_DATETIME_TIME_ZONE_FORMAT_WITH_MILLIS = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", UTC); + public static final String ADDITIONAL_DATA_PROPERTY = "net.logstash.log4j.JSONEventLayoutV2.UserFields"; + + public static String dateFormat(long timestamp) { + return ISO_DATETIME_TIME_ZONE_FORMAT_WITH_MILLIS.format(timestamp); + } + + public JSONEventLayoutV2() { + this(true); + } + + public JSONEventLayoutV2(boolean isLocationInfo) { + locationInfo = isLocationInfo; + } + + public String format(LoggingEvent loggingEvent) { + JSONObject lsEvent = createLogstashEvent(loggingEvent); + + return lsEvent.toString() + "\n"; + } + + protected JSONObject createLogstashEvent(LoggingEvent loggingEvent) { + String threadName = loggingEvent.getThreadName(); + Long timestamp = loggingEvent.getTimeStamp(); + + Map mdc = loggingEvent.getProperties(); + String ndc = loggingEvent.getNDC(); + + JSONObject logstashEvent = new JSONObject(); + String whoami = this.getClass().getSimpleName(); + + /** + * All v1 of the event format requires is + * "@timestamp" and "@version" + * Every other field is arbitrary + */ + addEventData(logstashEvent, fieldNames.getVersion(), version); + addEventData(logstashEvent, fieldNames.getTimestamp(), dateFormat(timestamp)); + + /** + * Extract and add fields from log4j config, if defined + */ + if (getUserFields() != null) { + String userFlds = getUserFields(); + LogLog.debug("[" + whoami + "] Got user data from log4j property: " + userFlds); + addUserFields(logstashEvent, userFlds); + } + + /** + * Extract fields from system properties, if defined + * Note that CLI props will override conflicts with log4j config + */ + if (System.getProperty(ADDITIONAL_DATA_PROPERTY) != null) { + if (getUserFields() != null) { + LogLog.warn("[" + whoami + "] Loading UserFields from command-line. This will override any UserFields set in the log4j configuration file"); + } + String userFieldsProperty = System.getProperty(ADDITIONAL_DATA_PROPERTY); + LogLog.debug("[" + whoami + "] Got user data from system property: " + userFieldsProperty); + addUserFields(logstashEvent, userFieldsProperty); + } + + /** + * Now we start injecting our own stuff. + */ + addEventData(logstashEvent, fieldNames.getHostName(), hostname); + addEventData(logstashEvent, fieldNames.getMessage(), loggingEvent.getRenderedMessage()); + + if (loggingEvent.getThrowableInformation() != null) { + final ThrowableInformation throwableInformation = loggingEvent.getThrowableInformation(); + + HashMap exceptionInformation = new HashMap(); + if (throwableInformation.getThrowable().getClass().getCanonicalName() != null) { + exceptionInformation.put(fieldNames.getExceptionClass(), throwableInformation.getThrowable().getClass().getCanonicalName()); + } + if (throwableInformation.getThrowable().getMessage() != null) { + exceptionInformation.put(fieldNames.getExceptionMessage(), throwableInformation.getThrowable().getMessage()); + } + if (throwableInformation.getThrowableStrRep() != null) { + String stackTrace = StringUtils.join(throwableInformation.getThrowableStrRep(), "\n"); + exceptionInformation.put(fieldNames.getStackTrace(), stackTrace); + } + if (fieldNames.getException() != null) { + addEventData(logstashEvent, fieldNames.getException(), exceptionInformation); + } else { + addEventData(logstashEvent, exceptionInformation); + } + + } + + if (getLocationInfo()) { + LocationInfo info = loggingEvent.getLocationInformation(); + Map locMap = new HashMap(); + + addEventData(locMap, fieldNames.getCallerFile(), info.getFileName()); + addEventData(locMap, fieldNames.getCallerLine(), info.getLineNumber()); + addEventData(locMap, fieldNames.getCallerClass(), info.getClassName()); + addEventData(locMap, fieldNames.getCallerMethod(), info.getMethodName()); + + if (fieldNames.getCaller() != null) { + addEventData(logstashEvent, fieldNames.getCaller(), locMap); + } else { + addEventData(logstashEvent, locMap); + } + + /* addEventData(logstashEvent, fieldNames.getCallerFile(), info.getFileName()); + addEventData(logstashEvent, fieldNames.getCallerLine(), info.getLineNumber()); + addEventData(logstashEvent, fieldNames.getCallerClass(), info.getClassName()); + addEventData(logstashEvent, fieldNames.getCallerMethod(), info.getMethodName());*/ + } + + addEventData(logstashEvent, fieldNames.getLogger(), loggingEvent.getLoggerName()); + + + if (fieldNames.getMdc() != null) { + addEventData(logstashEvent, fieldNames.getMdc(), mdc); + + } else { + addEventData(logstashEvent, mdc); + } + + + addEventData(logstashEvent, fieldNames.getNdc(), ndc); + addEventData(logstashEvent, fieldNames.getLevel(), loggingEvent.getLevel().toString()); + addEventData(logstashEvent, fieldNames.getThread(), threadName); + + return logstashEvent; + } + + private void addEventData(JSONObject logstashEvent, Map map) { + Set entries = map.entrySet(); + for (Map.Entry entry : entries) { + String key = entry.getKey().toString(); + Object value = entry.getValue(); + addEventData(logstashEvent, key, value); + } + } + + private void addEventData(JSONObject logstashEvent, String keyName, Object keyVal) { + if (keyVal != null && keyName != null) { + logstashEvent.put(keyName, keyVal); + } + } + + private void addEventData(Map map, String keyName, Object keyVal) { + if (keyVal != null && keyName != null) { + map.put(keyName, keyVal); + } + } + + //TODO: This should be just using a JSON string instead of comma separated "name:value" pairs + private void addUserFields(JSONObject logstashEvent, String data) { + if (data != null) { + String[] pairs = data.split(","); + for (String pair : pairs) { + String[] userField = pair.split(":", 2); + if (userField[0] != null) { + String key = userField[0]; + String val = userField[1]; + addEventData(logstashEvent, key, val); + } + } + } + } + + + public LogstashFieldNames getFieldNames() { + return fieldNames; + } + + public void setFieldNames(LogstashFieldNames fieldNames) { + this.fieldNames = fieldNames; + } + + public void setFieldsClassName(String fieldsClassName) { + try { + Class clazz = Class.forName(fieldsClassName); + Object o = clazz.newInstance(); + if (o instanceof LogstashFieldNames) { + setFieldNames((LogstashFieldNames) o); + } else { + errorHandler.error("Class for " + fieldsClassName + " is not a valid type for defining field names. Will use default field names"); + } + + } catch (Exception e) { + errorHandler.error("Failed to load class for FieldNames " + fieldsClassName, e, ErrorCode.GENERIC_FAILURE); + } + } + + public void setFlattenOutput(boolean isFlatten) { + fieldNames.setFlattenOutput(isFlatten); + } + + @Override + public boolean ignoresThrowable() { + return ignoreThrowable; + } + + /** + * Query whether log messages include location information. + * + * @return true if location information is included in log messages, false otherwise. + */ + @Override + public boolean getLocationInfo() { + return locationInfo; + } + + /** + * Set whether log messages should include location information. + * + * @param locationInfo true if location information should be included, false otherwise. + */ + @Override + public void setLocationInfo(boolean locationInfo) { + this.locationInfo = locationInfo; + } + + @Override + public String getUserFields() { + return customUserFields; + } + + @Override + public void setUserFields(String userFields) { + this.customUserFields = userFields; + } + + public void activateOptions() { + + //activeIgnoreThrowable = ignoreThrowable; + } +} diff --git a/src/main/java/net/logstash/log4j/fieldnames/LogstashCommonFieldNames.java b/src/main/java/net/logstash/log4j/fieldnames/LogstashCommonFieldNames.java new file mode 100644 index 0000000..fc2ebad --- /dev/null +++ b/src/main/java/net/logstash/log4j/fieldnames/LogstashCommonFieldNames.java @@ -0,0 +1,60 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.logstash.log4j.fieldnames; + +import java.util.ArrayList; +import java.util.List; + +/** + * Common field names + */ +public abstract class LogstashCommonFieldNames { + private String timestamp = "@timestamp"; + private String version = "@version"; + private String message = "message"; + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public List listCommonNames() { + List namesList = new ArrayList<>(); + + namesList.add(getTimestamp()); + namesList.add(getMessage()); + namesList.add(getVersion()); + + return namesList; + } +} diff --git a/src/main/java/net/logstash/log4j/fieldnames/LogstashFieldNames.java b/src/main/java/net/logstash/log4j/fieldnames/LogstashFieldNames.java new file mode 100644 index 0000000..4a65e8b --- /dev/null +++ b/src/main/java/net/logstash/log4j/fieldnames/LogstashFieldNames.java @@ -0,0 +1,266 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.logstash.log4j.fieldnames; + +import java.util.ArrayList; +import java.util.List; + +/** + * Names of standard fields that appear in the JSON output. + * + * Based upn the similar solution for logback + * https://github.com/logstash/logstash-logback-encoder/blob/master/src/main/java/net/logstash/logback/fieldnames/LogstashFieldNames.java + */ +public class LogstashFieldNames extends LogstashCommonFieldNames { + + private String logger = "loggername"; + private String thread = "threadname"; + private String level = "level"; + //private String levelValue = "levelvalue"; + + private String callerClass = "classname"; + private String callerMethod = "methodname"; + private String callerFile = "filename"; + private String callerLine = "linenumber"; + private String stackTrace = "stacktrace"; + private String tags = "tags"; + private String ndc = "ndc"; + + private String hostName = "hostname"; + private String exceptionClass = "exceptionclass"; + private String exceptionMessage = "exceptionmessage"; + + //IF we populate these, the output will create nested data for these names + private String exception; + private String caller; + private String mdc; + private String context; + + public static final String EXCEPTION_DEFAULT = "exception"; + public static final String CALLER_DEFAULT = "caller"; + public static final String MDC_DEFAULT = "mdc"; + public static final String CONTEXT_DEFAULT = "context"; + + + public void setFlattenOutput(Boolean isFlatten) { + if (isFlatten) { + setException(null); + setCaller(null); + setMdc(null); + setContext(null); + } else { + String exception = getException() != null ? getException() : EXCEPTION_DEFAULT; + String caller = getCaller() != null ? getCaller() : CALLER_DEFAULT; + String mdc = getMdc() != null ? getMdc() : MDC_DEFAULT; + String context = getContext() != null ? getContext() : CONTEXT_DEFAULT; + setException(exception); + setCaller(caller); + setMdc(mdc); + setContext(context); + } + } + + public String getLogger() { + return logger; + } + + public void setLogger(String logger) { + this.logger = logger; + } + + public String getThread() { + return thread; + } + + public void setThread(String thread) { + this.thread = thread; + } + + public String getLevel() { + return level; + } + + public void setLevel(String level) { + this.level = level; + } + + /** + public String getLevelValue() { + return levelValue; + } + + public void setLevelValue(String levelValue) { + this.levelValue = levelValue; + } + **/ + /** + * The name of the caller object field. + *

+ * If this returns null, then the caller data fields will be written inline at the root level of the JSON event output (e.g. as a sibling to all the other fields in this class). + *

+ * If this returns non-null, then the caller data fields will be written inside an object with field name returned by this method + */ + public String getCaller() { + return caller; + } + + public void setCaller(String caller) { + this.caller = caller; + } + + public String getCallerClass() { + return callerClass; + } + + public void setCallerClass(String callerClass) { + this.callerClass = callerClass; + } + + public String getCallerMethod() { + return callerMethod; + } + + public void setCallerMethod(String callerMethod) { + this.callerMethod = callerMethod; + } + + public String getCallerFile() { + return callerFile; + } + + public void setCallerFile(String callerFile) { + this.callerFile = callerFile; + } + + public String getCallerLine() { + return callerLine; + } + + public void setCallerLine(String callerLine) { + this.callerLine = callerLine; + } + + public String getStackTrace() { + return stackTrace; + } + + public void setStackTrace(String stackTrace) { + this.stackTrace = stackTrace; + } + + public String getTags() { + return tags; + } + + public void setTags(String tags) { + this.tags = tags; + } + + /** + * The name of the mdc object field. + *

+ * If this returns null, then the mdc fields will be written inline at the root level of the JSON event output (e.g. as a sibling to all the other fields in this class). + *

+ * If this returns non-null, then the mdc fields will be written inside an object with field name returned by this method + */ + public String getMdc() { + return mdc; + } + + public void setMdc(String mdc) { + this.mdc = mdc; + } + + /** + * The name of the context object field. + *

+ * If this returns null, then the context fields will be written inline at the root level of the JSON event output (e.g. as a sibling to all the other fields in this class). + *

+ * If this returns non-null, then the context fields will be written inside an object with field name returned by this method + */ + public String getContext() { + return context; + } + + public void setContext(String context) { + this.context = context; + } + + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + + public String getExceptionClass() { + return exceptionClass; + } + + public void setExceptionClass(String exceptionClass) { + this.exceptionClass = exceptionClass; + } + + public String getExceptionMessage() { + return exceptionMessage; + } + + public void setExceptionMessage(String exceptionMessage) { + this.exceptionMessage = exceptionMessage; + } + + /** + * The name of the exception object field. + *

+ * If this returns null, then the context fields will be written inline at the root level of the JSON event output (e.g. as a sibling to all the other fields in this class). + *

+ * If this returns non-null, then the context fields will be written inside an object with field name returned by this method + */ + public String getException() { + return exception; + } + + public void setException(String exception) { + this.exception = exception; + } + + + public String getNdc() { + return ndc; + } + + public void setNdc(String ndc) { + this.ndc = ndc; + } + + + public List listNames() { + List namesList = new ArrayList<>(); + + namesList.addAll(super.listCommonNames()); + namesList.add(getLogger()); + namesList.add(getThread()); + namesList.add(getLevel()); + namesList.add(getCallerClass()); + namesList.add(getCallerMethod()); + namesList.add(getCallerFile()); + namesList.add(getCallerLine()); + namesList.add(getStackTrace()); + namesList.add(getTags()); + namesList.add(getNdc()); + + return namesList; + } +} diff --git a/src/test/java/net/logstash/log4j/JSONEventLayoutV1Test.java b/src/test/java/net/logstash/log4j/JSONEventLayoutV1Test.java index 96ad821..22aab20 100644 --- a/src/test/java/net/logstash/log4j/JSONEventLayoutV1Test.java +++ b/src/test/java/net/logstash/log4j/JSONEventLayoutV1Test.java @@ -1,18 +1,29 @@ package net.logstash.log4j; import junit.framework.Assert; +import net.logstash.log4j.fieldnames.LogstashCommonFieldNames; +import net.logstash.log4j.fieldnames.LogstashFieldNames; import net.minidev.json.JSONObject; import net.minidev.json.JSONValue; import org.apache.log4j.*; -import org.apache.log4j.or.ObjectRenderer; 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 org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; + + +/* TODO: I made modifications so this test class would cover both V1 and V2 -- The intent being to use Junit's "Parameterized" + TODO: functionality to run the full suite of tests, once for each layout. Unfortunately, to keep true to the original tests + TODO: this required a bunch of "instanceof" conditionals - not ideal. So, going forward, it would be preferable to + TODO: refactor and clean this up. Maybe it makes more sense to just separate the test classes into V1 and V2 + */ /** * Created with IntelliJ IDEA. @@ -21,14 +32,14 @@ * Time: 12:07 AM * To change this template use File | Settings | File Templates. */ +@RunWith(Parameterized.class) public class JSONEventLayoutV1Test { - static Logger logger; - static MockAppenderV1 appender; - static MockAppenderV1 userFieldsAppender; - static JSONEventLayoutV1 userFieldsLayout; - static final String userFieldsSingle = new String("field1:value1"); - static final String userFieldsMulti = new String("field2:value2,field3:value3"); - static final String userFieldsSingleProperty = new String("field1:propval1"); + Logger logger; + MockAppenderV1 appender; + + static final String userFieldsSingle = "field1:value1"; + static final String userFieldsMulti = "field2:value2,field3:value3"; + static final String userFieldsSingleProperty = "field1:propval1"; static final String[] logstashFields = new String[]{ "message", @@ -37,9 +48,28 @@ public class JSONEventLayoutV1Test { "@version" }; - @BeforeClass - public static void setupTestAppender() { - appender = new MockAppenderV1(new JSONEventLayoutV1()); + private Layout jsonLayout; + + @Parameterized.Parameters + public static java.util.Collection data() { + + Layout[] layout1Array = new Layout[]{new JSONEventLayoutV1()}; + Layout[] layout2Array = new Layout[]{new JSONEventLayoutV2()}; + List list = new ArrayList(); + list.add(layout1Array); + list.add(layout2Array); + return list; + + } + + public JSONEventLayoutV1Test(Layout layout) { + jsonLayout = layout; + } + + @Before + public void setupTestAppender() { + + appender = new MockAppenderV1(jsonLayout); logger = Logger.getRootLogger(); appender.setThreshold(Level.TRACE); appender.setName("mockappenderv1"); @@ -63,20 +93,25 @@ public void testJSONEventLayoutIsJSON() { @Test public void testJSONEventLayoutHasUserFieldsFromProps() { - System.setProperty(JSONEventLayoutV1.ADDITIONAL_DATA_PROPERTY, userFieldsSingleProperty); + String additionalDataProperty = JSONEventLayoutV1.ADDITIONAL_DATA_PROPERTY; + if (appender.getLayout() instanceof JSONEventLayoutV2) { + JSONEventLayoutV2 layout = (JSONEventLayoutV2) appender.getLayout(); + additionalDataProperty = layout.ADDITIONAL_DATA_PROPERTY; + } + System.setProperty(additionalDataProperty, userFieldsSingleProperty); logger.info("this is an info message with user fields"); String message = appender.getMessages()[0]; 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); + System.clearProperty(additionalDataProperty); } @Test public void testJSONEventLayoutHasUserFieldsFromConfig() { - JSONEventLayoutV1 layout = (JSONEventLayoutV1) appender.getLayout(); + IJSONEventLayout layout = getJsonEventLayout(); String prevUserData = layout.getUserFields(); layout.setUserFields(userFieldsSingle); @@ -85,15 +120,16 @@ public void testJSONEventLayoutHasUserFieldsFromConfig() { 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'", "value1", jsonObject.get("field1")); layout.setUserFields(prevUserData); } + @Test public void testJSONEventLayoutUserFieldsMulti() { - JSONEventLayoutV1 layout = (JSONEventLayoutV1) appender.getLayout(); + IJSONEventLayout layout = getJsonEventLayout(); String prevUserData = layout.getUserFields(); layout.setUserFields(userFieldsMulti); @@ -102,9 +138,9 @@ public void testJSONEventLayoutUserFieldsMulti() { 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 'field2'" , jsonObject.containsKey("field2")); + Assert.assertTrue("Event does not contain field 'field2'", jsonObject.containsKey("field2")); Assert.assertEquals("Event does not contain value 'value2'", "value2", jsonObject.get("field2")); - Assert.assertTrue("Event does not contain field 'field3'" , jsonObject.containsKey("field3")); + Assert.assertTrue("Event does not contain field 'field3'", jsonObject.containsKey("field3")); Assert.assertEquals("Event does not contain value 'value3'", "value3", jsonObject.get("field3")); layout.setUserFields(prevUserData); @@ -112,11 +148,16 @@ public void testJSONEventLayoutUserFieldsMulti() { @Test public void testJSONEventLayoutUserFieldsPropOverride() { + String additionalDataProperty = JSONEventLayoutV1.ADDITIONAL_DATA_PROPERTY; + if (appender.getLayout() instanceof JSONEventLayoutV2) { + JSONEventLayoutV2 layout = (JSONEventLayoutV2) appender.getLayout(); + additionalDataProperty = layout.ADDITIONAL_DATA_PROPERTY; + } // set the property first - System.setProperty(JSONEventLayoutV1.ADDITIONAL_DATA_PROPERTY, userFieldsSingleProperty); + System.setProperty(additionalDataProperty, userFieldsSingleProperty); // set the config values - JSONEventLayoutV1 layout = (JSONEventLayoutV1) appender.getLayout(); + IJSONEventLayout layout = getJsonEventLayout(); String prevUserData = layout.getUserFields(); layout.setUserFields(userFieldsSingle); @@ -125,11 +166,11 @@ 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); - System.clearProperty(JSONEventLayoutV1.ADDITIONAL_DATA_PROPERTY); + System.clearProperty(additionalDataProperty); } @@ -139,7 +180,15 @@ public void testJSONEventLayoutHasKeys() { String message = appender.getMessages()[0]; Object obj = JSONValue.parse(message); JSONObject jsonObject = (JSONObject) obj; - for (String fieldName : logstashFields) { + + List fieldNames = Arrays.asList(logstashFields); + if (appender.getLayout() instanceof JSONEventLayoutV2) { + JSONEventLayoutV2 layout = (JSONEventLayoutV2) appender.getLayout(); + LogstashCommonFieldNames commonFieldNames = layout.getFieldNames(); + fieldNames = commonFieldNames.listCommonNames(); + } + + for (String fieldName : fieldNames) { Assert.assertTrue("Event does not contain field: " + fieldName, jsonObject.containsKey(fieldName)); } } @@ -158,30 +207,45 @@ public void testJSONEventLayoutHasNDC() { @Test public void testJSONEventLayoutHasMDC() { + MDC.put("foo", "bar"); logger.warn("I should have MDC data in my log"); String message = appender.getMessages()[0]; Object obj = JSONValue.parse(message); JSONObject jsonObject = (JSONObject) obj; - JSONObject mdc = (JSONObject) jsonObject.get("mdc"); - Assert.assertEquals("MDC is wrong","bar", mdc.get("foo")); + if (appender.getLayout() instanceof JSONEventLayoutV2) { + //flattened by default + Assert.assertEquals("MDC is wrong", "bar", jsonObject.get("foo")); + } else { + JSONObject mdc = (JSONObject) jsonObject.get("mdc"); + 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); JSONObject jsonObject = (JSONObject) obj; - JSONObject mdc = (JSONObject) jsonObject.get("mdc"); - JSONObject nested = (JSONObject) mdc.get("foo"); - Assert.assertTrue("Event is missing foo key", mdc.containsKey("foo")); - Assert.assertEquals("Nested MDC data is wrong", "baz", nested.get("bar")); + if (appender.getLayout() instanceof JSONEventLayoutV2) { + //flattened by default + Assert.assertTrue("Event is missing foo key", jsonObject.containsKey("foo")); + JSONObject nested = (JSONObject) jsonObject.get("foo"); + Assert.assertEquals("Nested MDC data is wrong", "baz", nested.get("bar")); + + } else { + + JSONObject mdc = (JSONObject) jsonObject.get("mdc"); + JSONObject nested = (JSONObject) mdc.get("foo"); + Assert.assertTrue("Event is missing foo key", mdc.containsKey("foo")); + Assert.assertEquals("Nested MDC data is wrong", "baz", nested.get("bar")); + } } @Test @@ -191,10 +255,17 @@ public void testJSONEventLayoutExceptions() { String message = appender.getMessages()[0]; Object obj = JSONValue.parse(message); JSONObject jsonObject = (JSONObject) obj; - JSONObject exceptionInformation = (JSONObject) jsonObject.get("exception"); - Assert.assertEquals("Exception class missing", "java.lang.IllegalArgumentException", exceptionInformation.get("exception_class")); - Assert.assertEquals("Exception exception message", exceptionMessage, exceptionInformation.get("exception_message")); + if (appender.getLayout() instanceof JSONEventLayoutV2) { + //flattened + JSONEventLayoutV2 layout = (JSONEventLayoutV2) appender.getLayout(); + Assert.assertEquals("Exception class missing", "java.lang.IllegalArgumentException", jsonObject.get(layout.getFieldNames().getExceptionClass())); + Assert.assertEquals("Exception exception message", exceptionMessage, jsonObject.get(layout.getFieldNames().getExceptionMessage())); + } else { + JSONObject exceptionInformation = (JSONObject) jsonObject.get("exception"); + Assert.assertEquals("Exception class missing", "java.lang.IllegalArgumentException", exceptionInformation.get("exception_class")); + Assert.assertEquals("Exception exception message", exceptionMessage, exceptionInformation.get("exception_message")); + } } @Test @@ -204,7 +275,13 @@ public void testJSONEventLayoutHasClassName() { Object obj = JSONValue.parse(message); JSONObject jsonObject = (JSONObject) obj; - Assert.assertEquals("Logged class does not match", this.getClass().getCanonicalName().toString(), jsonObject.get("class")); + String nameOfValueToGet = "class"; + if (appender.getLayout() instanceof JSONEventLayoutV2) { + JSONEventLayoutV2 layout = (JSONEventLayoutV2) appender.getLayout(); + nameOfValueToGet = layout.getFieldNames().getCallerClass(); + } + + Assert.assertEquals("Logged class does not match", this.getClass().getCanonicalName().toString(), jsonObject.get(nameOfValueToGet)); } @Test @@ -214,16 +291,30 @@ public void testJSONEventHasFileName() { Object obj = JSONValue.parse(message); JSONObject jsonObject = (JSONObject) obj; - Assert.assertNotNull("File value is missing", jsonObject.get("file")); + String nameOfValueToGet = "file"; + if (appender.getLayout() instanceof JSONEventLayoutV2) { + JSONEventLayoutV2 layout = (JSONEventLayoutV2) appender.getLayout(); + nameOfValueToGet = layout.getFieldNames().getCallerFile(); + } + + Assert.assertNotNull("File value is missing", jsonObject.get(nameOfValueToGet)); } + @Test public void testJSONEventHasLoggerName() { logger.warn("whoami"); String message = appender.getMessages()[0]; Object obj = JSONValue.parse(message); JSONObject jsonObject = (JSONObject) obj; - Assert.assertNotNull("LoggerName value is missing", jsonObject.get("logger_name")); + + String nameOfValueToGet = "logger_name"; + if (appender.getLayout() instanceof JSONEventLayoutV2) { + JSONEventLayoutV2 layout = (JSONEventLayoutV2) appender.getLayout(); + nameOfValueToGet = layout.getFieldNames().getLogger(); + } + + Assert.assertNotNull("LoggerName value is missing", jsonObject.get(nameOfValueToGet)); } @Test @@ -232,12 +323,19 @@ public void testJSONEventHasThreadName() { String message = appender.getMessages()[0]; Object obj = JSONValue.parse(message); JSONObject jsonObject = (JSONObject) obj; - Assert.assertNotNull("ThreadName value is missing", jsonObject.get("thread_name")); + + String nameOfValueToGet = "thread_name"; + if (appender.getLayout() instanceof JSONEventLayoutV2) { + JSONEventLayoutV2 layout = (JSONEventLayoutV2) appender.getLayout(); + nameOfValueToGet = layout.getFieldNames().getLogger(); + } + + Assert.assertNotNull("ThreadName value is missing", jsonObject.get(nameOfValueToGet)); } @Test public void testJSONEventLayoutNoLocationInfo() { - JSONEventLayoutV1 layout = (JSONEventLayoutV1) appender.getLayout(); + IJSONEventLayout layout = getJsonEventLayout(); boolean prevLocationInfo = layout.getLocationInfo(); layout.setLocationInfo(false); @@ -259,7 +357,7 @@ public void testJSONEventLayoutNoLocationInfo() { @Test @Ignore public void measureJSONEventLayoutLocationInfoPerformance() { - JSONEventLayoutV1 layout = (JSONEventLayoutV1) appender.getLayout(); + IJSONEventLayout layout = getJsonEventLayout(); boolean locationInfo = layout.getLocationInfo(); int iterations = 100000; long start, stop; @@ -289,6 +387,11 @@ public void measureJSONEventLayoutLocationInfoPerformance() { @Test public void testDateFormat() { long timestamp = 1364844991207L; - Assert.assertEquals("format does not produce expected output", "2013-04-01T19:36:31.207Z", JSONEventLayoutV1.dateFormat(timestamp)); + Assert.assertEquals("format does not produce expected output", "2013-04-01T19:36:31.207Z", JSONEventLayoutV2.dateFormat(timestamp)); + } + + protected IJSONEventLayout getJsonEventLayout() { + return (IJSONEventLayout) appender.getLayout(); } + } diff --git a/src/test/java/net/logstash/log4j/MockAppenderV1.java b/src/test/java/net/logstash/log4j/MockAppenderV1.java index 5ebe656..9ae9c3b 100644 --- a/src/test/java/net/logstash/log4j/MockAppenderV1.java +++ b/src/test/java/net/logstash/log4j/MockAppenderV1.java @@ -9,7 +9,7 @@ public class MockAppenderV1 extends AppenderSkeleton { - private static List messages = new ArrayList(); + private List messages = new ArrayList(); public MockAppenderV1(Layout layout){ this.layout = layout; @@ -27,7 +27,7 @@ public boolean requiresLayout(){ return true; } - public static String[] getMessages() { + public String[] getMessages() { return (String[]) messages.toArray(new String[messages.size()]); }