Use MongoDB to store Sitecore log entries centrally!

Posted 02/24/2015 by Akshay Sura

The idea is to store a copy of the Sitecore logs in a central location such as a common MongoDB. Why? hmm why not? If you are running multiple CD servers and you want to view the logs for all of them in one location, might be one reason. The reason will become apparent in the coming days/weeks!

For now, the process. I used the following blog posts as reference:

1. Anders Laub - Working with custom MongoDB collections in Sitecore 8 using WebApi

2. Thomas Stern - Storing Sitecore log events in MongoDB

First a class representing the data we want to store:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using log4net.helpers;
using log4net.spi;
using MongoDB.Bson;

namespace YOURNAMESPACE.Log
{
    public class LoggingEventEx 
    {
        //Id will get auto generated when a new record is added into MongoDB
        public ObjectId Id { get; set; }

        //The server name on which this logger is being run, you will find out the bigger purpose in the coming days/weeks!
        public string ServerId { get; set; }

        //fields we are interested in from LogginEvent without having to inherit it
        public string Domain { get; set; }
        public FixFlags Fix { get; set; }
        public string Identity { get; set; }
        public Level Level { get; set; }
        public LocationInfo LocationInformation { get; set; }
        public string LoggerName { get; set; }
        public IDictionary MappedContext { get; set; }
        public object MessageObject { get; set; }
        public string NestedContext { get; set; }
        public PropertiesCollection Properties { get; set; }
        public string RenderedMessage { get; set; }
        public static DateTime StartTime { get; set; }
        public string ThreadName { get; set; }
        public DateTime TimeStamp { get; set; }
        public string UserName { get; set; }
    }
}

Next, we need the LogRepository which will interact with MongoDB. You can use reflector to see how Sitecore is currently doing this for tracking and analytics:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.ContentSearch.Security;
using Sitecore.ContentSearch.Utilities;
using Sitecore.Diagnostics;
using log4net.spi;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Builders;

namespace YOURNAMESPACE.Log
{
    public class LogRepository
    {
        private readonly MongoCollection _LoggingEventExCollection;

        private readonly string _connectionString;
        private readonly string _collectionName;

        public LogRepository(string connectionString, string collectionName)
        {
            _connectionString = connectionString;
            _collectionName = collectionName;

            _LoggingEventExCollection = GetCollection(connectionString, _collectionName);
            Assert.IsNotNull(_LoggingEventExCollection, "LoggingEventExsCollection");
        }

        public IEnumerable<LoggingEventEx> GetAll()
        {
            return _LoggingEventExCollection.FindAllAs<LoggingEventEx>();
        }
        
        public LoggingEventEx Get(ObjectId id)
        {
            return _LoggingEventExCollection.FindOneAs<LoggingEventEx>(GetIDQuery(id));
        }
        
        public bool Set(LoggingEventEx LoggingEventEx)
        {
            return _LoggingEventExCollection.Insert(LoggingEventEx).Ok;
        }
        
        protected IMongoQuery GetIDQuery(ObjectId id)
        {
            return Query<LoggingEventEx>.EQ(c => c.Id, id);
        }
        
        private static MongoCollection GetCollection(string connectionString, string collectionName)
        {
            var url = new MongoUrl(connectionString);
            return new MongoClient(url).GetServer().GetDatabase(url.DatabaseName).GetCollection(collectionName);
        }
    }
}

Next, we need a class MongoLogger, implementing the AppenderSkeleton from the log4net implementation tucked away in Sitecore.Logging.dll. We will pass in the connection string name and the collection name from the config.

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using log4net.Appender;
using log4net.spi;
using MongoDB.Bson;

namespace YOURNAMESPACE.Log
{
    public class MongoLogger : AppenderSkeleton
    {
        public string ConnectionString { get; set; }
        public string CollectionName { get; set; }
        private LogRepository _logRepository;

        public LogRepository LogRepository
        {
            get
            {
                if (_logRepository == null)
                {
                    _logRepository = new LogRepository(ConfigurationManager.ConnectionStrings[ConnectionString].ConnectionString, CollectionName);
                }
                return _logRepository;
            }
        }

        protected override void Append(LoggingEvent loggingEvent)
        {
            LoggingEventEx logEventEx = ToDerived<LoggingEvent, LoggingEventEx>(loggingEvent);
            logEventEx.ServerId = System.Environment.MachineName; //get current machine name ;)
            LogRepository.Set(logEventEx);
        }

        //use relection to copy property values from LoggingEvent to LoggingEventEx
        private TDerived ToDerived<TBase, TDerived>(TBase tBase)
    where TDerived: new()
        {
            TDerived tDerived = new TDerived();
            foreach (PropertyInfo propBase in typeof(TBase).GetProperties())
            {
                PropertyInfo propDerived = typeof(TDerived).GetProperty(propBase.Name);
                propDerived.SetValue(tDerived, propBase.GetValue(tBase, null), null);
            }
            return tDerived;
        }
    }
}

Once all the above are done, we get to the configs. 

Add a connection string to the new MongoDB.

<add name="mongologger" connectionString="mongodb://localhost:27017/mongologger" />

Here what I did is to define an Appender called MongoDBAppender for log4net calling the MongoLogger:

    <appender name="MongoDBAppender" type="YOURNAMESPACE.Log.MongoLogger, YOURASSEMBLY">
      <connectionstring value="mongologger" />
      <collectionname value="log" />
    </appender>

I also added a Logger which by default inherits the root. It's generally not a good practice to add to the root node. 

The MongoDBAppender logger is explicitly configured to log to MongoDBAppender, and is configured not to inherit settings from parent loggers (additivity="false"). Therefore it doesn't log to LogFileAppender. If you set additivity="true" it will inherit settings and log to both LogFileAppender and MongoDBAppender.

Here we added the MongoDBAppender followed by LogFileAppender. What this does is that it duplicates the entries in to the MongoDB, which is exactly what I am looking for.

    <logger name="MongoLogger" additivity="false">
      <level value="INFO" />
      <appender-ref ref="MongoDBAppender" />
      <appender-ref ref="LogFileAppender" />
    </logger>

Final product, log entries in the MongoDB and the regular Log file.

Mongo Log

Please get in touch with me if you have questions or comments.


Share:

Archive

Syndication