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}