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