/*
* Copyright (C) 2009 Peter Monks (pmonks@gmail.com)
*
* Licensed 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 com.google.code.log4mongo;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.spi.ErrorCode;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.Mongo;
import com.mongodb.MongoException;
import com.mongodb.ServerAddress;
/**
* Log4J Appender that writes log events into a 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 (e.g., in the MongoDB shell or other external application).
*
* @author Peter Monks (pmonks@gmail.com)
* @see <a href="http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/Appender.html">Log4J Appender Interface</a>
* @see <a href="http://www.mongodb.org/">MongoDB</a>
* @version $Id$
*/
public class MongoDbAppender
extends BsonAppender
{
private final static String DEFAULT_MONGO_DB_HOSTNAME = "localhost";
private final static String 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 String 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 String password = null;
private Mongo mongo = null;
private DBCollection collection = null;
private boolean initialized = false;
/**
* @see org.apache.log4j.Appender#requiresLayout()
*/
public boolean requiresLayout()
{
return(false);
}
/**
* @see org.apache.log4j.AppenderSkeleton#activateOptions()
*/
@Override
public void activateOptions()
{
try
{
// Close previous connections if reactivating
if (mongo != null)
{
close();
}
List<ServerAddress> addresses = getServerAddresses(hostname, port);
if (addresses.size() < 2)
{
mongo = new Mongo(addresses.get(0));
}
else
{
// Replication set
mongo = new Mongo(addresses);
}
DB database = mongo.getDB(databaseName);
if (userName != null && userName.trim().length() > 0)
{
if (!database.authenticate(userName, password.toCharArray()))
{
throw new RuntimeException("Unable to authenticate with MongoDB server.");
}
// Allow password to be GCed
password = null;
}
setCollection(database.getCollection(collectionName));
initialized = true;
}
catch (Exception e)
{
errorHandler.error("Unexpected exception while initialising MongoDbAppender.", e, ErrorCode.GENERIC_FAILURE);
}
}
/**
* 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;
}
/**
* @see org.apache.log4j.Appender#close()
*/
public void close()
{
if (mongo != null) {
collection = null;
mongo.close();
}
}
/**
* @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>.
*/
public 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 String getPort()
{
return(port);
}
/**
* @param port The port to set <i>(must not be null, empty or blank)</i>.
*/
public void setPort(final String port)
{
// PRECONDITIONS
assert port != null : "port must not be null";
assert port.trim().length() > 0 : "port must not be empty or blank";
// 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 String password)
{
this.password = password;
}
/**
* @param bson The BSON object to insert into a MongoDB database collection.
*/
@Override
public void append(DBObject bson)
{
if (initialized && bson != null)
{
try {
getCollection().insert(bson);
} catch (MongoException e) {
errorHandler.error("Failed to insert document to MongoDB", e,
ErrorCode.WRITE_FAILURE);
}
}
}
/**
* Returns true if appender was successfully initialized. If this method
* returns false, the appender should not attempt to log events.
*
* @return true if appender was successfully initialized
*/
public boolean isInitialized() {
return initialized;
}
/**
*
* @return The MongoDB collection to which events are logged.
*/
protected DBCollection getCollection()
{
return(collection);
}
/**
* Returns a List of ServerAddress objects for each host specified in the hostname
* property. Returns an empty list if configuration is detected to be invalid, e.g.:
* <ul>
* <li>Port property doesn't contain either one port or one port per host</li>
* <li>After parsing port property to integers, there isn't either one port or one port per host</li>
* </ul>
*
* @param hostname Blank space delimited hostnames
* @param port Blank space delimited ports. Must specify one port for all hosts or a port per host.
* @return List of ServerAddresses to connect to
*/
private List<ServerAddress> getServerAddresses(String hostname, String port) {
List<ServerAddress> addresses = new ArrayList<ServerAddress>();
String[] hosts = hostname.split(" ");
String[] ports = port.split(" ");
if (ports.length != 1 && ports.length != hosts.length)
{
errorHandler.error("MongoDB appender port property must contain one port or a port per host", null,
ErrorCode.ADDRESS_PARSE_FAILURE);
}
else
{
List<Integer> portNums = getPortNums(ports);
// Validate number of ports again after parsing
if (portNums.size() != 1 && portNums.size() != hosts.length)
{
errorHandler.error("MongoDB appender port property must contain one port or a valid port per host", null,
ErrorCode.ADDRESS_PARSE_FAILURE);
}
else
{
boolean onePort = (portNums.size() == 1);
int i = 0;
for (String host : hosts)
{
int portNum = (onePort) ? portNums.get(0) : portNums.get(i);
try {
addresses.add(new ServerAddress(host.trim(), portNum));
}
catch (UnknownHostException e)
{
errorHandler.error("MongoDB appender hostname property contains unknown host", e,
ErrorCode.ADDRESS_PARSE_FAILURE);
}
i++;
}
}
}
return addresses;
}
private List<Integer> getPortNums(String[] ports) {
List<Integer> portNums = new ArrayList<Integer>();
for (String port : ports)
{
try
{
Integer portNum = Integer.valueOf(port.trim());
if (portNum < 0) {
errorHandler.error("MongoDB appender port property can't contain a negative integer", null,
ErrorCode.ADDRESS_PARSE_FAILURE);
}
else
{
portNums.add(portNum);
}
}
catch (NumberFormatException e)
{
errorHandler.error("MongoDB appender can't parse a port property value into an integer", e,
ErrorCode.ADDRESS_PARSE_FAILURE);
}
}
return portNums;
}
}