Search This Blog

2011-10-05

log4net appender for Raven DB

Recently I have started to look at noSQL databases and Document-Oriented DBs particularly for the .NET platform to solve some issues within our solution. One of the brilliant examples of such databases is Raven DB by Ayende. IMHO, one of the benefits of Raven DB is easy to start way. It has almost all I need out of the box: simple deploy, native .NET client with Linq, Map/Reduces indexes, RESTful API and so on and so forth.
Raven DB itself uses NLog by default and our solution uses log4net like thousands of other ones. So I decided to experiment with custom log4net appender for our needs because one of the first steps of moving to noSQL direction might be logging, due to simple implementation. Such an approach is far more flexible than logging to text files and might significantly reduce load on SQL servers in case of using ADO.NET appender.
So let's start.
First we need to decide what we are going to store. Since logging event  is simple and independent document  we can just wrap LoggingEvent.
// The log entry document entity that will be stored to the database.
public class Log : INamedDocument
{
  ...

  public Log(LoggingEvent logEvent)
  {
    if (logEvent == null)
    {
    throw new ArgumentNullException("logEvent");
    }

    this.LoggerName = logEvent.LoggerName;
    this.Domain = logEvent.Domain;
    this.Identity = logEvent.Identity;
    this.ThreadName = logEvent.ThreadName;
    this.UserName = logEvent.UserName;
    this.MessageObject = logEvent.MessageObject;
    this.TimeStamp = logEvent.TimeStamp;
    this.Exception = logEvent.ExceptionObject;
    this.Message = logEvent.RenderedMessage;
    this.Fix = logEvent.Fix.ToString();

    ...

    // Raven doesn't serialize unknown types like log4net's PropertiesDictionary
    this.Properties = logEvent.Properties.GetKeys().ToDictionary(key => key, key => logEvent.Properties[key].ToString());
  }

  public string LoggerName { get; set; }
  public string Domain { get; set; }
  public string Identity { get; set; }
  public string ThreadName { get; set; }
  public string UserName { get; set; }
  public object MessageObject { get; set; }
  public DateTime TimeStamp { get; set; }
  public object Exception { get; set; }
  public string Message { get; set; }
  public string Fix { get; set; }
  public IDictionary Properties { get; set; }
  ....
}
Next step will be choosing which appender template we need and which additional settings Raven requires. I think using of buffering appender makes sense in order not to drive database connection up the wall. As for additional setting of appender we might need connection string (or connection string name) and Raven's settings about max number of request per document session.
So our ".config" file would have something like this:


  
Ok, we have all the prerequisites and we can write some code.
Implementation is simple: on SendBuffer method it adds log events to a document session and saves changes to a DB server. On ActivateOptions it will initialize DB connection, i.e. create document store instance.
  public class RavenAppender : BufferingAppenderSkeleton
  {
    private readonly object lockObject = new object();

    private IDocumentStore documentStore;

    private IDocumentSession documentSession;

    #region Appender configuration properties

    /// 
    /// Gets or sets the connection string.
    /// 
    /// 
    /// The connection string.
    /// 
    public string ConnectionString { get; set; }

    /// 
    /// Gets or sets the max number of requests per session.
    /// By default the number of remote calls to the server per session is limited to 30.
    /// 
    /// 
    /// The max number of requests per session.
    /// 
    public int MaxNumberOfRequestsPerSession { get; set; }

    #endregion

    /// 
    /// Initializes a new instance of the  class.
    /// 
    public RavenAppender() {}

    /// 
    /// Initializes a new instance of the  class. 
 /// Can be used for unit testing
    /// 
    /// 
The document store.
    public RavenAppender(IDocumentStore documentStore)
    {
      if (documentStore == null)
      {
        throw new ArgumentNullException("documentStore");
      }

      this.documentStore = documentStore;
    }

    protected override void SendBuffer(LoggingEvent[] events)
    {
      if (events == null || !events.Any())
      {
        return;
      }

      this.CheckSession();

      foreach (var entry in events.Where(e => e != null).Select(e => new Log(e)))
      {
        this.documentSession.Store(entry);
      }

      this.Commit();
    }

    public override void ActivateOptions()
    {
      try
      {
        this.InitServer();
      }
      catch (Exception exception)
      {
        ErrorHandler.Error("Exception while initializing Raven Appender", exception, ErrorCode.GenericFailure);
        // throw;
      }
    }

    protected override void OnClose()
    {
      this.Flush();
      this.Commit();

      try
      {
        if (this.documentSession != null)
        {
          this.documentSession.Dispose();
        }

        if (this.documentStore != null && !this.documentStore.WasDisposed)
        {
          this.documentStore.Dispose();
        }
      }
      catch (Exception e)
      {
        ErrorHandler.Error("Exception while closing Raven Appender", e, ErrorCode.GenericFailure);
      }

      base.OnClose();
    }

    protected virtual void Commit()
    {
      if (this.documentSession == null)
      {
        return;
      }

      try
      {
        this.documentSession.SaveChanges();
      }
      catch (Exception e)
      {
        ErrorHandler.Error("Exception while commiting to the Raven DB", e, ErrorCode.GenericFailure);
      }
    }

    private void CheckSession()
    {
      if (this.documentSession != null)
      {
        return;
      }

      lock (this.lockObject)
      {
        if (this.documentSession != null)
        {
          if (this.documentSession.Advanced.NumberOfRequests >= this.documentSession.Advanced.MaxNumberOfRequestsPerSession)
          {
            this.Commit();
            this.documentSession.Dispose();
          }
          else
          {
            return;
          }
        }

        this.documentSession = this.documentStore.OpenSession();
        this.documentSession.Advanced.UseOptimisticConcurrency = true;

        if (this.MaxNumberOfRequestsPerSession > 0)
        {
          this.documentSession.Advanced.MaxNumberOfRequestsPerSession = this.MaxNumberOfRequestsPerSession;
        }
      }
    }

    private void InitServer()
    {
      if (this.documentStore != null)
      {
        return;
      }

      if (string.IsNullOrEmpty(this.ConnectionString))
      {
        var exception = new InvalidOperationException("Connection string is not specified.");
        ErrorHandler.Error("Connection string is not specified.", exception, ErrorCode.GenericFailure);

        return;
      }

      this.documentStore = new DocumentStore
      {
        ConnectionStringName = this.ConnectionString
      };

      this.documentStore.Initialize();
    }
  }
One detail we should care about is max number of requests per session. RavenDB's default limitation is 30 requests per session. On the one hand we have moved this option to the config file on the other hand we just recreate a session when max number of requests is reached.
So this is it: simple but effective. With RavenDB indexes it is possible to have extra-analysis or statistics of logged events, for instance, number of error per day / per module, new errors or warnings, etc.
Sources are available on GitHub.
NuGet package: http://nuget.org/List/Packages/log4net.Raven

Update:
The screenshot of the test Log documents



2011-08-30

Hard SVN branch switch for Git

I have been working with Git through git-svn bridge in SVN based organization together with Alexander Beletsky. We practice Git to solve lots of SVN issues.
There are several tricky moments with git-svn, for instance, SVN branch switching. If there is not quite big difference between your git repository and svn repo, you can use this advice.
But when I had returned from vacation, I found this approach was not working. I have missed one iteration so my Git repository should jump over huge list of changes. Saying in another words, I need just replace my "master" branch with SVN's latest iteration branch.
This solution helped me:

1. Create new git-svn branch.
git config --add svn-remote.SvnRemoteNewBranch.url https://svn/branches/NewIteration
git config --add svn-remote.SvnRemoteNewBranch.fetch :refs/remotes/SvnRemoteNewBranch
git svn fetch SvnRemoteNewBranch
git checkout -b LocalNewBranch -t SvnRemoteNewBranch
git svn rebase SvnRemoteNewBranch 

With it you will get the latest changes from svn to the "LocalNewBranch".
2. Replace "master" branch with svn's one.
git merge -s ours master
git checkout master
git merge LocalNewBranch  

This is it! Your "master" branch has the latest changes.


2011-05-17

JavaScript tip: on complete event

Recently I was fixing some issue in the JavaScipt event handler. Task was simple: to save user scroll position setting for the HTML control to the server.
Of course, it is possible to subscribe on the "scroll" event of the jQuery control and add a handler. But while user is scrolling it invokes event handler enormous number of times :-), so I need only "on scroll complete" event. And it took me some time to find one of the solutions.
If you need to have an "on complete" entry for some continuing event (like scrolling), described solution might help you.
Some event handler:

This code will write "scrolling..." to the console a lot of times while scrolling. In order to make it work just "on complete" we can add the timer function. Idea is simple: each time while scrolling it will cancel the timer function and then create it again. After the last scroll event (and after 1 sec) we will have an "on complete" function so it will write some message to the console.
EventHandler = function ($control) {
  if (!$control) {
    throw ("jQuery control is expected");
  }

  this.$control = $control;
  this.scrollCompleteHandling = null;
};

EventHandler.prototype.setupHandlers = function () {
  var self = this;
  this.$control.bind("scroll", function () {
    if (self.scrollCompleteHandling) {
      clearTimeout(self.scrollCompleteHandling);
    }
    self.scrollCompleteHandling = setTimeout(function () {
      console.log("scrolled...");
    }, 1000);
  });
};
It might not be the best option, but still it works :-)