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.