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  import java.util.Map.Entry;
24  import java.util.Set;
25  
26  import org.apache.logging.log4j.core.LogEvent;
27  import org.apache.logging.log4j.core.config.plugins.Plugin;
28  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
29  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
30  import org.apache.logging.log4j.core.helpers.Charsets;
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   * Appends a series of JSON events as strings serialized as bytes.
38   * 
39   * <h4>Complete well-formed JSON vs. fragment JSON</h4>
40   * <p>
41   * If you configure {@code complete="true"}, the appender outputs a well-formed JSON document. 
42   * By default, with {@code complete="false"}, you should include the
43   * output as an <em>external file</em> in a separate file to form a well-formed JSON document.
44   * </p>
45   * <p>
46   * A well-formed JSON document follows this pattern:
47   * </p>
48   * 
49   * <pre>[
50   *   {
51   *     "logger":"com.foo.Bar",
52   *     "timestamp":"1376681196470",
53   *     "level":"INFO",
54   *     "thread":"main",
55   *     "message":"Message flushed with immediate flush=true"
56   *   },
57   *   {
58   *     "logger":"com.foo.Bar",
59   *     "timestamp":"1376681196471",
60   *     "level":"ERROR",
61   *     "thread":"main",
62   *     "message":"Message flushed with immediate flush=true",
63   *     "throwable":"java.lang.IllegalArgumentException: badarg\\n\\tat org.apache.logging.log4j.core.appender.JSONCompleteFileAppenderTest.testFlushAtEndOfBatch(JSONCompleteFileAppenderTest.java:54)\\n\\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\\n\\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)\\n\\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\\n\\tat java.lang.reflect.Method.invoke(Method.java:606)\\n\\tat org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)\\n\\tat org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)\\n\\tat org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)\\n\\tat org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)\\n\\tat org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)\\n\\tat org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)\\n\\tat org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)\\n\\tat org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)\\n\\tat org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)\\n\\tat org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)\\n\\tat org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)\\n\\tat org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)\\n\\tat org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)\\n\\tat org.junit.runners.ParentRunner.run(ParentRunner.java:309)\\n\\tat org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)\\n\\tat org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)\\n"
64   *   }
65   * ]</pre>
66   * <p>
67   * If {@code complete="false"}, the appender does not write the JSON open array character "[" at the start of the document.
68   * and "]" and the end.
69   * </p>
70   * <p>
71   * This approach enforces the independence of the JSONLayout and the appender where you embed it.
72   * </p>
73   * <h4>Encoding</h4>
74   * <p>
75   * Appenders using this layout should have their {@code charset} set to {@code UTF-8} or {@code UTF-16}, otherwise
76   * events containing non ASCII characters could result in corrupted log files.
77   * </p>
78   * <h4>Pretty vs. compact XML</h4>
79   * <p>
80   * By default, the JSON layout is not compact (a.k.a. not "pretty") with {@code compact="false"}, which means the
81   * appender uses end-of-line characters and indents lines to format the text. If {@code compact="true"}, then no
82   * end-of-line or indentation is used. Message content may contain, of course, escaped end-of-lines.
83   * </p>
84   */
85  @Plugin(name = "JSONLayout", category = "Core", elementType = "layout", printObject = true)
86  public class JSONLayout extends AbstractStringLayout {
87  
88      private static final int DEFAULT_SIZE = 256;
89  
90      // We yield to \r\n for the default.
91      private static final String DEFAULT_EOL = "\r\n";
92      private static final String COMPACT_EOL = "";
93      private static final String DEFAULT_INDENT = "  ";
94      private static final String COMPACT_INDENT = "";
95  
96      private static final String[] FORMATS = new String[] { "json" };
97  
98      private final boolean locationInfo;
99      private final boolean properties;
100     private final boolean complete;
101     private final String eol;
102     private final String indent1;
103     private final String indent2;
104     private final String indent3;
105     private final String indent4;
106     private volatile boolean firstLayoutDone;
107 
108     protected JSONLayout(final boolean locationInfo, final boolean properties, final boolean complete, boolean compact,
109             final Charset charset) {
110         super(charset);
111         this.locationInfo = locationInfo;
112         this.properties = properties;
113         this.complete = complete;
114         this.eol = compact ? COMPACT_EOL : DEFAULT_EOL;
115         this.indent1 = compact ? COMPACT_INDENT : DEFAULT_INDENT;
116         this.indent2 = this.indent1 + this.indent1;
117         this.indent3 = this.indent2 + this.indent1;
118         this.indent4 = this.indent3 + this.indent1;
119     }
120 
121     /**
122      * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the log4j.dtd.
123      * 
124      * @param event
125      *            The LogEvent.
126      * @return The XML representation of the LogEvent.
127      */
128     @Override
129     public String toSerializable(final LogEvent event) {
130         final StringBuilder buf = new StringBuilder(DEFAULT_SIZE);
131         // DC locking to avoid synchronizing the whole layout.
132         boolean check = this.firstLayoutDone; 
133         if (!this.firstLayoutDone) {
134             synchronized(this) {
135                 check = this.firstLayoutDone;
136                 if (!check) {
137                     this.firstLayoutDone = true;
138                 } else {
139                     buf.append(',');
140                     buf.append(this.eol);                                
141                 }
142             }
143         } else {
144             buf.append(',');
145             buf.append(this.eol);                                            
146         }
147         buf.append(this.indent1);
148         buf.append('{');
149         buf.append(this.eol);
150         buf.append(this.indent2);
151         buf.append("\"logger\":\"");
152         String name = event.getLoggerName();
153         if (name.isEmpty()) {
154             name = "root";
155         }
156         buf.append(Transform.escapeJsonControlCharacters(name));
157         buf.append("\",");
158         buf.append(this.eol);
159         buf.append(this.indent2);
160         buf.append("\"timestamp\":\"");
161         buf.append(event.getMillis());
162         buf.append("\",");
163         buf.append(this.eol);
164         buf.append(this.indent2);
165         buf.append("\"level\":\"");
166         buf.append(Transform.escapeJsonControlCharacters(String.valueOf(event.getLevel())));
167         buf.append("\",");
168         buf.append(this.eol);
169         buf.append(this.indent2);
170         buf.append("\"thread\":\"");
171         buf.append(Transform.escapeJsonControlCharacters(event.getThreadName()));
172         buf.append("\",");
173         buf.append(this.eol);
174 
175         final Message msg = event.getMessage();
176         if (msg != null) {
177             boolean jsonSupported = false;
178             if (msg instanceof MultiformatMessage) {
179                 final String[] formats = ((MultiformatMessage) msg).getFormats();
180                 for (final String format : formats) {
181                     if (format.equalsIgnoreCase("JSON")) {
182                         jsonSupported = true;
183                         break;
184                     }
185                 }
186             }
187             buf.append(this.indent2);
188             buf.append("\"message\":\"");
189             if (jsonSupported) {
190                 buf.append(((MultiformatMessage) msg).getFormattedMessage(FORMATS));
191             } else {
192                 buf.append(Transform.escapeJsonControlCharacters(event.getMessage().getFormattedMessage()));
193             }
194             buf.append('\"');
195         }
196 
197         if (event.getContextStack().getDepth() > 0) {
198             buf.append(",");
199             buf.append(this.eol);
200             buf.append("\"ndc\":");
201             buf.append(Transform.escapeJsonControlCharacters(event.getContextStack().toString()));
202             buf.append("\"");
203         }
204 
205         final Throwable throwable = event.getThrown();
206         if (throwable != null) {
207             buf.append(",");
208             buf.append(this.eol);
209             buf.append(this.indent2);
210             buf.append("\"throwable\":\"");
211             final List<String> list = Throwables.toStringList(throwable);
212             for (final String str : list) {
213                 buf.append(Transform.escapeJsonControlCharacters(str));
214                 buf.append("\\\\n");
215             }
216             buf.append("\"");
217         }
218 
219         if (this.locationInfo) {
220             final StackTraceElement element = event.getSource();
221             buf.append(",");
222             buf.append(this.eol);
223             buf.append(this.indent2);
224             buf.append("\"LocationInfo\":{");
225             buf.append(this.eol);
226             buf.append(this.indent3);
227             buf.append("\"class\":\"");
228             buf.append(Transform.escapeJsonControlCharacters(element.getClassName()));
229             buf.append("\",");
230             buf.append(this.eol);
231             buf.append(this.indent3);
232             buf.append("\"method\":\"");
233             buf.append(Transform.escapeJsonControlCharacters(element.getMethodName()));
234             buf.append("\",");
235             buf.append(this.eol);
236             buf.append(this.indent3);
237             buf.append("\"file\":\"");
238             buf.append(Transform.escapeJsonControlCharacters(element.getFileName()));
239             buf.append("\",");
240             buf.append(this.eol);
241             buf.append(this.indent3);
242             buf.append("\"line\":\"");
243             buf.append(element.getLineNumber());
244             buf.append("\"");
245             buf.append(this.eol);
246             buf.append(this.indent2);
247             buf.append("}");
248         }
249 
250         if (this.properties && event.getContextMap().size() > 0) {
251             buf.append(",");
252             buf.append(this.eol);
253             buf.append(this.indent2);
254             buf.append("\"Properties\":[");
255             buf.append(this.eol);
256             final Set<Entry<String, String>> entrySet = event.getContextMap().entrySet();
257             int i = 1;
258             for (final Map.Entry<String, String> entry : entrySet) {
259                 buf.append(this.indent3);
260                 buf.append('{');
261                 buf.append(this.eol);
262                 buf.append(this.indent4);
263                 buf.append("\"name\":\"");
264                 buf.append(Transform.escapeJsonControlCharacters(entry.getKey()));
265                 buf.append("\",");
266                 buf.append(this.eol);
267                 buf.append(this.indent4);
268                 buf.append("\"value\":\"");
269                 buf.append(Transform.escapeJsonControlCharacters(String.valueOf(entry.getValue())));
270                 buf.append("\"");
271                 buf.append(this.eol);
272                 buf.append(this.indent3);
273                 buf.append("}");
274                 if (i < entrySet.size()) {
275                     buf.append(",");
276                 }
277                 buf.append(this.eol);
278                 i++;
279             }
280             buf.append(this.indent2);
281             buf.append("]");
282         }
283 
284         buf.append(this.eol);
285         buf.append(this.indent1);
286         buf.append("}");
287 
288         return buf.toString();
289     }
290 
291     /**
292      * Returns appropriate JSON headers.
293      * 
294      * @return a byte array containing the header, opening the JSON array.
295      */
296     @Override
297     public byte[] getHeader() {
298         if (!this.complete) {
299             return null;
300         }
301         final StringBuilder buf = new StringBuilder();
302         buf.append('[');
303         buf.append(this.eol);
304         return buf.toString().getBytes(this.getCharset());
305     }
306 
307     /**
308      * Returns appropriate JSON footer.
309      * 
310      * @return a byte array containing the footer, closing the JSON array.
311      */
312     @Override
313     public byte[] getFooter() {
314         if (!this.complete) {
315             return null;
316         }
317         return (this.eol + "]" + this.eol).getBytes(this.getCharset());
318     }
319 
320     /**
321      * XMLLayout's content format is specified by:
322      * <p/>
323      * Key: "dtd" Value: "log4j-events.dtd"
324      * <p/>
325      * Key: "version" Value: "2.0"
326      * 
327      * @return Map of content format keys supporting XMLLayout
328      */
329     @Override
330     public Map<String, String> getContentFormat() {
331         final Map<String, String> result = new HashMap<String, String>();
332         result.put("version", "2.0");
333         return result;
334     }
335 
336     @Override
337     /**
338      * @return The content type.
339      */
340     public String getContentType() {
341         return "application/json; charset=" + this.getCharset();
342     }
343 
344     /**
345      * Creates an XML Layout.
346      * 
347      * @param locationInfo
348      *            If "true", includes the location information in the generated JSON.
349      * @param properties
350      *            If "true", includes the thread context in the generated JSON.
351      * @param completeStr
352      *            If "true", includes the JSON header and footer, defaults to "false".
353      * @param compactStr
354      *            If "true", does not use end-of-lines and indentation, defaults to "false".
355      * @param charsetName
356      *            The character set to use, if {@code null}, uses "UTF-8".
357      * @return An XML Layout.
358      */
359     @PluginFactory
360     public static JSONLayout createLayout(
361             @PluginAttribute("locationInfo") final String locationInfo,
362             @PluginAttribute("properties") final String properties, 
363             @PluginAttribute("complete") final String completeStr,
364             @PluginAttribute("compact") final String compactStr, 
365             @PluginAttribute("charset") final String charsetName) {
366         final Charset charset = Charsets.getSupportedCharset(charsetName, Charsets.UTF_8);
367         final boolean info = Boolean.parseBoolean(locationInfo);
368         final boolean props = Boolean.parseBoolean(properties);
369         final boolean complete = Boolean.parseBoolean(completeStr);
370         final boolean compact = Boolean.parseBoolean(compactStr);
371         return new JSONLayout(info, props, complete, compact, charset);
372     }
373 }