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