/*
* Copyright (C) 2009 Peter Monks (pmonks@gmail.com)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package com.lowereast.guiceymongo.logging;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.spi.ErrorCode;
import org.apache.log4j.spi.LocationInfo;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.spi.ThrowableInformation;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.Mongo;
/**
* Log4J Appender that writes log events into the MongoDB document oriented database. Log events are fully parsed and stored
* as structured records in MongoDB (this appender does not require, nor use a Log4J layout).
*
* The appender does <u>not</u> create any indexes on the data that's stored - it is assumed that if query performance is
* required those would be created externally (eg. in the mongodb shell or an external reporting application).
*
* An example BSON structure for a single log entry is as follows:
*
* <pre>
* {
* "_id" : ObjectId("f1c0895fd5eee04a445deb00"),
* "timestamp" : "Thu Oct 22 2009 16:46:29 GMT-0700 (Pacific Daylight Time)",
* "level" : "ERROR",
* "thread" : "main",
* "message" : "Error entry",
* "fileName" : "TestMongoDbAppender.java",
* "method" : "testLogWithChainedExceptions",
* "lineNumber" : "147",
* "class" : {
* "fullyQualifiedClassName" : "com.google.code.log4mongo.TestMongoDbAppender",
* "package" : [ "com", "google", "code", "log4mongo" ],
* "className" : "TestMongoDbAppender"
* },
* "throwables" : [
* {
* "message" : "I'm an innocent bystander.",
* "stackTrace" : [
* {
* "fileName" : "TestMongoDbAppender.java",
* "method" : "testLogWithChainedExceptions",
* "lineNumber" : 147,
* "class" : {
* "fullyQualifiedClassName" : "com.google.code.log4mongo.TestMongoDbAppender",
* "package" : [ "com", "google", "code", "log4mongo" ],
* "className" : "TestMongoDbAppender"
* }
* },
* {
* "method" : "invoke0",
* "lineNumber" : -2,
* "class" : {
* "fullyQualifiedClassName" : "sun.reflect.NativeMethodAccessorImpl",
* "package" : [ "sun", "reflect" ],
* "className" : "NativeMethodAccessorImpl"
* }
* },
* ... 8< ...
* ]
* },
* {
* "message" : "I'm the real culprit!",
* "stackTrace" : [
* {
* "fileName" : "TestMongoDbAppender.java",
* "method" : "testLogWithChainedExceptions",
* "lineNumber" : 145,
* "class" : {
* "fullyQualifiedClassName" : "com.google.code.log4mongo.TestMongoDbAppender",
* "package" : [ "com", "google", "code", "log4mongo" ],
* "className" : "TestMongoDbAppender"
* }
* },
* ... 8< ...
* ]
* }
* ]
* }
* </pre>
*
* @author Peter Monks (pmonks@gmail.com)
* @see http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/Appender.html
* @see http://www.mongodb.org/
* @version $Id$
*/
public class MongoDbAppender
extends AppenderSkeleton
{
private final static String DEFAULT_MONGO_DB_HOSTNAME = "localhost";
private final static int DEFAULT_MONGO_DB_PORT = 27017;
private final static String DEFAULT_MONGO_DB_DATABASE_NAME = "log4mongo";
private final static String DEFAULT_MONGO_DB_COLLECTION_NAME = "logevents";
private String hostname = DEFAULT_MONGO_DB_HOSTNAME;
private int port = DEFAULT_MONGO_DB_PORT;
private String databaseName = DEFAULT_MONGO_DB_DATABASE_NAME;
private String collectionName = DEFAULT_MONGO_DB_COLLECTION_NAME;
private String userName = null;
private char[] password = null;
private DBCollection collection = null;
private Map<String, Object> extraInformation = new HashMap<String, Object>();
/**
* @see org.apache.log4j.Appender#requiresLayout()
*/
public boolean requiresLayout()
{
return(false);
}
/**
* @see org.apache.log4j.AppenderSkeleton#activateOptions()
*/
@Override
public void activateOptions()
{
try
{
Mongo mongo = new Mongo(hostname, port);
DB database = mongo.getDB(databaseName);
if (userName != null && userName.trim().length() > 0)
{
if (!database.authenticate(userName, password))
{
throw new RuntimeException("Unable to authenticate with MongoDB server.");
}
// Allow password to be GCed
password = null;
}
setCollection(database.getCollection(collectionName));
}
catch (Exception e)
{
errorHandler.error("Unexpected exception while initialising MongoDbAppender.", e, ErrorCode.GENERIC_FAILURE);
}
}
/**
* @see org.apache.log4j.AppenderSkeleton#append(org.apache.log4j.spi.LoggingEvent)
*/
@Override
protected void append(final LoggingEvent loggingEvent)
{
DBObject bson = bsonifyLoggingEvent(loggingEvent);
if (bson != null)
{
collection.insert(bson);
}
}
/**
* Note: this method is primarily intended for use by the unit tests.
*
* @param collection The MongoDB collection to use when logging events.
*/
public void setCollection(final DBCollection collection)
{
// PRECONDITIONS
assert collection != null : "collection must not be null.";
// Body
this.collection = collection;
}
/**
* BSONifies a single Log4J LoggingEvent object.
*
* @param loggingEvent The LoggingEvent object to BSONify <i>(may be null)</i>.
* @return The BSONified equivalent of the LoggingEvent object <i>(may be null)</i>.
*/
private DBObject bsonifyLoggingEvent(final LoggingEvent loggingEvent)
{
DBObject result = null;
if (loggingEvent != null)
{
result = new BasicDBObject(this.extraInformation);
result.put("timestamp", new Date(loggingEvent.getTimeStamp()));
nullSafePut(result, "level", loggingEvent.getLevel().toString());
nullSafePut(result, "thread", loggingEvent.getThreadName());
nullSafePut(result, "message", loggingEvent.getMessage());
addLocationInformation(result, loggingEvent.getLocationInformation());
addThrowableInformation(result, loggingEvent.getThrowableInformation());
}
return(result);
}
/**
* Adds the LocationInfo object to an existing BSON object.
*
* @param bson The BSON object to add the location info to <i>(must not be null)</i>.
* @param locationInfo The LocationInfo object to add to the BSON object <i>(may be null)</i>.
*/
private void addLocationInformation(DBObject bson, final LocationInfo locationInfo)
{
if (locationInfo != null)
{
nullSafePut(bson, "fileName", locationInfo.getFileName());
nullSafePut(bson, "method", locationInfo.getMethodName());
nullSafePut(bson, "lineNumber", locationInfo.getLineNumber());
nullSafePut(bson, "class", bsonifyClassName(locationInfo.getClassName()));
}
}
/**
* Adds the ThrowableInformation object to an existing BSON object.
*
* @param bson The BSON object to add the throwable info to <i>(must not be null)</i>.
* @param throwableInfo The ThrowableInformation object to add to the BSON object <i>(may be null)</i>.
*/
private void addThrowableInformation(DBObject bson, final ThrowableInformation throwableInfo)
{
if (throwableInfo != null)
{
Throwable currentThrowable = throwableInfo.getThrowable();
List throwables = new BasicDBList();
while (currentThrowable != null)
{
DBObject throwableBson = bsonifyThrowable(currentThrowable);
if (throwableBson != null)
{
throwables.add(throwableBson);
}
currentThrowable = currentThrowable.getCause();
}
if (throwables.size() > 0)
{
bson.put("throwables", throwables);
}
}
}
/**
* BSONifies the given Throwable.
*
* @param throwable The throwable object to BSONify <i>(may be null)</i>.
* @return The BSONified equivalent of the Throwable object <i>(may be null)</i>.
*/
private DBObject bsonifyThrowable(final Throwable throwable)
{
DBObject result = null;
if (throwable != null)
{
result = new BasicDBObject();
nullSafePut(result, "message", throwable.getMessage());
nullSafePut(result, "stackTrace", bsonifyStackTrace(throwable.getStackTrace()));
}
return(result);
}
/**
* BSONifies the given stack trace.
*
* @param stackTrace The stack trace object to BSONify <i>(may be null)</i>.
* @return The BSONified equivalent of the stack trace object <i>(may be null)</i>.
*/
private DBObject bsonifyStackTrace(final StackTraceElement[] stackTrace)
{
BasicDBList result = null;
if (stackTrace != null && stackTrace.length > 0)
{
result = new BasicDBList();
for (StackTraceElement element : stackTrace)
{
DBObject bson = bsonifyStackTraceElement(element);
if (bson != null)
{
result.add(bson);
}
}
}
return(result);
}
/**
* BSONifies the given stack trace element.
*
* @param element The stack trace element object to BSONify <i>(may be null)</i>.
* @return The BSONified equivalent of the stack trace element object <i>(may be null)</i>.
*/
private DBObject bsonifyStackTraceElement(final StackTraceElement element)
{
DBObject result = null;
if (element != null)
{
result = new BasicDBObject();
nullSafePut(result, "fileName", element.getFileName());
nullSafePut(result, "method", element.getMethodName());
nullSafePut(result, "lineNumber", element.getLineNumber());
nullSafePut(result, "class", bsonifyClassName(element.getClassName()));
}
return(result);
}
/**
* BSONifies the given class name.
*
* @param className The class name to BSONify <i>(may be null)</i>.
* @return The BSONified equivalent of the class name <i>(may be null)</i>.
*/
private DBObject bsonifyClassName(final String className)
{
DBObject result = null;
if (className != null && className.trim().length() > 0)
{
result = new BasicDBObject();
result.put("fullyQualifiedClassName", className);
List packageComponents = new BasicDBList();
String[] packageAndClassName = className.split("\\.");
packageComponents.addAll(Arrays.asList(Arrays.copyOf(packageAndClassName, packageAndClassName.length - 1)));
if (packageComponents.size() > 0)
{
result.put("package", packageComponents);
}
result.put("className", packageAndClassName[packageAndClassName.length - 1]);
}
return(result);
}
/**
* Adds the given value to the given key, except if it's null (in which case this method does nothing).
*
* @param bson The BSON object to add the key/value to <i>(must not be null)</i>.
* @param key The key of the object <i>(must not be null)</i>.
* @param value The value of the object <i>(may be null)</i>.
*/
private void nullSafePut(DBObject bson, final String key, final Object value)
{
if (value != null)
{
if (value instanceof String)
{
String stringValue = (String)value;
if (stringValue.trim().length() > 0)
{
bson.put(key, stringValue);
}
}
else
{
bson.put(key, value);
}
}
}
/**
* @see org.apache.log4j.Appender#close()
*/
public void close()
{
collection = null;
}
/**
* @return The hostname of the MongoDB server <i>(will not be null, empty or blank)</i>.
*/
public String getHostname()
{
return(hostname);
}
/**
* @param hostname The MongoDB hostname to set <i>(must not be null, empty or blank)</i>.
*/
private void setHostname(final String hostname)
{
// PRECONDITIONS
assert hostname != null : "hostname must not be null";
assert hostname.trim().length() > 0 : "hostname must not be empty or blank";
// Body
this.hostname = hostname;
}
/**
* @return The port of the MongoDB server <i>(will be > 0)</i>.
*/
public int getPort()
{
return(port);
}
/**
* @param port The port to set <i>(must be > 0)</i>.
*/
private void setPort(final int port)
{
// PRECONDITIONS
assert port > 0 : "port must be > 0";
// Body
this.port = port;
}
/**
* @return The database used in the MongoDB server <i>(will not be null, empty or blank)</i>.
*/
public String getDatabaseName()
{
return(databaseName);
}
/**
* @param databaseName The database to use in the MongoDB server <i>(must not be null, empty or blank)</i>.
*/
public void setDatabaseName(final String databaseName)
{
// PRECONDITIONS
assert databaseName != null : "database must not be null";
assert databaseName.trim().length() > 0 : "database must not be empty or blank";
// Body
this.databaseName = databaseName;
}
/**
* @return The collection used within the database in the MongoDB server <i>(will not be null, empty or blank)</i>.
*/
public String getCollectionName()
{
return(collectionName);
}
/**
* @param collectionName The collection used within the database in the MongoDB server <i>(must not be null, empty or blank)</i>.
*/
public void setCollectionName(final String collectionName)
{
// PRECONDITIONS
assert collectionName != null : "collection must not be null";
assert collectionName.trim().length() > 0 : "collection must not be empty or blank";
// Body
this.collectionName = collectionName;
}
/**
* @return The userName used to authenticate with MongoDB <i>(may be null)</i>.
*/
public String getUserName()
{
return(userName);
}
/**
* @param userName The userName to use when authenticating with MongoDB <i>(may be null)</i>.
*/
public void setUserName(final String userName)
{
this.userName = userName;
}
/**
* @param password The password to use when authenticating with MongoDB <i>(may be null)</i>.
*/
public void setPassword(final char[] password)
{
this.password = password;
}
public void setExtraInfo(String key, Object value) {
}
}