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.nio.charset.StandardCharsets; 028import java.util.ArrayList; 029 030import org.apache.logging.log4j.Level; 031import org.apache.logging.log4j.core.Layout; 032import org.apache.logging.log4j.core.LogEvent; 033import org.apache.logging.log4j.core.config.LoggerConfig; 034import org.apache.logging.log4j.core.config.Node; 035import org.apache.logging.log4j.core.config.plugins.Plugin; 036import org.apache.logging.log4j.core.config.plugins.PluginAttribute; 037import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; 038import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; 039import org.apache.logging.log4j.core.config.plugins.PluginFactory; 040import org.apache.logging.log4j.core.util.Transform; 041import org.apache.logging.log4j.util.Strings; 042 043/** 044 * Outputs events as rows in an HTML table on an HTML page. 045 * <p> 046 * Appenders using this layout should have their encoding set to UTF-8 or UTF-16, otherwise events containing non ASCII 047 * characters could result in corrupted log files. 048 * </p> 049 */ 050@Plugin(name = "HtmlLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) 051public final class HtmlLayout extends AbstractStringLayout { 052 053 /** 054 * Default font family: {@value}. 055 */ 056 public static final String DEFAULT_FONT_FAMILY = "arial,sans-serif"; 057 058 private static final String TRACE_PREFIX = "<br /> "; 059 private static final String REGEXP = Strings.LINE_SEPARATOR.equals("\n") ? "\n" : Strings.LINE_SEPARATOR + "|\n"; 060 private static final String DEFAULT_TITLE = "Log4j Log Messages"; 061 private static final String DEFAULT_CONTENT_TYPE = "text/html"; 062 063 private final long jvmStartTime = ManagementFactory.getRuntimeMXBean().getStartTime(); 064 065 // Print no location info by default 066 private final boolean locationInfo; 067 private final String title; 068 private final String contentType; 069 private final String font; 070 private final String fontSize; 071 private final String headerSize; 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 HtmlLayout(final boolean locationInfo, final String title, final String contentType, final Charset charset, 103 final String font, final String fontSize, final String headerSize) { 104 super(charset); 105 this.locationInfo = locationInfo; 106 this.title = title; 107 this.contentType = addCharsetToContentType(contentType); 108 this.font = font; 109 this.fontSize = fontSize; 110 this.headerSize = headerSize; 111 } 112 113 /** 114 * For testing purposes. 115 */ 116 public String getTitle() { 117 return title; 118 } 119 120 /** 121 * For testing purposes. 122 */ 123 public boolean isLocationInfo() { 124 return locationInfo; 125 } 126 127 @Override 128 public boolean requiresLocation() { 129 return locationInfo; 130 } 131 132 private String addCharsetToContentType(final String contentType) { 133 if (contentType == null) { 134 return DEFAULT_CONTENT_TYPE + "; charset=" + getCharset(); 135 } 136 return contentType.contains("charset") ? contentType : contentType + "; charset=" + getCharset(); 137 } 138 139 /** 140 * Formats as a String. 141 * 142 * @param event The Logging Event. 143 * @return A String containing the LogEvent as HTML. 144 */ 145 @Override 146 public String toSerializable(final LogEvent event) { 147 final StringBuilder sbuf = getStringBuilder(); 148 149 sbuf.append(Strings.LINE_SEPARATOR).append("<tr>").append(Strings.LINE_SEPARATOR); 150 151 sbuf.append("<td>"); 152 sbuf.append(event.getTimeMillis() - jvmStartTime); 153 sbuf.append("</td>").append(Strings.LINE_SEPARATOR); 154 155 final String escapedThread = Transform.escapeHtmlTags(event.getThreadName()); 156 sbuf.append("<td title=\"").append(escapedThread).append(" thread\">"); 157 sbuf.append(escapedThread); 158 sbuf.append("</td>").append(Strings.LINE_SEPARATOR); 159 160 sbuf.append("<td title=\"Level\">"); 161 if (event.getLevel().equals(Level.DEBUG)) { 162 sbuf.append("<font color=\"#339933\">"); 163 sbuf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel()))); 164 sbuf.append("</font>"); 165 } else if (event.getLevel().isMoreSpecificThan(Level.WARN)) { 166 sbuf.append("<font color=\"#993300\"><strong>"); 167 sbuf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel()))); 168 sbuf.append("</strong></font>"); 169 } else { 170 sbuf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel()))); 171 } 172 sbuf.append("</td>").append(Strings.LINE_SEPARATOR); 173 174 String escapedLogger = Transform.escapeHtmlTags(event.getLoggerName()); 175 if (Strings.isEmpty(escapedLogger)) { 176 escapedLogger = LoggerConfig.ROOT; 177 } 178 sbuf.append("<td title=\"").append(escapedLogger).append(" logger\">"); 179 sbuf.append(escapedLogger); 180 sbuf.append("</td>").append(Strings.LINE_SEPARATOR); 181 182 if (locationInfo) { 183 final StackTraceElement element = event.getSource(); 184 sbuf.append("<td>"); 185 sbuf.append(Transform.escapeHtmlTags(element.getFileName())); 186 sbuf.append(':'); 187 sbuf.append(element.getLineNumber()); 188 sbuf.append("</td>").append(Strings.LINE_SEPARATOR); 189 } 190 191 sbuf.append("<td title=\"Message\">"); 192 sbuf.append(Transform.escapeHtmlTags(event.getMessage().getFormattedMessage()).replaceAll(REGEXP, "<br />")); 193 sbuf.append("</td>").append(Strings.LINE_SEPARATOR); 194 sbuf.append("</tr>").append(Strings.LINE_SEPARATOR); 195 196 if (event.getContextStack() != null && !event.getContextStack().isEmpty()) { 197 sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : ").append(fontSize); 198 sbuf.append(";\" colspan=\"6\" "); 199 sbuf.append("title=\"Nested Diagnostic Context\">"); 200 sbuf.append("NDC: ").append(Transform.escapeHtmlTags(event.getContextStack().toString())); 201 sbuf.append("</td></tr>").append(Strings.LINE_SEPARATOR); 202 } 203 204 if (event.getContextData() != null && !event.getContextData().isEmpty()) { 205 sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : ").append(fontSize); 206 sbuf.append(";\" colspan=\"6\" "); 207 sbuf.append("title=\"Mapped Diagnostic Context\">"); 208 sbuf.append("MDC: ").append(Transform.escapeHtmlTags(event.getContextData().toMap().toString())); 209 sbuf.append("</td></tr>").append(Strings.LINE_SEPARATOR); 210 } 211 212 final Throwable throwable = event.getThrown(); 213 if (throwable != null) { 214 sbuf.append("<tr><td bgcolor=\"#993300\" style=\"color:White; font-size : ").append(fontSize); 215 sbuf.append(";\" colspan=\"6\">"); 216 appendThrowableAsHtml(throwable, sbuf); 217 sbuf.append("</td></tr>").append(Strings.LINE_SEPARATOR); 218 } 219 220 return sbuf.toString(); 221 } 222 223 @Override 224 /** 225 * @return The content type. 226 */ 227 public String getContentType() { 228 return contentType; 229 } 230 231 private void appendThrowableAsHtml(final Throwable throwable, final StringBuilder sbuf) { 232 final StringWriter sw = new StringWriter(); 233 final PrintWriter pw = new PrintWriter(sw); 234 try { 235 throwable.printStackTrace(pw); 236 } catch (final RuntimeException ex) { 237 // Ignore the exception. 238 } 239 pw.flush(); 240 final LineNumberReader reader = new LineNumberReader(new StringReader(sw.toString())); 241 final ArrayList<String> lines = new ArrayList<>(); 242 try { 243 String line = reader.readLine(); 244 while (line != null) { 245 lines.add(line); 246 line = reader.readLine(); 247 } 248 } catch (final IOException ex) { 249 if (ex instanceof InterruptedIOException) { 250 Thread.currentThread().interrupt(); 251 } 252 lines.add(ex.toString()); 253 } 254 boolean first = true; 255 for (final String line : lines) { 256 if (!first) { 257 sbuf.append(TRACE_PREFIX); 258 } else { 259 first = false; 260 } 261 sbuf.append(Transform.escapeHtmlTags(line)); 262 sbuf.append(Strings.LINE_SEPARATOR); 263 } 264 } 265 266 private StringBuilder appendLs(final StringBuilder sbuilder, final String s) { 267 sbuilder.append(s).append(Strings.LINE_SEPARATOR); 268 return sbuilder; 269 } 270 271 private StringBuilder append(final StringBuilder sbuilder, final String s) { 272 sbuilder.append(s); 273 return sbuilder; 274 } 275 276 /** 277 * Returns appropriate HTML headers. 278 * @return The header as a byte array. 279 */ 280 @Override 281 public byte[] getHeader() { 282 final StringBuilder sbuf = new StringBuilder(); 283 append(sbuf, "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" "); 284 appendLs(sbuf, "\"http://www.w3.org/TR/html4/loose.dtd\">"); 285 appendLs(sbuf, "<html>"); 286 appendLs(sbuf, "<head>"); 287 append(sbuf, "<meta charset=\""); 288 append(sbuf, getCharset().toString()); 289 appendLs(sbuf, "\"/>"); 290 append(sbuf, "<title>").append(title); 291 appendLs(sbuf, "</title>"); 292 appendLs(sbuf, "<style type=\"text/css\">"); 293 appendLs(sbuf, "<!--"); 294 append(sbuf, "body, table {font-family:").append(font).append("; font-size: "); 295 appendLs(sbuf, headerSize).append(";}"); 296 appendLs(sbuf, "th {background: #336699; color: #FFFFFF; text-align: left;}"); 297 appendLs(sbuf, "-->"); 298 appendLs(sbuf, "</style>"); 299 appendLs(sbuf, "</head>"); 300 appendLs(sbuf, "<body bgcolor=\"#FFFFFF\" topmargin=\"6\" leftmargin=\"6\">"); 301 appendLs(sbuf, "<hr size=\"1\" noshade=\"noshade\">"); 302 appendLs(sbuf, "Log session start time " + new java.util.Date() + "<br>"); 303 appendLs(sbuf, "<br>"); 304 appendLs(sbuf, 305 "<table cellspacing=\"0\" cellpadding=\"4\" border=\"1\" bordercolor=\"#224466\" width=\"100%\">"); 306 appendLs(sbuf, "<tr>"); 307 appendLs(sbuf, "<th>Time</th>"); 308 appendLs(sbuf, "<th>Thread</th>"); 309 appendLs(sbuf, "<th>Level</th>"); 310 appendLs(sbuf, "<th>Logger</th>"); 311 if (locationInfo) { 312 appendLs(sbuf, "<th>File:Line</th>"); 313 } 314 appendLs(sbuf, "<th>Message</th>"); 315 appendLs(sbuf, "</tr>"); 316 return sbuf.toString().getBytes(getCharset()); 317 } 318 319 /** 320 * Returns the appropriate HTML footers. 321 * @return the footer as a byte array. 322 */ 323 @Override 324 public byte[] getFooter() { 325 final StringBuilder sbuf = new StringBuilder(); 326 appendLs(sbuf, "</table>"); 327 appendLs(sbuf, "<br>"); 328 appendLs(sbuf, "</body></html>"); 329 return getBytes(sbuf.toString()); 330 } 331 332 /** 333 * Creates an HTML Layout. 334 * @param locationInfo If "true", location information will be included. The default is false. 335 * @param title The title to include in the file header. If none is specified the default title will be used. 336 * @param contentType The content type. Defaults to "text/html". 337 * @param charset The character set to use. If not specified, the default will be used. 338 * @param fontSize The font size of the text. 339 * @param font The font to use for the text. 340 * @return An HTML Layout. 341 */ 342 @PluginFactory 343 public static HtmlLayout createLayout( 344 @PluginAttribute(value = "locationInfo") final boolean locationInfo, 345 @PluginAttribute(value = "title", defaultString = DEFAULT_TITLE) final String title, 346 @PluginAttribute("contentType") String contentType, 347 @PluginAttribute(value = "charset", defaultString = "UTF-8") final Charset charset, 348 @PluginAttribute("fontSize") String fontSize, 349 @PluginAttribute(value = "fontName", defaultString = DEFAULT_FONT_FAMILY) final String font) { 350 final FontSize fs = FontSize.getFontSize(fontSize); 351 fontSize = fs.getFontSize(); 352 final String headerSize = fs.larger().getFontSize(); 353 if (contentType == null) { 354 contentType = DEFAULT_CONTENT_TYPE + "; charset=" + charset; 355 } 356 return new HtmlLayout(locationInfo, title, contentType, charset, font, fontSize, headerSize); 357 } 358 359 /** 360 * Creates an HTML Layout using the default settings. 361 * 362 * @return an HTML Layout. 363 */ 364 public static HtmlLayout createDefaultLayout() { 365 return newBuilder().build(); 366 } 367 368 @PluginBuilderFactory 369 public static Builder newBuilder() { 370 return new Builder(); 371 } 372 373 public static class Builder implements org.apache.logging.log4j.core.util.Builder<HtmlLayout> { 374 375 @PluginBuilderAttribute 376 private boolean locationInfo = false; 377 378 @PluginBuilderAttribute 379 private String title = DEFAULT_TITLE; 380 381 @PluginBuilderAttribute 382 private String contentType = null; // defer default value in order to use specified charset 383 384 @PluginBuilderAttribute 385 private Charset charset = StandardCharsets.UTF_8; 386 387 @PluginBuilderAttribute 388 private FontSize fontSize = FontSize.SMALL; 389 390 @PluginBuilderAttribute 391 private String fontName = DEFAULT_FONT_FAMILY; 392 393 private Builder() { 394 } 395 396 public Builder withLocationInfo(final boolean locationInfo) { 397 this.locationInfo = locationInfo; 398 return this; 399 } 400 401 public Builder withTitle(final String title) { 402 this.title = title; 403 return this; 404 } 405 406 public Builder withContentType(final String contentType) { 407 this.contentType = contentType; 408 return this; 409 } 410 411 public Builder withCharset(final Charset charset) { 412 this.charset = charset; 413 return this; 414 } 415 416 public Builder withFontSize(final FontSize fontSize) { 417 this.fontSize = fontSize; 418 return this; 419 } 420 421 public Builder withFontName(final String fontName) { 422 this.fontName = fontName; 423 return this; 424 } 425 426 @Override 427 public HtmlLayout build() { 428 // TODO: extract charset from content-type 429 if (contentType == null) { 430 contentType = DEFAULT_CONTENT_TYPE + "; charset=" + charset; 431 } 432 return new HtmlLayout(locationInfo, title, contentType, charset, fontName, fontSize.getFontSize(), 433 fontSize.larger().getFontSize()); 434 } 435 } 436}