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 2013/08/16 22:34:33 UTC

svn commit: r1514887 - in /logging/log4j/log4j2/trunk: core/src/main/java/org/apache/logging/log4j/core/helpers/ core/src/main/java/org/apache/logging/log4j/core/layout/ core/src/test/java/org/apache/logging/log4j/core/appender/ core/src/test/java/org/...

Author: ggregory
Date: Fri Aug 16 20:34:32 2013
New Revision: 1514887

URL: http://svn.apache.org/r1514887
Log:
[LOG4J2-356] Create a JSON Layout. (TODO: Refactor a private method b/w XML and JSON layout.)

Added:
    logging/log4j/log4j2/trunk/core/src/main/java/org/apache/logging/log4j/core/layout/JSONLayout.java   (with props)
    logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/appender/JSONCompleteFileAppenderTest.java   (with props)
    logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/layout/JSONLayoutTest.java   (with props)
    logging/log4j/log4j2/trunk/core/src/test/resources/JSONCompleteFileAppenderTest.xml   (with props)
Modified:
    logging/log4j/log4j2/trunk/core/src/main/java/org/apache/logging/log4j/core/helpers/Transform.java
    logging/log4j/log4j2/trunk/src/changes/changes.xml
    logging/log4j/log4j2/trunk/src/site/xdoc/manual/layouts.xml.vm

Modified: logging/log4j/log4j2/trunk/core/src/main/java/org/apache/logging/log4j/core/helpers/Transform.java
URL: http://svn.apache.org/viewvc/logging/log4j/log4j2/trunk/core/src/main/java/org/apache/logging/log4j/core/helpers/Transform.java?rev=1514887&r1=1514886&r2=1514887&view=diff
==============================================================================
--- logging/log4j/log4j2/trunk/core/src/main/java/org/apache/logging/log4j/core/helpers/Transform.java (original)
+++ logging/log4j/log4j2/trunk/core/src/main/java/org/apache/logging/log4j/core/helpers/Transform.java Fri Aug 16 20:34:32 2013
@@ -108,4 +108,75 @@ public final class Transform {
             }
         }
     }
+
+    /**
+     * This method takes a string which may contain JSON reserved chars and 
+     * escapes them.
+     *
+     * @param input The text to be converted.
+     * @return The input string with the special characters replaced.
+     */
+    public static String escapeJsonControlCharacters(final String input) {
+        // Check if the string is null, zero length or devoid of special characters
+        // if so, return what was sent in.
+    
+        // TODO: escaped Unicode chars.
+        
+        if (Strings.isEmpty(input)
+            || (input.indexOf('"') == -1 &&
+            input.indexOf('\\') == -1 &&
+            input.indexOf('/') == -1 &&
+            input.indexOf('\b') == -1 &&
+            input.indexOf('\f') == -1 &&
+            input.indexOf('\n') == -1 &&
+            input.indexOf('\r') == -1 && 
+            input.indexOf('\t') == -1)) {
+            return input;
+        }
+    
+        final StringBuilder buf = new StringBuilder(input.length() + 6);
+        
+        final int len = input.length();
+        for (int i = 0; i < len; i++) {
+            final char ch = input.charAt(i);
+            final String escBs = "\\\\";
+            switch (ch) {
+            case '"':
+                buf.append(escBs);
+                buf.append(ch);                
+                break;
+            case '\\':
+                buf.append(escBs);
+                buf.append(ch);                
+                break;
+            case '/':
+                buf.append(escBs);
+                buf.append(ch);                
+                break;
+            case '\b':
+                buf.append(escBs);
+                buf.append('b');                
+                break;
+            case '\f':
+                buf.append(escBs);
+                buf.append('f');                
+                break;
+            case '\n':
+                buf.append(escBs);
+                buf.append('n');                
+                break;
+            case '\r':
+                buf.append(escBs);
+                buf.append('r');                
+                break;
+            case '\t':
+                buf.append(escBs);
+                buf.append('t');                
+                break;
+            default: 
+                buf.append(ch);                
+            } 
+        }
+        return buf.toString();
+    }
 }

Added: logging/log4j/log4j2/trunk/core/src/main/java/org/apache/logging/log4j/core/layout/JSONLayout.java
URL: http://svn.apache.org/viewvc/logging/log4j/log4j2/trunk/core/src/main/java/org/apache/logging/log4j/core/layout/JSONLayout.java?rev=1514887&view=auto
==============================================================================
--- logging/log4j/log4j2/trunk/core/src/main/java/org/apache/logging/log4j/core/layout/JSONLayout.java (added)
+++ logging/log4j/log4j2/trunk/core/src/main/java/org/apache/logging/log4j/core/layout/JSONLayout.java Fri Aug 16 20:34:32 2013
@@ -0,0 +1,405 @@
+/*
+ * 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.IOException;
+import java.io.InterruptedIOException;
+import java.io.LineNumberReader;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginAttr;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+import org.apache.logging.log4j.core.helpers.Charsets;
+import org.apache.logging.log4j.core.helpers.Transform;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.message.MultiformatMessage;
+
+/**
+ * Appends a series of JSON events as strings serialized as bytes.
+ * 
+ * <h4>Complete well-formed JSON vs. fragment JSON</h4>
+ * <p>
+ * If you configure {@code complete="true"}, the appender outputs a well-formed JSON document. 
+ * By default, with {@code complete="false"}, you should include the
+ * output as an <em>external file</em> in a separate file to form a well-formed JSON document.
+ * </p>
+ * <p>
+ * A well-formed JSON document follows this pattern:
+ * </p>
+ * 
+ * <pre>[
+ *   {
+ *     "logger":"com.foo.Bar",
+ *     "timestamp":"1376681196470",
+ *     "level":"INFO",
+ *     "thread":"main",
+ *     "message":"Message flushed with immediate flush=true"
+ *   },
+ *   {
+ *     "logger":"com.foo.Bar",
+ *     "timestamp":"1376681196471",
+ *     "level":"ERROR",
+ *     "thread":"main",
+ *     "message":"Message flushed with immediate flush=true",
+ *     "throwable":"java.lang.IllegalArgumentException: badarg\\n\\tat org.apache.logging.log4j.core.appender.JSONCompleteFileAppenderTest.testFlushAtEndOfBatch(JSONCompleteFileAppenderTest.java:54)\\n\\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\\n\\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)\\n\\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\\n\\tat java.lang.reflect.Method.invoke(Method.java:606)\\n\\tat org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)\\n\\tat org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)\\n\\tat org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)\\n\\tat org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)\\n\\tat org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)\\n\\tat org.junit.runners.BlockJUnit4ClassRunner.runChild
 (BlockJUnit4ClassRunner.java:70)\\n\\tat org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)\\n\\tat org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)\\n\\tat org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)\\n\\tat org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)\\n\\tat org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)\\n\\tat org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)\\n\\tat org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)\\n\\tat org.junit.runners.ParentRunner.run(ParentRunner.java:309)\\n\\tat org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)\\n\\tat org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(Remot
 eTestRunner.java:683)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)\\n"
+ *   }
+ * ]</pre>
+ * <p>
+ * If {@code complete="false"}, the appender does not write the JSON open array character "[" at the start of the document.
+ * and "]" and the end.
+ * </p>
+ * <p>
+ * This approach enforces the independence of the JSONLayout and the appender where you embed it.
+ * </p>
+ * <h4>Encoding</h4>
+ * <p>
+ * Appenders using this layout should have their {@code charset} set to {@code UTF-8} or {@code UTF-16}, otherwise
+ * events containing non ASCII characters could result in corrupted log files.
+ * </p>
+ * <h4>Pretty vs. compact XML</h4>
+ * <p>
+ * By default, the JSON layout is not compact (a.k.a. not "pretty") with {@code compact="false"}, which means the
+ * appender uses end-of-line characters and indents lines to format the text. If {@code compact="true"}, then no
+ * end-of-line or indentation is used. Message content may contain, of course, escaped end-of-lines.
+ * </p>
+ */
+@Plugin(name = "JSONLayout", category = "Core", elementType = "layout", printObject = true)
+public class JSONLayout extends AbstractStringLayout {
+
+    private static final int DEFAULT_SIZE = 256;
+
+    // We yield to \r\n for the default.
+    private static final String DEFAULT_EOL = "\r\n";
+    private static final String COMPACT_EOL = "";
+    private static final String DEFAULT_INDENT = "  ";
+    private static final String COMPACT_INDENT = "";
+
+    private static final String[] FORMATS = new String[] { "json" };
+
+    private final boolean locationInfo;
+    private final boolean properties;
+    private final boolean complete;
+    private final String eol;
+    private final String indent1;
+    private final String indent2;
+    private final String indent3;
+    private final String indent4;
+    private volatile boolean firstLayoutDone;
+
+    protected JSONLayout(final boolean locationInfo, final boolean properties, final boolean complete, boolean compact,
+            final Charset charset) {
+        super(charset);
+        this.locationInfo = locationInfo;
+        this.properties = properties;
+        this.complete = complete;
+        this.eol = compact ? COMPACT_EOL : DEFAULT_EOL;
+        this.indent1 = compact ? COMPACT_INDENT : DEFAULT_INDENT;
+        this.indent2 = this.indent1 + this.indent1;
+        this.indent3 = this.indent2 + this.indent1;
+        this.indent4 = this.indent3 + this.indent1;
+    }
+
+    /**
+     * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the log4j.dtd.
+     * 
+     * @param event
+     *            The LogEvent.
+     * @return The XML representation of the LogEvent.
+     */
+    @Override
+    public String toSerializable(final LogEvent event) {
+        final StringBuilder buf = new StringBuilder(DEFAULT_SIZE);
+        // DC locking to avoid synchronizing the whole layout.
+        boolean check = this.firstLayoutDone; 
+        if (!this.firstLayoutDone) {
+            synchronized(this) {
+                check = this.firstLayoutDone;
+                if (!check) {
+                    this.firstLayoutDone = true;
+                } else {
+                    buf.append(',');
+                    buf.append(this.eol);                                
+                }
+            }
+        } else {
+            buf.append(',');
+            buf.append(this.eol);                                            
+        }
+        buf.append(this.indent1);
+        buf.append('{');
+        buf.append(this.eol);
+        buf.append(this.indent2);
+        buf.append("\"logger\":\"");
+        String name = event.getLoggerName();
+        if (name.isEmpty()) {
+            name = "root";
+        }
+        buf.append(Transform.escapeJsonControlCharacters(name));
+        buf.append("\",");
+        buf.append(this.eol);
+        buf.append(this.indent2);
+        buf.append("\"timestamp\":\"");
+        buf.append(event.getMillis());
+        buf.append("\",");
+        buf.append(this.eol);
+        buf.append(this.indent2);
+        buf.append("\"level\":\"");
+        buf.append(Transform.escapeJsonControlCharacters(String.valueOf(event.getLevel())));
+        buf.append("\",");
+        buf.append(this.eol);
+        buf.append(this.indent2);
+        buf.append("\"thread\":\"");
+        buf.append(Transform.escapeJsonControlCharacters(event.getThreadName()));
+        buf.append("\",");
+        buf.append(this.eol);
+
+        final Message msg = event.getMessage();
+        if (msg != null) {
+            boolean jsonSupported = false;
+            if (msg instanceof MultiformatMessage) {
+                final String[] formats = ((MultiformatMessage) msg).getFormats();
+                for (final String format : formats) {
+                    if (format.equalsIgnoreCase("JSON")) {
+                        jsonSupported = true;
+                        break;
+                    }
+                }
+            }
+            buf.append(this.indent2);
+            buf.append("\"message\":\"");
+            if (jsonSupported) {
+                buf.append(((MultiformatMessage) msg).getFormattedMessage(FORMATS));
+            } else {
+                Transform.appendEscapingCDATA(buf, event.getMessage().getFormattedMessage());
+            }
+            buf.append('\"');
+        }
+
+        if (event.getContextStack().getDepth() > 0) {
+            buf.append(",");
+            buf.append(this.eol);
+            buf.append("\"ndc\":");
+            Transform.appendEscapingCDATA(buf, event.getContextStack().toString());
+            buf.append("\"");
+        }
+
+        final Throwable throwable = event.getThrown();
+        if (throwable != null) {
+            buf.append(",");
+            buf.append(this.eol);
+            buf.append(this.indent2);
+            buf.append("\"throwable\":\"");
+            final List<String> list = this.getThrowableStringList(throwable);
+            for (final String str : list) {
+                buf.append(Transform.escapeJsonControlCharacters(str));
+                buf.append("\\\\n");
+            }
+            buf.append("\"");
+        }
+
+        if (this.locationInfo) {
+            final StackTraceElement element = event.getSource();
+            buf.append(",");
+            buf.append(this.eol);
+            buf.append(this.indent2);
+            buf.append("\"LocationInfo\":{");
+            buf.append(this.eol);
+            buf.append(this.indent3);
+            buf.append("\"class\":\"");
+            buf.append(Transform.escapeJsonControlCharacters(element.getClassName()));
+            buf.append("\",");
+            buf.append(this.eol);
+            buf.append(this.indent3);
+            buf.append("\"method\":\"");
+            buf.append(Transform.escapeJsonControlCharacters(element.getMethodName()));
+            buf.append("\",");
+            buf.append(this.eol);
+            buf.append(this.indent3);
+            buf.append("\"file\":\"");
+            buf.append(Transform.escapeJsonControlCharacters(element.getFileName()));
+            buf.append("\",");
+            buf.append(this.eol);
+            buf.append(this.indent3);
+            buf.append("\"line\":\"");
+            buf.append(element.getLineNumber());
+            buf.append("\"");
+            buf.append(this.eol);
+            buf.append(this.indent2);
+            buf.append("}");
+        }
+
+        if (this.properties && event.getContextMap().size() > 0) {
+            buf.append(",");
+            buf.append(this.eol);
+            buf.append(this.indent2);
+            buf.append("\"Properties\":[");
+            buf.append(this.eol);
+            final Set<Entry<String, String>> entrySet = event.getContextMap().entrySet();
+            int i = 1;
+            for (final Map.Entry<String, String> entry : entrySet) {
+                buf.append(this.indent3);
+                buf.append('{');
+                buf.append(this.eol);
+                buf.append(this.indent4);
+                buf.append("\"name\":\"");
+                buf.append(Transform.escapeJsonControlCharacters(entry.getKey()));
+                buf.append("\",");
+                buf.append(this.eol);
+                buf.append(this.indent4);
+                buf.append("\"value\":\"");
+                buf.append(Transform.escapeJsonControlCharacters(String.valueOf(entry.getValue())));
+                buf.append("\"");
+                buf.append(this.eol);
+                buf.append(this.indent3);
+                buf.append("}");
+                if (i < entrySet.size()) {
+                    buf.append(",");
+                }
+                buf.append(this.eol);
+                i++;
+            }
+            buf.append(this.indent2);
+            buf.append("]");
+        }
+
+        buf.append(this.eol);
+        buf.append(this.indent1);
+        buf.append("}");
+
+        return buf.toString();
+    }
+
+    /**
+     * Returns appropriate JSON headers.
+     * 
+     * @return a byte array containing the header, opening the JSON array.
+     */
+    @Override
+    public byte[] getHeader() {
+        if (!this.complete) {
+            return null;
+        }
+        final StringBuilder buf = new StringBuilder();
+        buf.append('[');
+        buf.append(this.eol);
+        return buf.toString().getBytes(this.getCharset());
+    }
+
+    /**
+     * Returns appropriate JSON footer.
+     * 
+     * @return a byte array containing the footer, closing the JSON array.
+     */
+    @Override
+    public byte[] getFooter() {
+        if (!this.complete) {
+            return null;
+        }
+        return (this.eol + "]" + this.eol).getBytes(this.getCharset());
+    }
+
+    /**
+     * XMLLayout's content format is specified by:
+     * <p/>
+     * Key: "dtd" Value: "log4j-events.dtd"
+     * <p/>
+     * Key: "version" Value: "2.0"
+     * 
+     * @return Map of content format keys supporting XMLLayout
+     */
+    @Override
+    public Map<String, String> getContentFormat() {
+        final Map<String, String> result = new HashMap<String, String>();
+        result.put("version", "2.0");
+        return result;
+    }
+
+    @Override
+    /**
+     * @return The content type.
+     */
+    public String getContentType() {
+        return "application/json; charset=" + this.getCharset();
+    }
+
+    private List<String> getThrowableStringList(final Throwable throwable) {
+        final StringWriter sw = new StringWriter();
+        final PrintWriter pw = new PrintWriter(sw);
+        try {
+            throwable.printStackTrace(pw);
+        } catch (final RuntimeException ex) {
+            // Ignore any exceptions.
+        }
+        pw.flush();
+        final LineNumberReader reader = new LineNumberReader(new StringReader(sw.toString()));
+        final ArrayList<String> lines = new ArrayList<String>();
+        try {
+            String line = reader.readLine();
+            while (line != null) {
+                lines.add(line);
+                line = reader.readLine();
+            }
+        } catch (final IOException ex) {
+            if (ex instanceof InterruptedIOException) {
+                Thread.currentThread().interrupt();
+            }
+            lines.add(ex.toString());
+        }
+        return lines;
+    }
+
+    /**
+     * Creates an XML Layout.
+     * 
+     * @param locationInfo
+     *            If "true", includes the location information in the generated JSON.
+     * @param properties
+     *            If "true", includes the thread context in the generated JSON.
+     * @param completeStr
+     *            If "true", includes the JSON header and footer, defaults to "false".
+     * @param compactStr
+     *            If "true", does not use end-of-lines and indentation, defaults to "false".
+     * @param charsetName
+     *            The character set to use, if {@code null}, uses "UTF-8".
+     * @return An XML Layout.
+     */
+    @PluginFactory
+    public static JSONLayout createLayout(
+            @PluginAttr("locationInfo") final String locationInfo,
+            @PluginAttr("properties") final String properties, 
+            @PluginAttr("complete") final String completeStr,
+            @PluginAttr("compact") final String compactStr, 
+            @PluginAttr("charset") final String charsetName) {
+        final Charset charset = Charsets.getSupportedCharset(charsetName, Charsets.UTF_8);
+        final boolean info = Boolean.parseBoolean(locationInfo);
+        final boolean props = Boolean.parseBoolean(properties);
+        final boolean complete = Boolean.parseBoolean(completeStr);
+        final boolean compact = Boolean.parseBoolean(compactStr);
+        return new JSONLayout(info, props, complete, compact, charset);
+    }
+}

Propchange: logging/log4j/log4j2/trunk/core/src/main/java/org/apache/logging/log4j/core/layout/JSONLayout.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: logging/log4j/log4j2/trunk/core/src/main/java/org/apache/logging/log4j/core/layout/JSONLayout.java
------------------------------------------------------------------------------
    svn:keywords = Id

Added: logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/appender/JSONCompleteFileAppenderTest.java
URL: http://svn.apache.org/viewvc/logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/appender/JSONCompleteFileAppenderTest.java?rev=1514887&view=auto
==============================================================================
--- logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/appender/JSONCompleteFileAppenderTest.java (added)
+++ logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/appender/JSONCompleteFileAppenderTest.java Fri Aug 16 20:34:32 2013
@@ -0,0 +1,108 @@
+/*
+ * 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.appender;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.LifeCycle;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests a "complete" XML file a.k.a. a well-formed XML file.
+ */
+public class JSONCompleteFileAppenderTest {
+
+    @BeforeClass
+    public static void beforeClass() {
+        System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, "JSONCompleteFileAppenderTest.xml");
+    }
+
+    @Test
+    public void testFlushAtEndOfBatch() throws Exception {
+        final File file = new File("target", "JSONCompleteFileAppenderTest.log");
+        // System.out.println(f.getAbsolutePath());
+        file.delete();
+        final Logger log = LogManager.getLogger("com.foo.Bar");
+        final String logMsg = "Message flushed with immediate flush=true";
+        log.info(logMsg);
+        log.error(logMsg, new IllegalArgumentException("badarg"));
+        ((LifeCycle) LogManager.getContext()).stop(); // stops async thread
+        try {
+            final BufferedReader reader = new BufferedReader(new FileReader(file));
+            String line1;
+            String line2;
+            String line3;
+            String line4;
+            String line5;
+            try {
+                line1 = reader.readLine();
+                line2 = reader.readLine();
+                line3 = reader.readLine();
+                line4 = reader.readLine();
+                line5 = reader.readLine();
+            } finally {
+                reader.close();
+            }
+            assertNotNull("line1", line1);
+            final String msg1 = "[";
+            assertTrue("line1 incorrect: [" + line1 + "], does not contain: [" + msg1 + "]", line1.equals(msg1));
+
+            assertNotNull("line2", line2);
+            final String msg2 = "  {";
+            assertTrue("line2 incorrect: [" + line2 + "], does not contain: [" + msg2 + "]", line2.equals(msg2));
+
+            assertNotNull("line3", line3);
+            final String msg3 = "    \"logger\":\"com.foo.Bar\",";
+            assertTrue("line3 incorrect: [" + line3 + "], does not contain: [" + msg3 + "]", line3.contains(msg3));
+
+            assertNotNull("line4", line4);
+            final String msg4 = "\"timestamp\":";
+            assertTrue("line4 incorrect: [" + line4 + "], does not contain: [" + msg4 + "]", line4.contains(msg4));
+
+            assertNotNull("line5", line5);
+            final String msg5 = "    \"level\":\"INFO\",";
+            assertTrue("line5 incorrect: [" + line5 + "], does not contain: [" + msg5 + "]", line5.contains(msg5));
+
+            final String location = "testFlushAtEndOfBatch";
+            assertTrue("no location", !line1.contains(location));
+
+            if (false) {
+                // now check that we can parse the array
+                ScriptEngineManager manager = new ScriptEngineManager();
+                ScriptEngine engine = manager.getEngineByName("JavaScript");
+                assertNotNull(engine);
+                // stopping the logger context does not seem to flush the file...
+                Object eval = engine.eval(new FileReader(file));
+                assertNotNull(eval);
+            }
+        } finally {
+            file.delete();
+        }
+    }
+}

Propchange: logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/appender/JSONCompleteFileAppenderTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/appender/JSONCompleteFileAppenderTest.java
------------------------------------------------------------------------------
    svn:keywords = Id

Added: logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/layout/JSONLayoutTest.java
URL: http://svn.apache.org/viewvc/logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/layout/JSONLayoutTest.java?rev=1514887&view=auto
==============================================================================
--- logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/layout/JSONLayoutTest.java (added)
+++ logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/layout/JSONLayoutTest.java Fri Aug 16 20:34:32 2013
@@ -0,0 +1,133 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.BasicConfigurationFactory;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.apache.logging.log4j.core.helpers.Charsets;
+import org.apache.logging.log4j.test.appender.ListAppender;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests the JSONLayout class.
+ */
+public class JSONLayoutTest {
+    static ConfigurationFactory cf = new BasicConfigurationFactory();
+
+    @AfterClass
+    public static void cleanupClass() {
+        ConfigurationFactory.removeConfigurationFactory(cf);
+    }
+
+    @BeforeClass
+    public static void setupClass() {
+        ConfigurationFactory.setConfigurationFactory(cf);
+        final LoggerContext ctx = (LoggerContext) LogManager.getContext();
+        ctx.reconfigure();
+    }
+
+    LoggerContext ctx = (LoggerContext) LogManager.getContext();
+
+    Logger root = this.ctx.getLogger("");
+
+    @Test
+    public void testContentType() {
+        final JSONLayout layout = JSONLayout.createLayout(null, null, null, null, null);
+        assertEquals("application/json; charset=UTF-8", layout.getContentType());
+    }
+
+    @Test
+    public void testDefaultCharset() {
+        final JSONLayout layout = JSONLayout.createLayout(null, null, null, null, null);
+        assertEquals(Charsets.UTF_8, layout.getCharset());
+    }
+
+    /**
+     * Test case for MDC conversion pattern.
+     */
+    @Test
+    public void testLayout() throws Exception {
+
+        // set up appender
+        final JSONLayout layout = JSONLayout.createLayout("true", "true", "true", "false", null);
+        final ListAppender appender = new ListAppender("List", null, layout, true, false);
+        appender.start();
+
+        // set appender on root and set level to debug
+        this.root.addAppender(appender);
+        this.root.setLevel(Level.DEBUG);
+
+        // output starting message
+        this.root.debug("starting mdc pattern test");
+
+        this.root.debug("empty mdc");
+
+        ThreadContext.put("key1", "value1");
+        ThreadContext.put("key2", "value2");
+
+        this.root.debug("filled mdc");
+
+        ThreadContext.remove("key1");
+        ThreadContext.remove("key2");
+
+        this.root.error("finished mdc pattern test", new NullPointerException("test"));
+
+        appender.stop();
+
+        final List<String> list = appender.getMessages();
+
+        // System.out.println(list);
+        // [[, {, "logger":"root",, "timestamp":"1376676700199",, "level":"DEBUG",, "thread":"main",,
+        // "message":"starting mdc pattern test",, "LocationInfo":{,
+        // "class":"org.apache.logging.log4j.core.layout.JSONLayoutTest",, "method":"testLayout",,
+        // "file":"JSONLayoutTest.java",, "line":"87", }, },, {, "logger":"root",, "timestamp":"1376676700203",,
+        // "level":"DEBUG",, "thread":"main",, "message":"empty mdc",, "LocationInfo":{,
+        // "class":"org.apache.logging.log4j.core.layout.JSONLayoutTest",, "method":"testLayout",,
+        // "file":"JSONLayoutTest.java",, "line":"89", }, },, {, "logger":"root",, "timestamp":"1376676700204",,
+        // "level":"DEBUG",, "thread":"main",, "message":"filled mdc",, "LocationInfo":{,
+        // "class":"org.apache.logging.log4j.core.layout.JSONLayoutTest",, "method":"testLayout",,
+        // "file":"JSONLayoutTest.java",, "line":"94", },, "Properties":[, {, "name":"key2",, "value":"value2", },, {,
+        // "name":"key1",, "value":"value1", }, ], },, {, "logger":"root",, "timestamp":"1376676700204",,
+        // "level":"ERROR",, "thread":"main",, "message":"finished mdc pattern test",,
+        // "throwable":"java.lang.NullPointerException: test\\n\\tat org.apache.logging.log4j.core.layout.JSONLayoutTest.testLayout(JSONLayoutTest.java:99)\\n\\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\\n\\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)\\n\\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\\n\\tat java.lang.reflect.Method.invoke(Method.java:606)\\n\\tat org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)\\n\\tat org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)\\n\\tat org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)\\n\\tat org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)\\n\\tat org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)\\n\\tat org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)\\n\\tat or
 g.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)\\n\\tat org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)\\n\\tat org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)\\n\\tat org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)\\n\\tat org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)\\n\\tat org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)\\n\\tat org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)\\n\\tat org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)\\n\\tat org.junit.runners.ParentRunner.run(ParentRunner.java:309)\\n\\tat org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)\\n\\tat org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)\\n\\tat org.eclipse.jdt.internal.jun
 it.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)\\n",,
+        // "LocationInfo":{, "class":"org.apache.logging.log4j.core.layout.JSONLayoutTest",, "method":"testLayout",,
+        // "file":"JSONLayoutTest.java",, "line":"99", }, },, ]]
+
+        this.checkAt("[", 0, list);
+        this.checkAt("{", 1, list);
+        this.checkAt("\"logger\":\"root\",", 2, list);
+        this.checkAt("\"level\":\"DEBUG\",", 4, list);
+        this.checkAt("\"message\":\"starting mdc pattern test\",", 6, list);
+    }
+
+    private void checkAt(String expected, int lineIndex, List<String> list) {
+        final String trimedLine = list.get(lineIndex).trim();
+        assertTrue("Incorrect line index " + lineIndex + ": \"" + trimedLine + "\"", trimedLine.equals(expected));
+    }
+}

Propchange: logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/layout/JSONLayoutTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: logging/log4j/log4j2/trunk/core/src/test/java/org/apache/logging/log4j/core/layout/JSONLayoutTest.java
------------------------------------------------------------------------------
    svn:keywords = Id

Added: logging/log4j/log4j2/trunk/core/src/test/resources/JSONCompleteFileAppenderTest.xml
URL: http://svn.apache.org/viewvc/logging/log4j/log4j2/trunk/core/src/test/resources/JSONCompleteFileAppenderTest.xml?rev=1514887&view=auto
==============================================================================
--- logging/log4j/log4j2/trunk/core/src/test/resources/JSONCompleteFileAppenderTest.xml (added)
+++ logging/log4j/log4j2/trunk/core/src/test/resources/JSONCompleteFileAppenderTest.xml Fri Aug 16 20:34:32 2013
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+  <Appenders>
+    <File name="XmlFile" fileName="target/JSONCompleteFileAppenderTest.log" immediateFlush="true" append="false">
+      <JSONLayout complete="true" charset="UTF-8"/>
+    </File>
+  </Appenders>
+  
+  <Loggers>
+    <Root level="info" includeLocation="false">
+      <AppenderRef ref="XmlFile"/>
+    </Root>
+  </Loggers>
+</Configuration>

Propchange: logging/log4j/log4j2/trunk/core/src/test/resources/JSONCompleteFileAppenderTest.xml
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: logging/log4j/log4j2/trunk/core/src/test/resources/JSONCompleteFileAppenderTest.xml
------------------------------------------------------------------------------
    svn:keywords = Id

Modified: logging/log4j/log4j2/trunk/src/changes/changes.xml
URL: http://svn.apache.org/viewvc/logging/log4j/log4j2/trunk/src/changes/changes.xml?rev=1514887&r1=1514886&r2=1514887&view=diff
==============================================================================
--- logging/log4j/log4j2/trunk/src/changes/changes.xml (original)
+++ logging/log4j/log4j2/trunk/src/changes/changes.xml Fri Aug 16 20:34:32 2013
@@ -21,6 +21,9 @@
   </properties>
   <body>
     <release version="2.0-beta9" date="soon, very soon" description="Bug fixes and enhancements">
+      <action issue="LOG4J2-356" dev="ggregory" type="add">
+        Create a JSON Layout.
+      </action>
       <action issue="LOG4J2-343" dev="rpopma" type="fix" due-to="Henning Schmiedehausen">
         Removed unnecessary generics from Appender interface and implementing classes.
       </action>

Modified: logging/log4j/log4j2/trunk/src/site/xdoc/manual/layouts.xml.vm
URL: http://svn.apache.org/viewvc/logging/log4j/log4j2/trunk/src/site/xdoc/manual/layouts.xml.vm?rev=1514887&r1=1514886&r2=1514887&view=diff
==============================================================================
--- logging/log4j/log4j2/trunk/src/site/xdoc/manual/layouts.xml.vm (original)
+++ logging/log4j/log4j2/trunk/src/site/xdoc/manual/layouts.xml.vm Fri Aug 16 20:34:32 2013
@@ -39,6 +39,59 @@
           <a href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Charset</a> to
           insure the byte array contains correct values.
         </p>
+        <a name="JSONLayout"/>
+        <subsection name="JSONLayout">
+          <!-- From Javadoc of org.apache.logging.log4j.core.layout.JSONLayout -->
+          <p>
+          Appends a series of JSON events as strings serialized as bytes..
+          </p>
+          <h4>Complete well-formed JSON vs. fragment JSON</h4>
+          <p>
+          If you configure <code>complete="true"</code>, the appender outputs a well-formed JSON document. By default, 
+          with <code>complete="false"</code>, you should include the output as an <em>external file</em> in a 
+          separate file to form a well-formed JSON document.
+          </p>
+          <p>
+          A well-formed JSON document follows this pattern:
+          </p>
+          <pre class="prettyprint linenums">[
+  {
+    "logger":"com.foo.Bar",
+    "timestamp":"1376681196470",
+    "level":"INFO",
+    "thread":"main",
+    "message":"Message flushed with immediate flush=true"
+  },
+  {
+    "logger":"com.foo.Bar",
+    "timestamp":"1376681196471",
+    "level":"ERROR",
+    "thread":"main",
+    "message":"Message flushed with immediate flush=true",
+    "throwable":"java.lang.IllegalArgumentException: badarg\\n\\tat org.apache.logging.log4j.core.appender.JSONCompleteFileAppenderTest.testFlushAtEndOfBatch(JSONCompleteFileAppenderTest.java:54)\\n\\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\\n\\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)\\n\\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\\n\\tat java.lang.reflect.Method.invoke(Method.java:606)\\n\\tat org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)\\n\\tat org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)\\n\\tat org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)\\n\\tat org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)\\n\\tat org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)\\n\\tat org.junit.runners.BlockJUnit4ClassRunner.runChild(Bl
 ockJUnit4ClassRunner.java:70)\\n\\tat org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)\\n\\tat org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)\\n\\tat org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)\\n\\tat org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)\\n\\tat org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)\\n\\tat org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)\\n\\tat org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)\\n\\tat org.junit.runners.ParentRunner.run(ParentRunner.java:309)\\n\\tat org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)\\n\\tat org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTe
 stRunner.java:683)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)\\n"
+  }
+]
+</pre>
+          <p>
+          If <code>complete="false"</code>, the appender does not write the JSON open array character "[" at the start 
+          of the document. and "]" and the end.
+          </p>
+          <p>
+          This approach enforces the independence of the JSONLayout and the appender where you embed it.
+          </p>
+          <h4>Encoding</h4>
+          <p>
+          Appenders using this layout should have their <code>charset</code> set to <code>UTF-8</code> or 
+          <code>UTF-16</code>, otherwise events containing non ASCII characters could result in corrupted log files.
+          </p>
+          <h4>Pretty vs. compact XML</h4>
+          <p>
+          By default, the JSON layout is not compact (a.k.a. not "pretty") with <code>compact="false"</code>, which 
+          means the appender uses end-of-line characters and indents lines to format the text. If 
+          <code>compact="true"</code>,  then no end-of-line or indentation is used. Message content may contain, 
+          of course, escaped end-of-lines.
+          </p>
+        </subsection>
         <a name="HTMLLayout"/>
         <subsection name="HTMLLayout">
           <p>The HTMLLayout generates an HTML page and adds each LogEvent to a row in a table.