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.nio.charset.Charset; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Map.Entry; 024import java.util.Set; 025 026import org.apache.logging.log4j.core.LogEvent; 027import org.apache.logging.log4j.core.config.plugins.Plugin; 028import org.apache.logging.log4j.core.config.plugins.PluginAttribute; 029import org.apache.logging.log4j.core.config.plugins.PluginFactory; 030import org.apache.logging.log4j.core.helpers.Charsets; 031import org.apache.logging.log4j.core.helpers.Throwables; 032import org.apache.logging.log4j.core.helpers.Transform; 033import org.apache.logging.log4j.message.Message; 034import org.apache.logging.log4j.message.MultiformatMessage; 035 036/** 037 * Appends a series of JSON events as strings serialized as bytes. 038 * 039 * <h4>Complete well-formed JSON vs. fragment JSON</h4> 040 * <p> 041 * If you configure {@code complete="true"}, the appender outputs a well-formed JSON document. 042 * By default, with {@code complete="false"}, you should include the 043 * output as an <em>external file</em> in a separate file to form a well-formed JSON document. 044 * </p> 045 * <p> 046 * A well-formed JSON document follows this pattern: 047 * </p> 048 * 049 * <pre>[ 050 * { 051 * "logger":"com.foo.Bar", 052 * "timestamp":"1376681196470", 053 * "level":"INFO", 054 * "thread":"main", 055 * "message":"Message flushed with immediate flush=true" 056 * }, 057 * { 058 * "logger":"com.foo.Bar", 059 * "timestamp":"1376681196471", 060 * "level":"ERROR", 061 * "thread":"main", 062 * "message":"Message flushed with immediate flush=true", 063 * "throwable":"java.lang.IllegalArgumentException: badarg\\n\\tat org.apache.logging.log4j.core.appender.JSONCompleteFileAppenderTest.testFlushAtEndOfBatch(JSONCompleteFileAppenderTest.java:54)\\n\\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\\n\\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)\\n\\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\\n\\tat java.lang.reflect.Method.invoke(Method.java:606)\\n\\tat org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)\\n\\tat org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)\\n\\tat org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)\\n\\tat org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)\\n\\tat org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)\\n\\tat org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)\\n\\tat org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)\\n\\tat org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)\\n\\tat org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)\\n\\tat org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)\\n\\tat org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)\\n\\tat org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)\\n\\tat org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)\\n\\tat org.junit.runners.ParentRunner.run(ParentRunner.java:309)\\n\\tat org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)\\n\\tat org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)\\n\\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)\\n" 064 * } 065 * ]</pre> 066 * <p> 067 * If {@code complete="false"}, the appender does not write the JSON open array character "[" at the start of the document. 068 * and "]" and the end. 069 * </p> 070 * <p> 071 * This approach enforces the independence of the JSONLayout and the appender where you embed it. 072 * </p> 073 * <h4>Encoding</h4> 074 * <p> 075 * Appenders using this layout should have their {@code charset} set to {@code UTF-8} or {@code UTF-16}, otherwise 076 * events containing non ASCII characters could result in corrupted log files. 077 * </p> 078 * <h4>Pretty vs. compact XML</h4> 079 * <p> 080 * By default, the JSON layout is not compact (a.k.a. not "pretty") with {@code compact="false"}, which means the 081 * appender uses end-of-line characters and indents lines to format the text. If {@code compact="true"}, then no 082 * end-of-line or indentation is used. Message content may contain, of course, escaped end-of-lines. 083 * </p> 084 */ 085@Plugin(name = "JSONLayout", category = "Core", elementType = "layout", printObject = true) 086public class JSONLayout extends AbstractStringLayout { 087 088 private static final int DEFAULT_SIZE = 256; 089 090 // We yield to \r\n for the default. 091 private static final String DEFAULT_EOL = "\r\n"; 092 private static final String COMPACT_EOL = ""; 093 private static final String DEFAULT_INDENT = " "; 094 private static final String COMPACT_INDENT = ""; 095 096 private static final String[] FORMATS = new String[] { "json" }; 097 098 private final boolean locationInfo; 099 private final boolean properties; 100 private final boolean complete; 101 private final String eol; 102 private final String indent1; 103 private final String indent2; 104 private final String indent3; 105 private final String indent4; 106 private volatile boolean firstLayoutDone; 107 108 protected JSONLayout(final boolean locationInfo, final boolean properties, final boolean complete, boolean compact, 109 final Charset charset) { 110 super(charset); 111 this.locationInfo = locationInfo; 112 this.properties = properties; 113 this.complete = complete; 114 this.eol = compact ? COMPACT_EOL : DEFAULT_EOL; 115 this.indent1 = compact ? COMPACT_INDENT : DEFAULT_INDENT; 116 this.indent2 = this.indent1 + this.indent1; 117 this.indent3 = this.indent2 + this.indent1; 118 this.indent4 = this.indent3 + this.indent1; 119 } 120 121 /** 122 * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the log4j.dtd. 123 * 124 * @param event 125 * The LogEvent. 126 * @return The XML representation of the LogEvent. 127 */ 128 @Override 129 public String toSerializable(final LogEvent event) { 130 final StringBuilder buf = new StringBuilder(DEFAULT_SIZE); 131 // DC locking to avoid synchronizing the whole layout. 132 boolean check = this.firstLayoutDone; 133 if (!this.firstLayoutDone) { 134 synchronized(this) { 135 check = this.firstLayoutDone; 136 if (!check) { 137 this.firstLayoutDone = true; 138 } else { 139 buf.append(','); 140 buf.append(this.eol); 141 } 142 } 143 } else { 144 buf.append(','); 145 buf.append(this.eol); 146 } 147 buf.append(this.indent1); 148 buf.append('{'); 149 buf.append(this.eol); 150 buf.append(this.indent2); 151 buf.append("\"logger\":\""); 152 String name = event.getLoggerName(); 153 if (name.isEmpty()) { 154 name = "root"; 155 } 156 buf.append(Transform.escapeJsonControlCharacters(name)); 157 buf.append("\","); 158 buf.append(this.eol); 159 buf.append(this.indent2); 160 buf.append("\"timestamp\":\""); 161 buf.append(event.getMillis()); 162 buf.append("\","); 163 buf.append(this.eol); 164 buf.append(this.indent2); 165 buf.append("\"level\":\""); 166 buf.append(Transform.escapeJsonControlCharacters(String.valueOf(event.getLevel()))); 167 buf.append("\","); 168 buf.append(this.eol); 169 buf.append(this.indent2); 170 buf.append("\"thread\":\""); 171 buf.append(Transform.escapeJsonControlCharacters(event.getThreadName())); 172 buf.append("\","); 173 buf.append(this.eol); 174 175 final Message msg = event.getMessage(); 176 if (msg != null) { 177 boolean jsonSupported = false; 178 if (msg instanceof MultiformatMessage) { 179 final String[] formats = ((MultiformatMessage) msg).getFormats(); 180 for (final String format : formats) { 181 if (format.equalsIgnoreCase("JSON")) { 182 jsonSupported = true; 183 break; 184 } 185 } 186 } 187 buf.append(this.indent2); 188 buf.append("\"message\":\""); 189 if (jsonSupported) { 190 buf.append(((MultiformatMessage) msg).getFormattedMessage(FORMATS)); 191 } else { 192 buf.append(Transform.escapeJsonControlCharacters(event.getMessage().getFormattedMessage())); 193 } 194 buf.append('\"'); 195 } 196 197 if (event.getContextStack().getDepth() > 0) { 198 buf.append(","); 199 buf.append(this.eol); 200 buf.append("\"ndc\":"); 201 buf.append(Transform.escapeJsonControlCharacters(event.getContextStack().toString())); 202 buf.append("\""); 203 } 204 205 final Throwable throwable = event.getThrown(); 206 if (throwable != null) { 207 buf.append(","); 208 buf.append(this.eol); 209 buf.append(this.indent2); 210 buf.append("\"throwable\":\""); 211 final List<String> list = Throwables.toStringList(throwable); 212 for (final String str : list) { 213 buf.append(Transform.escapeJsonControlCharacters(str)); 214 buf.append("\\\\n"); 215 } 216 buf.append("\""); 217 } 218 219 if (this.locationInfo) { 220 final StackTraceElement element = event.getSource(); 221 buf.append(","); 222 buf.append(this.eol); 223 buf.append(this.indent2); 224 buf.append("\"LocationInfo\":{"); 225 buf.append(this.eol); 226 buf.append(this.indent3); 227 buf.append("\"class\":\""); 228 buf.append(Transform.escapeJsonControlCharacters(element.getClassName())); 229 buf.append("\","); 230 buf.append(this.eol); 231 buf.append(this.indent3); 232 buf.append("\"method\":\""); 233 buf.append(Transform.escapeJsonControlCharacters(element.getMethodName())); 234 buf.append("\","); 235 buf.append(this.eol); 236 buf.append(this.indent3); 237 buf.append("\"file\":\""); 238 buf.append(Transform.escapeJsonControlCharacters(element.getFileName())); 239 buf.append("\","); 240 buf.append(this.eol); 241 buf.append(this.indent3); 242 buf.append("\"line\":\""); 243 buf.append(element.getLineNumber()); 244 buf.append("\""); 245 buf.append(this.eol); 246 buf.append(this.indent2); 247 buf.append("}"); 248 } 249 250 if (this.properties && event.getContextMap().size() > 0) { 251 buf.append(","); 252 buf.append(this.eol); 253 buf.append(this.indent2); 254 buf.append("\"Properties\":["); 255 buf.append(this.eol); 256 final Set<Entry<String, String>> entrySet = event.getContextMap().entrySet(); 257 int i = 1; 258 for (final Map.Entry<String, String> entry : entrySet) { 259 buf.append(this.indent3); 260 buf.append('{'); 261 buf.append(this.eol); 262 buf.append(this.indent4); 263 buf.append("\"name\":\""); 264 buf.append(Transform.escapeJsonControlCharacters(entry.getKey())); 265 buf.append("\","); 266 buf.append(this.eol); 267 buf.append(this.indent4); 268 buf.append("\"value\":\""); 269 buf.append(Transform.escapeJsonControlCharacters(String.valueOf(entry.getValue()))); 270 buf.append("\""); 271 buf.append(this.eol); 272 buf.append(this.indent3); 273 buf.append("}"); 274 if (i < entrySet.size()) { 275 buf.append(","); 276 } 277 buf.append(this.eol); 278 i++; 279 } 280 buf.append(this.indent2); 281 buf.append("]"); 282 } 283 284 buf.append(this.eol); 285 buf.append(this.indent1); 286 buf.append("}"); 287 288 return buf.toString(); 289 } 290 291 /** 292 * Returns appropriate JSON headers. 293 * 294 * @return a byte array containing the header, opening the JSON array. 295 */ 296 @Override 297 public byte[] getHeader() { 298 if (!this.complete) { 299 return null; 300 } 301 final StringBuilder buf = new StringBuilder(); 302 buf.append('['); 303 buf.append(this.eol); 304 return buf.toString().getBytes(this.getCharset()); 305 } 306 307 /** 308 * Returns appropriate JSON footer. 309 * 310 * @return a byte array containing the footer, closing the JSON array. 311 */ 312 @Override 313 public byte[] getFooter() { 314 if (!this.complete) { 315 return null; 316 } 317 return (this.eol + "]" + this.eol).getBytes(this.getCharset()); 318 } 319 320 /** 321 * XMLLayout's content format is specified by: 322 * <p/> 323 * Key: "dtd" Value: "log4j-events.dtd" 324 * <p/> 325 * Key: "version" Value: "2.0" 326 * 327 * @return Map of content format keys supporting XMLLayout 328 */ 329 @Override 330 public Map<String, String> getContentFormat() { 331 final Map<String, String> result = new HashMap<String, String>(); 332 result.put("version", "2.0"); 333 return result; 334 } 335 336 @Override 337 /** 338 * @return The content type. 339 */ 340 public String getContentType() { 341 return "application/json; charset=" + this.getCharset(); 342 } 343 344 /** 345 * Creates an XML Layout. 346 * 347 * @param locationInfo 348 * If "true", includes the location information in the generated JSON. 349 * @param properties 350 * If "true", includes the thread context in the generated JSON. 351 * @param completeStr 352 * If "true", includes the JSON header and footer, defaults to "false". 353 * @param compactStr 354 * If "true", does not use end-of-lines and indentation, defaults to "false". 355 * @param charsetName 356 * The character set to use, if {@code null}, uses "UTF-8". 357 * @return An XML Layout. 358 */ 359 @PluginFactory 360 public static JSONLayout createLayout( 361 @PluginAttribute("locationInfo") final String locationInfo, 362 @PluginAttribute("properties") final String properties, 363 @PluginAttribute("complete") final String completeStr, 364 @PluginAttribute("compact") final String compactStr, 365 @PluginAttribute("charset") final String charsetName) { 366 final Charset charset = Charsets.getSupportedCharset(charsetName, Charsets.UTF_8); 367 final boolean info = Boolean.parseBoolean(locationInfo); 368 final boolean props = Boolean.parseBoolean(properties); 369 final boolean complete = Boolean.parseBoolean(completeStr); 370 final boolean compact = Boolean.parseBoolean(compactStr); 371 return new JSONLayout(info, props, complete, compact, charset); 372 } 373}