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.io.IOException;
020import java.io.Writer;
021import java.nio.charset.Charset;
022import java.nio.charset.StandardCharsets;
023import java.util.HashMap;
024import java.util.LinkedHashMap;
025import java.util.Map;
026
027import org.apache.logging.log4j.core.Layout;
028import org.apache.logging.log4j.core.LogEvent;
029import org.apache.logging.log4j.core.config.Configuration;
030import org.apache.logging.log4j.core.config.DefaultConfiguration;
031import org.apache.logging.log4j.core.config.Node;
032import org.apache.logging.log4j.core.config.plugins.Plugin;
033import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
034import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
035import org.apache.logging.log4j.core.config.plugins.PluginElement;
036import org.apache.logging.log4j.core.util.KeyValuePair;
037
038/**
039 * Appends a series of JSON events as strings serialized as bytes.
040 *
041 * <h3>Complete well-formed JSON vs. fragment JSON</h3>
042 * <p>
043 * If you configure {@code complete="true"}, the appender outputs a well-formed JSON document. By default, with
044 * {@code complete="false"}, you should include the output as an <em>external file</em> in a separate file to form a
045 * well-formed JSON document.
046 * </p>
047 * <p>
048 * If {@code complete="false"}, the appender does not write the JSON open array character "[" at the start
049 * of the document, "]" and the end, nor comma "," between records.
050 * </p>
051 * <h3>Encoding</h3>
052 * <p>
053 * Appenders using this layout should have their {@code charset} set to {@code UTF-8} or {@code UTF-16}, otherwise
054 * events containing non ASCII characters could result in corrupted log files.
055 * </p>
056 * <h3>Pretty vs. compact JSON</h3>
057 * <p>
058 * By default, the JSON layout is not compact (a.k.a. "pretty") with {@code compact="false"}, which means the
059 * appender uses end-of-line characters and indents lines to format the text. If {@code compact="true"}, then no
060 * end-of-line or indentation is used. Message content may contain, of course, escaped end-of-lines.
061 * </p>
062 * <h3>Additional Fields</h3>
063 * <p>
064 * This property allows addition of custom fields into generated JSON.
065 * {@code <JsonLayout><KeyValuePair key="foo" value="bar"/></JsonLayout>} inserts {@code "foo":"bar"} directly
066 * into JSON output. Supports Lookup expressions.
067 * </p>
068 */
069@Plugin(name = "JsonLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
070public final class JsonLayout extends AbstractJacksonLayout {
071
072    private static final String DEFAULT_FOOTER = "]";
073
074    private static final String DEFAULT_HEADER = "[";
075
076    static final String CONTENT_TYPE = "application/json";
077
078    public static class Builder<B extends Builder<B>> extends AbstractJacksonLayout.Builder<B>
079            implements org.apache.logging.log4j.core.util.Builder<JsonLayout> {
080
081        @PluginBuilderAttribute
082        private boolean propertiesAsList;
083
084        @PluginElement("AdditionalField")
085        private KeyValuePair[] additionalFields;
086
087        public Builder() {
088            super();
089            setCharset(StandardCharsets.UTF_8);
090        }
091
092        @Override
093        public JsonLayout build() {
094            final boolean encodeThreadContextAsList = isProperties() && propertiesAsList;
095            final String headerPattern = toStringOrNull(getHeader());
096            final String footerPattern = toStringOrNull(getFooter());
097            return new JsonLayout(getConfiguration(), isLocationInfo(), isProperties(), encodeThreadContextAsList,
098                    isComplete(), isCompact(), getEventEol(), headerPattern, footerPattern, getCharset(),
099                    isIncludeStacktrace(), isStacktraceAsString(), isIncludeNullDelimiter(),
100                    getAdditionalFields());
101        }
102
103        public boolean isPropertiesAsList() {
104            return propertiesAsList;
105        }
106
107        public B setPropertiesAsList(final boolean propertiesAsList) {
108            this.propertiesAsList = propertiesAsList;
109            return asBuilder();
110        }
111
112        public KeyValuePair[] getAdditionalFields() {
113            return additionalFields;
114        }
115
116        public B setAdditionalFields(KeyValuePair[] additionalFields) {
117            this.additionalFields = additionalFields;
118            return asBuilder();
119        }
120    }
121
122    /**
123     * @deprecated Use {@link #newBuilder()} instead
124     */
125    @Deprecated
126    protected JsonLayout(final Configuration config, final boolean locationInfo, final boolean properties,
127            final boolean encodeThreadContextAsList,
128            final boolean complete, final boolean compact, final boolean eventEol, final String headerPattern,
129            final String footerPattern, final Charset charset, final boolean includeStacktrace) {
130        super(config, new JacksonFactory.JSON(encodeThreadContextAsList, includeStacktrace, false).newWriter(
131                locationInfo, properties, compact),
132                charset, compact, complete, eventEol,
133                PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(headerPattern).setDefaultPattern(DEFAULT_HEADER).build(),
134                PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(footerPattern).setDefaultPattern(DEFAULT_FOOTER).build(),
135                false, null);
136    }
137
138    private JsonLayout(final Configuration config, final boolean locationInfo, final boolean properties,
139                       final boolean encodeThreadContextAsList,
140                       final boolean complete, final boolean compact, final boolean eventEol,
141                       final String headerPattern, final String footerPattern, final Charset charset,
142                       final boolean includeStacktrace, final boolean stacktraceAsString,
143                       final boolean includeNullDelimiter,
144                       final KeyValuePair[] additionalFields) {
145        super(config, new JacksonFactory.JSON(encodeThreadContextAsList, includeStacktrace, stacktraceAsString).newWriter(
146                locationInfo, properties, compact),
147                charset, compact, complete, eventEol,
148                PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(headerPattern).setDefaultPattern(DEFAULT_HEADER).build(),
149                PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(footerPattern).setDefaultPattern(DEFAULT_FOOTER).build(),
150                includeNullDelimiter,
151                additionalFields);
152    }
153
154    /**
155     * Returns appropriate JSON header.
156     *
157     * @return a byte array containing the header, opening the JSON array.
158     */
159    @Override
160    public byte[] getHeader() {
161        if (!this.complete) {
162            return null;
163        }
164        final StringBuilder buf = new StringBuilder();
165        final String str = serializeToString(getHeaderSerializer());
166        if (str != null) {
167            buf.append(str);
168        }
169        buf.append(this.eol);
170        return getBytes(buf.toString());
171    }
172
173    /**
174     * Returns appropriate JSON footer.
175     *
176     * @return a byte array containing the footer, closing the JSON array.
177     */
178    @Override
179    public byte[] getFooter() {
180        if (!this.complete) {
181            return null;
182        }
183        final StringBuilder buf = new StringBuilder();
184        buf.append(this.eol);
185        final String str = serializeToString(getFooterSerializer());
186        if (str != null) {
187            buf.append(str);
188        }
189        buf.append(this.eol);
190        return getBytes(buf.toString());
191    }
192
193    @Override
194    public Map<String, String> getContentFormat() {
195        final Map<String, String> result = new HashMap<>();
196        result.put("version", "2.0");
197        return result;
198    }
199
200    /**
201     * @return The content type.
202     */
203    @Override
204    public String getContentType() {
205        return CONTENT_TYPE + "; charset=" + this.getCharset();
206    }
207
208    /**
209     * Creates a JSON Layout.
210     * @param config
211     *           The plugin configuration.
212     * @param locationInfo
213     *            If "true", includes the location information in the generated JSON.
214     * @param properties
215     *            If "true", includes the thread context map in the generated JSON.
216     * @param propertiesAsList
217     *            If true, the thread context map is included as a list of map entry objects, where each entry has
218     *            a "key" attribute (whose value is the key) and a "value" attribute (whose value is the value).
219     *            Defaults to false, in which case the thread context map is included as a simple map of key-value
220     *            pairs.
221     * @param complete
222     *            If "true", includes the JSON header and footer, and comma between records.
223     * @param compact
224     *            If "true", does not use end-of-lines and indentation, defaults to "false".
225     * @param eventEol
226     *            If "true", forces an EOL after each log event (even if compact is "true"), defaults to "false". This
227     *            allows one even per line, even in compact mode.
228     * @param headerPattern
229     *            The header pattern, defaults to {@code "["} if null.
230     * @param footerPattern
231     *            The header pattern, defaults to {@code "]"} if null.
232     * @param charset
233     *            The character set to use, if {@code null}, uses "UTF-8".
234     * @param includeStacktrace
235     *            If "true", includes the stacktrace of any Throwable in the generated JSON, defaults to "true".
236     * @return A JSON Layout.
237     *
238     * @deprecated Use {@link #newBuilder()} instead
239     */
240    @Deprecated
241    public static JsonLayout createLayout(
242            final Configuration config,
243            final boolean locationInfo,
244            final boolean properties,
245            final boolean propertiesAsList,
246            final boolean complete,
247            final boolean compact,
248            final boolean eventEol,
249            final String headerPattern,
250            final String footerPattern,
251            final Charset charset,
252            final boolean includeStacktrace) {
253        final boolean encodeThreadContextAsList = properties && propertiesAsList;
254        return new JsonLayout(config, locationInfo, properties, encodeThreadContextAsList, complete, compact, eventEol,
255                headerPattern, footerPattern, charset, includeStacktrace, false, false, null);
256    }
257
258    @PluginBuilderFactory
259    public static <B extends Builder<B>> B newBuilder() {
260        return new Builder<B>().asBuilder();
261    }
262
263    /**
264     * Creates a JSON Layout using the default settings. Useful for testing.
265     *
266     * @return A JSON Layout.
267     */
268    public static JsonLayout createDefaultLayout() {
269        return new JsonLayout(new DefaultConfiguration(), false, false, false, false, false, false,
270                DEFAULT_HEADER, DEFAULT_FOOTER, StandardCharsets.UTF_8, true, false, false, null);
271    }
272
273    @Override
274    public void toSerializable(final LogEvent event, final Writer writer) throws IOException {
275        if (complete && eventCount > 0) {
276            writer.append(", ");
277        }
278        super.toSerializable(event, writer);
279    }
280}