Skip to content

Commit 168031d

Browse files
committed
Added JSON layout and Splunk appender
1 parent c7dc640 commit 168031d

File tree

11 files changed

+1096
-2
lines changed

11 files changed

+1096
-2
lines changed

build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ apply plugin: 'maven-publish'
55
apply plugin: 'com.jfrog.bintray'
66

77
group = 'com.obsidiandynamics.log4jextras'
8-
version = '0.1.0'
8+
version = '0.1.0-SNAPSHOT'
99

1010
def envUser = 'BINTRAY_USER'
1111
def envKey = 'BINTRAY_KEY'
@@ -71,6 +71,11 @@ allprojects {
7171
}
7272

7373
subprojects {
74+
dependencies {
75+
compile project(':')
76+
77+
testCompile project(':').sourceSets.test.output
78+
}
7479
}
7580

7681
task jacocoRootReport(type: JacocoReport) {

json/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ def packageName = 'log4j-extras-json'
22
version = project(':').version
33

44
dependencies {
5+
compile 'com.google.code.gson:gson:2.8.1'
56
}
67

78
jar {
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package com.obsidiandynamics.log4jextras.json;
2+
3+
import java.text.*;
4+
import java.util.*;
5+
6+
import org.apache.log4j.*;
7+
import org.apache.log4j.spi.*;
8+
9+
import com.google.gson.*;
10+
11+
/**
12+
* Layout for JSON logging.<p>
13+
*
14+
* Adapted from https://github.com/michaeltandy/log4j-json.
15+
*/
16+
public final class JsonLayout extends Layout {
17+
private final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
18+
private final String hostname = getHostname().toLowerCase();
19+
private final String username = System.getProperty("user.name").toLowerCase();
20+
private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
21+
22+
private Level minimumLevelForSlowLogging = Level.ALL;
23+
private String mdcRoot;
24+
private List<String> mdcFieldsToLog = Collections.emptyList();
25+
26+
@Override
27+
public String format(LoggingEvent le) {
28+
final Map<String, Object> r = new LinkedHashMap<>();
29+
r.put("timestamp", dateFormat.format(new Date(le.timeStamp)));
30+
r.put("host", hostname);
31+
r.put("user", username);
32+
r.put("level", le.getLevel().toString());
33+
r.put("thread", le.getThreadName());
34+
r.put("ndc",le.getNDC());
35+
if (le.getLevel().isGreaterOrEqual(minimumLevelForSlowLogging)) {
36+
r.put("class", le.getLocationInformation().getClassName());
37+
r.put("line", safeParseInt(le.getLocationInformation().getLineNumber()));
38+
r.put("method", le.getLocationInformation().getMethodName());
39+
}
40+
r.put("message", safeToString(le.getMessage()));
41+
r.put("throwable", formatThrowable(le) );
42+
43+
for (String mdcKey : mdcFieldsToLog) {
44+
if (! r.containsKey(mdcKey)) {
45+
r.put(mdcKey, safeToString(le.getMDC(mdcKey)));
46+
}
47+
}
48+
49+
if (mdcRoot != null) {
50+
final Object mdcValue = le.getMDC(mdcRoot);
51+
if (mdcValue != null) {
52+
final String[] fields = ((String) mdcValue).split(",");
53+
for (String field : fields) {
54+
final String trimmedField = field.trim();
55+
r.put(trimmedField, safeToString(le.getMDC(trimmedField)));
56+
}
57+
}
58+
}
59+
60+
after(le, r);
61+
return gson.toJson(r) + "\n";
62+
}
63+
64+
/**
65+
* Method called near the end of formatting a LoggingEvent in case users
66+
* want to override the default object fields.
67+
*
68+
* @param le The event being logged.
69+
* @param r The map which will be output.
70+
*/
71+
public void after(LoggingEvent le, Map<String,Object> r) {}
72+
73+
/**
74+
* LoggingEvent messages can have any type, and we call toString on them. As
75+
* the user can define the <code>toString</code> method, we should catch any exceptions.
76+
*
77+
* @param obj The object to parse.
78+
* @return The string value.
79+
*/
80+
private static String safeToString(Object obj) {
81+
if (obj == null) return null;
82+
try {
83+
return obj.toString();
84+
} catch (Throwable t) {
85+
return "Error getting message: " + t.getMessage();
86+
}
87+
}
88+
89+
/**
90+
* Safe integer parser, for when line numbers aren't available. See for
91+
* example https://github.com/michaeltandy/log4j-json/issues/1
92+
*
93+
* @param obj The object to parse.
94+
* @return The int value
95+
*/
96+
private static Integer safeParseInt(String obj) {
97+
try {
98+
return Integer.parseInt(obj.toString());
99+
} catch (NumberFormatException t) {
100+
return null;
101+
}
102+
}
103+
104+
/**
105+
* If a throwable is present, format it with newlines between stack trace
106+
* elements. Otherwise return null.
107+
*
108+
* @param le The logging event.
109+
*/
110+
private String formatThrowable(LoggingEvent le) {
111+
if (le.getThrowableInformation() == null ||
112+
le.getThrowableInformation().getThrowable() == null)
113+
return null;
114+
115+
return mkString(le.getThrowableStrRep(), "\n");
116+
}
117+
118+
private String mkString(Object[] parts,String separator) {
119+
final StringBuilder sb = new StringBuilder();
120+
for (int i = 0; ; i++) {
121+
sb.append(parts[i]);
122+
if (i == parts.length - 1)
123+
return sb.toString();
124+
sb.append(separator);
125+
}
126+
}
127+
128+
@Override
129+
public boolean ignoresThrowable() {
130+
return false;
131+
}
132+
133+
@Override
134+
public void activateOptions() {}
135+
136+
private static String getHostname() {
137+
String hostname;
138+
try {
139+
hostname = java.net.InetAddress.getLocalHost().getHostName();
140+
} catch (Exception e) {
141+
hostname = "Unknown, " + e.getMessage();
142+
}
143+
return hostname;
144+
}
145+
146+
public void setMinimumLevelForSlowLogging(String level) {
147+
minimumLevelForSlowLogging = Level.toLevel(level, Level.ALL);
148+
}
149+
150+
public void setMdcRoot(String mdcRoot) {
151+
this.mdcRoot = mdcRoot;
152+
}
153+
154+
public void setMdcFieldsToLog(String toLog) {
155+
if (toLog == null || toLog.isEmpty()) {
156+
mdcFieldsToLog = Collections.emptyList();
157+
} else {
158+
final ArrayList<String> listToLog = new ArrayList<>();
159+
for (String token : toLog.split(",")) {
160+
token = token.trim();
161+
if (! token.isEmpty()) {
162+
listToLog.add(token);
163+
}
164+
}
165+
mdcFieldsToLog = Collections.unmodifiableList(listToLog);
166+
}
167+
}
168+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package com.obsidiandynamics.log4jextras.json;
2+
3+
import static org.junit.Assert.*;
4+
5+
import org.apache.log4j.*;
6+
import org.junit.*;
7+
import com.obsidiandynamics.log4jextras.*;
8+
9+
/**
10+
* Adapted from https://github.com/michaeltandy/log4j-json.
11+
*/
12+
public final class JsonLayoutTest {
13+
private static Logger LOG;
14+
15+
@BeforeClass
16+
public static void beforeClass() {
17+
System.setProperty("log4j.configuration", "log4j-test.properties");
18+
LOG = Logger.getLogger(JsonLayoutTest.class);
19+
}
20+
21+
@AfterClass
22+
public static void afterClass() {
23+
System.clearProperty("log4j.configuration");
24+
LOG = null;
25+
}
26+
27+
@Before
28+
public void before() {
29+
TestAppender.baos.reset();
30+
MDC.clear();
31+
}
32+
33+
@Test
34+
public void testDemonstration() {
35+
LOG.info("Example of some logging");
36+
LOG.warn("Some text\nwith a newline", new Exception("Outer Exception", new Exception("Nested Exception")));
37+
LOG.fatal("Text may be complicated & have many symbols\n¬!£$%^&*()_+{}:@~<>?,./;'#[]-=`\\| \t\n");
38+
39+
final String whatWasLogged = TestAppender.baos.toString();
40+
final String[] lines = whatWasLogged.split("\n");
41+
42+
assertEquals(3,lines.length);
43+
assertTrue(lines[0].contains("INFO"));
44+
assertTrue(lines[0].contains("Example of some logging"));
45+
46+
assertTrue(lines[1].contains("newline"));
47+
assertTrue(lines[1].contains("Outer Exception"));
48+
assertTrue(lines[1].contains("Nested Exception"));
49+
50+
assertTrue(lines[2].contains("have many symbols"));
51+
}
52+
53+
@Test
54+
public void testObjectHandling() {
55+
LOG.info(new Object() {
56+
@Override public String toString() {
57+
throw new RuntimeException("Hypothetical failure");
58+
}
59+
});
60+
LOG.warn(null);
61+
62+
final String whatWasLogged = TestAppender.baos.toString();
63+
final String[] lines = whatWasLogged.split("\n");
64+
assertEquals(2,lines.length);
65+
66+
assertTrue(lines[0].contains("Hypothetical"));
67+
assertTrue(lines[1].contains("WARN"));
68+
}
69+
70+
@Test
71+
public void testLogMethod() {
72+
// Test for https://github.com/michaeltandy/log4j-json/issues/1
73+
LOG.log("asdf", Level.INFO, "this is the log message", null);
74+
75+
final String whatWasLogged = TestAppender.baos.toString();
76+
final String[] lines = whatWasLogged.split("\n");
77+
assertEquals(1,lines.length);
78+
assertTrue(lines[0].contains("this is the log message"));
79+
}
80+
81+
@Test
82+
public void testMinimumLevelForSlowLogging() {
83+
LOG.info("Info level logging");
84+
LOG.debug("Debug level logging");
85+
86+
final String whatWasLogged = TestAppender.baos.toString();
87+
final String[] lines = whatWasLogged.split("\n");
88+
assertEquals(2,lines.length);
89+
90+
assertTrue(lines[0].contains("INFO"));
91+
assertTrue(lines[0].contains("class"));
92+
assertTrue(lines[0].contains("line"));
93+
assertTrue(lines[0].contains("method"));
94+
95+
assertTrue(lines[1].contains("DEBUG"));
96+
assertFalse(lines[1].contains("class"));
97+
assertFalse(lines[1].contains("line"));
98+
assertFalse(lines[1].contains("method"));
99+
}
100+
101+
@Test
102+
public void testSelectiveMdcLogging() {
103+
MDC.put("asdf", "value_for_key_asdf");
104+
MDC.put("qwer", "value_for_key_qwer");
105+
MDC.put("thread", "attempt to overwrite thread in output");
106+
107+
LOG.info("Example of some logging");
108+
109+
MDC.clear();
110+
111+
final String whatWasLogged = TestAppender.baos.toString();
112+
final String[] lines = whatWasLogged.split("\n");
113+
114+
assertEquals(1,lines.length);
115+
assertTrue(lines[0].contains("value_for_key_asdf"));
116+
assertFalse(lines[0].contains("value_for_key_qwer"));
117+
118+
assertTrue(lines[0].contains("thread"));
119+
assertFalse(lines[0].contains("attempt to overwrite thread in output"));
120+
}
121+
}

json/src/test/resources/log4j-test.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ log4j.appender.a.layout=com.obsidiandynamics.log4jextras.json.JsonLayout
66
log4j.appender.a.layout.MinimumLevelForSlowLogging=INFO
77
log4j.appender.a.layout.MdcFieldsToLog=asdf , , thread
88

9-
log4j.appender.b=com.obsidiandynamics.log4jextras.json.TestAppender
9+
log4j.appender.b=com.obsidiandynamics.log4jextras.TestAppender
1010
log4j.appender.b.layout=com.obsidiandynamics.log4jextras.json.JsonLayout
1111
log4j.appender.b.layout.MinimumLevelForSlowLogging=INFO
1212
log4j.appender.b.layout.MdcFieldsToLog= asdf , , thread

splunk/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ def packageName = 'log4j-extras-splunk'
22
version = project(':').version
33

44
dependencies {
5+
compile 'org.apache.httpcomponents:httpclient:4.5.3'
6+
compile 'org.apache.httpcomponents:httpasyncclient:4.1.3'
57
}
68

79
jar {

0 commit comments

Comments
 (0)