diff --git a/src/main/java/org/log4mongo/MongoDbPatternLayoutDateAppender.java b/src/main/java/org/log4mongo/MongoDbPatternLayoutDateAppender.java new file mode 100644 index 0000000..dc43985 --- /dev/null +++ b/src/main/java/org/log4mongo/MongoDbPatternLayoutDateAppender.java @@ -0,0 +1,101 @@ +package org.log4mongo; + +import com.mongodb.DBObject; +import com.mongodb.MongoException; +import com.mongodb.util.JSON; + +import java.util.Date; + +import org.apache.log4j.Layout; +import org.apache.log4j.spi.ErrorCode; +import org.apache.log4j.spi.LoggingEvent; +import org.bson.Document; + +/** + * A Log4J Appender that uses a PatternLayout to write log events into a MongoDB database. + * This appender is same as the MongoDbPatternLayoutAppender, only difference is that the + * MongoDbPatternLayoutDateAppender will save pattern layout 'timestamp'(%d) in the mongodb as a + * Date type instead of String. + *

+ * The conversion pattern specifies the format of a JSON document. The document can contain + * sub-documents and the elements can be strings or arrays. + *

+ * For some Log4J appenders (especially file appenders) blank space padding is often used to get + * fields in adjacent rows to line up. For example, %-5p is often used to make all log levels the + * same width in characters. Since each value is stored in a separate property in the document, it + * usually doesn't make sense to use blank space padding with MongoDbPatternLayoutDateAppender. + *

+ * The appender does not create any indexes on the data that's stored. If query performance + * is required, indexes must be created externally (e.g., in the mongo shell or an external + * reporting application). + * + */ +public class MongoDbPatternLayoutDateAppender extends MongoDbAppender { + + @Override + public boolean requiresLayout() { + return (true); + } + + /** + * Inserts a BSON representation of a LoggingEvent into a MongoDB collection. A PatternLayout is + * used to format a JSON document containing data available in the LoggingEvent and, optionally, + * additional data returned by custom PatternConverters. Here timestamp is stored as a Date in mongodb. + *

+ * The format of the JSON document is specified in the .layout.ConversionPattern property. + * + * @param loggingEvent + * The LoggingEvent that will be formatted and stored in MongoDB + */ + @Override + protected void append(final LoggingEvent loggingEvent) { + if (isInitialized()) { + DBObject bson = null; + String json = layout.format(loggingEvent); + + if (json.length() > 0) { + Object obj = JSON.parse(json); + if (obj instanceof DBObject) { + bson = (DBObject) obj; + String dateKey = getDateKeyFromPatternLayout(layout); + if (dateKey != null) { + //saving time stamp as a date instead of string + bson.put(dateKey, new Date(loggingEvent.getTimeStamp())); + } + } + } + + if (bson != null) { + try { + getCollection().insertOne(new Document(bson.toMap())); + } catch (MongoException e) { + errorHandler.error("Failed to insert document to MongoDB", e, + ErrorCode.WRITE_FAILURE); + } + } + } + } + + /** + * this method returns the key of the date which is mentioned in layout pattern + * @param layout + * @return key of Date (%d) + */ + private String getDateKeyFromPatternLayout(Layout layout) { + String dateKey = null; + String conversionPattern = ((MongoDbPatternLayout)layout).getConversionPattern(); + String[] splitPattern = conversionPattern.split(","); + for (String pattern : splitPattern) { + if (pattern.contains("%d")) { + dateKey = pattern.split(":")[0]; + } + } + // here we are removing double quotes '"' and opening curly braces '{' from the Key + if (dateKey != null) { + dateKey = dateKey.replaceAll("\"|\"$", ""); + dateKey = dateKey.replaceAll("\\{", ""); + } + return dateKey; + } + +} diff --git a/src/test/java/org/log4mongo/TestMongoDbAppenderAuth.java b/src/test/java/org/log4mongo/TestMongoDbAppenderAuth.java index 80eda75..1e56e22 100644 --- a/src/test/java/org/log4mongo/TestMongoDbAppenderAuth.java +++ b/src/test/java/org/log4mongo/TestMongoDbAppenderAuth.java @@ -15,12 +15,17 @@ package org.log4mongo; -import com.mongodb.Mongo; +import com.mongodb.BasicDBObject; import com.mongodb.MongoClient; import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; import org.apache.log4j.PropertyConfigurator; +import org.bson.Document; import org.junit.*; +import java.util.ArrayList; +import java.util.Collections; + /** * Authentication-related JUnit unit tests for MongoDbAppender. *

@@ -90,7 +95,22 @@ public void testAppenderActivateNoAuth() { */ @Test public void testAppenderActivateWithAuth() { - mongo.getDB(TEST_DATABASE_NAME).addUser(username, password.toCharArray()); + Document result = null; + MongoDatabase db = mongo.getDatabase(TEST_DATABASE_NAME); + BasicDBObject getUsersInfoCommand = new BasicDBObject("usersInfo", + new BasicDBObject("user", username).append("db", TEST_DATABASE_NAME)); + BasicDBObject dropUserCommand = new BasicDBObject("dropUser", username); + BasicDBObject createUserCommand = new BasicDBObject("createUser", username).append("pwd", password).append("roles", + Collections.singletonList(new BasicDBObject("role", "dbOwner").append("db", TEST_DATABASE_NAME))); + + // If test user exists from a previous run, drop it + result = db.runCommand(getUsersInfoCommand); + ArrayList users = (ArrayList) result.get("users"); + if (!users.isEmpty()) { + db.runCommand(dropUserCommand); + } + + db.runCommand(createUserCommand); PropertyConfigurator.configure(LOG4J_AUTH_PROPS); } diff --git a/src/test/java/org/log4mongo/TestMongoDbPatternLayoutDate.java b/src/test/java/org/log4mongo/TestMongoDbPatternLayoutDate.java new file mode 100644 index 0000000..23d9978 --- /dev/null +++ b/src/test/java/org/log4mongo/TestMongoDbPatternLayoutDate.java @@ -0,0 +1,104 @@ +package org.log4mongo; + +import static org.junit.Assert.assertNotNull; + +import java.util.Date; +import java.util.Properties; + +import org.apache.log4j.Logger; +import org.apache.log4j.PropertyConfigurator; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.mongodb.DBObject; +import com.mongodb.MongoClient; +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; + +/** + * JUnit unit tests for PatternLayout style logging with Date as a BSON object. + *

+ * Since tests may depend on different Log4J property settings, each test reconfigures an appender + * using a Properties object. + *

+ * Note: these tests require that a MongoDB server is running, and (by default) assumes that server + * is listening on the default port (27017) on localhost. + */ +public class TestMongoDbPatternLayoutDate { + + public static final String TEST_MONGO_SERVER_HOSTNAME = "localhost"; + + public static final int TEST_MONGO_SERVER_PORT = 27017; + + private static final String TEST_DATABASE_NAME = "log4mongotest"; + + private static final String TEST_COLLECTION_NAME = "logeventslayout"; + + private static final String APPENDER_NAME = "MongoDBPatternLayout"; + + private final MongoClient mongo; + + private MongoCollection collection; + + public TestMongoDbPatternLayoutDate() throws Exception { + mongo = new MongoClient(TEST_MONGO_SERVER_HOSTNAME, TEST_MONGO_SERVER_PORT); + } + + @BeforeClass + public static void setUpBeforeClass() throws Exception { + MongoClient mongo = new MongoClient(TEST_MONGO_SERVER_HOSTNAME, TEST_MONGO_SERVER_PORT); + mongo.dropDatabase(TEST_DATABASE_NAME); + } + + @AfterClass + public static void tearDownAfterClass() throws Exception { + MongoClient mongo = new MongoClient(TEST_MONGO_SERVER_HOSTNAME, TEST_MONGO_SERVER_PORT); + mongo.dropDatabase(TEST_DATABASE_NAME); + } + + @Before + public void setUp() throws Exception { + // Ensure both the appender and the JUnit test use the same collection + // object - provides consistency across reads (JUnit) & writes (Log4J) + collection = mongo.getDatabase(TEST_DATABASE_NAME).getCollection(TEST_COLLECTION_NAME); + collection.drop(); + } + + /** + * Here the timestamp we get from DB is Date type. Hence it is valid here to type cast timestamp into java.util.Date + * and it does not throw any exception. If timestamp is stored as string in the db then it will not cast into the + * java.util.Date and it will throw typecast exception. + */ + @Test(expected = Test.None.class) + public void testDateStoredInMongodbIsISOObject() { + PropertyConfigurator.configure(getValidPatternLayoutProperties()); + + MongoDbAppender appender = (MongoDbAppender) Logger.getRootLogger().getAppender( + APPENDER_NAME); + + collection = mongo.getDatabase(TEST_DATABASE_NAME).getCollection(TEST_COLLECTION_NAME); + appender.setCollection(collection); + + FindIterable entries = collection.find(DBObject.class); + for (DBObject entry : entries) { + assertNotNull(entry); + //here date is type cast into Date. It will not throw the exception as the date saved in DB is ISODate + Date date = (Date)entry.get("timestamp"); + } + } + + private Properties getValidPatternLayoutProperties() { + Properties props = new Properties(); + props.put("log4j.rootLogger", "DEBUG, MongoDBPatternLayout"); + props.put("log4j.appender.MongoDBPatternLayout", + "org.log4mongo.MongoDbPatternLayoutDateAppender"); + props.put("log4j.appender.MongoDBPatternLayout.databaseName", "log4mongotest"); + props.put("log4j.appender.MongoDBPatternLayout.layout", "org.log4mongo.CustomPatternLayout"); + props.put( + "log4j.appender.MongoDBPatternLayout.layout.ConversionPattern", + "{\"extra\":\"%e\",\"timestamp\":\"%d{DATE}\",\"level\":\"%p\",\"class\":\"%c{1}\",\"message\":\"%m\"}"); + return props; + } +}