001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019
020package org.apache.hadoop.log;
021
022import org.apache.log4j.Layout;
023import org.apache.log4j.helpers.ISO8601DateFormat;
024import org.apache.log4j.spi.LoggingEvent;
025import org.apache.log4j.spi.ThrowableInformation;
026import org.codehaus.jackson.JsonFactory;
027import org.codehaus.jackson.JsonGenerator;
028import org.codehaus.jackson.JsonNode;
029import org.codehaus.jackson.map.MappingJsonFactory;
030import org.codehaus.jackson.map.ObjectMapper;
031import org.codehaus.jackson.node.ContainerNode;
032
033import java.io.IOException;
034import java.io.StringWriter;
035import java.io.Writer;
036import java.text.DateFormat;
037import java.util.Date;
038
039/**
040 * This offers a log layout for JSON, with some test entry points. It's purpose is
041 * to allow Log4J to generate events that are easy for other programs to parse, but which are somewhat
042 * human-readable.
043 *
044 * Some features.
045 *
046 * <ol>
047 *     <li>Every event is a standalone JSON clause</li>
048 *     <li>Time is published as a time_t event since 1/1/1970
049 *      -this is the fastest to generate.</li>
050 *     <li>An ISO date is generated, but this is cached and will only be accurate to within a second</li>
051 *     <li>the stack trace is included as an array</li>
052 * </ol>
053 *
054 * A simple log event will resemble the following
055 * <pre>
056 *     {"name":"test","time":1318429136789,"date":"2011-10-12 15:18:56,789","level":"INFO","thread":"main","message":"test message"}
057 * </pre>
058 *
059 * An event with an error will contain data similar to that below (which has been reformatted to be multi-line).
060 *
061 * <pre>
062 *     {
063 *     "name":"testException",
064 *     "time":1318429136789,
065 *     "date":"2011-10-12 15:18:56,789",
066 *     "level":"INFO",
067 *     "thread":"quoted\"",
068 *     "message":"new line\n and {}",
069 *     "exceptionclass":"java.net.NoRouteToHostException",
070 *     "stack":[
071 *         "java.net.NoRouteToHostException: that box caught fire 3 years ago",
072 *         "\tat org.apache.hadoop.log.TestLog4Json.testException(TestLog4Json.java:49)",
073 *         "\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)",
074 *         "\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)",
075 *         "\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)",
076 *         "\tat java.lang.reflect.Method.invoke(Method.java:597)",
077 *         "\tat junit.framework.TestCase.runTest(TestCase.java:168)",
078 *         "\tat junit.framework.TestCase.runBare(TestCase.java:134)",
079 *         "\tat junit.framework.TestResult$1.protect(TestResult.java:110)",
080 *         "\tat junit.framework.TestResult.runProtected(TestResult.java:128)",
081 *         "\tat junit.framework.TestResult.run(TestResult.java:113)",
082 *         "\tat junit.framework.TestCase.run(TestCase.java:124)",
083 *         "\tat junit.framework.TestSuite.runTest(TestSuite.java:232)",
084 *         "\tat junit.framework.TestSuite.run(TestSuite.java:227)",
085 *         "\tat org.junit.internal.runners.JUnit38ClassRunner.run(JUnit38ClassRunner.java:83)",
086 *         "\tat org.apache.maven.surefire.junit4.JUnit4TestSet.execute(JUnit4TestSet.java:59)",
087 *         "\tat org.apache.maven.surefire.suite.AbstractDirectoryTestSuite.executeTestSet(AbstractDirectoryTestSuite.java:120)",
088 *         "\tat org.apache.maven.surefire.suite.AbstractDirectoryTestSuite.execute(AbstractDirectoryTestSuite.java:145)",
089 *         "\tat org.apache.maven.surefire.Surefire.run(Surefire.java:104)",
090 *         "\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)",
091 *         "\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)",
092 *         "\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)",
093 *         "\tat java.lang.reflect.Method.invoke(Method.java:597)",
094 *         "\tat org.apache.maven.surefire.booter.SurefireBooter.runSuitesInProcess(SurefireBooter.java:290)",
095 *         "\tat org.apache.maven.surefire.booter.SurefireBooter.main(SurefireBooter.java:1017)"
096 *         ]
097 *     }
098 * </pre>
099 */
100public class Log4Json extends Layout {
101
102  /**
103   * Jackson factories are thread safe when constructing parsers and generators.
104   * They are not thread safe in configure methods; if there is to be any
105   * configuration it must be done in a static intializer block.
106   */
107  private static final JsonFactory factory = new MappingJsonFactory();
108  public static final String DATE = "date";
109  public static final String EXCEPTION_CLASS = "exceptionclass";
110  public static final String LEVEL = "level";
111  public static final String MESSAGE = "message";
112  public static final String NAME = "name";
113  public static final String STACK = "stack";
114  public static final String THREAD = "thread";
115  public static final String TIME = "time";
116  public static final String JSON_TYPE = "application/json";
117
118  private final DateFormat dateFormat;
119
120  public Log4Json() {
121    dateFormat = new ISO8601DateFormat();
122  }
123
124
125  /**
126   * @return the mime type of JSON
127   */
128  @Override
129  public String getContentType() {
130    return JSON_TYPE;
131  }
132
133  @Override
134  public String format(LoggingEvent event) {
135    try {
136      return toJson(event);
137    } catch (IOException e) {
138      //this really should not happen, and rather than throw an exception
139      //which may hide the real problem, the log class is printed
140      //in JSON format. The classname is used to ensure valid JSON is 
141      //returned without playing escaping games
142      return "{ \"logfailure\":\"" + e.getClass().toString() + "\"}";
143    }
144  }
145
146  /**
147   * Convert an event to JSON
148   *
149   * @param event the event -must not be null
150   * @return a string value
151   * @throws IOException on problems generating the JSON
152   */
153  public String toJson(LoggingEvent event) throws IOException {
154    StringWriter writer = new StringWriter();
155    toJson(writer, event);
156    return writer.toString();
157  }
158
159  /**
160   * Convert an event to JSON
161   *
162   * @param writer the destination writer
163   * @param event the event -must not be null
164   * @return the writer
165   * @throws IOException on problems generating the JSON
166   */
167  public Writer toJson(final Writer writer, final LoggingEvent event)
168      throws IOException {
169    ThrowableInformation ti = event.getThrowableInformation();
170    toJson(writer,
171           event.getLoggerName(),
172           event.getTimeStamp(),
173           event.getLevel().toString(),
174           event.getThreadName(),
175           event.getRenderedMessage(),
176           ti);
177    return writer;
178  }
179
180  /**
181   * Build a JSON entry from the parameters. This is public for testing.
182   *
183   * @param writer destination
184   * @param loggerName logger name
185   * @param timeStamp time_t value
186   * @param level level string
187   * @param threadName name of the thread
188   * @param message rendered message
189   * @param ti nullable thrown information
190   * @return the writer
191   * @throws IOException on any problem
192   */
193  public Writer toJson(final Writer writer,
194                       final String loggerName,
195                       final long timeStamp,
196                       final String level,
197                       final String threadName,
198                       final String message,
199                       final ThrowableInformation ti) throws IOException {
200    JsonGenerator json = factory.createJsonGenerator(writer);
201    json.writeStartObject();
202    json.writeStringField(NAME, loggerName);
203    json.writeNumberField(TIME, timeStamp);
204    Date date = new Date(timeStamp);
205    json.writeStringField(DATE, dateFormat.format(date));
206    json.writeStringField(LEVEL, level);
207    json.writeStringField(THREAD, threadName);
208    json.writeStringField(MESSAGE, message);
209    if (ti != null) {
210      //there is some throwable info, but if the log event has been sent over the wire,
211      //there may not be a throwable inside it, just a summary.
212      Throwable thrown = ti.getThrowable();
213      String eclass = (thrown != null) ?
214          thrown.getClass().getName()
215          : "";
216      json.writeStringField(EXCEPTION_CLASS, eclass);
217      String[] stackTrace = ti.getThrowableStrRep();
218      json.writeArrayFieldStart(STACK);
219      for (String row : stackTrace) {
220        json.writeString(row);
221      }
222      json.writeEndArray();
223    }
224    json.writeEndObject();
225    json.flush();
226    json.close();
227    return writer;
228  }
229
230  /**
231   * This appender does not ignore throwables
232   *
233   * @return false, always
234   */
235  @Override
236  public boolean ignoresThrowable() {
237    return false;
238  }
239
240  /**
241   * Do nothing
242   */
243  @Override
244  public void activateOptions() {
245  }
246
247  /**
248   * For use in tests
249   *
250   * @param json incoming JSON to parse
251   * @return a node tree
252   * @throws IOException on any parsing problems
253   */
254  public static ContainerNode parse(String json) throws IOException {
255    ObjectMapper mapper = new ObjectMapper(factory);
256    JsonNode jsonNode = mapper.readTree(json);
257    if (!(jsonNode instanceof ContainerNode)) {
258      throw new IOException("Wrong JSON data: " + json);
259    }
260    return (ContainerNode) jsonNode;
261  }
262}