/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.log; import org.apache.log4j.Layout; import org.apache.log4j.helpers.ISO8601DateFormat; import org.apache.log4j.spi.LoggingEvent; import org.apache.log4j.spi.ThrowableInformation; import org.codehaus.jackson.JsonFactory; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.MappingJsonFactory; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.node.ContainerNode; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.text.DateFormat; import java.util.Date; /** * This offers a log layout for JSON, with some test entry points. It's purpose is * to allow Log4J to generate events that are easy for other programs to parse, but which are somewhat * human-readable. * * Some features. * * <ol> * <li>Every event is a standalone JSON clause</li> * <li>Time is published as a time_t event since 1/1/1970 * -this is the fastest to generate.</li> * <li>An ISO date is generated, but this is cached and will only be accurate to within a second</li> * <li>the stack trace is included as an array</li> * </ol> * * A simple log event will resemble the following * <pre> * {"name":"test","time":1318429136789,"date":"2011-10-12 15:18:56,789","level":"INFO","thread":"main","message":"test message"} * </pre> * * An event with an error will contain data similar to that below (which has been reformatted to be multi-line). * * <pre> * { * "name":"testException", * "time":1318429136789, * "date":"2011-10-12 15:18:56,789", * "level":"INFO", * "thread":"quoted\"", * "message":"new line\n and {}", * "exceptionclass":"java.net.NoRouteToHostException", * "stack":[ * "java.net.NoRouteToHostException: that box caught fire 3 years ago", * "\tat org.apache.hadoop.log.TestLog4Json.testException(TestLog4Json.java:49)", * "\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)", * "\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)", * "\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)", * "\tat java.lang.reflect.Method.invoke(Method.java:597)", * "\tat junit.framework.TestCase.runTest(TestCase.java:168)", * "\tat junit.framework.TestCase.runBare(TestCase.java:134)", * "\tat junit.framework.TestResult$1.protect(TestResult.java:110)", * "\tat junit.framework.TestResult.runProtected(TestResult.java:128)", * "\tat junit.framework.TestResult.run(TestResult.java:113)", * "\tat junit.framework.TestCase.run(TestCase.java:124)", * "\tat junit.framework.TestSuite.runTest(TestSuite.java:232)", * "\tat junit.framework.TestSuite.run(TestSuite.java:227)", * "\tat org.junit.internal.runners.JUnit38ClassRunner.run(JUnit38ClassRunner.java:83)", * "\tat org.apache.maven.surefire.junit4.JUnit4TestSet.execute(JUnit4TestSet.java:59)", * "\tat org.apache.maven.surefire.suite.AbstractDirectoryTestSuite.executeTestSet(AbstractDirectoryTestSuite.java:120)", * "\tat org.apache.maven.surefire.suite.AbstractDirectoryTestSuite.execute(AbstractDirectoryTestSuite.java:145)", * "\tat org.apache.maven.surefire.Surefire.run(Surefire.java:104)", * "\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)", * "\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)", * "\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)", * "\tat java.lang.reflect.Method.invoke(Method.java:597)", * "\tat org.apache.maven.surefire.booter.SurefireBooter.runSuitesInProcess(SurefireBooter.java:290)", * "\tat org.apache.maven.surefire.booter.SurefireBooter.main(SurefireBooter.java:1017)" * ] * } * </pre> */ public class Log4Json extends Layout { /** * Jackson factories are thread safe when constructing parsers and generators. * They are not thread safe in configure methods; if there is to be any * configuration it must be done in a static intializer block. */ private static final JsonFactory factory = new MappingJsonFactory(); public static final String DATE = "date"; public static final String EXCEPTION_CLASS = "exceptionclass"; public static final String LEVEL = "level"; public static final String MESSAGE = "message"; public static final String NAME = "name"; public static final String STACK = "stack"; public static final String THREAD = "thread"; public static final String TIME = "time"; public static final String JSON_TYPE = "application/json"; private final DateFormat dateFormat; public Log4Json() { dateFormat = new ISO8601DateFormat(); } /** * @return the mime type of JSON */ @Override public String getContentType() { return JSON_TYPE; } @Override public String format(LoggingEvent event) { try { return toJson(event); } catch (IOException e) { //this really should not happen, and rather than throw an exception //which may hide the real problem, the log class is printed //in JSON format. The classname is used to ensure valid JSON is //returned without playing escaping games return "{ \"logfailure\":\"" + e.getClass().toString() + "\"}"; } } /** * Convert an event to JSON * * @param event the event -must not be null * @return a string value * @throws IOException on problems generating the JSON */ public String toJson(LoggingEvent event) throws IOException { StringWriter writer = new StringWriter(); toJson(writer, event); return writer.toString(); } /** * Convert an event to JSON * * @param writer the destination writer * @param event the event -must not be null * @return the writer * @throws IOException on problems generating the JSON */ public Writer toJson(final Writer writer, final LoggingEvent event) throws IOException { ThrowableInformation ti = event.getThrowableInformation(); toJson(writer, event.getLoggerName(), event.getTimeStamp(), event.getLevel().toString(), event.getThreadName(), event.getRenderedMessage(), ti); return writer; } /** * Build a JSON entry from the parameters. This is public for testing. * * @param writer destination * @param loggerName logger name * @param timeStamp time_t value * @param level level string * @param threadName name of the thread * @param message rendered message * @param ti nullable thrown information * @return the writer * @throws IOException on any problem */ public Writer toJson(final Writer writer, final String loggerName, final long timeStamp, final String level, final String threadName, final String message, final ThrowableInformation ti) throws IOException { JsonGenerator json = factory.createJsonGenerator(writer); json.writeStartObject(); json.writeStringField(NAME, loggerName); json.writeNumberField(TIME, timeStamp); Date date = new Date(timeStamp); json.writeStringField(DATE, dateFormat.format(date)); json.writeStringField(LEVEL, level); json.writeStringField(THREAD, threadName); json.writeStringField(MESSAGE, message); if (ti != null) { //there is some throwable info, but if the log event has been sent over the wire, //there may not be a throwable inside it, just a summary. Throwable thrown = ti.getThrowable(); String eclass = (thrown != null) ? thrown.getClass().getName() : ""; json.writeStringField(EXCEPTION_CLASS, eclass); String[] stackTrace = ti.getThrowableStrRep(); json.writeArrayFieldStart(STACK); for (String row : stackTrace) { json.writeString(row); } json.writeEndArray(); } json.writeEndObject(); json.flush(); json.close(); return writer; } /** * This appender does not ignore throwables * * @return false, always */ @Override public boolean ignoresThrowable() { return false; } /** * Do nothing */ @Override public void activateOptions() { } /** * For use in tests * * @param json incoming JSON to parse * @return a node tree * @throws IOException on any parsing problems */ public static ContainerNode parse(String json) throws IOException { ObjectMapper mapper = new ObjectMapper(factory); JsonNode jsonNode = mapper.readTree(json); if (!(jsonNode instanceof ContainerNode)) { throw new IOException("Wrong JSON data: " + json); } return (ContainerNode) jsonNode; } }