The goal is to create logging class and use RavenDB for storage. I originally wanted to use NLog, which doesn’t come with RavenDb target but one can easily add it. I did that too but my target was not behaving as intended, I was spinning my wheels trying to figure it out and finally decided to create my own service.
My intention is also to inject this logging service into clients using StructureMap. Here is my initial SM configuration:
x.For<ILogger>().Use<Logger>()
.Ctor<IDocumentStore>("store").Is(c => c.GetInstance<IDocumentStore>())
.Ctor<ILogEntry>("logEntry").Is(c => c.GetInstance<ILogEntry>())
.Ctor<ILogLevelConfig>("logLevelConfig")
.Is(c => c.GetInstance<ILogLevelConfig>());
x.For<IDocumentStore>()
.Singleton()
.Use(() => new DocumentStore { Url="http://localhost:8080/" })
.OnCreation<IDocumentStore>(c => c.Initialize());
// ILogEntry is implemented by a class, which will eventually be saved in Raven
// ILogLevelConfig is used for configuration purposes
Here is simplified version of the Logger:
public class Logger : ILogger
{
private readonly ILogEntry _logEntry;
private readonly IDocumentStore _store;
private readonly ILogLevelConfig _logLevelConfig;
public Logger(IDocumentStore store, ILogEntry logEntry, ILogLevelConfig logLevelConfig)
{
_logEntry = logEntry;
_logEntry.TimeStamp = new DateTimeOffset(DateTime.UtcNow);
_logLevelConfig = logLevelConfig;
_store = store;
}
public void Info(string message, string userId = null)
{
if (!_logLevelConfig.IsAllEnabled && !_logLevelConfig.IsInfoEnabled) return;
_logEntry.LogLevel = LogLevel.Info;
_logEntry.Message = message;
_logEntry.UserIdentity = userId;
Write();
}
private void Write()
{
using(var session = _store.OpenSession())
{
session.Store(_logEntry);
session.SaveChanges();
}
}
Now, let’s simulate injecting the Logger into some client:
var logger = ObjectFactory.GetInstance<ILogger>();
logger.Info("info 1");
logger.Info("info 2");
Questions:
- The way
Write()is implemented logger will hit the database twice [for ‘info 1’ and then for ‘info 2’. This is going to be extremely chatty approach if I decide to be very detailed with my logging. What can I do collect all mylogger.Info(...)into one and then have thesession.SaveChanges()be called once and save all my entries in one go? - When I substitute
IDocumentStoreimplementation like this:
x.For<IDocumentStore>().Singleton().Use(() => new EmbeddableDocumentStore { RunInMemory = true, UseEmbeddedHttpServer = true }).OnCreation<IDocumentStore>(c => c.Initialize());then only the second call tologger.Info(..)gets actually saved in the database. Why?
One issue with your object model is that a
ILogEntryinstance is tied to theILoggerinstance instead of being created for each log request as it should be. If you take a look at how NLog or log4net are implemented you will see that they create a corresponding log entry instance for every log request.You will see in typical logging/tracing implementations the notion of a Flush. This is what “flushes” log entries which have not yet been flushed. This allows the batching of several log entries. Again, existing logging implementations have this notions already implemented so you only have to override an existing class. For example NLog has a BufferingWrapper target for this exact purpose.
This may be resolved by updating your object model as described above.
Overall, I would try to extend an existing logging framework with a RavenDB target/appender instead of trying to roll your own. Logging is something that works well as a framework and you can learn quite a bit about logging practices in general by working with them.