View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.layout;
18  
19  import org.apache.logging.log4j.Level;
20  import org.apache.logging.log4j.core.LogEvent;
21  import org.apache.logging.log4j.core.config.plugins.Plugin;
22  import org.apache.logging.log4j.core.config.plugins.PluginAttr;
23  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
24  import org.apache.logging.log4j.core.helpers.Transform;
25  
26  import java.io.IOException;
27  import java.io.InterruptedIOException;
28  import java.io.LineNumberReader;
29  import java.io.PrintWriter;
30  import java.io.StringReader;
31  import java.io.StringWriter;
32  import java.lang.management.ManagementFactory;
33  import java.nio.charset.Charset;
34  import java.util.ArrayList;
35  
36  /**
37   * This layout outputs events in a HTML table.
38   * <p/>
39   * Appenders using this layout should have their encoding set to UTF-8 or UTF-16, otherwise events containing
40   * non ASCII characters could result in corrupted log files.
41   */
42  @Plugin(name = "HTMLLayout", type = "Core", elementType = "layout", printObject = true)
43  public final class HTMLLayout extends AbstractStringLayout {
44  
45      private static final int BUF_SIZE = 256;
46  
47      private static final String TRACE_PREFIX = "<br>&nbsp;&nbsp;&nbsp;&nbsp;";
48  
49      private static final String LINE_SEP = System.getProperty("line.separator");
50  
51      private static final String DEFAULT_TITLE = "Log4J Log Messages";
52  
53      private static final String DEFAULT_CONTENT_TYPE = "text/html";
54  
55      private final long jvmStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
56  
57      // Print no location info by default
58      private final boolean locationInfo;
59  
60      private final String title;
61  
62      private final String contentType;
63  
64      private HTMLLayout(boolean locationInfo, String title, String contentType, Charset charset) {
65          super(charset);
66          this.locationInfo = locationInfo;
67          this.title = title;
68          this.contentType = contentType;
69      }
70  
71      /**
72       * Format as a String.
73       * @param event The Logging Event.
74       * @return A String containging the LogEvent as HTML.
75       */
76      public String formatAs(LogEvent event) {
77          StringBuilder sbuf = new StringBuilder(BUF_SIZE);
78  
79          sbuf.append(LINE_SEP).append("<tr>").append(LINE_SEP);
80  
81          sbuf.append("<td>");
82          sbuf.append(event.getMillis() - jvmStartTime);
83          sbuf.append("</td>").append(LINE_SEP);
84  
85          String escapedThread = Transform.escapeTags(event.getThreadName());
86          sbuf.append("<td title=\"").append(escapedThread).append(" thread\">");
87          sbuf.append(escapedThread);
88          sbuf.append("</td>").append(LINE_SEP);
89  
90          sbuf.append("<td title=\"Level\">");
91          if (event.getLevel().equals(Level.DEBUG)) {
92              sbuf.append("<font color=\"#339933\">");
93              sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel())));
94              sbuf.append("</font>");
95          } else if (event.getLevel().isAtLeastAsSpecificAs(Level.WARN)) {
96              sbuf.append("<font color=\"#993300\"><strong>");
97              sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel())));
98              sbuf.append("</strong></font>");
99          } else {
100             sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel())));
101         }
102         sbuf.append("</td>").append(LINE_SEP);
103 
104         String escapedLogger = Transform.escapeTags(event.getLoggerName());
105         if (escapedLogger.length() == 0) {
106             escapedLogger = "root";
107         }
108         sbuf.append("<td title=\"").append(escapedLogger).append(" logger\">");
109         sbuf.append(escapedLogger);
110         sbuf.append("</td>").append(LINE_SEP);
111 
112         if (locationInfo) {
113             StackTraceElement element = event.getSource();
114             sbuf.append("<td>");
115             sbuf.append(Transform.escapeTags(element.getFileName()));
116             sbuf.append(':');
117             sbuf.append(element.getLineNumber());
118             sbuf.append("</td>").append(LINE_SEP);
119         }
120 
121         sbuf.append("<td title=\"Message\">");
122         sbuf.append(Transform.escapeTags(event.getMessage().getFormattedMessage()));
123         sbuf.append("</td>").append(LINE_SEP);
124         sbuf.append("</tr>").append(LINE_SEP);
125 
126         if (event.getContextStack().size() > 0) {
127             sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : xx-small;\" colspan=\"6\" ");
128             sbuf.append("title=\"Nested Diagnostic Context\">");
129             sbuf.append("NDC: ").append(Transform.escapeTags(event.getContextStack().toString()));
130             sbuf.append("</td></tr>").append(LINE_SEP);
131         }
132 
133         if (event.getContextMap().size() > 0) {
134             sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : xx-small;\" colspan=\"6\" ");
135             sbuf.append("title=\"Mapped Diagnostic Context\">");
136             sbuf.append("MDC: ").append(Transform.escapeTags(event.getContextMap().toString()));
137             sbuf.append("</td></tr>").append(LINE_SEP);
138         }
139 
140         Throwable throwable = event.getThrown();
141         if (throwable != null) {
142             sbuf.append("<tr><td bgcolor=\"#993300\" style=\"color:White; font-size : xx-small;\" colspan=\"6\">");
143             appendThrowableAsHTML(throwable, sbuf);
144             sbuf.append("</td></tr>").append(LINE_SEP);
145         }
146 
147         return sbuf.toString();
148     }
149 
150     private void appendThrowableAsHTML(Throwable throwable, StringBuilder sbuf) {
151         StringWriter sw = new StringWriter();
152         PrintWriter pw = new PrintWriter(sw);
153         try {
154             throwable.printStackTrace(pw);
155         } catch (RuntimeException ex) {
156             // Ignore the exception.
157         }
158         pw.flush();
159         LineNumberReader reader = new LineNumberReader(new StringReader(sw.toString()));
160         ArrayList<String> lines = new ArrayList<String>();
161         try {
162           String line = reader.readLine();
163           while (line != null) {
164             lines.add(line);
165             line = reader.readLine();
166           }
167         } catch (IOException ex) {
168             if (ex instanceof InterruptedIOException) {
169                 Thread.currentThread().interrupt();
170             }
171             lines.add(ex.toString());
172         }
173         boolean first = true;
174         for (String line : lines) {
175             if (!first) {
176                 sbuf.append(TRACE_PREFIX);
177             } else {
178                 first = false;
179             }
180             sbuf.append(Transform.escapeTags(line));
181             sbuf.append(LINE_SEP);
182         }
183     }
184 
185     /**
186      * Returns appropriate HTML headers.
187      * @return The header as a byte array.
188      */
189     @Override
190     public byte[] getHeader() {
191         StringBuilder sbuf = new StringBuilder();
192         sbuf.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" ");
193         sbuf.append("\"http://www.w3.org/TR/html4/loose.dtd\">");
194         sbuf.append(LINE_SEP);
195         sbuf.append("<html>").append(LINE_SEP);
196         sbuf.append("<head>").append(LINE_SEP);
197         sbuf.append("<title>").append(title).append("</title>").append(LINE_SEP);
198         sbuf.append("<style type=\"text/css\">").append(LINE_SEP);
199         sbuf.append("<!--").append(LINE_SEP);
200         sbuf.append("body, table {font-family: arial,sans-serif; font-size: x-small;}").append(LINE_SEP);
201         sbuf.append("th {background: #336699; color: #FFFFFF; text-align: left;}").append(LINE_SEP);
202         sbuf.append("-->").append(LINE_SEP);
203         sbuf.append("</style>").append(LINE_SEP);
204         sbuf.append("</head>").append(LINE_SEP);
205         sbuf.append("<body bgcolor=\"#FFFFFF\" topmargin=\"6\" leftmargin=\"6\">").append(LINE_SEP);
206         sbuf.append("<hr size=\"1\" noshade>").append(LINE_SEP);
207         sbuf.append("Log session start time " + new java.util.Date() + "<br>").append(LINE_SEP);
208         sbuf.append("<br>").append(LINE_SEP);
209         sbuf.append(
210             "<table cellspacing=\"0\" cellpadding=\"4\" border=\"1\" bordercolor=\"#224466\" width=\"100%\">");
211         sbuf.append(LINE_SEP);
212         sbuf.append("<tr>").append(LINE_SEP);
213         sbuf.append("<th>Time</th>").append(LINE_SEP);
214         sbuf.append("<th>Thread</th>").append(LINE_SEP);
215         sbuf.append("<th>Level</th>").append(LINE_SEP);
216         sbuf.append("<th>Logger</th>").append(LINE_SEP);
217         if (locationInfo) {
218             sbuf.append("<th>File:Line</th>").append(LINE_SEP);
219         }
220         sbuf.append("<th>Message</th>").append(LINE_SEP);
221         sbuf.append("</tr>").append(LINE_SEP);
222         return sbuf.toString().getBytes(getCharset());
223     }
224 
225     /**
226      * Returns the appropriate HTML footers.
227      * @return the footer as a byet array.
228      */
229     @Override
230     public byte[] getFooter() {
231         StringBuilder sbuf = new StringBuilder();
232         sbuf.append("</table>").append(LINE_SEP);
233         sbuf.append("<br>").append(LINE_SEP);
234         sbuf.append("</body></html>");
235         return sbuf.toString().getBytes(getCharset());
236     }
237 
238     /**
239      * Create an HTML Layout.
240      * @param locationInfo If "true", location information will be included. The default is false.
241      * @param title The title to include in the file header. If none is specified the default title will be used.
242      * @param contentType The content type. Defaults to "text/html".
243      * @param charset The character set to use. If not specified, the default will be used.
244      * @return An HTML Layout.
245      */
246     @PluginFactory
247     public static HTMLLayout createLayout(@PluginAttr("locationInfo") String locationInfo,
248                                           @PluginAttr("title") String title,
249                                           @PluginAttr("contentType") String contentType,
250                                           @PluginAttr("charset") String charset) {
251         Charset c = Charset.isSupported("UTF-8") ? Charset.forName("UTF-8") : Charset.defaultCharset();
252         if (charset != null) {
253             if (Charset.isSupported(charset)) {
254                 c = Charset.forName(charset);
255             } else {
256                 LOGGER.error("Charset " + charset + " is not supported for layout, using " + c.displayName());
257             }
258         }
259         boolean info = locationInfo == null ? false : Boolean.valueOf(locationInfo);
260         if (title == null) {
261             title = DEFAULT_TITLE;
262         }
263         if (contentType == null) {
264             contentType = DEFAULT_CONTENT_TYPE;
265         }
266         return new HTMLLayout(info, title, contentType, c);
267     }
268 }