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.Marker;
25  import org.apache.logging.log4j.core.LogEvent;
26  import org.apache.logging.log4j.core.config.plugins.Plugin;
27  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
28  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
29  import org.apache.logging.log4j.core.helpers.Charsets;
30  import org.apache.logging.log4j.core.helpers.Strings;
31  import org.apache.logging.log4j.core.helpers.Throwables;
32  import org.apache.logging.log4j.core.helpers.Transform;
33  import org.apache.logging.log4j.message.Message;
34  import org.apache.logging.log4j.message.MultiformatMessage;
35  
36  
37  /**
38   * Appends a series of {@code event} elements as defined in the <a href="log4j.dtd">log4j.dtd</a>.
39   *
40   * <h4>Complete well-formed XML vs. fragment XML</h4>
41   * <p>
42   * If you configure {@code complete="true"}, the appender outputs a well-formed XML document where the default namespace
43   * is the log4j namespace {@value #XML_NAMESPACE}. By default, with {@code complete="false"}, you should include the
44   * output as an <em>external entity</em> in a separate file to form a well-formed XML document, in which case the
45   * appender uses {@code namespacePrefix} with a default of {@value #DEFAULT_NS_PREFIX}.
46   * </p>
47   * <p>
48   * A well-formed XML document follows this pattern:
49   * </p>
50   *
51   * <pre>
52   * &lt;?xml version="1.0" encoding=&quotUTF-8&quot?&gt;
53   * &lt;Events xmlns="http://logging.apache.org/log4j/2.0/events"&gt;
54   * &nbsp;&nbsp;&lt;Event logger="com.foo.Bar" timestamp="1373436580419" level="INFO" thread="main"&gt;
55   * &nbsp;&nbsp;&nbsp;&nbsp;&lt;Message>&lt;![CDATA[This is a log message 1]]&gt;&lt;/Message&gt;
56   * &nbsp;&nbsp;&lt;/Event&gt;
57   * &nbsp;&nbsp;&lt;Event logger="com.foo.Baz" timestamp="1373436580420" level="INFO" thread="main"&gt;
58   * &nbsp;&nbsp;&nbsp;&nbsp;&lt;Message>&lt;![CDATA[This is a log message 2]]&gt;&lt;/Message&gt;
59   * &nbsp;&nbsp;&lt;/Event&gt;
60   * &lt;/Events&gt;
61   * </pre>
62   * <p>
63   * If {@code complete="false"}, the appender does not write the XML processing instruction and the root element.
64   * </p>
65   * <p>
66   * This approach enforces the independence of the XMLLayout and the appender where you embed it.
67   * </p>
68   * <h4>Encoding</h4>
69   * <p>
70   * Appenders using this layout should have their {@code charset} set to {@code UTF-8} or {@code UTF-16}, otherwise
71   * events containing non ASCII characters could result in corrupted log files.
72   * </p>
73   * <h4>Pretty vs. compact XML</h4>
74   * <p>
75   * By default, the XML layout is not compact (a.k.a. not "pretty") with {@code compact="false"}, which means the
76   * appender uses end-of-line characters and indents lines to format the XML. If {@code compact="true"}, then no
77   * end-of-line or indentation is used. Message content may contain, of course, end-of-lines.
78   * </p>
79   */
80  @Plugin(name = "XMLLayout", category = "Core", elementType = "layout", printObject = true)
81  public class XMLLayout extends AbstractStringLayout {
82  
83      private static final String XML_NAMESPACE = "http://logging.apache.org/log4j/2.0/events";
84      private static final String ROOT_TAG = "Events";
85      private static final int DEFAULT_SIZE = 256;
86  
87      // We yield to \r\n for the default.
88      private static final String DEFAULT_EOL = "\r\n";
89      private static final String COMPACT_EOL = "";
90      private static final String DEFAULT_INDENT = "  ";
91      private static final String COMPACT_INDENT = "";
92      private static final String DEFAULT_NS_PREFIX = "log4j";
93  
94      private static final String[] FORMATS = new String[] {"xml"};
95  
96      private final boolean locationInfo;
97      private final boolean properties;
98      private final boolean complete;
99      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 }