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