You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by gg...@apache.org on 2014/09/11 18:47:51 UTC
git commit: [LOG4J2-428] Implement a GELF layout.
Repository: logging-log4j2
Updated Branches:
refs/heads/master 57f0977bd -> a720c1e6a
[LOG4J2-428] Implement a GELF layout.
Project: http://git-wip-us.apache.org/repos/asf/logging-log4j2/repo
Commit: http://git-wip-us.apache.org/repos/asf/logging-log4j2/commit/a720c1e6
Tree: http://git-wip-us.apache.org/repos/asf/logging-log4j2/tree/a720c1e6
Diff: http://git-wip-us.apache.org/repos/asf/logging-log4j2/diff/a720c1e6
Branch: refs/heads/master
Commit: a720c1e6aee54cd923444535d9a3165ad107e0d5
Parents: 57f0977
Author: Gary Gregory <ga...@gmail.com>
Authored: Thu Sep 11 12:47:46 2014 -0400
Committer: Gary Gregory <ga...@gmail.com>
Committed: Thu Sep 11 12:47:46 2014 -0400
----------------------------------------------------------------------
log4j-core/pom.xml | 6 +
.../logging/log4j/core/layout/GelfLayout.java | 181 +++++++++++++++++++
.../logging/log4j/core/layout/JsonLayout.java | 4 +-
.../log4j/core/layout/GelfLayoutTest.java | 131 ++++++++++++++
pom.xml | 6 +
src/changes/changes.xml | 3 +
6 files changed, 330 insertions(+), 1 deletion(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/a720c1e6/log4j-core/pom.xml
----------------------------------------------------------------------
diff --git a/log4j-core/pom.xml b/log4j-core/pom.xml
index b277506..cfb8e87 100644
--- a/log4j-core/pom.xml
+++ b/log4j-core/pom.xml
@@ -201,6 +201,12 @@
<artifactId>org.apache.felix.framework</artifactId>
<scope>test</scope>
</dependency>
+ <!-- GELF -->
+ <dependency>
+ <groupId>net.javacrumbs.json-unit</groupId>
+ <artifactId>json-unit</artifactId>
+ <scope>test</scope>
+ </dependency>
<!-- Other -->
<dependency>
<groupId>commons-codec</groupId>
http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/a720c1e6/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/GelfLayout.java
----------------------------------------------------------------------
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/GelfLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/GelfLayout.java
new file mode 100644
index 0000000..78d3293
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/GelfLayout.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.logging.log4j.core.layout;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.Map;
+import java.util.zip.GZIPOutputStream;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginElement;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+import org.apache.logging.log4j.core.net.Severity;
+import org.apache.logging.log4j.core.util.Charsets;
+import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.status.StatusLogger;
+
+/**
+ * Lays out Graylog Extended Log Format (GELF) 1.1 log events.
+ * <p>
+ * GZIP compresses the JSON if log event data is larger than 1 KiB. Does not implement chunking.
+ * </p>
+ * <p>
+ * Configure as follows to send to a Graylog2 server:
+ * </p>
+ *
+ * <pre>
+ * <Appenders>
+ * <Socket name="Graylog" protocol="udp" host="graylog.domain.com" port="12201">
+ * <GelfLayout host="someserver">
+ * <KeyValuePair key="additionalField1" value="additional value 1"/>
+ * <KeyValuePair key="additionalField2" value="additional value 2"/>
+ * </GelfLayout>
+ * </Socket>
+ * </Appenders>
+ * </pre>
+ *
+ * @see http://graylog2.org/gelf
+ * @see http://graylog2.org/resources/gelf/specification
+ */
+@Plugin(name = "GelfLayout", category = "Core", elementType = "layout", printObject = true)
+public final class GelfLayout extends AbstractStringLayout {
+
+ private static final byte[] EMPTY_BYTES = new byte[0];
+ private static final char C = ',';
+ private static final int COMPRESSION_THRESHOLD = 1024;
+ private static final char Q = '\"';
+ private static final String QC = "\",";
+ private static final String QU = "\"_";
+ private static final long serialVersionUID = 1L;
+ private static final BigDecimal TIME_DIVISOR = new BigDecimal(1000);
+
+ @PluginFactory
+ public static GelfLayout createLayout(
+ //@formatter:off
+ @PluginAttribute("host") final String host,
+ @PluginElement("AdditionalField") final KeyValuePair[] additionalFields,
+ @PluginAttribute(value = "compressionThreshold",
+ defaultInt= COMPRESSION_THRESHOLD) final int compressionThreshold) {
+ // @formatter:on
+ return new GelfLayout(host, additionalFields, compressionThreshold);
+ }
+
+ static String formatTimestamp(final long timeMillis) {
+ return new BigDecimal(timeMillis).divide(TIME_DIVISOR).toPlainString();
+ }
+
+ private final KeyValuePair[] additionalFields;
+
+ private final String host;
+
+ private final int compressionThreshold;
+
+ public GelfLayout(final String host, final KeyValuePair[] additionalFields, final int compressionThreshold) {
+ super(Charsets.UTF_8);
+ this.host = host;
+ this.additionalFields = additionalFields;
+ this.compressionThreshold = compressionThreshold;
+ }
+
+ private byte[] compress(final byte[] bytes) {
+ try {
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8);
+ final GZIPOutputStream stream = new GZIPOutputStream(baos);
+ stream.write(bytes);
+ stream.finish();
+ stream.close();
+ return baos.toByteArray();
+ } catch (final IOException e) {
+ StatusLogger.getLogger().error(e);
+ return EMPTY_BYTES;
+ }
+ }
+
+ private String escapeJson(final String s) {
+ return s.replace("\\", "\\\\").replace("\"", "\\\"");
+ }
+
+ /**
+ * http://en.wikipedia.org/wiki/Syslog#Severity_levels
+ */
+ private int formatLevel(final Level level) {
+ return Severity.getSeverity(level).getCode();
+ }
+
+ private String formatThrowable(final Throwable throwable) {
+ final StringWriter sw = new StringWriter();
+ final PrintWriter pw = new PrintWriter(sw);
+ throwable.printStackTrace(pw);
+ return sw.toString();
+ }
+
+ @Override
+ public Map<String, String> getContentFormat() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public String getContentType() {
+ return JsonLayout.CONTENT_TYPE + "; charset=" + this.getCharset();
+ }
+
+ @Override
+ public byte[] toByteArray(final LogEvent event) {
+ final byte[] bytes = toSerializable(event).getBytes(getCharset());
+ return bytes.length > compressionThreshold ? compress(bytes) : bytes;
+ }
+
+ @Override
+ public String toSerializable(final LogEvent event) {
+ final StringBuilder builder = new StringBuilder(256);
+ builder.append('{');
+ builder.append("\"version\":\"1.1\",");
+ builder.append("\"host\":\"").append(escapeJson(host)).append(QC);
+ builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C);
+ builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C);
+ if (event.getThreadName() != null) {
+ builder.append("\"_thread\":\"").append(escapeJson(event.getThreadName())).append(QC);
+ }
+ if (event.getLoggerName() != null) {
+ builder.append("\"_logger\":\"").append(escapeJson(event.getLoggerName())).append(QC);
+ }
+
+ for (final KeyValuePair additionalField : additionalFields) {
+ builder.append(QU).append(escapeJson(additionalField.getKey())).append("\":\"")
+ .append(escapeJson(additionalField.getValue())).append(QC);
+ }
+ for (final Map.Entry<String, String> entry : event.getContextMap().entrySet()) {
+ builder.append(QU).append(escapeJson(entry.getKey())).append("\":\"").append(escapeJson(entry.getValue()))
+ .append(QC);
+ }
+ if (event.getThrown() != null) {
+ builder.append("\"full_message\":\"").append(escapeJson(formatThrowable(event.getThrown()))).append(QC);
+ }
+
+ builder.append("\"short_message\":\"").append(escapeJson(event.getMessage().getFormattedMessage())).append(Q);
+ builder.append('}');
+ return builder.toString();
+ }
+}
http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/a720c1e6/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/JsonLayout.java
----------------------------------------------------------------------
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/JsonLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/JsonLayout.java
index a743a61..8cae234 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/JsonLayout.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/JsonLayout.java
@@ -792,6 +792,8 @@ import org.apache.logging.log4j.core.util.Charsets;
@Plugin(name = "JsonLayout", category = "Core", elementType = "layout", printObject = true)
public final class JsonLayout extends AbstractJacksonLayout {
+ static final String CONTENT_TYPE = "application/json";
+
private static final long serialVersionUID = 1L;
protected JsonLayout(final boolean locationInfo, final boolean properties, final boolean complete, final boolean compact,
@@ -840,7 +842,7 @@ public final class JsonLayout extends AbstractJacksonLayout {
* @return The content type.
*/
public String getContentType() {
- return "application/json; charset=" + this.getCharset();
+ return CONTENT_TYPE + "; charset=" + this.getCharset();
}
/**
http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/a720c1e6/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/GelfLayoutTest.java
----------------------------------------------------------------------
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/GelfLayoutTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/GelfLayoutTest.java
new file mode 100644
index 0000000..dcedc01
--- /dev/null
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/GelfLayoutTest.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.logging.log4j.core.layout;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.*;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.test.appender.ListAppender;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.List;
+
+import static net.javacrumbs.jsonunit.JsonAssert.assertJsonEquals;
+
+public class GelfLayoutTest {
+ LoggerContext ctx = (LoggerContext) LogManager.getContext();
+ Logger root = ctx.getLogger("");
+
+ private static final String HOSTNAME = "TheHost";
+ private static final String KEY1 = "Key1";
+ private static final String KEY2 = "Key2";
+ private static final String VALUE1 = "Value1";
+ private static final String VALUE2 = "Value2";
+ private static final String MDCKEY1 = "MdcKey1";
+ private static final String MDCKEY2 = "MdcKey2";
+ private static final String MDCVALUE1 = "MdcValue1";
+ private static final String MDCVALUE2 = "MdcValue2";
+
+ private static final String LINE1 = "empty mdc";
+ private static final String LINE2 = "filled mdc";
+
+ static ConfigurationFactory configFactory = new BasicConfigurationFactory();
+
+ @BeforeClass
+ public static void setupClass() {
+ ThreadContext.clearAll();
+ ConfigurationFactory.setConfigurationFactory(configFactory);
+ final LoggerContext ctx = (LoggerContext) LogManager.getContext();
+ ctx.reconfigure();
+ }
+
+ @AfterClass
+ public static void cleanupClass() {
+ ConfigurationFactory.removeConfigurationFactory(configFactory);
+ ThreadContext.clearAll();
+ }
+
+ @Test
+ public void testLayout() throws Exception {
+ for (final Appender appender : root.getAppenders().values()) {
+ root.removeAppender(appender);
+ }
+ // set up appender
+ final GelfLayout layout = GelfLayout.createLayout(HOSTNAME, new KeyValuePair[] {
+ new KeyValuePair(KEY1, VALUE1),
+ new KeyValuePair(KEY2, VALUE2), }, 1024);
+ // ConsoleAppender appender = new ConsoleAppender("Console", layout);
+ final ListAppender eventAppender = new ListAppender("Events", null, null, true, false);
+ final ListAppender appender = new ListAppender("Layouted", null, layout, true, false);
+ eventAppender.start();
+ appender.start();
+
+ // set appender on root and set level to debug
+ root.addAppender(eventAppender);
+ root.addAppender(appender);
+ root.setLevel(Level.DEBUG);
+
+ root.debug(LINE1);
+
+ ThreadContext.put(MDCKEY1, MDCVALUE1);
+ ThreadContext.put(MDCKEY2, MDCVALUE2);
+
+ root.info(LINE2);
+
+ ThreadContext.clearMap();
+
+ appender.stop();
+
+ final List<LogEvent> events = eventAppender.getEvents();
+ final List<String> list = appender.getMessages();
+
+ //@formatter:off
+ assertJsonEquals("{" +
+ "\"version\": \"1.1\"," +
+ "\"host\": \"" + HOSTNAME + "\"," +
+ "\"timestamp\": "+GelfLayout.formatTimestamp(events.get(0).getTimeMillis())+"," +
+ "\"level\": 7," +
+ "\"_thread\": \"main\"," +
+ "\"_logger\": \"\"," +
+ "\"short_message\": \"" + LINE1 + "\"," +
+ "\"_" + KEY1 + "\": \"" + VALUE1 + "\"," +
+ "\"_" + KEY2 + "\": \"" + VALUE2 + "\"" +
+ "}",
+ list.get(0));
+
+ assertJsonEquals("{" +
+ "\"version\": \"1.1\"," +
+ "\"host\": \"" + HOSTNAME + "\"," +
+ "\"timestamp\": "+GelfLayout.formatTimestamp(events.get(1).getTimeMillis())+"," +
+ "\"level\": 6," +
+ "\"_thread\": \"main\"," +
+ "\"_logger\": \"\"," +
+ "\"short_message\": \"" + LINE2 + "\"," +
+ "\"_" + KEY1 + "\": \"" + VALUE1 + "\"," +
+ "\"_" + KEY2 + "\": \"" + VALUE2 + "\"," +
+ "\"_" + MDCKEY1 + "\": \"" + MDCVALUE1 + "\"," +
+ "\"_" + MDCKEY2 + "\": \"" + MDCVALUE2 + "\"" +
+ "}",
+ list.get(1));
+ //@formatter:on
+ }
+}
http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/a720c1e6/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index e47bdcc..7b49964 100644
--- a/pom.xml
+++ b/pom.xml
@@ -618,6 +618,12 @@
<artifactId>lightcouch</artifactId>
<version>0.0.6</version>
</dependency>
+ <dependency>
+ <groupId>net.javacrumbs.json-unit</groupId>
+ <artifactId>json-unit</artifactId>
+ <version>1.1.6</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</dependencyManagement>
<build>
http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/a720c1e6/src/changes/changes.xml
----------------------------------------------------------------------
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index fc709ee..85fc681 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -24,6 +24,9 @@
</properties>
<body>
<release version="2.1" date="2014-??-??" description="Bug fixes and enhancements">
+ <action issue="LOG4J2-428" dev="ggregory" type="add" due-to="Mark Paluch">
+ Implement a GELF layout.
+ </action>
<action issue="LOG4J2-391" dev="rgoers" type="fix" due-to="Kamal Bahadur">
FlumePersistentManager now handles LockConflictExceptions in Berkeley Db when sending a batch.
</action>