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>
+ * &lt;Appenders&gt;
+ *        &lt;Socket name="Graylog" protocol="udp" host="graylog.domain.com" port="12201"&gt;
+ *            &lt;GelfLayout host="someserver"&gt;
+ *                &lt;KeyValuePair key="additionalField1" value="additional value 1"/&gt;
+ *                &lt;KeyValuePair key="additionalField2" value="additional value 2"/&gt;
+ *            &lt;/GelfLayout&gt;
+ *        &lt;/Socket&gt;
+ * &lt;/Appenders&gt;
+ * </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>