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