Add HTMLLayout
git-svn-id: https://svn.apache.org/repos/asf/logging/log4j/branches/BRANCH_2_0_EXPERIMENTAL/rgoers@1127414 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/log4j2-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java b/log4j2-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
index 846a8af..3167c17 100644
--- a/log4j2-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
+++ b/log4j2-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
@@ -45,10 +45,16 @@
private Object externalContext = null;
+ private static final long JVM_START_TIME = System.currentTimeMillis();
+
public LoggerContext() {
reconfigure();
}
+ public static long getStartTime() {
+ return JVM_START_TIME;
+ }
+
public void setExternalContext(Object context) {
this.externalContext = context;
}
diff --git a/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/ListAppender.java b/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/ListAppender.java
index 8d85e60..bd75ab1 100644
--- a/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/ListAppender.java
+++ b/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/ListAppender.java
@@ -33,19 +33,29 @@
* This appender is primarily used for testing. Use in a real environment is discouraged as the
* List could eventually grow to cause an OutOfMemoryError.
*/
-@Plugin(name="List",type="Core",elementType="appender",printObject=true)
+@Plugin(name = "List", type = "Core", elementType = "appender", printObject = true)
public class ListAppender extends AppenderBase {
private List<LogEvent> events = new ArrayList<LogEvent>();
private List<String> messages = new ArrayList<String>();
+ private final boolean newLine;
+
public ListAppender(String name) {
super(name, null, null);
+ newLine = false;
}
- public ListAppender(String name, Filters filters, Layout layout) {
+ public ListAppender(String name, Filters filters, Layout layout, boolean newline) {
super(name, filters, layout);
+ this.newLine = newline;
+ if (layout != null) {
+ byte[] bytes = layout.getHeader();
+ if (bytes != null) {
+ write(bytes);
+ }
+ }
}
public synchronized void append(LogEvent event) {
@@ -53,7 +63,41 @@
if (layout == null) {
events.add(event);
} else {
- messages.add(new String(layout.format(event)));
+ write(layout.format(event));
+ }
+ }
+
+ private void write(byte[] bytes) {
+ String str = new String(bytes);
+ if (newLine) {
+ int index = 0;
+ while (index < str.length()) {
+ int end = str.indexOf("\n", index);
+ if (index == end) {
+ if (!messages.get(messages.size() - 1).equals("")) {
+ messages.add("");
+ }
+ } else if (end >= 0) {
+ messages.add(str.substring(index, end));
+ } else {
+ messages.add(str.substring(index));
+ break;
+ }
+ index = end + 1;
+ }
+ } else {
+ messages.add(str);
+ }
+ }
+
+ public void stop() {
+ super.stop();
+ Layout layout = getLayout();
+ if (layout != null) {
+ byte[] bytes = layout.getFooter();
+ if (bytes != null) {
+ write(bytes);
+ }
}
}
@@ -72,6 +116,7 @@
@PluginFactory
public static ListAppender createAppender(@PluginAttr("name") String name,
+ @PluginAttr("entryPerNewLine") String newLine,
@PluginElement("layout") Layout layout,
@PluginElement("filters") Filters filters) {
@@ -80,6 +125,8 @@
return null;
}
- return new ListAppender(name, filters, layout);
+ boolean nl = (newLine == null) ? false : Boolean.parseBoolean(newLine);
+
+ return new ListAppender(name, filters, layout, nl);
}
}
diff --git a/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/OutputStreamManager.java b/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/OutputStreamManager.java
index 0819b28..f63815f 100644
--- a/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/OutputStreamManager.java
+++ b/log4j2-core/src/main/java/org/apache/logging/log4j/core/appender/OutputStreamManager.java
@@ -48,8 +48,6 @@
private int count;
- private byte[] header = null;
-
private byte[] footer = null;
public StringBuilder buffer = new StringBuilder();
@@ -86,13 +84,17 @@
}
public synchronized void setHeader(byte[] header) {
- if (header == null) {
- this.header = header;
+ if (header != null) {
+ try {
+ this.os.write(header, 0, header.length);
+ } catch (IOException ioe) {
+ logger.error("Unable to write header", ioe);
+ }
}
}
public synchronized void setFooter(byte[] footer) {
- if (footer == null) {
+ if (footer != null) {
this.footer = footer;
}
}
diff --git a/log4j2-core/src/main/java/org/apache/logging/log4j/core/helpers/Transform.java b/log4j2-core/src/main/java/org/apache/logging/log4j/core/helpers/Transform.java
new file mode 100644
index 0000000..2d44458
--- /dev/null
+++ b/log4j2-core/src/main/java/org/apache/logging/log4j/core/helpers/Transform.java
@@ -0,0 +1,110 @@
+/*
+ * 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.helpers;
+
+
+/**
+ * Utility class for transforming strings.
+ */
+public class Transform {
+
+ private static final String CDATA_START = "<![CDATA[";
+ private static final String CDATA_END = "]]>";
+ private static final String CDATA_PSEUDO_END = "]]>";
+ private static final String CDATA_EMBEDED_END = CDATA_END + CDATA_PSEUDO_END + CDATA_START;
+ private static final int CDATA_END_LEN = CDATA_END.length();
+
+ /**
+ * This method takes a string which may contain HTML tags (ie,
+ * <b>, <table>, etc) and replaces any
+ * '<', '>' , '&' or '"'
+ * characters with respective predefined entity references.
+ *
+ * @param input The text to be converted.
+ * @return The input string with the special characters replaced.
+ */
+ static public String escapeTags(final String input) {
+ //Check if the string is null, zero length or devoid of special characters
+ // if so, return what was sent in.
+
+ if (input == null
+ || input.length() == 0
+ || (input.indexOf('"') == -1 &&
+ input.indexOf('&') == -1 &&
+ input.indexOf('<') == -1 &&
+ input.indexOf('>') == -1)) {
+ return input;
+ }
+
+ //Use a StringBuffer in lieu of String concatenation -- it is
+ //much more efficient this way.
+
+ StringBuilder buf = new StringBuilder(input.length() + 6);
+ char ch = ' ';
+
+ int len = input.length();
+ for (int i = 0; i < len; i++) {
+ ch = input.charAt(i);
+ if (ch > '>') {
+ buf.append(ch);
+ } else if (ch == '<') {
+ buf.append("<");
+ } else if (ch == '>') {
+ buf.append(">");
+ } else if (ch == '&') {
+ buf.append("&");
+ } else if (ch == '"') {
+ buf.append(""");
+ } else {
+ buf.append(ch);
+ }
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Ensures that embeded CDEnd strings (]]>) are handled properly
+ * within message, NDC and throwable tag text.
+ *
+ * @param buf StringBuffer holding the XML data to this point. The
+ * initial CDStart (<![CDATA[) and final CDEnd (]]>) of the CDATA
+ * section are the responsibility of the calling method.
+ * @param str The String that is inserted into an existing CDATA Section within buf.
+ */
+ static public void appendEscapingCDATA(final StringBuilder buf,
+ final String str) {
+ if (str != null) {
+ int end = str.indexOf(CDATA_END);
+ if (end < 0) {
+ buf.append(str);
+ } else {
+ int start = 0;
+ while (end > -1) {
+ buf.append(str.substring(start, end));
+ buf.append(CDATA_EMBEDED_END);
+ start = end + CDATA_END_LEN;
+ if (start < str.length()) {
+ end = str.indexOf(CDATA_END, start);
+ } else {
+ return;
+ }
+ }
+ buf.append(str.substring(start));
+ }
+ }
+ }
+}
diff --git a/log4j2-core/src/main/java/org/apache/logging/log4j/core/layout/HTMLLayout.java b/log4j2-core/src/main/java/org/apache/logging/log4j/core/layout/HTMLLayout.java
new file mode 100644
index 0000000..8b8a283
--- /dev/null
+++ b/log4j2-core/src/main/java/org/apache/logging/log4j/core/layout/HTMLLayout.java
@@ -0,0 +1,260 @@
+/*
+ * 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.core.LogEvent;
+import org.apache.logging.log4j.core.LoggerContext;
+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.Transform;
+
+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;
+
+/**
+ * This layout outputs events in a HTML table.
+ * <p/>
+ * Appenders using this layout should have their encoding set to UTF-8 or UTF-16, otherwise events containing
+ * non ASCII characters could result in corrupted log files.
+ */
+@Plugin(name="HTMLLayout",type="Core",elementType="layout",printObject=true)
+public class HTMLLayout extends LayoutBase {
+
+ protected final int BUF_SIZE = 256;
+ protected final int MAX_CAPACITY = 1024;
+
+ static String TRACE_PREFIX = "<br> ";
+
+ // output buffer appended to when format() is invoked
+ private StringBuilder sbuf = new StringBuilder(BUF_SIZE);
+
+ // Print no location info by default
+ protected final boolean locationInfo;
+
+ private static final String DEFAULT_TITLE = "Log4J Log Messages";
+
+ private static final String DEFAULT_CONTENT_TYPE = "text/html";
+
+ protected final String title;
+
+ protected final String contentType;
+
+ protected final Charset charset;
+
+ public HTMLLayout(boolean locationInfo, String title, String contentType, Charset charset) {
+ this.locationInfo = locationInfo;
+ this.title = title;
+ this.contentType = contentType;
+ this.charset = charset;
+ }
+
+ public byte[] format(LogEvent event) {
+
+ if (sbuf.capacity() > MAX_CAPACITY) {
+ sbuf = new StringBuilder(BUF_SIZE);
+ } else {
+ sbuf.setLength(0);
+ }
+
+ sbuf.append(LINE_SEP).append("<tr>").append(LINE_SEP);
+
+ sbuf.append("<td>");
+ sbuf.append(event.getMillis() - LoggerContext.getStartTime());
+ sbuf.append("</td>").append(LINE_SEP);
+
+ String escapedThread = Transform.escapeTags(event.getThreadName());
+ sbuf.append("<td title=\"" + escapedThread + " thread\">");
+ sbuf.append(escapedThread);
+ sbuf.append("</td>").append(LINE_SEP);
+
+ sbuf.append("<td title=\"Level\">");
+ if (event.getLevel().equals(Level.DEBUG)) {
+ sbuf.append("<font color=\"#339933\">");
+ sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel())));
+ sbuf.append("</font>");
+ } else if (event.getLevel().isAtLeastAsSpecificAs(Level.WARN)) {
+ sbuf.append("<font color=\"#993300\"><strong>");
+ sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel())));
+ sbuf.append("</strong></font>");
+ } else {
+ sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel())));
+ }
+ sbuf.append("</td>").append(LINE_SEP);
+
+ String escapedLogger = Transform.escapeTags(event.getLoggerName());
+ if (escapedLogger.length() == 0) {
+ escapedLogger = "root";
+ }
+ sbuf.append("<td title=\"").append(escapedLogger).append(" category\">");
+ sbuf.append(escapedLogger);
+ sbuf.append("</td>").append(LINE_SEP);
+
+ if (locationInfo) {
+ StackTraceElement element = event.getSource();
+ sbuf.append("<td>");
+ sbuf.append(Transform.escapeTags(element.getFileName()));
+ sbuf.append(':');
+ sbuf.append(element.getLineNumber());
+ sbuf.append("</td>").append(LINE_SEP);
+ }
+
+ sbuf.append("<td title=\"Message\">");
+ sbuf.append(Transform.escapeTags(event.getMessage().getFormattedMessage()));
+ sbuf.append("</td>").append(LINE_SEP);
+ sbuf.append("</tr>").append(LINE_SEP);
+
+ if (event.getContextStack().size() > 0) {
+ sbuf.append(
+ "<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : xx-small;\" colspan=\"6\" title=\"Nested Diagnostic Context\">");
+ sbuf.append("NDC: " + Transform.escapeTags(event.getContextStack().toString()));
+ sbuf.append("</td></tr>").append(LINE_SEP);
+ }
+
+
+ if (event.getContextMap().size() > 0) {
+ sbuf.append(
+ "<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : xx-small;\" colspan=\"6\" title=\"Mapped Diagnostic Context\">");
+ sbuf.append("MDC: " + Transform.escapeTags(event.getContextMap().toString()));
+ sbuf.append("</td></tr>").append(LINE_SEP);
+ }
+
+ Throwable throwable = event.getThrown();
+ if (throwable != null) {
+ sbuf.append("<tr><td bgcolor=\"#993300\" style=\"color:White; font-size : xx-small;\" colspan=\"6\">");
+ appendThrowableAsHTML(throwable, sbuf);
+ sbuf.append("</td></tr>").append(LINE_SEP);
+ }
+
+ return sbuf.toString().getBytes(charset);
+ }
+
+ void appendThrowableAsHTML(Throwable throwable, StringBuilder sbuf) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ try {
+ throwable.printStackTrace(pw);
+ } catch(RuntimeException ex) {
+ }
+ pw.flush();
+ LineNumberReader reader = new LineNumberReader(new StringReader(sw.toString()));
+ ArrayList<String> lines = new ArrayList<String>();
+ try {
+ String line = reader.readLine();
+ while(line != null) {
+ lines.add(line);
+ line = reader.readLine();
+ }
+ } catch(IOException ex) {
+ if (ex instanceof InterruptedIOException) {
+ Thread.currentThread().interrupt();
+ }
+ lines.add(ex.toString());
+ }
+ boolean first = true;
+ for (String line : lines) {
+ if (!first) {
+ sbuf.append(TRACE_PREFIX);
+ } else {
+ first = false;
+ }
+ sbuf.append(Transform.escapeTags(line));
+ sbuf.append(LINE_SEP);
+ }
+ }
+
+ /**
+ * Returns appropriate HTML headers.
+ */
+ @Override
+ public byte[] getHeader() {
+ StringBuilder sbuf = new StringBuilder();
+ sbuf.append(
+ "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">");
+ sbuf.append(LINE_SEP);
+ sbuf.append("<html>").append(LINE_SEP);
+ sbuf.append("<head>").append(LINE_SEP);
+ sbuf.append("<title>").append(title).append("</title>").append(LINE_SEP);
+ sbuf.append("<style type=\"text/css\">").append(LINE_SEP);
+ sbuf.append("<!--").append(LINE_SEP);
+ sbuf.append("body, table {font-family: arial,sans-serif; font-size: x-small;}").append(LINE_SEP);
+ sbuf.append("th {background: #336699; color: #FFFFFF; text-align: left;}").append(LINE_SEP);
+ sbuf.append("-->").append(LINE_SEP);
+ sbuf.append("</style>").append(LINE_SEP);
+ sbuf.append("</head>").append(LINE_SEP);
+ sbuf.append("<body bgcolor=\"#FFFFFF\" topmargin=\"6\" leftmargin=\"6\">").append(LINE_SEP);
+ sbuf.append("<hr size=\"1\" noshade>").append(LINE_SEP);
+ sbuf.append("Log session start time " + new java.util.Date() + "<br>").append(LINE_SEP);
+ sbuf.append("<br>").append(LINE_SEP);
+ sbuf.append(
+ "<table cellspacing=\"0\" cellpadding=\"4\" border=\"1\" bordercolor=\"#224466\" width=\"100%\">");
+ sbuf.append(LINE_SEP);
+ sbuf.append("<tr>").append(LINE_SEP);
+ sbuf.append("<th>Time</th>").append(LINE_SEP);
+ sbuf.append("<th>Thread</th>").append(LINE_SEP);
+ sbuf.append("<th>Level</th>").append(LINE_SEP);
+ sbuf.append("<th>Logger</th>").append(LINE_SEP);
+ if (locationInfo) {
+ sbuf.append("<th>File:Line</th>").append(LINE_SEP);
+ }
+ sbuf.append("<th>Message</th>").append(LINE_SEP);
+ sbuf.append("</tr>").append(LINE_SEP);
+ return sbuf.toString().getBytes(charset);
+ }
+
+ /**
+ * Returns the appropriate HTML footers.
+ */
+ @Override
+ public byte[] getFooter() {
+ StringBuilder sbuf = new StringBuilder();
+ sbuf.append("</table>").append(LINE_SEP);
+ sbuf.append("<br>").append(LINE_SEP);
+ sbuf.append("</body></html>");
+ return sbuf.toString().getBytes(charset);
+ }
+
+ @PluginFactory
+ public static HTMLLayout createLayout(@PluginAttr("locationInfo") String locationInfo,
+ @PluginAttr("title") String title,
+ @PluginAttr("contentType") String contentType,
+ @PluginAttr("charset") String charset) {
+ Charset c = Charset.isSupported("UTF-8") ? Charset.forName("UTF-8") : Charset.defaultCharset();
+ if (charset != null) {
+ if (Charset.isSupported(charset)) {
+ c = Charset.forName(charset);
+ } else {
+ logger.error("Charset " + charset + " is not supported for layout, using " + c.displayName());
+ }
+ }
+ boolean info = locationInfo == null ? false : Boolean.valueOf(locationInfo);
+ if (title == null) {
+ title = DEFAULT_TITLE;
+ }
+ if (contentType == null) {
+ contentType = DEFAULT_CONTENT_TYPE;
+ }
+ return new HTMLLayout(info, title, contentType, c);
+ }
+}
diff --git a/log4j2-core/src/test/java/org/apache/logging/log4j/core/layout/HTMLLayoutTest.java b/log4j2-core/src/test/java/org/apache/logging/log4j/core/layout/HTMLLayoutTest.java
new file mode 100644
index 0000000..3b5b95a
--- /dev/null
+++ b/log4j2-core/src/test/java/org/apache/logging/log4j/core/layout/HTMLLayoutTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.BasicConfigurationFactory;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.ConsoleAppender;
+import org.apache.logging.log4j.core.appender.FileAppender;
+import org.apache.logging.log4j.core.appender.FileManager;
+import org.apache.logging.log4j.core.appender.ListAppender;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.apache.logging.log4j.core.util.Compare;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.FileOutputStream;
+import java.util.List;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ *
+ */
+public class HTMLLayoutTest {
+ LoggerContext ctx = (LoggerContext) LogManager.getContext();
+ Logger root = ctx.getLogger("");
+
+ static ConfigurationFactory cf = new BasicConfigurationFactory();
+
+ @BeforeClass
+ public static void setupClass() {
+ ConfigurationFactory.setConfigurationFactory(cf);
+ LoggerContext ctx = (LoggerContext) LogManager.getContext();
+ ctx.reconfigure();
+ }
+
+ @AfterClass
+ public static void cleanupClass() {
+ ConfigurationFactory.removeConfigurationFactory(cf);
+ }
+
+ private static final String body =
+ "<tr><td bgcolor=\"#993300\" style=\"color:White; font-size : xx-small;\" colspan=\"6\">java.lang.NullPointerException: test";
+
+
+ /**
+ * Test case for MDC conversion pattern.
+ */
+ @Test
+ public void testLayout() throws Exception {
+
+ // set up appender
+ HTMLLayout layout = HTMLLayout.createLayout("true", null, null, null);
+ ListAppender appender = new ListAppender("List", null, layout, true);
+ appender.start();
+
+ // set appender on root and set level to debug
+ root.addAppender(appender);
+ root.setLevel(Level.DEBUG);
+
+ // output starting message
+ root.debug("starting mdc pattern test");
+
+ root.debug("empty mdc");
+
+ ThreadContext.put("key1", "value1");
+ ThreadContext.put("key2", "value2");
+
+ root.debug("filled mdc");
+
+ ThreadContext.remove("key1");
+ ThreadContext.remove("key2");
+
+ root.error("finished mdc pattern test", new NullPointerException("test"));
+
+ appender.stop();
+
+ List<String> list = appender.getMessages();
+
+ assertTrue("Incorrect number of lines. Expected 93, actual " + list.size(), list.size() == 93);
+ assertTrue("Incorrect header", list.get(3).equals("<title>Log4J Log Messages</title>"));
+ assertTrue("Incorrect footer", list.get(92).equals("</body></html>"));
+ assertTrue("Incorrect body", list.get(61).equals(body));
+ }
+}