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