Skip to content

Commit a959973

Browse files
committed
v1 event format support
1 parent 9d664ce commit a959973

File tree

5 files changed

+375
-0
lines changed

5 files changed

+375
-0
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package net.logstash.log4j;
2+
3+
import net.logstash.log4j.data.HostData;
4+
import net.minidev.json.JSONObject;
5+
import org.apache.commons.lang.StringUtils;
6+
import org.apache.commons.lang.time.FastDateFormat;
7+
import org.apache.log4j.Layout;
8+
import org.apache.log4j.spi.LocationInfo;
9+
import org.apache.log4j.spi.LoggingEvent;
10+
import org.apache.log4j.spi.ThrowableInformation;
11+
12+
import java.util.Date;
13+
import java.util.HashMap;
14+
import java.util.Map;
15+
16+
public class JSONEventLayoutV1 extends Layout {
17+
18+
private boolean locationInfo = false;
19+
20+
private String tags;
21+
private boolean ignoreThrowable = false;
22+
23+
private boolean activeIgnoreThrowable = ignoreThrowable;
24+
private String hostname = new HostData().getHostName();
25+
private String threadName;
26+
private long timestamp;
27+
private String ndc;
28+
private Map mdc;
29+
private LocationInfo info;
30+
private HashMap<String, Object> exceptionInformation;
31+
private static Integer version = 1;
32+
33+
private JSONObject logstashEvent;
34+
public static final FastDateFormat ISO_DATETIME_TIME_ZONE_FORMAT_WITH_MILLIS = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");
35+
36+
public static String dateFormat(long timestamp) {
37+
return ISO_DATETIME_TIME_ZONE_FORMAT_WITH_MILLIS.format(new Date(timestamp));
38+
}
39+
40+
/**
41+
* For backwards compatibility, the default is to generate location information
42+
* in the log messages.
43+
*/
44+
public JSONEventLayoutV1() {
45+
this(true);
46+
}
47+
48+
/**
49+
* Creates a layout that optionally inserts location information into log messages.
50+
*
51+
* @param locationInfo whether or not to include location information in the log messages.
52+
*/
53+
public JSONEventLayoutV1(boolean locationInfo) {
54+
this.locationInfo = locationInfo;
55+
}
56+
57+
public String format(LoggingEvent loggingEvent) {
58+
threadName = loggingEvent.getThreadName();
59+
timestamp = loggingEvent.getTimeStamp();
60+
exceptionInformation = new HashMap<String, Object>();
61+
mdc = loggingEvent.getProperties();
62+
ndc = loggingEvent.getNDC();
63+
64+
logstashEvent = new JSONObject();
65+
66+
/**
67+
* All v1 of the event format requires is
68+
* "@timestamp" and "@version"
69+
* Every other field is arbitrary
70+
*/
71+
logstashEvent.put("@version", version);
72+
logstashEvent.put("@timestamp", dateFormat(timestamp));
73+
74+
/**
75+
* Now we start injecting our own stuff.
76+
*/
77+
logstashEvent.put("source_host", hostname);
78+
logstashEvent.put("message", loggingEvent.getRenderedMessage());
79+
80+
if (loggingEvent.getThrowableInformation() != null) {
81+
final ThrowableInformation throwableInformation = loggingEvent.getThrowableInformation();
82+
if (throwableInformation.getThrowable().getClass().getCanonicalName() != null) {
83+
exceptionInformation.put("exception_class", throwableInformation.getThrowable().getClass().getCanonicalName());
84+
}
85+
if (throwableInformation.getThrowable().getMessage() != null) {
86+
exceptionInformation.put("exception_message", throwableInformation.getThrowable().getMessage());
87+
}
88+
if (throwableInformation.getThrowableStrRep() != null) {
89+
String stackTrace = StringUtils.join(throwableInformation.getThrowableStrRep(), "\n");
90+
exceptionInformation.put("stacktrace", stackTrace);
91+
}
92+
addEventData("exception", exceptionInformation);
93+
}
94+
95+
if (locationInfo) {
96+
info = loggingEvent.getLocationInformation();
97+
addEventData("file", info.getFileName());
98+
addEventData("line_number", info.getLineNumber());
99+
addEventData("class", info.getClassName());
100+
addEventData("method", info.getMethodName());
101+
}
102+
103+
addEventData("logger_name", loggingEvent.getLoggerName());
104+
addEventData("mdc", mdc);
105+
addEventData("ndc", ndc);
106+
addEventData("level", loggingEvent.getLevel().toString());
107+
addEventData("thread_name", threadName);
108+
109+
return logstashEvent.toString() + "\n";
110+
}
111+
112+
public boolean ignoresThrowable() {
113+
return ignoreThrowable;
114+
}
115+
116+
/**
117+
* Query whether log messages include location information.
118+
*
119+
* @return true if location information is included in log messages, false otherwise.
120+
*/
121+
public boolean getLocationInfo() {
122+
return locationInfo;
123+
}
124+
125+
/**
126+
* Set whether log messages should include location information.
127+
*
128+
* @param locationInfo true if location information should be included, false otherwise.
129+
*/
130+
public void setLocationInfo(boolean locationInfo) {
131+
this.locationInfo = locationInfo;
132+
}
133+
134+
public void activateOptions() {
135+
activeIgnoreThrowable = ignoreThrowable;
136+
}
137+
138+
private void addEventData(String keyname, Object keyval) {
139+
if (null != keyval) {
140+
logstashEvent.put(keyname, keyval);
141+
}
142+
}
143+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
log4j.RootCategory=TRACE, Console
2+
3+
log4j.appender.Console=org.apache.log4j.ConsoleAppender
4+
log4j.appender.Console.Threshold=TRACE
5+
log4j.appender.Console.layout=net.logstash.log4j.JSONEventLayoutV1

src/test/java/net/logstash/log4j/JSONEventLayoutTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import org.apache.log4j.Level;
77
import org.apache.log4j.Logger;
88
import org.apache.log4j.NDC;
9+
import org.junit.Before;
910
import org.junit.After;
11+
import org.junit.AfterClass;
1012
import org.junit.BeforeClass;
1113
import org.junit.Ignore;
1214
import org.junit.Test;
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package net.logstash.log4j;
2+
3+
import junit.framework.Assert;
4+
import net.minidev.json.JSONObject;
5+
import net.minidev.json.JSONValue;
6+
import org.apache.log4j.Level;
7+
import org.apache.log4j.Logger;
8+
import org.apache.log4j.NDC;
9+
import org.junit.After;
10+
import org.junit.Before;
11+
import org.junit.AfterClass;
12+
import org.junit.BeforeClass;
13+
import org.junit.Ignore;
14+
import org.junit.Test;
15+
16+
/**
17+
* Created with IntelliJ IDEA.
18+
* User: jvincent
19+
* Date: 12/5/12
20+
* Time: 12:07 AM
21+
* To change this template use File | Settings | File Templates.
22+
*/
23+
public class JSONEventV1LayoutTest {
24+
static Logger logger;
25+
static MockAppenderV1 appender;
26+
static final String[] logstashFields = new String[]{
27+
"message",
28+
"source_host",
29+
"@timestamp",
30+
"@version"
31+
};
32+
33+
@BeforeClass
34+
public static void setupTestAppender() {
35+
appender = new MockAppenderV1(new JSONEventLayoutV1());
36+
logger = Logger.getRootLogger();
37+
appender.setThreshold(Level.TRACE);
38+
appender.setName("mockappenderv1");
39+
appender.activateOptions();
40+
logger.addAppender(appender);
41+
}
42+
43+
@After
44+
public void clearTestAppender() {
45+
NDC.clear();
46+
appender.clear();
47+
appender.close();
48+
}
49+
50+
@Test
51+
public void testJSONEventLayoutIsJSON() {
52+
logger.info("this is an info message");
53+
String message = appender.getMessages()[0];
54+
Assert.assertTrue("Event is not valid JSON", JSONValue.isValidJsonStrict(message));
55+
}
56+
57+
@Test
58+
public void testJSONEventLayoutHasKeys() {
59+
logger.info("this is a test message");
60+
String message = appender.getMessages()[0];
61+
Object obj = JSONValue.parse(message);
62+
JSONObject jsonObject = (JSONObject) obj;
63+
for (String fieldName : logstashFields) {
64+
Assert.assertTrue("Event does not contain field: " + fieldName, jsonObject.containsKey(fieldName));
65+
}
66+
}
67+
68+
@Test
69+
public void testJSONEventLayoutHasNDC() {
70+
String ndcData = new String("json-layout-test");
71+
NDC.push(ndcData);
72+
logger.warn("I should have NDC data in my log");
73+
String message = appender.getMessages()[0];
74+
Object obj = JSONValue.parse(message);
75+
JSONObject jsonObject = (JSONObject) obj;
76+
77+
Assert.assertEquals("NDC is wrong", ndcData, jsonObject.get("ndc"));
78+
}
79+
80+
@Test
81+
public void testJSONEventLayoutExceptions() {
82+
String exceptionMessage = new String("shits on fire, yo");
83+
logger.fatal("uh-oh", new IllegalArgumentException(exceptionMessage));
84+
String message = appender.getMessages()[0];
85+
Object obj = JSONValue.parse(message);
86+
JSONObject jsonObject = (JSONObject) obj;
87+
JSONObject exceptionInformation = (JSONObject) jsonObject.get("exception");
88+
89+
Assert.assertEquals("Exception class missing", "java.lang.IllegalArgumentException", exceptionInformation.get("exception_class"));
90+
Assert.assertEquals("Exception exception message", exceptionMessage, exceptionInformation.get("exception_message"));
91+
}
92+
93+
@Test
94+
public void testJSONEventLayoutHasClassName() {
95+
logger.warn("warning dawg");
96+
String message = appender.getMessages()[0];
97+
Object obj = JSONValue.parse(message);
98+
JSONObject jsonObject = (JSONObject) obj;
99+
100+
Assert.assertEquals("Logged class does not match", this.getClass().getCanonicalName().toString(), jsonObject.get("class"));
101+
}
102+
103+
@Test
104+
public void testJSONEventHasFileName() {
105+
logger.warn("whoami");
106+
String message = appender.getMessages()[0];
107+
Object obj = JSONValue.parse(message);
108+
JSONObject jsonObject = (JSONObject) obj;
109+
110+
Assert.assertNotNull("File value is missing", jsonObject.get("file"));
111+
}
112+
113+
@Test
114+
public void testJSONEventHasLoggerName() {
115+
logger.warn("whoami");
116+
String message = appender.getMessages()[0];
117+
Object obj = JSONValue.parse(message);
118+
JSONObject jsonObject = (JSONObject) obj;
119+
Assert.assertNotNull("LoggerName value is missing", jsonObject.get("logger_name"));
120+
}
121+
122+
@Test
123+
public void testJSONEventHasThreadName() {
124+
logger.warn("whoami");
125+
String message = appender.getMessages()[0];
126+
Object obj = JSONValue.parse(message);
127+
JSONObject jsonObject = (JSONObject) obj;
128+
Assert.assertNotNull("ThreadName value is missing", jsonObject.get("thread_name"));
129+
}
130+
131+
@Test
132+
public void testJSONEventLayoutNoLocationInfo() {
133+
JSONEventLayoutV1 layout = (JSONEventLayoutV1) appender.getLayout();
134+
boolean prevLocationInfo = layout.getLocationInfo();
135+
136+
layout.setLocationInfo(false);
137+
138+
logger.warn("warning dawg");
139+
String message = appender.getMessages()[0];
140+
Object obj = JSONValue.parse(message);
141+
JSONObject jsonObject = (JSONObject) obj;
142+
143+
Assert.assertFalse("atFields contains file value", jsonObject.containsKey("file"));
144+
Assert.assertFalse("atFields contains line_number value", jsonObject.containsKey("line_number"));
145+
Assert.assertFalse("atFields contains class value", jsonObject.containsKey("class"));
146+
Assert.assertFalse("atFields contains method value", jsonObject.containsKey("method"));
147+
148+
// Revert the change to the layout to leave it as we found it.
149+
layout.setLocationInfo(prevLocationInfo);
150+
}
151+
152+
@Test
153+
@Ignore
154+
public void measureJSONEventLayoutLocationInfoPerformance() {
155+
JSONEventLayoutV1 layout = (JSONEventLayoutV1) appender.getLayout();
156+
boolean locationInfo = layout.getLocationInfo();
157+
int iterations = 100000;
158+
long start, stop;
159+
160+
start = System.currentTimeMillis();
161+
for (int i = 0; i < iterations; i++) {
162+
logger.warn("warning dawg");
163+
}
164+
stop = System.currentTimeMillis();
165+
long firstMeasurement = stop - start;
166+
167+
layout.setLocationInfo(!locationInfo);
168+
start = System.currentTimeMillis();
169+
for (int i = 0; i < iterations; i++) {
170+
logger.warn("warning dawg");
171+
}
172+
stop = System.currentTimeMillis();
173+
long secondMeasurement = stop - start;
174+
175+
System.out.println("First Measurement (locationInfo: " + locationInfo + "): " + firstMeasurement);
176+
System.out.println("Second Measurement (locationInfo: " + !locationInfo + "): " + secondMeasurement);
177+
178+
// Clean up
179+
layout.setLocationInfo(!locationInfo);
180+
}
181+
182+
@Test
183+
@Ignore
184+
public void testDateFormat() {
185+
long timestamp = 1364844991207L;
186+
Assert.assertEquals("format does not produce expected output", "2013-04-01T21:36:31.207+02:00", JSONEventLayoutV1.dateFormat(timestamp));
187+
}
188+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package net.logstash.log4j;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
import org.apache.log4j.AppenderSkeleton;
7+
import org.apache.log4j.spi.LoggingEvent;
8+
import org.apache.log4j.Layout;
9+
10+
public class MockAppenderV1 extends AppenderSkeleton {
11+
12+
private static List messages = new ArrayList();
13+
14+
public MockAppenderV1(Layout layout){
15+
this.layout = layout;
16+
}
17+
@Override
18+
protected void append(LoggingEvent event){
19+
messages.add(layout.format(event));
20+
}
21+
22+
public void close(){
23+
messages.clear();
24+
}
25+
26+
public boolean requiresLayout(){
27+
return true;
28+
}
29+
30+
public static String[] getMessages() {
31+
return (String[]) messages.toArray(new String[messages.size()]);
32+
}
33+
34+
public void clear() {
35+
messages.clear();
36+
}
37+
}

0 commit comments

Comments
 (0)