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 */ 017package org.apache.logging.log4j.core.layout; 018 019import java.io.IOException; 020import java.io.InterruptedIOException; 021import java.io.LineNumberReader; 022import java.io.PrintWriter; 023import java.io.StringReader; 024import java.io.StringWriter; 025import java.lang.management.ManagementFactory; 026import java.nio.charset.Charset; 027import java.util.ArrayList; 028import org.apache.logging.log4j.Level; 029import org.apache.logging.log4j.core.Layout; 030import org.apache.logging.log4j.core.LogEvent; 031import org.apache.logging.log4j.core.config.Node; 032import org.apache.logging.log4j.core.config.plugins.Plugin; 033import org.apache.logging.log4j.core.config.plugins.PluginAttribute; 034import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; 035import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; 036import org.apache.logging.log4j.core.config.plugins.PluginFactory; 037import org.apache.logging.log4j.core.util.Constants; 038import 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) 048public 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 /> "; 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}