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.PluginAttr; 035 import org.apache.logging.log4j.core.config.plugins.PluginFactory; 036 import org.apache.logging.log4j.core.helpers.Charsets; 037 import org.apache.logging.log4j.core.helpers.Constants; 038 import org.apache.logging.log4j.core.helpers.Transform; 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", category = "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 @Override 119 public String toSerializable(final LogEvent event) { 120 final StringBuilder sbuf = new StringBuilder(BUF_SIZE); 121 122 sbuf.append(Constants.LINE_SEP).append("<tr>").append(Constants.LINE_SEP); 123 124 sbuf.append("<td>"); 125 sbuf.append(event.getMillis() - jvmStartTime); 126 sbuf.append("</td>").append(Constants.LINE_SEP); 127 128 final String escapedThread = Transform.escapeTags(event.getThreadName()); 129 sbuf.append("<td title=\"").append(escapedThread).append(" thread\">"); 130 sbuf.append(escapedThread); 131 sbuf.append("</td>").append(Constants.LINE_SEP); 132 133 sbuf.append("<td title=\"Level\">"); 134 if (event.getLevel().equals(Level.DEBUG)) { 135 sbuf.append("<font color=\"#339933\">"); 136 sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel()))); 137 sbuf.append("</font>"); 138 } else if (event.getLevel().isAtLeastAsSpecificAs(Level.WARN)) { 139 sbuf.append("<font color=\"#993300\"><strong>"); 140 sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel()))); 141 sbuf.append("</strong></font>"); 142 } else { 143 sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel()))); 144 } 145 sbuf.append("</td>").append(Constants.LINE_SEP); 146 147 String escapedLogger = Transform.escapeTags(event.getLoggerName()); 148 if (escapedLogger.length() == 0) { 149 escapedLogger = "root"; 150 } 151 sbuf.append("<td title=\"").append(escapedLogger).append(" logger\">"); 152 sbuf.append(escapedLogger); 153 sbuf.append("</td>").append(Constants.LINE_SEP); 154 155 if (locationInfo) { 156 final StackTraceElement element = event.getSource(); 157 sbuf.append("<td>"); 158 sbuf.append(Transform.escapeTags(element.getFileName())); 159 sbuf.append(':'); 160 sbuf.append(element.getLineNumber()); 161 sbuf.append("</td>").append(Constants.LINE_SEP); 162 } 163 164 sbuf.append("<td title=\"Message\">"); 165 sbuf.append(Transform.escapeTags(event.getMessage().getFormattedMessage()).replaceAll(REGEXP, "<br />")); 166 sbuf.append("</td>").append(Constants.LINE_SEP); 167 sbuf.append("</tr>").append(Constants.LINE_SEP); 168 169 if (event.getContextStack().getDepth() > 0) { 170 sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : ").append(fontSize); 171 sbuf.append(";\" colspan=\"6\" "); 172 sbuf.append("title=\"Nested Diagnostic Context\">"); 173 sbuf.append("NDC: ").append(Transform.escapeTags(event.getContextStack().toString())); 174 sbuf.append("</td></tr>").append(Constants.LINE_SEP); 175 } 176 177 if (event.getContextMap().size() > 0) { 178 sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : ").append(fontSize); 179 sbuf.append(";\" colspan=\"6\" "); 180 sbuf.append("title=\"Mapped Diagnostic Context\">"); 181 sbuf.append("MDC: ").append(Transform.escapeTags(event.getContextMap().toString())); 182 sbuf.append("</td></tr>").append(Constants.LINE_SEP); 183 } 184 185 final Throwable throwable = event.getThrown(); 186 if (throwable != null) { 187 sbuf.append("<tr><td bgcolor=\"#993300\" style=\"color:White; font-size : ").append(fontSize); 188 sbuf.append(";\" colspan=\"6\">"); 189 appendThrowableAsHTML(throwable, sbuf); 190 sbuf.append("</td></tr>").append(Constants.LINE_SEP); 191 } 192 193 return sbuf.toString(); 194 } 195 196 /** 197 * HTMLLayout's format is sufficiently specified via the content type. The format could be defined via a DTD, 198 * but isn't at this time - returning empty Map/unspecified. 199 * @return empty Map 200 */ 201 @Override 202 public Map<String, String> getContentFormat() { 203 return new HashMap<String, String>(); 204 } 205 206 @Override 207 /** 208 * @return The content type. 209 */ 210 public String getContentType() { 211 return "text/html; charset=" + this.getCharset(); 212 } 213 214 private void appendThrowableAsHTML(final Throwable throwable, final StringBuilder sbuf) { 215 final StringWriter sw = new StringWriter(); 216 final PrintWriter pw = new PrintWriter(sw); 217 try { 218 throwable.printStackTrace(pw); 219 } catch (final RuntimeException ex) { 220 // Ignore the exception. 221 } 222 pw.flush(); 223 final LineNumberReader reader = new LineNumberReader(new StringReader(sw.toString())); 224 final ArrayList<String> lines = new ArrayList<String>(); 225 try { 226 String line = reader.readLine(); 227 while (line != null) { 228 lines.add(line); 229 line = reader.readLine(); 230 } 231 } catch (final IOException ex) { 232 if (ex instanceof InterruptedIOException) { 233 Thread.currentThread().interrupt(); 234 } 235 lines.add(ex.toString()); 236 } 237 boolean first = true; 238 for (final String line : lines) { 239 if (!first) { 240 sbuf.append(TRACE_PREFIX); 241 } else { 242 first = false; 243 } 244 sbuf.append(Transform.escapeTags(line)); 245 sbuf.append(Constants.LINE_SEP); 246 } 247 } 248 249 /** 250 * Returns appropriate HTML headers. 251 * @return The header as a byte array. 252 */ 253 @Override 254 public byte[] getHeader() { 255 final StringBuilder sbuf = new StringBuilder(); 256 sbuf.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" "); 257 sbuf.append("\"http://www.w3.org/TR/html4/loose.dtd\">"); 258 sbuf.append(Constants.LINE_SEP); 259 sbuf.append("<html>").append(Constants.LINE_SEP); 260 sbuf.append("<head>").append(Constants.LINE_SEP); 261 sbuf.append("<meta charset=\"").append(getCharset()).append("\"/>").append(Constants.LINE_SEP); 262 sbuf.append("<title>").append(title).append("</title>").append(Constants.LINE_SEP); 263 sbuf.append("<style type=\"text/css\">").append(Constants.LINE_SEP); 264 sbuf.append("<!--").append(Constants.LINE_SEP); 265 sbuf.append("body, table {font-family:").append(font).append("; font-size: "); 266 sbuf.append(headerSize).append(";}").append(Constants.LINE_SEP); 267 sbuf.append("th {background: #336699; color: #FFFFFF; text-align: left;}").append(Constants.LINE_SEP); 268 sbuf.append("-->").append(Constants.LINE_SEP); 269 sbuf.append("</style>").append(Constants.LINE_SEP); 270 sbuf.append("</head>").append(Constants.LINE_SEP); 271 sbuf.append("<body bgcolor=\"#FFFFFF\" topmargin=\"6\" leftmargin=\"6\">").append(Constants.LINE_SEP); 272 sbuf.append("<hr size=\"1\" noshade>").append(Constants.LINE_SEP); 273 sbuf.append("Log session start time " + new java.util.Date() + "<br>").append(Constants.LINE_SEP); 274 sbuf.append("<br>").append(Constants.LINE_SEP); 275 sbuf.append( 276 "<table cellspacing=\"0\" cellpadding=\"4\" border=\"1\" bordercolor=\"#224466\" width=\"100%\">"); 277 sbuf.append(Constants.LINE_SEP); 278 sbuf.append("<tr>").append(Constants.LINE_SEP); 279 sbuf.append("<th>Time</th>").append(Constants.LINE_SEP); 280 sbuf.append("<th>Thread</th>").append(Constants.LINE_SEP); 281 sbuf.append("<th>Level</th>").append(Constants.LINE_SEP); 282 sbuf.append("<th>Logger</th>").append(Constants.LINE_SEP); 283 if (locationInfo) { 284 sbuf.append("<th>File:Line</th>").append(Constants.LINE_SEP); 285 } 286 sbuf.append("<th>Message</th>").append(Constants.LINE_SEP); 287 sbuf.append("</tr>").append(Constants.LINE_SEP); 288 return sbuf.toString().getBytes(getCharset()); 289 } 290 291 /** 292 * Returns the appropriate HTML footers. 293 * @return the footer as a byet array. 294 */ 295 @Override 296 public byte[] getFooter() { 297 final StringBuilder sbuf = new StringBuilder(); 298 sbuf.append("</table>").append(Constants.LINE_SEP); 299 sbuf.append("<br>").append(Constants.LINE_SEP); 300 sbuf.append("</body></html>"); 301 return sbuf.toString().getBytes(getCharset()); 302 } 303 304 /** 305 * Create an HTML Layout. 306 * @param locationInfo If "true", location information will be included. The default is false. 307 * @param title The title to include in the file header. If none is specified the default title will be used. 308 * @param contentType The content type. Defaults to "text/html". 309 * @param charsetName The character set to use. If not specified, the default will be used. 310 * @param fontSize The font size of the text. 311 * @param font The font to use for the text. 312 * @return An HTML Layout. 313 */ 314 @PluginFactory 315 public static HTMLLayout createLayout(@PluginAttr("locationInfo") final String locationInfo, 316 @PluginAttr("title") String title, 317 @PluginAttr("contentType") String contentType, 318 @PluginAttr("charset") final String charsetName, 319 @PluginAttr("fontSize") String fontSize, 320 @PluginAttr("fontName") String font) { 321 final Charset charset = Charsets.getSupportedCharset(charsetName, Charsets.UTF_8); 322 if (font == null) { 323 font = "arial,sans-serif"; 324 } 325 final FontSize fs = FontSize.getFontSize(fontSize); 326 fontSize = fs.getFontSize(); 327 final String headerSize = fs.larger().getFontSize(); 328 final boolean info = locationInfo == null ? false : Boolean.valueOf(locationInfo); 329 if (title == null) { 330 title = DEFAULT_TITLE; 331 } 332 if (contentType == null) { 333 contentType = DEFAULT_CONTENT_TYPE; 334 } 335 return new HTMLLayout(info, title, contentType, charset, font, fontSize, headerSize); 336 } 337 }