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