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