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