View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.layout;
18  
19  import java.nio.charset.Charset;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  
24  import org.apache.logging.log4j.core.LogEvent;
25  import org.apache.logging.log4j.core.config.plugins.Plugin;
26  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
27  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
28  import org.apache.logging.log4j.core.helpers.Charsets;
29  import org.apache.logging.log4j.core.helpers.Strings;
30  import org.apache.logging.log4j.core.helpers.Throwables;
31  import org.apache.logging.log4j.core.helpers.Transform;
32  import org.apache.logging.log4j.message.Message;
33  import org.apache.logging.log4j.message.MultiformatMessage;
34  
35  
36  /**
37   * Appends a series of {@code event} elements as defined in the <a href="log4j.dtd">log4j.dtd</a>.
38   *
39   * <h4>Complete well-formed XML vs. fragment XML</h4>
40   * <p>
41   * If you configure {@code complete="true"}, the appender outputs a well-formed XML document where the default namespace
42   * is the log4j namespace {@value #XML_NAMESPACE}. By default, with {@code complete="false"}, you should include the
43   * output as an <em>external entity</em> in a separate file to form a well-formed XML document, in which case the
44   * appender uses {@code namespacePrefix} with a default of {@value #DEFAULT_NS_PREFIX}.
45   * </p>
46   * <p>
47   * A well-formed XML document follows this pattern:
48   * </p>
49   *
50   * <pre>
51   * &lt;?xml version="1.0" encoding=&quotUTF-8&quot?&gt;
52   * &lt;Events xmlns="http://logging.apache.org/log4j/2.0/events"&gt;
53   * &nbsp;&nbsp;&lt;Event logger="com.foo.Bar" timestamp="1373436580419" level="INFO" thread="main"&gt;
54   * &nbsp;&nbsp;&nbsp;&nbsp;&lt;Message>&lt;![CDATA[This is a log message 1]]&gt;&lt;/Message&gt;
55   * &nbsp;&nbsp;&lt;/Event&gt;
56   * &nbsp;&nbsp;&lt;Event logger="com.foo.Baz" timestamp="1373436580420" level="INFO" thread="main"&gt;
57   * &nbsp;&nbsp;&nbsp;&nbsp;&lt;Message>&lt;![CDATA[This is a log message 2]]&gt;&lt;/Message&gt;
58   * &nbsp;&nbsp;&lt;/Event&gt;
59   * &lt;/Events&gt;
60   * </pre>
61   * <p>
62   * If {@code complete="false"}, the appender does not write the XML processing instruction and the root element.
63   * </p>
64   * <p>
65   * This approach enforces the independence of the XMLLayout and the appender where you embed it.
66   * </p>
67   * <h4>Encoding</h4>
68   * <p>
69   * Appenders using this layout should have their {@code charset} set to {@code UTF-8} or {@code UTF-16}, otherwise
70   * events containing non ASCII characters could result in corrupted log files.
71   * </p>
72   * <h4>Pretty vs. compact XML</h4>
73   * <p>
74   * By default, the XML layout is not compact (a.k.a. not "pretty") with {@code compact="false"}, which means the
75   * appender uses end-of-line characters and indents lines to format the XML. If {@code compact="true"}, then no
76   * end-of-line or indentation is used. Message content may contain, of course, end-of-lines.
77   * </p>
78   */
79  @Plugin(name = "XMLLayout", category = "Core", elementType = "layout", printObject = true)
80  public class XMLLayout extends AbstractStringLayout {
81  
82      private static final String XML_NAMESPACE = "http://logging.apache.org/log4j/2.0/events";
83      private static final String ROOT_TAG = "Events";
84      private static final int DEFAULT_SIZE = 256;
85  
86      // We yield to \r\n for the default.
87      private static final String DEFAULT_EOL = "\r\n";
88      private static final String COMPACT_EOL = "";
89      private static final String DEFAULT_INDENT = "  ";
90      private static final String COMPACT_INDENT = "";
91      private static final String DEFAULT_NS_PREFIX = "log4j";
92  
93      private static final String[] FORMATS = new String[] {"xml"};
94  
95      private final boolean locationInfo;
96      private final boolean properties;
97      private final boolean complete;
98      private final String namespacePrefix;
99      private final String eol;
100     private final String indent1;
101     private final String indent2;
102     private final String indent3;
103 
104     protected XMLLayout(final boolean locationInfo, final boolean properties, final boolean complete,
105                         boolean compact, final String nsPrefix, final Charset charset) {
106         super(charset);
107         this.locationInfo = locationInfo;
108         this.properties = properties;
109         this.complete = complete;
110         this.eol = compact ? COMPACT_EOL : DEFAULT_EOL;
111         this.indent1 = compact ? COMPACT_INDENT : DEFAULT_INDENT;
112         this.indent2 = this.indent1 + this.indent1;
113         this.indent3 = this.indent2 + this.indent1;
114         this.namespacePrefix = (Strings.isEmpty(nsPrefix) ? DEFAULT_NS_PREFIX : nsPrefix) + ":";
115     }
116 
117     /**
118      * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the log4j.dtd.
119      *
120      * @param event The LogEvent.
121      * @return The XML representation of the LogEvent.
122      */
123     @Override
124     public String toSerializable(final LogEvent event) {
125         final StringBuilder buf = new StringBuilder(DEFAULT_SIZE);
126 
127         buf.append(this.indent1);
128         buf.append('<');
129         if (!complete) {
130             buf.append(this.namespacePrefix);
131         }
132         buf.append("Event logger=\"");
133         String name = event.getLoggerName();
134         if (name.isEmpty()) {
135             name = "root";
136         }
137         buf.append(Transform.escapeHtmlTags(name));
138         buf.append("\" timestamp=\"");
139         buf.append(event.getMillis());
140         buf.append("\" level=\"");
141         buf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel())));
142         buf.append("\" thread=\"");
143         buf.append(Transform.escapeHtmlTags(event.getThreadName()));
144         buf.append("\">");
145         buf.append(this.eol);
146 
147         final Message msg = event.getMessage();
148         if (msg != null) {
149             boolean xmlSupported = false;
150             if (msg instanceof MultiformatMessage) {
151                 final String[] formats = ((MultiformatMessage) msg).getFormats();
152                 for (final String format : formats) {
153                     if (format.equalsIgnoreCase("XML")) {
154                         xmlSupported = true;
155                         break;
156                     }
157                 }
158             }
159             buf.append(this.indent2);
160             buf.append('<');
161             if (!complete) {
162                 buf.append(this.namespacePrefix);
163             }
164             buf.append("Message>");
165             if (xmlSupported) {
166                 buf.append(((MultiformatMessage) msg).getFormattedMessage(FORMATS));
167             } else {
168                 buf.append("<![CDATA[");
169                 // Append the rendered message. Also make sure to escape any
170                 // existing CDATA sections.
171                 Transform.appendEscapingCDATA(buf, event.getMessage().getFormattedMessage());
172                 buf.append("]]>");
173             }
174             buf.append("</");
175             if (!complete) {
176                 buf.append(this.namespacePrefix);
177             }
178             buf.append("Message>");
179             buf.append(this.eol);
180         }
181 
182         if (event.getContextStack().getDepth() > 0) {
183             buf.append(this.indent2);
184             buf.append('<');
185             if (!complete) {
186                 buf.append(this.namespacePrefix);
187             }
188             buf.append("NDC><![CDATA[");
189             Transform.appendEscapingCDATA(buf, event.getContextStack().toString());
190             buf.append("]]></");
191             if (!complete) {
192                 buf.append(this.namespacePrefix);
193             }
194             buf.append("NDC>");
195             buf.append(this.eol);
196         }
197 
198         final Throwable throwable = event.getThrown();
199         if (throwable != null) {
200             final List<String> s = Throwables.toStringList(throwable);
201             buf.append(this.indent2);
202             buf.append('<');
203             if (!complete) {
204                 buf.append(this.namespacePrefix);
205             }
206             buf.append("Throwable><![CDATA[");
207             for (final String str : s) {
208                 Transform.appendEscapingCDATA(buf, str);
209                 buf.append(this.eol);
210             }
211             buf.append("]]></");
212             if (!complete) {
213                 buf.append(this.namespacePrefix);
214             }
215             buf.append("Throwable>");
216             buf.append(this.eol);
217         }
218 
219         if (locationInfo) {
220             final StackTraceElement element = event.getSource();
221             buf.append(this.indent2);
222             buf.append('<');
223             if (!complete) {
224                 buf.append(this.namespacePrefix);
225             }
226             buf.append("LocationInfo class=\"");
227             buf.append(Transform.escapeHtmlTags(element.getClassName()));
228             buf.append("\" method=\"");
229             buf.append(Transform.escapeHtmlTags(element.getMethodName()));
230             buf.append("\" file=\"");
231             buf.append(Transform.escapeHtmlTags(element.getFileName()));
232             buf.append("\" line=\"");
233             buf.append(element.getLineNumber());
234             buf.append("\"/>");
235             buf.append(this.eol);
236         }
237 
238         if (properties && event.getContextMap().size() > 0) {
239             buf.append(this.indent2);
240             buf.append('<');
241             if (!complete) {
242                 buf.append(this.namespacePrefix);
243             }
244             buf.append("Properties>");
245             buf.append(this.eol);
246             for (final Map.Entry<String, String> entry : event.getContextMap().entrySet()) {
247                 buf.append(this.indent3);
248                 buf.append('<');
249                 if (!complete) {
250                     buf.append(this.namespacePrefix);
251                 }
252                 buf.append("Data name=\"");
253                 buf.append(Transform.escapeHtmlTags(entry.getKey()));
254                 buf.append("\" value=\"");
255                 buf.append(Transform.escapeHtmlTags(String.valueOf(entry.getValue())));
256                 buf.append("\"/>");
257                 buf.append(this.eol);
258             }
259             buf.append(this.indent2);
260             buf.append("</");
261             if (!complete) {
262                 buf.append(this.namespacePrefix);
263             }
264             buf.append("Properties>");
265             buf.append(this.eol);
266         }
267 
268         buf.append(this.indent1);
269         buf.append("</");
270         if (!complete) {
271             buf.append(this.namespacePrefix);
272         }
273         buf.append("Event>");
274         buf.append(this.eol);
275 
276         return buf.toString();
277     }
278 
279     /**
280      * Returns appropriate XML headers.
281      * <ol>
282      * <li>XML processing instruction</li>
283      * <li>XML root element</li>
284      * </ol>
285      *
286      * @return a byte array containing the header.
287      */
288     @Override
289     public byte[] getHeader() {
290         if (!complete) {
291             return null;
292         }
293         final StringBuilder buf = new StringBuilder();
294         buf.append("<?xml version=\"1.0\" encoding=\"");
295         buf.append(this.getCharset().name());
296         buf.append("\"?>");
297         buf.append(this.eol);
298         // Make the log4j namespace the default namespace, no need to use more space with a namespace prefix.
299         buf.append('<');
300         buf.append(ROOT_TAG);
301         buf.append(" xmlns=\"" + XML_NAMESPACE + "\">");
302         buf.append(this.eol);
303         return buf.toString().getBytes(this.getCharset());
304     }
305 
306 
307     /**
308      * Returns appropriate XML footer.
309      *
310      * @return a byte array containing the footer, closing the XML root element.
311      */
312     @Override
313     public byte[] getFooter() {
314         if (!complete) {
315             return null;
316         }
317         return ("</" + ROOT_TAG + ">" + this.eol).getBytes(getCharset());
318     }
319 
320     /**
321      * XMLLayout's content format is specified by:<p/>
322      * Key: "dtd" Value: "log4j-events.dtd"<p/>
323      * Key: "version" Value: "2.0"
324      * @return Map of content format keys supporting XMLLayout
325      */
326     @Override
327     public Map<String, String> getContentFormat() {
328         final Map<String, String> result = new HashMap<String, String>();
329         //result.put("dtd", "log4j-events.dtd");
330         result.put("xsd", "log4j-events.xsd");
331         result.put("version", "2.0");
332         return result;
333     }
334 
335     @Override
336     /**
337      * @return The content type.
338      */
339     public String getContentType() {
340         return "text/xml; charset=" + this.getCharset();
341     }
342 
343     /**
344      * Creates an XML Layout.
345      *
346      * @param locationInfo If "true", includes the location information in the generated XML.
347      * @param properties If "true", includes the thread context in the generated XML.
348      * @param completeStr If "true", includes the XML header and footer, defaults to "false".
349      * @param compactStr If "true", does not use end-of-lines and indentation, defaults to "false".
350      * @param namespacePrefix The namespace prefix, defaults to {@value #DEFAULT_NS_PREFIX}
351      * @param charsetName The character set to use, if {@code null}, uses "UTF-8".
352      * @return An XML Layout.
353      */
354     @PluginFactory
355     public static XMLLayout createLayout(
356             @PluginAttribute("locationInfo") final String locationInfo,
357             @PluginAttribute("properties") final String properties,
358             @PluginAttribute("complete") final String completeStr,
359             @PluginAttribute("compact") final String compactStr,
360             @PluginAttribute("namespacePrefix") final String namespacePrefix,
361             @PluginAttribute("charset") final String charsetName) {
362         final Charset charset = Charsets.getSupportedCharset(charsetName, Charsets.UTF_8);
363         final boolean info = Boolean.parseBoolean(locationInfo);
364         final boolean props = Boolean.parseBoolean(properties);
365         final boolean complete = Boolean.parseBoolean(completeStr);
366         final boolean compact = Boolean.parseBoolean(compactStr);
367         return new XMLLayout(info, props, complete, compact, namespacePrefix, charset);
368     }
369 }