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 org.apache.logging.log4j.Level; 020 import org.apache.logging.log4j.core.LogEvent; 021 import org.apache.logging.log4j.core.config.plugins.Plugin; 022 import org.apache.logging.log4j.core.config.plugins.PluginAttr; 023 import org.apache.logging.log4j.core.config.plugins.PluginFactory; 024 import org.apache.logging.log4j.core.helpers.Charsets; 025 import org.apache.logging.log4j.core.helpers.Constants; 026 import org.apache.logging.log4j.core.helpers.Transform; 027 028 import java.io.IOException; 029 import java.io.InterruptedIOException; 030 import java.io.LineNumberReader; 031 import java.io.PrintWriter; 032 import java.io.StringReader; 033 import java.io.StringWriter; 034 import java.lang.management.ManagementFactory; 035 import java.nio.charset.Charset; 036 import java.util.ArrayList; 037 038 /** 039 * This layout outputs events in a HTML table. 040 * <p/> 041 * Appenders using this layout should have their encoding set to UTF-8 or UTF-16, otherwise events containing 042 * non ASCII characters could result in corrupted log files. 043 */ 044 @Plugin(name = "HTMLLayout", type = "Core", elementType = "layout", printObject = true) 045 public final class HTMLLayout extends AbstractStringLayout { 046 047 private static final int BUF_SIZE = 256; 048 049 private static final String TRACE_PREFIX = "<br /> "; 050 051 private static final String REGEXP = Constants.LINE_SEP.equals("\n") ? "\n" : Constants.LINE_SEP + "|\n"; 052 053 private static final String DEFAULT_TITLE = "Log4J Log Messages"; 054 055 private static final String DEFAULT_CONTENT_TYPE = "text/html"; 056 057 private final long jvmStartTime = ManagementFactory.getRuntimeMXBean().getStartTime(); 058 059 // Print no location info by default 060 private final boolean locationInfo; 061 062 private final String title; 063 064 private final String contentType; 065 066 /**Possible font sizes */ 067 private enum FontSize { 068 SMALLER("smaller"), XXSMALL("xx-small"), XSMALL("x-small"), SMALL("small"), MEDIUM("medium"), LARGE("large"), 069 XLARGE("x-large"), XXLARGE("xx-large"), LARGER("larger"); 070 071 private final String size; 072 073 private FontSize(final String size) { 074 this.size = size; 075 } 076 077 public String getFontSize() { 078 return size; 079 } 080 081 public static FontSize getFontSize(final String size) { 082 for (final FontSize fontSize : values()) { 083 if (fontSize.size.equals(size)) { 084 return fontSize; 085 } 086 } 087 return SMALL; 088 } 089 090 public FontSize larger() { 091 return this.ordinal() < XXLARGE.ordinal() ? FontSize.values()[this.ordinal() + 1] : this; 092 } 093 } 094 095 private final String font; 096 private final String fontSize; 097 private final String headerSize; 098 099 private HTMLLayout(final boolean locationInfo, final String title, final String contentType, final Charset charset, 100 final String font, final String fontSize, final String headerSize) { 101 super(charset); 102 this.locationInfo = locationInfo; 103 this.title = title; 104 this.contentType = contentType; 105 this.font = font; 106 this.fontSize = fontSize; 107 this.headerSize = headerSize; 108 } 109 110 /** 111 * Format as a String. 112 * 113 * @param event The Logging Event. 114 * @return A String containging the LogEvent as HTML. 115 */ 116 public String toSerializable(final LogEvent event) { 117 final StringBuilder sbuf = new StringBuilder(BUF_SIZE); 118 119 sbuf.append(Constants.LINE_SEP).append("<tr>").append(Constants.LINE_SEP); 120 121 sbuf.append("<td>"); 122 sbuf.append(event.getMillis() - jvmStartTime); 123 sbuf.append("</td>").append(Constants.LINE_SEP); 124 125 final String escapedThread = Transform.escapeTags(event.getThreadName()); 126 sbuf.append("<td title=\"").append(escapedThread).append(" thread\">"); 127 sbuf.append(escapedThread); 128 sbuf.append("</td>").append(Constants.LINE_SEP); 129 130 sbuf.append("<td title=\"Level\">"); 131 if (event.getLevel().equals(Level.DEBUG)) { 132 sbuf.append("<font color=\"#339933\">"); 133 sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel()))); 134 sbuf.append("</font>"); 135 } else if (event.getLevel().isAtLeastAsSpecificAs(Level.WARN)) { 136 sbuf.append("<font color=\"#993300\"><strong>"); 137 sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel()))); 138 sbuf.append("</strong></font>"); 139 } else { 140 sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel()))); 141 } 142 sbuf.append("</td>").append(Constants.LINE_SEP); 143 144 String escapedLogger = Transform.escapeTags(event.getLoggerName()); 145 if (escapedLogger.length() == 0) { 146 escapedLogger = "root"; 147 } 148 sbuf.append("<td title=\"").append(escapedLogger).append(" logger\">"); 149 sbuf.append(escapedLogger); 150 sbuf.append("</td>").append(Constants.LINE_SEP); 151 152 if (locationInfo) { 153 final StackTraceElement element = event.getSource(); 154 sbuf.append("<td>"); 155 sbuf.append(Transform.escapeTags(element.getFileName())); 156 sbuf.append(':'); 157 sbuf.append(element.getLineNumber()); 158 sbuf.append("</td>").append(Constants.LINE_SEP); 159 } 160 161 sbuf.append("<td title=\"Message\">"); 162 sbuf.append(Transform.escapeTags(event.getMessage().getFormattedMessage()).replaceAll(REGEXP, "<br />")); 163 sbuf.append("</td>").append(Constants.LINE_SEP); 164 sbuf.append("</tr>").append(Constants.LINE_SEP); 165 166 if (event.getContextStack().getDepth() > 0) { 167 sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : ").append(fontSize); 168 sbuf.append(";\" colspan=\"6\" "); 169 sbuf.append("title=\"Nested Diagnostic Context\">"); 170 sbuf.append("NDC: ").append(Transform.escapeTags(event.getContextStack().toString())); 171 sbuf.append("</td></tr>").append(Constants.LINE_SEP); 172 } 173 174 if (event.getContextMap().size() > 0) { 175 sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : ").append(fontSize); 176 sbuf.append(";\" colspan=\"6\" "); 177 sbuf.append("title=\"Mapped Diagnostic Context\">"); 178 sbuf.append("MDC: ").append(Transform.escapeTags(event.getContextMap().toString())); 179 sbuf.append("</td></tr>").append(Constants.LINE_SEP); 180 } 181 182 final Throwable throwable = event.getThrown(); 183 if (throwable != null) { 184 sbuf.append("<tr><td bgcolor=\"#993300\" style=\"color:White; font-size : ").append(fontSize); 185 sbuf.append(";\" colspan=\"6\">"); 186 appendThrowableAsHTML(throwable, sbuf); 187 sbuf.append("</td></tr>").append(Constants.LINE_SEP); 188 } 189 190 return sbuf.toString(); 191 } 192 193 @Override 194 /** 195 * @return The content type. 196 */ 197 public String getContentType() { 198 return "text/html"; 199 } 200 201 private void appendThrowableAsHTML(final Throwable throwable, final StringBuilder sbuf) { 202 final StringWriter sw = new StringWriter(); 203 final PrintWriter pw = new PrintWriter(sw); 204 try { 205 throwable.printStackTrace(pw); 206 } catch (final RuntimeException ex) { 207 // Ignore the exception. 208 } 209 pw.flush(); 210 final LineNumberReader reader = new LineNumberReader(new StringReader(sw.toString())); 211 final ArrayList<String> lines = new ArrayList<String>(); 212 try { 213 String line = reader.readLine(); 214 while (line != null) { 215 lines.add(line); 216 line = reader.readLine(); 217 } 218 } catch (final IOException ex) { 219 if (ex instanceof InterruptedIOException) { 220 Thread.currentThread().interrupt(); 221 } 222 lines.add(ex.toString()); 223 } 224 boolean first = true; 225 for (final String line : lines) { 226 if (!first) { 227 sbuf.append(TRACE_PREFIX); 228 } else { 229 first = false; 230 } 231 sbuf.append(Transform.escapeTags(line)); 232 sbuf.append(Constants.LINE_SEP); 233 } 234 } 235 236 /** 237 * Returns appropriate HTML headers. 238 * @return The header as a byte array. 239 */ 240 @Override 241 public byte[] getHeader() { 242 final StringBuilder sbuf = new StringBuilder(); 243 sbuf.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" "); 244 sbuf.append("\"http://www.w3.org/TR/html4/loose.dtd\">"); 245 sbuf.append(Constants.LINE_SEP); 246 sbuf.append("<html>").append(Constants.LINE_SEP); 247 sbuf.append("<head>").append(Constants.LINE_SEP); 248 sbuf.append("<title>").append(title).append("</title>").append(Constants.LINE_SEP); 249 sbuf.append("<style type=\"text/css\">").append(Constants.LINE_SEP); 250 sbuf.append("<!--").append(Constants.LINE_SEP); 251 sbuf.append("body, table {font-family:").append(font).append("; font-size: "); 252 sbuf.append(headerSize).append(";}").append(Constants.LINE_SEP); 253 sbuf.append("th {background: #336699; color: #FFFFFF; text-align: left;}").append(Constants.LINE_SEP); 254 sbuf.append("-->").append(Constants.LINE_SEP); 255 sbuf.append("</style>").append(Constants.LINE_SEP); 256 sbuf.append("</head>").append(Constants.LINE_SEP); 257 sbuf.append("<body bgcolor=\"#FFFFFF\" topmargin=\"6\" leftmargin=\"6\">").append(Constants.LINE_SEP); 258 sbuf.append("<hr size=\"1\" noshade>").append(Constants.LINE_SEP); 259 sbuf.append("Log session start time " + new java.util.Date() + "<br>").append(Constants.LINE_SEP); 260 sbuf.append("<br>").append(Constants.LINE_SEP); 261 sbuf.append( 262 "<table cellspacing=\"0\" cellpadding=\"4\" border=\"1\" bordercolor=\"#224466\" width=\"100%\">"); 263 sbuf.append(Constants.LINE_SEP); 264 sbuf.append("<tr>").append(Constants.LINE_SEP); 265 sbuf.append("<th>Time</th>").append(Constants.LINE_SEP); 266 sbuf.append("<th>Thread</th>").append(Constants.LINE_SEP); 267 sbuf.append("<th>Level</th>").append(Constants.LINE_SEP); 268 sbuf.append("<th>Logger</th>").append(Constants.LINE_SEP); 269 if (locationInfo) { 270 sbuf.append("<th>File:Line</th>").append(Constants.LINE_SEP); 271 } 272 sbuf.append("<th>Message</th>").append(Constants.LINE_SEP); 273 sbuf.append("</tr>").append(Constants.LINE_SEP); 274 return sbuf.toString().getBytes(getCharset()); 275 } 276 277 /** 278 * Returns the appropriate HTML footers. 279 * @return the footer as a byet array. 280 */ 281 @Override 282 public byte[] getFooter() { 283 final StringBuilder sbuf = new StringBuilder(); 284 sbuf.append("</table>").append(Constants.LINE_SEP); 285 sbuf.append("<br>").append(Constants.LINE_SEP); 286 sbuf.append("</body></html>"); 287 return sbuf.toString().getBytes(getCharset()); 288 } 289 290 /** 291 * Create an HTML Layout. 292 * @param locationInfo If "true", location information will be included. The default is false. 293 * @param title The title to include in the file header. If none is specified the default title will be used. 294 * @param contentType The content type. Defaults to "text/html". 295 * @param charsetName The character set to use. If not specified, the default will be used. 296 * @param fontSize The font size of the text. 297 * @param font The font to use for the text. 298 * @return An HTML Layout. 299 */ 300 @PluginFactory 301 public static HTMLLayout createLayout(@PluginAttr("locationInfo") final String locationInfo, 302 @PluginAttr("title") String title, 303 @PluginAttr("contentType") String contentType, 304 @PluginAttr("charset") final String charsetName, 305 @PluginAttr("fontSize") String fontSize, 306 @PluginAttr("fontName") String font) { 307 final Charset charset = Charsets.getSupportedCharset(charsetName); 308 if (font == null) { 309 font = "arial,sans-serif"; 310 } 311 final FontSize fs = FontSize.getFontSize(fontSize); 312 fontSize = fs.getFontSize(); 313 final String headerSize = fs.larger().getFontSize(); 314 final boolean info = locationInfo == null ? false : Boolean.valueOf(locationInfo); 315 if (title == null) { 316 title = DEFAULT_TITLE; 317 } 318 if (contentType == null) { 319 contentType = DEFAULT_CONTENT_TYPE; 320 } 321 return new HTMLLayout(info, title, contentType, charset, font, fontSize, headerSize); 322 } 323 }