Nick's .NET Travels

Continually looking for the yellow brick road so I can catch me a wizard....

Adding Logging to Client Applications using MetroLog not NLog

I wanted to add some logging to my set of applications and was somewhat disappointed to discover the complete lack of PCL support in NLog. After a quick search to find out what others are using I came across MetroLog which seems to be very similar in a lot of regards to NLog (in fact they claim the API surface is very similar indeed). I went to install the NuGet package…

image

and noticed that my Core library (a PCL) wasn’t in the list. Clearly my PCL profile doesn’t match one of the supported profiles which is a bit painful. However, it does support all my client application types so I was happy at least to use MetroLog.

image

I did have a couple of issues installing the NuGet package, namely Visual Studio decided to crash mid way through installing the packages. This meant I had to manually install Microsoft.Bcl.Compression which is a dependency for the Windows Phone 8.0 project, and then uninstall and reinstall the support for the Windows and Desktop projects.

After all this I was building successfully again so it was time to think about how to structure the logging support. Clearly with logging I want it to be a simple as possible and yet accessible virtually anywhere within the application. I want to define a service that is available within my Core library in a similar way to my IDataService and ISyncService implementation. I also wanted the log output to be written out to a sqlite database file for ease of access (there are plenty of third party tools capable of viewing sqlite db files) but rather than use the SqliteTarget that comes with MetroLog I felt I had to write my own (as you do). Luckily this whole process is relatively simple.

I start by creating an ILogWriterService interface which will provide the low level API for writing a LogEntry to a local Mobile Service sqlite table (I’m going to use the same process that is used to cache my synchronized data, except for the time being at least, the data won’t be synchronized anywhere).

public interface ILogWriterService
{

    IMobileServiceSyncTable<LogEntry> LogTable { get; }
    Task Initialize();
}

public class LogEntry : BaseEntityData
{
    public DateTime LogDateTime { get; set; }
    public string Entry { get; set; }
}

public class LogWriterService : ILogWriterService
{
    private readonly MobileServiceClient mobileService = new MobileServiceClient(Constants.MobileServiceRootUri);

    private IMobileServiceClient MobileService
    {
        get { return mobileService; }
    }

    public IMobileServiceSyncTable<LogEntry> LogTable
    {
        get { return MobileService.GetSyncTable<LogEntry>(); }
    }

    public async Task Initialize()
    {
        var data = new MobileServiceSQLiteStore("log.db");
        data.DefineTable<LogEntry>();

        await MobileService.SyncContext.InitializeAsync(data, new MobileServiceSyncHandler());
    }
}

Next I define the high level service interface, ILogService:

public interface ILogService
{
    void Debug(string message);

    void Exception(string message, Exception ex);
}

So far, all of these classes have been in the Core library. However, the implementation of the ILogService has to be in the Shared.Client project as it needs to be used by all the client projects.

public class LogService : ILogService
{
    public ILogWriterService Writer { get; set; }

    public ILogger Logger { get; set; }

    public LogService(ILogWriterService writer)
    {
        Writer = writer;
        var target = new MobileServiceTarget(Writer);

        LogManagerFactory.DefaultConfiguration.AddTarget(LogLevel.Debug, target);

        Logger = LogManagerFactory.DefaultLogManager.GetLogger("Default");
    }

    public void Debug(string message)
    {
        Logger.Debug(message);
    }

    public void Exception(string message, Exception ex)
    {
        Logger.Error(message, ex);
    }
}

As you can see the implementation sets up the MetroLog logger but uses a custom MobileServiceTarget as the destination. This is implemented as follows:

public class MobileServiceTarget : Target
{
    public ILogWriterService Writer { get; set; }
    public MobileServiceTarget(ILogWriterService writer)
        : base(new SingleLineLayout())
    {
        Writer = writer;
    }

    private bool InitCompleted { get; set; }
    protected async override Task<LogWriteOperation> WriteAsyncCore(LogWriteContext context, LogEventInfo entry)
    {
        try
        {
            if (!InitCompleted)
            {
                await Writer.Initialize();
                InitCompleted = true;
            }
            var log = new LogEntry { LogDateTime = DateTime.Now, Entry = entry.ToJson() };
            await Writer.LogTable.InsertAsync(log);
            return new LogWriteOperation(this, entry, true);
        }
        catch (Exception ex)
        {
            return new LogWriteOperation(this, entry, false);
        }
    }
}

I of course need to register the implementations with Autofac:

builder.RegisterType<LogWriterService>().As<ILogWriterService>();
builder.RegisterType<LogService>().As<ILogService>();

And the last thing is a static helper class that makes logging for two core scenarios really easy:

public static class LogHelper
{
    public static void Log<TEntity>(TEntity entity, [CallerMemberName] string caller = null)
    {
        var json = JsonConvert.SerializeObject(entity);
        Log(typeof(TEntity).Name + ": " + json, caller);
    }

    public static void Log(string message = null, [CallerMemberName] string caller = null)
    {
        try
        {
            InternalWriteLog("[" + caller + "] " + message);
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
    }

    public static void Log(this Exception ex, string message = null, [CallerMemberName] string caller = null)
    {
        try
        {
            Debug.WriteLine("Exception ({0}): {1}", caller, ex.Message);
            InternalWriteException(caller + ": " + message, ex);
        }
        catch (Exception ext)
        {
            Debug.WriteLine(ext.Message);
        }
    }

    private static ILogService logService;

    private static ILogService LogService
    {
        get
        {
            if (logService == null)
            {
                logService = ServiceLocator.Current.GetInstance<ILogService>();

            }
            return logService;
        }
    }

    private static void InternalWriteLog(string message)
    {
        try
        {

            LogService.Debug(message);
        }
        catch (Exception ext)
        {
            Debug.WriteLine(ext.Message);
        }
    }

    private static void InternalWriteException(string message, Exception ex)
    {
        try
        {
            LogService.Exception(message, ex);
        }
        catch (Exception ext)
        {
            Debug.WriteLine(ext.Message);
        }
    }
}

The first scenario is a simple string output eg:

LogHelper.Log("Startup complete");

The second is logging the output of an Exception:

try
{
   …
}
catch (Exception ex)
{
    ex.Log();
}

Note that the Exception logging also does a Debug.WriteLine which is convenient during development to pick up any issues in the Output window.

Comments are closed