-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added JSON layout and Splunk appender
- Loading branch information
Showing
11 changed files
with
1,096 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
168 changes: 168 additions & 0 deletions
168
json/src/main/java/com/obsidiandynamics/log4jextras/json/JsonLayout.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
package com.obsidiandynamics.log4jextras.json; | ||
|
||
import java.text.*; | ||
import java.util.*; | ||
|
||
import org.apache.log4j.*; | ||
import org.apache.log4j.spi.*; | ||
|
||
import com.google.gson.*; | ||
|
||
/** | ||
* Layout for JSON logging.<p> | ||
* | ||
* Adapted from https://github.com/michaeltandy/log4j-json. | ||
*/ | ||
public final class JsonLayout extends Layout { | ||
private final Gson gson = new GsonBuilder().disableHtmlEscaping().create(); | ||
private final String hostname = getHostname().toLowerCase(); | ||
private final String username = System.getProperty("user.name").toLowerCase(); | ||
private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); | ||
|
||
private Level minimumLevelForSlowLogging = Level.ALL; | ||
private String mdcRoot; | ||
private List<String> mdcFieldsToLog = Collections.emptyList(); | ||
|
||
@Override | ||
public String format(LoggingEvent le) { | ||
final Map<String, Object> r = new LinkedHashMap<>(); | ||
r.put("timestamp", dateFormat.format(new Date(le.timeStamp))); | ||
r.put("host", hostname); | ||
r.put("user", username); | ||
r.put("level", le.getLevel().toString()); | ||
r.put("thread", le.getThreadName()); | ||
r.put("ndc",le.getNDC()); | ||
if (le.getLevel().isGreaterOrEqual(minimumLevelForSlowLogging)) { | ||
r.put("class", le.getLocationInformation().getClassName()); | ||
r.put("line", safeParseInt(le.getLocationInformation().getLineNumber())); | ||
r.put("method", le.getLocationInformation().getMethodName()); | ||
} | ||
r.put("message", safeToString(le.getMessage())); | ||
r.put("throwable", formatThrowable(le) ); | ||
|
||
for (String mdcKey : mdcFieldsToLog) { | ||
if (! r.containsKey(mdcKey)) { | ||
r.put(mdcKey, safeToString(le.getMDC(mdcKey))); | ||
} | ||
} | ||
|
||
if (mdcRoot != null) { | ||
final Object mdcValue = le.getMDC(mdcRoot); | ||
if (mdcValue != null) { | ||
final String[] fields = ((String) mdcValue).split(","); | ||
for (String field : fields) { | ||
final String trimmedField = field.trim(); | ||
r.put(trimmedField, safeToString(le.getMDC(trimmedField))); | ||
} | ||
} | ||
} | ||
|
||
after(le, r); | ||
return gson.toJson(r) + "\n"; | ||
} | ||
|
||
/** | ||
* Method called near the end of formatting a LoggingEvent in case users | ||
* want to override the default object fields. | ||
* | ||
* @param le The event being logged. | ||
* @param r The map which will be output. | ||
*/ | ||
public void after(LoggingEvent le, Map<String,Object> r) {} | ||
|
||
/** | ||
* LoggingEvent messages can have any type, and we call toString on them. As | ||
* the user can define the <code>toString</code> method, we should catch any exceptions. | ||
* | ||
* @param obj The object to parse. | ||
* @return The string value. | ||
*/ | ||
private static String safeToString(Object obj) { | ||
if (obj == null) return null; | ||
try { | ||
return obj.toString(); | ||
} catch (Throwable t) { | ||
return "Error getting message: " + t.getMessage(); | ||
} | ||
} | ||
|
||
/** | ||
* Safe integer parser, for when line numbers aren't available. See for | ||
* example https://github.com/michaeltandy/log4j-json/issues/1 | ||
* | ||
* @param obj The object to parse. | ||
* @return The int value | ||
*/ | ||
private static Integer safeParseInt(String obj) { | ||
try { | ||
return Integer.parseInt(obj.toString()); | ||
} catch (NumberFormatException t) { | ||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* If a throwable is present, format it with newlines between stack trace | ||
* elements. Otherwise return null. | ||
* | ||
* @param le The logging event. | ||
*/ | ||
private String formatThrowable(LoggingEvent le) { | ||
if (le.getThrowableInformation() == null || | ||
le.getThrowableInformation().getThrowable() == null) | ||
return null; | ||
|
||
return mkString(le.getThrowableStrRep(), "\n"); | ||
} | ||
|
||
private String mkString(Object[] parts,String separator) { | ||
final StringBuilder sb = new StringBuilder(); | ||
for (int i = 0; ; i++) { | ||
sb.append(parts[i]); | ||
if (i == parts.length - 1) | ||
return sb.toString(); | ||
sb.append(separator); | ||
} | ||
} | ||
|
||
@Override | ||
public boolean ignoresThrowable() { | ||
return false; | ||
} | ||
|
||
@Override | ||
public void activateOptions() {} | ||
|
||
private static String getHostname() { | ||
String hostname; | ||
try { | ||
hostname = java.net.InetAddress.getLocalHost().getHostName(); | ||
} catch (Exception e) { | ||
hostname = "Unknown, " + e.getMessage(); | ||
} | ||
return hostname; | ||
} | ||
|
||
public void setMinimumLevelForSlowLogging(String level) { | ||
minimumLevelForSlowLogging = Level.toLevel(level, Level.ALL); | ||
} | ||
|
||
public void setMdcRoot(String mdcRoot) { | ||
this.mdcRoot = mdcRoot; | ||
} | ||
|
||
public void setMdcFieldsToLog(String toLog) { | ||
if (toLog == null || toLog.isEmpty()) { | ||
mdcFieldsToLog = Collections.emptyList(); | ||
} else { | ||
final ArrayList<String> listToLog = new ArrayList<>(); | ||
for (String token : toLog.split(",")) { | ||
token = token.trim(); | ||
if (! token.isEmpty()) { | ||
listToLog.add(token); | ||
} | ||
} | ||
mdcFieldsToLog = Collections.unmodifiableList(listToLog); | ||
} | ||
} | ||
} |
121 changes: 121 additions & 0 deletions
121
json/src/test/java/com/obsidiandynamics/log4jextras/json/JsonLayoutTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package com.obsidiandynamics.log4jextras.json; | ||
|
||
import static org.junit.Assert.*; | ||
|
||
import org.apache.log4j.*; | ||
import org.junit.*; | ||
import com.obsidiandynamics.log4jextras.*; | ||
|
||
/** | ||
* Adapted from https://github.com/michaeltandy/log4j-json. | ||
*/ | ||
public final class JsonLayoutTest { | ||
private static Logger LOG; | ||
|
||
@BeforeClass | ||
public static void beforeClass() { | ||
System.setProperty("log4j.configuration", "log4j-test.properties"); | ||
LOG = Logger.getLogger(JsonLayoutTest.class); | ||
} | ||
|
||
@AfterClass | ||
public static void afterClass() { | ||
System.clearProperty("log4j.configuration"); | ||
LOG = null; | ||
} | ||
|
||
@Before | ||
public void before() { | ||
TestAppender.baos.reset(); | ||
MDC.clear(); | ||
} | ||
|
||
@Test | ||
public void testDemonstration() { | ||
LOG.info("Example of some logging"); | ||
LOG.warn("Some text\nwith a newline", new Exception("Outer Exception", new Exception("Nested Exception"))); | ||
LOG.fatal("Text may be complicated & have many symbols\n¬!£$%^&*()_+{}:@~<>?,./;'#[]-=`\\| \t\n"); | ||
|
||
final String whatWasLogged = TestAppender.baos.toString(); | ||
final String[] lines = whatWasLogged.split("\n"); | ||
|
||
assertEquals(3,lines.length); | ||
assertTrue(lines[0].contains("INFO")); | ||
assertTrue(lines[0].contains("Example of some logging")); | ||
|
||
assertTrue(lines[1].contains("newline")); | ||
assertTrue(lines[1].contains("Outer Exception")); | ||
assertTrue(lines[1].contains("Nested Exception")); | ||
|
||
assertTrue(lines[2].contains("have many symbols")); | ||
} | ||
|
||
@Test | ||
public void testObjectHandling() { | ||
LOG.info(new Object() { | ||
@Override public String toString() { | ||
throw new RuntimeException("Hypothetical failure"); | ||
} | ||
}); | ||
LOG.warn(null); | ||
|
||
final String whatWasLogged = TestAppender.baos.toString(); | ||
final String[] lines = whatWasLogged.split("\n"); | ||
assertEquals(2,lines.length); | ||
|
||
assertTrue(lines[0].contains("Hypothetical")); | ||
assertTrue(lines[1].contains("WARN")); | ||
} | ||
|
||
@Test | ||
public void testLogMethod() { | ||
// Test for https://github.com/michaeltandy/log4j-json/issues/1 | ||
LOG.log("asdf", Level.INFO, "this is the log message", null); | ||
|
||
final String whatWasLogged = TestAppender.baos.toString(); | ||
final String[] lines = whatWasLogged.split("\n"); | ||
assertEquals(1,lines.length); | ||
assertTrue(lines[0].contains("this is the log message")); | ||
} | ||
|
||
@Test | ||
public void testMinimumLevelForSlowLogging() { | ||
LOG.info("Info level logging"); | ||
LOG.debug("Debug level logging"); | ||
|
||
final String whatWasLogged = TestAppender.baos.toString(); | ||
final String[] lines = whatWasLogged.split("\n"); | ||
assertEquals(2,lines.length); | ||
|
||
assertTrue(lines[0].contains("INFO")); | ||
assertTrue(lines[0].contains("class")); | ||
assertTrue(lines[0].contains("line")); | ||
assertTrue(lines[0].contains("method")); | ||
|
||
assertTrue(lines[1].contains("DEBUG")); | ||
assertFalse(lines[1].contains("class")); | ||
assertFalse(lines[1].contains("line")); | ||
assertFalse(lines[1].contains("method")); | ||
} | ||
|
||
@Test | ||
public void testSelectiveMdcLogging() { | ||
MDC.put("asdf", "value_for_key_asdf"); | ||
MDC.put("qwer", "value_for_key_qwer"); | ||
MDC.put("thread", "attempt to overwrite thread in output"); | ||
|
||
LOG.info("Example of some logging"); | ||
|
||
MDC.clear(); | ||
|
||
final String whatWasLogged = TestAppender.baos.toString(); | ||
final String[] lines = whatWasLogged.split("\n"); | ||
|
||
assertEquals(1,lines.length); | ||
assertTrue(lines[0].contains("value_for_key_asdf")); | ||
assertFalse(lines[0].contains("value_for_key_qwer")); | ||
|
||
assertTrue(lines[0].contains("thread")); | ||
assertFalse(lines[0].contains("attempt to overwrite thread in output")); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.