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}