Skip to content

Commit

Permalink
Added JSON layout and Splunk appender
Browse files Browse the repository at this point in the history
  • Loading branch information
ekoutanov committed Oct 18, 2017
1 parent c7dc640 commit 168031d
Show file tree
Hide file tree
Showing 11 changed files with 1,096 additions and 2 deletions.
7 changes: 6 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ apply plugin: 'maven-publish'
apply plugin: 'com.jfrog.bintray'

group = 'com.obsidiandynamics.log4jextras'
version = '0.1.0'
version = '0.1.0-SNAPSHOT'

def envUser = 'BINTRAY_USER'
def envKey = 'BINTRAY_KEY'
Expand Down Expand Up @@ -71,6 +71,11 @@ allprojects {
}

subprojects {
dependencies {
compile project(':')

testCompile project(':').sourceSets.test.output
}
}

task jacocoRootReport(type: JacocoReport) {
Expand Down
1 change: 1 addition & 0 deletions json/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ def packageName = 'log4j-extras-json'
version = project(':').version

dependencies {
compile 'com.google.code.gson:gson:2.8.1'
}

jar {
Expand Down
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);
}
}
}
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"));
}
}
2 changes: 1 addition & 1 deletion json/src/test/resources/log4j-test.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ log4j.appender.a.layout=com.obsidiandynamics.log4jextras.json.JsonLayout
log4j.appender.a.layout.MinimumLevelForSlowLogging=INFO
log4j.appender.a.layout.MdcFieldsToLog=asdf , , thread

log4j.appender.b=com.obsidiandynamics.log4jextras.json.TestAppender
log4j.appender.b=com.obsidiandynamics.log4jextras.TestAppender
log4j.appender.b.layout=com.obsidiandynamics.log4jextras.json.JsonLayout
log4j.appender.b.layout.MinimumLevelForSlowLogging=INFO
log4j.appender.b.layout.MdcFieldsToLog= asdf , , thread
2 changes: 2 additions & 0 deletions splunk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ def packageName = 'log4j-extras-splunk'
version = project(':').version

dependencies {
compile 'org.apache.httpcomponents:httpclient:4.5.3'
compile 'org.apache.httpcomponents:httpasyncclient:4.1.3'
}

jar {
Expand Down
Loading

0 comments on commit 168031d

Please sign in to comment.