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;
023import java.util.Map.Entry;
024import java.util.Set;
025
026import org.apache.logging.log4j.core.LogEvent;
027import org.apache.logging.log4j.core.config.plugins.Plugin;
028import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
029import org.apache.logging.log4j.core.config.plugins.PluginFactory;
030import org.apache.logging.log4j.core.helpers.Charsets;
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 * Appends a series of JSON events as strings serialized as bytes.
038 * 
039 * <h4>Complete well-formed JSON vs. fragment JSON</h4>
040 * <p>
041 * If you configure {@code complete="true"}, the appender outputs a well-formed JSON document. 
042 * By default, with {@code complete="false"}, you should include the
043 * output as an <em>external file</em> in a separate file to form a well-formed JSON document.
044 * </p>
045 * <p>
046 * A well-formed JSON document follows this pattern:
047 * </p>
048 * 
049 * <pre>[
050 *   {
051 *     "logger":"com.foo.Bar",
052 *     "timestamp":"1376681196470",
053 *     "level":"INFO",
054 *     "thread":"main",
055 *     "message":"Message flushed with immediate flush=true"
056 *   },
057 *   {
058 *     "logger":"com.foo.Bar",
059 *     "timestamp":"1376681196471",
060 *     "level":"ERROR",
061 *     "thread":"main",
062 *     "message":"Message flushed with immediate flush=true",
063 *     "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"
064 *   }
065 * ]</pre>
066 * <p>
067 * If {@code complete="false"}, the appender does not write the JSON open array character "[" at the start of the document.
068 * and "]" and the end.
069 * </p>
070 * <p>
071 * This approach enforces the independence of the JSONLayout and the appender where you embed it.
072 * </p>
073 * <h4>Encoding</h4>
074 * <p>
075 * Appenders using this layout should have their {@code charset} set to {@code UTF-8} or {@code UTF-16}, otherwise
076 * events containing non ASCII characters could result in corrupted log files.
077 * </p>
078 * <h4>Pretty vs. compact XML</h4>
079 * <p>
080 * By default, the JSON layout is not compact (a.k.a. not "pretty") with {@code compact="false"}, which means the
081 * appender uses end-of-line characters and indents lines to format the text. If {@code compact="true"}, then no
082 * end-of-line or indentation is used. Message content may contain, of course, escaped end-of-lines.
083 * </p>
084 */
085@Plugin(name = "JSONLayout", category = "Core", elementType = "layout", printObject = true)
086public class JSONLayout extends AbstractStringLayout {
087
088    private static final int DEFAULT_SIZE = 256;
089
090    // We yield to \r\n for the default.
091    private static final String DEFAULT_EOL = "\r\n";
092    private static final String COMPACT_EOL = "";
093    private static final String DEFAULT_INDENT = "  ";
094    private static final String COMPACT_INDENT = "";
095
096    private static final String[] FORMATS = new String[] { "json" };
097
098    private final boolean locationInfo;
099    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}