001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements. See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache license, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License. You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the license for the specific language governing permissions and
015     * limitations under the license.
016     */
017    package org.apache.logging.log4j.core.layout;
018    
019    import java.io.IOException;
020    import java.io.InterruptedIOException;
021    import java.io.LineNumberReader;
022    import java.io.PrintWriter;
023    import java.io.StringReader;
024    import java.io.StringWriter;
025    import java.lang.management.ManagementFactory;
026    import java.nio.charset.Charset;
027    import java.util.ArrayList;
028    import java.util.HashMap;
029    import java.util.Map;
030    
031    import org.apache.logging.log4j.Level;
032    import org.apache.logging.log4j.core.LogEvent;
033    import org.apache.logging.log4j.core.config.plugins.Plugin;
034    import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
035    import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
036    import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
037    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
038    import org.apache.logging.log4j.core.util.Charsets;
039    import org.apache.logging.log4j.core.util.Constants;
040    import org.apache.logging.log4j.core.util.Transform;
041    
042    /**
043     * Outputs events as rows in an HTML table on an HTML page.
044     * <p/>
045     * Appenders using this layout should have their encoding set to UTF-8 or UTF-16, otherwise events containing
046     * non ASCII characters could result in corrupted log files.
047     */
048    @Plugin(name = "HtmlLayout", category = "Core", elementType = "layout", printObject = true)
049    public final class HtmlLayout extends AbstractStringLayout {
050    
051        private static final int BUF_SIZE = 256;
052    
053        private static final String TRACE_PREFIX = "<br />&nbsp;&nbsp;&nbsp;&nbsp;";
054    
055        private static final String REGEXP = Constants.LINE_SEPARATOR.equals("\n") ? "\n" : Constants.LINE_SEPARATOR + "|\n";
056    
057        private static final String DEFAULT_TITLE = "Log4j Log Messages";
058    
059        private static final String DEFAULT_CONTENT_TYPE = "text/html";
060    
061        public static final String DEFAULT_FONT_FAMILY = "arial,sans-serif";
062    
063        private final long jvmStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
064    
065        // Print no location info by default
066        private final boolean locationInfo;
067    
068        private final String title;
069    
070        private final String contentType;
071    
072        /**Possible font sizes */
073        public static enum FontSize {
074            SMALLER("smaller"), XXSMALL("xx-small"), XSMALL("x-small"), SMALL("small"), MEDIUM("medium"), LARGE("large"),
075            XLARGE("x-large"), XXLARGE("xx-large"),  LARGER("larger");
076    
077            private final String size;
078    
079            private FontSize(final String size) {
080                this.size = size;
081            }
082    
083            public String getFontSize() {
084                return size;
085            }
086    
087            public static FontSize getFontSize(final String size) {
088                for (final FontSize fontSize : values()) {
089                    if (fontSize.size.equals(size)) {
090                        return fontSize;
091                    }
092                }
093                return SMALL;
094            }
095    
096            public FontSize larger() {
097                return this.ordinal() < XXLARGE.ordinal() ? FontSize.values()[this.ordinal() + 1] : this;
098            }
099        }
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    }