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 java.io.IOException;
20  import java.io.InterruptedIOException;
21  import java.io.LineNumberReader;
22  import java.io.PrintWriter;
23  import java.io.StringReader;
24  import java.io.StringWriter;
25  import java.lang.management.ManagementFactory;
26  import java.nio.charset.Charset;
27  import java.nio.charset.StandardCharsets;
28  import java.util.ArrayList;
29  
30  import org.apache.logging.log4j.Level;
31  import org.apache.logging.log4j.core.Layout;
32  import org.apache.logging.log4j.core.LogEvent;
33  import org.apache.logging.log4j.core.config.Node;
34  import org.apache.logging.log4j.core.config.plugins.Plugin;
35  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
36  import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
37  import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
38  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
39  import org.apache.logging.log4j.core.util.Constants;
40  import org.apache.logging.log4j.core.util.Transform;
41  
42  /**
43   * Outputs events as rows in an HTML table on an HTML page.
44   * <p>
45   * Appenders using this layout should have their encoding set to UTF-8 or UTF-16, otherwise events containing non ASCII
46   * characters could result in corrupted log files.
47   * </p>
48   */
49  @Plugin(name = "HtmlLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
50  public final class HtmlLayout extends AbstractStringLayout {
51  
52      private static final long serialVersionUID = 1L;
53  
54      private static final int BUF_SIZE = 256;
55  
56      private static final String TRACE_PREFIX = "<br />&nbsp;&nbsp;&nbsp;&nbsp;";
57  
58      private static final String REGEXP = Constants.LINE_SEPARATOR.equals("\n") ? "\n" : Constants.LINE_SEPARATOR + "|\n";
59  
60      private static final String DEFAULT_TITLE = "Log4j Log Messages";
61  
62      private static final String DEFAULT_CONTENT_TYPE = "text/html";
63  
64      public static final String DEFAULT_FONT_FAMILY = "arial,sans-serif";
65  
66      private final long jvmStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
67  
68      // Print no location info by default
69      private final boolean locationInfo;
70  
71      private final String title;
72  
73      private final String contentType;
74  
75      /**Possible font sizes */
76      public static enum FontSize {
77          SMALLER("smaller"), XXSMALL("xx-small"), XSMALL("x-small"), SMALL("small"), MEDIUM("medium"), LARGE("large"),
78          XLARGE("x-large"), XXLARGE("xx-large"),  LARGER("larger");
79  
80          private final String size;
81  
82          private FontSize(final String size) {
83              this.size = size;
84          }
85  
86          public String getFontSize() {
87              return size;
88          }
89  
90          public static FontSize getFontSize(final String size) {
91              for (final FontSize fontSize : values()) {
92                  if (fontSize.size.equals(size)) {
93                      return fontSize;
94                  }
95              }
96              return SMALL;
97          }
98  
99          public FontSize larger() {
100             return this.ordinal() < XXLARGE.ordinal() ? FontSize.values()[this.ordinal() + 1] : this;
101         }
102     }
103 
104     private final String font;
105     private final String fontSize;
106     private final String headerSize;
107 
108     private HtmlLayout(final boolean locationInfo, final String title, final String contentType, final Charset charset,
109             final String font, final String fontSize, final String headerSize) {
110         super(charset);
111         this.locationInfo = locationInfo;
112         this.title = title;
113         this.contentType = addCharsetToContentType(contentType);
114         this.font = font;
115         this.fontSize = fontSize;
116         this.headerSize = headerSize;
117     }
118 
119     private String addCharsetToContentType(final String contentType) {
120         if (contentType == null) {
121             return DEFAULT_CONTENT_TYPE + "; charset=" + getCharset();
122         }
123         return contentType.contains("charset") ? contentType : contentType + "; charset=" + getCharset();
124     }
125 
126     /**
127      * Format as a String.
128      *
129      * @param event The Logging Event.
130      * @return A String containing the LogEvent as HTML.
131      */
132     @Override
133     public String toSerializable(final LogEvent event) {
134         final StringBuilder sbuf = new StringBuilder(BUF_SIZE);
135 
136         sbuf.append(Constants.LINE_SEPARATOR).append("<tr>").append(Constants.LINE_SEPARATOR);
137 
138         sbuf.append("<td>");
139         sbuf.append(event.getTimeMillis() - jvmStartTime);
140         sbuf.append("</td>").append(Constants.LINE_SEPARATOR);
141 
142         final String escapedThread = Transform.escapeHtmlTags(event.getThreadName());
143         sbuf.append("<td title=\"").append(escapedThread).append(" thread\">");
144         sbuf.append(escapedThread);
145         sbuf.append("</td>").append(Constants.LINE_SEPARATOR);
146 
147         sbuf.append("<td title=\"Level\">");
148         if (event.getLevel().equals(Level.DEBUG)) {
149             sbuf.append("<font color=\"#339933\">");
150             sbuf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel())));
151             sbuf.append("</font>");
152         } else if (event.getLevel().isMoreSpecificThan(Level.WARN)) {
153             sbuf.append("<font color=\"#993300\"><strong>");
154             sbuf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel())));
155             sbuf.append("</strong></font>");
156         } else {
157             sbuf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel())));
158         }
159         sbuf.append("</td>").append(Constants.LINE_SEPARATOR);
160 
161         String escapedLogger = Transform.escapeHtmlTags(event.getLoggerName());
162         if (escapedLogger.isEmpty()) {
163             escapedLogger = "root";
164         }
165         sbuf.append("<td title=\"").append(escapedLogger).append(" logger\">");
166         sbuf.append(escapedLogger);
167         sbuf.append("</td>").append(Constants.LINE_SEPARATOR);
168 
169         if (locationInfo) {
170             final StackTraceElement element = event.getSource();
171             sbuf.append("<td>");
172             sbuf.append(Transform.escapeHtmlTags(element.getFileName()));
173             sbuf.append(':');
174             sbuf.append(element.getLineNumber());
175             sbuf.append("</td>").append(Constants.LINE_SEPARATOR);
176         }
177 
178         sbuf.append("<td title=\"Message\">");
179         sbuf.append(Transform.escapeHtmlTags(event.getMessage().getFormattedMessage()).replaceAll(REGEXP, "<br />"));
180         sbuf.append("</td>").append(Constants.LINE_SEPARATOR);
181         sbuf.append("</tr>").append(Constants.LINE_SEPARATOR);
182 
183         if (event.getContextStack() != null && !event.getContextStack().isEmpty()) {
184             sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : ").append(fontSize);
185             sbuf.append(";\" colspan=\"6\" ");
186             sbuf.append("title=\"Nested Diagnostic Context\">");
187             sbuf.append("NDC: ").append(Transform.escapeHtmlTags(event.getContextStack().toString()));
188             sbuf.append("</td></tr>").append(Constants.LINE_SEPARATOR);
189         }
190 
191         if (event.getContextMap() != null && !event.getContextMap().isEmpty()) {
192             sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : ").append(fontSize);
193             sbuf.append(";\" colspan=\"6\" ");
194             sbuf.append("title=\"Mapped Diagnostic Context\">");
195             sbuf.append("MDC: ").append(Transform.escapeHtmlTags(event.getContextMap().toString()));
196             sbuf.append("</td></tr>").append(Constants.LINE_SEPARATOR);
197         }
198 
199         final Throwable throwable = event.getThrown();
200         if (throwable != null) {
201             sbuf.append("<tr><td bgcolor=\"#993300\" style=\"color:White; font-size : ").append(fontSize);
202             sbuf.append(";\" colspan=\"6\">");
203             appendThrowableAsHtml(throwable, sbuf);
204             sbuf.append("</td></tr>").append(Constants.LINE_SEPARATOR);
205         }
206 
207         return sbuf.toString();
208     }
209 
210     @Override
211     /**
212      * @return The content type.
213      */
214     public String getContentType() {
215         return contentType;
216     }
217 
218     private void appendThrowableAsHtml(final Throwable throwable, final StringBuilder sbuf) {
219         final StringWriter sw = new StringWriter();
220         final PrintWriter pw = new PrintWriter(sw);
221         try {
222             throwable.printStackTrace(pw);
223         } catch (final RuntimeException ex) {
224             // Ignore the exception.
225         }
226         pw.flush();
227         final LineNumberReader reader = new LineNumberReader(new StringReader(sw.toString()));
228         final ArrayList<String> lines = new ArrayList<>();
229         try {
230           String line = reader.readLine();
231           while (line != null) {
232             lines.add(line);
233             line = reader.readLine();
234           }
235         } catch (final IOException ex) {
236             if (ex instanceof InterruptedIOException) {
237                 Thread.currentThread().interrupt();
238             }
239             lines.add(ex.toString());
240         }
241         boolean first = true;
242         for (final String line : lines) {
243             if (!first) {
244                 sbuf.append(TRACE_PREFIX);
245             } else {
246                 first = false;
247             }
248             sbuf.append(Transform.escapeHtmlTags(line));
249             sbuf.append(Constants.LINE_SEPARATOR);
250         }
251     }
252 
253     /**
254      * Returns appropriate HTML headers.
255      * @return The header as a byte array.
256      */
257     @Override
258     public byte[] getHeader() {
259         final StringBuilder sbuf = new StringBuilder();
260         sbuf.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" ");
261         sbuf.append("\"http://www.w3.org/TR/html4/loose.dtd\">");
262         sbuf.append(Constants.LINE_SEPARATOR);
263         sbuf.append("<html>").append(Constants.LINE_SEPARATOR);
264         sbuf.append("<head>").append(Constants.LINE_SEPARATOR);
265         sbuf.append("<meta charset=\"").append(getCharset()).append("\"/>").append(Constants.LINE_SEPARATOR);
266         sbuf.append("<title>").append(title).append("</title>").append(Constants.LINE_SEPARATOR);
267         sbuf.append("<style type=\"text/css\">").append(Constants.LINE_SEPARATOR);
268         sbuf.append("<!--").append(Constants.LINE_SEPARATOR);
269         sbuf.append("body, table {font-family:").append(font).append("; font-size: ");
270         sbuf.append(headerSize).append(";}").append(Constants.LINE_SEPARATOR);
271         sbuf.append("th {background: #336699; color: #FFFFFF; text-align: left;}").append(Constants.LINE_SEPARATOR);
272         sbuf.append("-->").append(Constants.LINE_SEPARATOR);
273         sbuf.append("</style>").append(Constants.LINE_SEPARATOR);
274         sbuf.append("</head>").append(Constants.LINE_SEPARATOR);
275         sbuf.append("<body bgcolor=\"#FFFFFF\" topmargin=\"6\" leftmargin=\"6\">").append(Constants.LINE_SEPARATOR);
276         sbuf.append("<hr size=\"1\" noshade=\"noshade\">").append(Constants.LINE_SEPARATOR);
277         sbuf.append("Log session start time " + new java.util.Date() + "<br>").append(Constants.LINE_SEPARATOR);
278         sbuf.append("<br>").append(Constants.LINE_SEPARATOR);
279         sbuf.append(
280             "<table cellspacing=\"0\" cellpadding=\"4\" border=\"1\" bordercolor=\"#224466\" width=\"100%\">");
281         sbuf.append(Constants.LINE_SEPARATOR);
282         sbuf.append("<tr>").append(Constants.LINE_SEPARATOR);
283         sbuf.append("<th>Time</th>").append(Constants.LINE_SEPARATOR);
284         sbuf.append("<th>Thread</th>").append(Constants.LINE_SEPARATOR);
285         sbuf.append("<th>Level</th>").append(Constants.LINE_SEPARATOR);
286         sbuf.append("<th>Logger</th>").append(Constants.LINE_SEPARATOR);
287         if (locationInfo) {
288             sbuf.append("<th>File:Line</th>").append(Constants.LINE_SEPARATOR);
289         }
290         sbuf.append("<th>Message</th>").append(Constants.LINE_SEPARATOR);
291         sbuf.append("</tr>").append(Constants.LINE_SEPARATOR);
292         return sbuf.toString().getBytes(getCharset());
293     }
294 
295     /**
296      * Returns the appropriate HTML footers.
297      * @return the footer as a byet array.
298      */
299     @Override
300     public byte[] getFooter() {
301         final StringBuilder sbuf = new StringBuilder();
302         sbuf.append("</table>").append(Constants.LINE_SEPARATOR);
303         sbuf.append("<br>").append(Constants.LINE_SEPARATOR);
304         sbuf.append("</body></html>");
305         return getBytes(sbuf.toString());
306     }
307 
308     /**
309      * Create an HTML Layout.
310      * @param locationInfo If "true", location information will be included. The default is false.
311      * @param title The title to include in the file header. If none is specified the default title will be used.
312      * @param contentType The content type. Defaults to "text/html".
313      * @param charset The character set to use. If not specified, the default will be used.
314      * @param fontSize The font size of the text.
315      * @param font The font to use for the text.
316      * @return An HTML Layout.
317      */
318     @PluginFactory
319     public static HtmlLayout createLayout(
320             @PluginAttribute(value = "locationInfo", defaultBoolean = false) final boolean locationInfo,
321             @PluginAttribute(value = "title", defaultString = DEFAULT_TITLE) final String title,
322             @PluginAttribute("contentType") String contentType,
323             @PluginAttribute(value = "charset", defaultString = "UTF-8") final Charset charset,
324             @PluginAttribute("fontSize") String fontSize,
325             @PluginAttribute(value = "fontName", defaultString = DEFAULT_FONT_FAMILY) final String font) {
326         final FontSize fs = FontSize.getFontSize(fontSize);
327         fontSize = fs.getFontSize();
328         final String headerSize = fs.larger().getFontSize();
329         if (contentType == null) {
330             contentType = DEFAULT_CONTENT_TYPE + "; charset=" + charset;
331         }
332         return new HtmlLayout(locationInfo, title, contentType, charset, font, fontSize, headerSize);
333     }
334 
335     /**
336      * Creates an HTML Layout using the default settings.
337      *
338      * @return an HTML Layout.
339      */
340     public static HtmlLayout createDefaultLayout() {
341         return newBuilder().build();
342     }
343 
344     @PluginBuilderFactory
345     public static Builder newBuilder() {
346         return new Builder();
347     }
348 
349     public static class Builder implements org.apache.logging.log4j.core.util.Builder<HtmlLayout> {
350 
351         @PluginBuilderAttribute
352         private boolean locationInfo = false;
353 
354         @PluginBuilderAttribute
355         private String title = DEFAULT_TITLE;
356 
357         @PluginBuilderAttribute
358         private String contentType = null; // defer default value in order to use specified charset
359 
360         @PluginBuilderAttribute
361         private Charset charset = StandardCharsets.UTF_8;
362 
363         @PluginBuilderAttribute
364         private FontSize fontSize = FontSize.SMALL;
365 
366         @PluginBuilderAttribute
367         private String fontName = DEFAULT_FONT_FAMILY;
368 
369         private Builder() {
370         }
371 
372         public Builder withLocationInfo(final boolean locationInfo) {
373             this.locationInfo = locationInfo;
374             return this;
375         }
376 
377         public Builder withTitle(final String title) {
378             this.title = title;
379             return this;
380         }
381 
382         public Builder withContentType(final String contentType) {
383             this.contentType = contentType;
384             return this;
385         }
386 
387         public Builder withCharset(final Charset charset) {
388             this.charset = charset;
389             return this;
390         }
391 
392         public Builder withFontSize(final FontSize fontSize) {
393             this.fontSize = fontSize;
394             return this;
395         }
396 
397         public Builder withFontName(final String fontName) {
398             this.fontName = fontName;
399             return this;
400         }
401 
402         @Override
403         public HtmlLayout build() {
404             // TODO: extract charset from content-type
405             if (contentType == null) {
406                 contentType = DEFAULT_CONTENT_TYPE + "; charset=" + charset;
407             }
408             return new HtmlLayout(locationInfo, title, contentType, charset, fontName, fontSize.getFontSize(),
409                 fontSize.larger().getFontSize());
410         }
411     }
412 }