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.nio.charset.Charset; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023 024import org.apache.logging.log4j.Marker; 025import org.apache.logging.log4j.core.LogEvent; 026import org.apache.logging.log4j.core.config.plugins.Plugin; 027import org.apache.logging.log4j.core.config.plugins.PluginAttribute; 028import org.apache.logging.log4j.core.config.plugins.PluginFactory; 029import org.apache.logging.log4j.core.helpers.Charsets; 030import org.apache.logging.log4j.core.helpers.Strings; 031import org.apache.logging.log4j.core.helpers.Throwables; 032import org.apache.logging.log4j.core.helpers.Transform; 033import org.apache.logging.log4j.message.Message; 034import org.apache.logging.log4j.message.MultiformatMessage; 035 036 037/** 038 * Appends a series of {@code event} elements as defined in the <a href="log4j.dtd">log4j.dtd</a>. 039 * 040 * <h4>Complete well-formed XML vs. fragment XML</h4> 041 * <p> 042 * If you configure {@code complete="true"}, the appender outputs a well-formed XML document where the default namespace 043 * is the log4j namespace {@value #XML_NAMESPACE}. By default, with {@code complete="false"}, you should include the 044 * output as an <em>external entity</em> in a separate file to form a well-formed XML document, in which case the 045 * appender uses {@code namespacePrefix} with a default of {@value #DEFAULT_NS_PREFIX}. 046 * </p> 047 * <p> 048 * A well-formed XML document follows this pattern: 049 * </p> 050 * 051 * <pre> 052 * <?xml version="1.0" encoding="UTF-8"?> 053 * <Events xmlns="http://logging.apache.org/log4j/2.0/events"> 054 * <Event logger="com.foo.Bar" timestamp="1373436580419" level="INFO" thread="main"> 055 * <Message><![CDATA[This is a log message 1]]></Message> 056 * </Event> 057 * <Event logger="com.foo.Baz" timestamp="1373436580420" level="INFO" thread="main"> 058 * <Message><![CDATA[This is a log message 2]]></Message> 059 * </Event> 060 * </Events> 061 * </pre> 062 * <p> 063 * If {@code complete="false"}, the appender does not write the XML processing instruction and the root element. 064 * </p> 065 * <p> 066 * This approach enforces the independence of the XMLLayout and the appender where you embed it. 067 * </p> 068 * <h4>Encoding</h4> 069 * <p> 070 * Appenders using this layout should have their {@code charset} set to {@code UTF-8} or {@code UTF-16}, otherwise 071 * events containing non ASCII characters could result in corrupted log files. 072 * </p> 073 * <h4>Pretty vs. compact XML</h4> 074 * <p> 075 * By default, the XML layout is not compact (a.k.a. not "pretty") with {@code compact="false"}, which means the 076 * appender uses end-of-line characters and indents lines to format the XML. If {@code compact="true"}, then no 077 * end-of-line or indentation is used. Message content may contain, of course, end-of-lines. 078 * </p> 079 */ 080@Plugin(name = "XMLLayout", category = "Core", elementType = "layout", printObject = true) 081public class XMLLayout extends AbstractStringLayout { 082 083 private static final String XML_NAMESPACE = "http://logging.apache.org/log4j/2.0/events"; 084 private static final String ROOT_TAG = "Events"; 085 private static final int DEFAULT_SIZE = 256; 086 087 // We yield to \r\n for the default. 088 private static final String DEFAULT_EOL = "\r\n"; 089 private static final String COMPACT_EOL = ""; 090 private static final String DEFAULT_INDENT = " "; 091 private static final String COMPACT_INDENT = ""; 092 private static final String DEFAULT_NS_PREFIX = "log4j"; 093 094 private static final String[] FORMATS = new String[] {"xml"}; 095 096 private final boolean locationInfo; 097 private final boolean properties; 098 private final boolean complete; 099 private final String namespacePrefix; 100 private final String eol; 101 private final String indent1; 102 private final String indent2; 103 private final String indent3; 104 105 protected XMLLayout(final boolean locationInfo, final boolean properties, final boolean complete, 106 boolean compact, final String nsPrefix, final Charset charset) { 107 super(charset); 108 this.locationInfo = locationInfo; 109 this.properties = properties; 110 this.complete = complete; 111 this.eol = compact ? COMPACT_EOL : DEFAULT_EOL; 112 this.indent1 = compact ? COMPACT_INDENT : DEFAULT_INDENT; 113 this.indent2 = this.indent1 + this.indent1; 114 this.indent3 = this.indent2 + this.indent1; 115 this.namespacePrefix = (Strings.isEmpty(nsPrefix) ? DEFAULT_NS_PREFIX : nsPrefix) + ":"; 116 } 117 118 /** 119 * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the log4j.dtd. 120 * 121 * @param event The LogEvent. 122 * @return The XML representation of the LogEvent. 123 */ 124 @Override 125 public String toSerializable(final LogEvent event) { 126 final StringBuilder buf = new StringBuilder(DEFAULT_SIZE); 127 128 buf.append(this.indent1); 129 buf.append('<'); 130 if (!complete) { 131 buf.append(this.namespacePrefix); 132 } 133 buf.append("Event logger=\""); 134 String name = event.getLoggerName(); 135 if (name.isEmpty()) { 136 name = "root"; 137 } 138 buf.append(Transform.escapeHtmlTags(name)); 139 buf.append("\" timestamp=\""); 140 buf.append(event.getMillis()); 141 buf.append("\" level=\""); 142 buf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel()))); 143 buf.append("\" thread=\""); 144 buf.append(Transform.escapeHtmlTags(event.getThreadName())); 145 buf.append("\">"); 146 buf.append(this.eol); 147 148 final Message msg = event.getMessage(); 149 if (msg != null) { 150 boolean xmlSupported = false; 151 if (msg instanceof MultiformatMessage) { 152 final String[] formats = ((MultiformatMessage) msg).getFormats(); 153 for (final String format : formats) { 154 if (format.equalsIgnoreCase("XML")) { 155 xmlSupported = true; 156 break; 157 } 158 } 159 } 160 buf.append(this.indent2); 161 buf.append('<'); 162 if (!complete) { 163 buf.append(this.namespacePrefix); 164 } 165 buf.append("Message>"); 166 if (xmlSupported) { 167 buf.append(((MultiformatMessage) msg).getFormattedMessage(FORMATS)); 168 } else { 169 buf.append("<![CDATA["); 170 // Append the rendered message. Also make sure to escape any 171 // existing CDATA sections. 172 Transform.appendEscapingCDATA(buf, event.getMessage().getFormattedMessage()); 173 buf.append("]]>"); 174 } 175 buf.append("</"); 176 if (!complete) { 177 buf.append(this.namespacePrefix); 178 } 179 buf.append("Message>"); 180 buf.append(this.eol); 181 } 182 183 if (event.getContextStack().getDepth() > 0) { 184 buf.append(this.indent2); 185 buf.append('<'); 186 if (!complete) { 187 buf.append(this.namespacePrefix); 188 } 189 buf.append("NDC><![CDATA["); 190 Transform.appendEscapingCDATA(buf, event.getContextStack().toString()); 191 buf.append("]]></"); 192 if (!complete) { 193 buf.append(this.namespacePrefix); 194 } 195 buf.append("NDC>"); 196 buf.append(this.eol); 197 } 198 199 if (event.getMarker() != null) { 200 final Marker marker = event.getMarker(); 201 buf.append(this.indent2); 202 buf.append('<'); 203 if (!complete) { 204 buf.append(this.namespacePrefix); 205 } 206 buf.append("Marker"); 207 final Marker parent = marker.getParent(); 208 if (parent != null) { 209 buf.append(" parent=\"").append(Transform.escapeHtmlTags(parent.getName())).append("\""); 210 } 211 buf.append('>'); 212 buf.append(Transform.escapeHtmlTags(marker.getName())); 213 buf.append("</"); 214 if (!complete) { 215 buf.append(this.namespacePrefix); 216 } 217 buf.append("Marker>"); 218 buf.append(this.eol); 219 } 220 221 final Throwable throwable = event.getThrown(); 222 if (throwable != null) { 223 final List<String> s = Throwables.toStringList(throwable); 224 buf.append(this.indent2); 225 buf.append('<'); 226 if (!complete) { 227 buf.append(this.namespacePrefix); 228 } 229 buf.append("Throwable><![CDATA["); 230 for (final String str : s) { 231 Transform.appendEscapingCDATA(buf, str); 232 buf.append(this.eol); 233 } 234 buf.append("]]></"); 235 if (!complete) { 236 buf.append(this.namespacePrefix); 237 } 238 buf.append("Throwable>"); 239 buf.append(this.eol); 240 } 241 242 if (locationInfo) { 243 final StackTraceElement element = event.getSource(); 244 buf.append(this.indent2); 245 buf.append('<'); 246 if (!complete) { 247 buf.append(this.namespacePrefix); 248 } 249 buf.append("LocationInfo class=\""); 250 buf.append(Transform.escapeHtmlTags(element.getClassName())); 251 buf.append("\" method=\""); 252 buf.append(Transform.escapeHtmlTags(element.getMethodName())); 253 buf.append("\" file=\""); 254 buf.append(Transform.escapeHtmlTags(element.getFileName())); 255 buf.append("\" line=\""); 256 buf.append(element.getLineNumber()); 257 buf.append("\"/>"); 258 buf.append(this.eol); 259 } 260 261 if (properties && event.getContextMap().size() > 0) { 262 buf.append(this.indent2); 263 buf.append('<'); 264 if (!complete) { 265 buf.append(this.namespacePrefix); 266 } 267 buf.append("Properties>"); 268 buf.append(this.eol); 269 for (final Map.Entry<String, String> entry : event.getContextMap().entrySet()) { 270 buf.append(this.indent3); 271 buf.append('<'); 272 if (!complete) { 273 buf.append(this.namespacePrefix); 274 } 275 buf.append("Data name=\""); 276 buf.append(Transform.escapeHtmlTags(entry.getKey())); 277 buf.append("\" value=\""); 278 buf.append(Transform.escapeHtmlTags(String.valueOf(entry.getValue()))); 279 buf.append("\"/>"); 280 buf.append(this.eol); 281 } 282 buf.append(this.indent2); 283 buf.append("</"); 284 if (!complete) { 285 buf.append(this.namespacePrefix); 286 } 287 buf.append("Properties>"); 288 buf.append(this.eol); 289 } 290 291 buf.append(this.indent1); 292 buf.append("</"); 293 if (!complete) { 294 buf.append(this.namespacePrefix); 295 } 296 buf.append("Event>"); 297 buf.append(this.eol); 298 299 return buf.toString(); 300 } 301 302 /** 303 * Returns appropriate XML headers. 304 * <ol> 305 * <li>XML processing instruction</li> 306 * <li>XML root element</li> 307 * </ol> 308 * 309 * @return a byte array containing the header. 310 */ 311 @Override 312 public byte[] getHeader() { 313 if (!complete) { 314 return null; 315 } 316 final StringBuilder buf = new StringBuilder(); 317 buf.append("<?xml version=\"1.0\" encoding=\""); 318 buf.append(this.getCharset().name()); 319 buf.append("\"?>"); 320 buf.append(this.eol); 321 // Make the log4j namespace the default namespace, no need to use more space with a namespace prefix. 322 buf.append('<'); 323 buf.append(ROOT_TAG); 324 buf.append(" xmlns=\"" + XML_NAMESPACE + "\">"); 325 buf.append(this.eol); 326 return buf.toString().getBytes(this.getCharset()); 327 } 328 329 330 /** 331 * Returns appropriate XML footer. 332 * 333 * @return a byte array containing the footer, closing the XML root element. 334 */ 335 @Override 336 public byte[] getFooter() { 337 if (!complete) { 338 return null; 339 } 340 return ("</" + ROOT_TAG + ">" + this.eol).getBytes(getCharset()); 341 } 342 343 /** 344 * XMLLayout's content format is specified by:<p/> 345 * Key: "dtd" Value: "log4j-events.dtd"<p/> 346 * Key: "version" Value: "2.0" 347 * @return Map of content format keys supporting XMLLayout 348 */ 349 @Override 350 public Map<String, String> getContentFormat() { 351 final Map<String, String> result = new HashMap<String, String>(); 352 //result.put("dtd", "log4j-events.dtd"); 353 result.put("xsd", "log4j-events.xsd"); 354 result.put("version", "2.0"); 355 return result; 356 } 357 358 @Override 359 /** 360 * @return The content type. 361 */ 362 public String getContentType() { 363 return "text/xml; charset=" + this.getCharset(); 364 } 365 366 /** 367 * Creates an XML Layout. 368 * 369 * @param locationInfo If "true", includes the location information in the generated XML. 370 * @param properties If "true", includes the thread context in the generated XML. 371 * @param completeStr If "true", includes the XML header and footer, defaults to "false". 372 * @param compactStr If "true", does not use end-of-lines and indentation, defaults to "false". 373 * @param namespacePrefix The namespace prefix, defaults to {@value #DEFAULT_NS_PREFIX} 374 * @param charsetName The character set to use, if {@code null}, uses "UTF-8". 375 * @return An XML Layout. 376 */ 377 @PluginFactory 378 public static XMLLayout createLayout( 379 @PluginAttribute("locationInfo") final String locationInfo, 380 @PluginAttribute("properties") final String properties, 381 @PluginAttribute("complete") final String completeStr, 382 @PluginAttribute("compact") final String compactStr, 383 @PluginAttribute("namespacePrefix") final String namespacePrefix, 384 @PluginAttribute("charset") final String charsetName) { 385 final Charset charset = Charsets.getSupportedCharset(charsetName, Charsets.UTF_8); 386 final boolean info = Boolean.parseBoolean(locationInfo); 387 final boolean props = Boolean.parseBoolean(properties); 388 final boolean complete = Boolean.parseBoolean(completeStr); 389 final boolean compact = Boolean.parseBoolean(compactStr); 390 return new XMLLayout(info, props, complete, compact, namespacePrefix, charset); 391 } 392}