dotnetco.de
NLog Logo

Logging in Xamarin Forms using NLog

For exception logging you could use online services like Microsofts AppCenter but sometimes you need a more detailled log and more flexibility, like changing log target and log level on the fly. Therefore here is an overview how to do logging in Xamarin Forms apps using NLog. The summary is based on several other existing articles so you will find a link list at the end of this article.

What is NLog?

NLog already exists for years (first release was in January 2011). It’s a universal logger for .net applications. NLog is maintained on Github and released under BSD License so you could do nearly everything, like using in private and commercial applications etc. Configuration is done within config files and could be changed on the fly. You have different log levels (like debug, info, error) and different log targets (currently 89 different targets like file, console, etc). Check the NLog Wiki for more details.

How to implement in Xamarin Forms

Add NLog Package

Add the NLog nuget package to your shared code. Current Release is 4.6.8. No need to add any package to your Android or iOS Project.

Add Interface

In your shared project, add the interface “ILogService.cs” for your logging. I’ve placed it in folder “Interfaces”. Only method needs to be the initialization:

using System.Reflection;

namespace MyApp.Interfaces
{
    public interface ILogService
    {
        void Initialize(Assembly assembly, string assemblyName);
    }
}

Add LogService Class

Next, add a “LogService.cs” to your shared project. I’ve created a folder “Helpers” and placed it there. The location differs depending on your platform. On some pages I’ve seen that $”{assemblyName}.NLog.config” should work fine but take care that on Android the platform has to be Droid, not Android. Therefore I’m using the if-clause to be sure to have the correct name.

using System;
using System.Reflection;
using MyApp.Interfaces;
using NLog;
using NLog.Config;
using Xamarin.Forms;

namespace MyApp.Helpers
{
    public class LogService : ILogService
    {
        public void Initialize(Assembly assembly, string assemblyName)
        {
            string resourcePrefix;

            if (Device.RuntimePlatform == Device.iOS)
                resourcePrefix = "MyApp.iOS";
            else if (Device.RuntimePlatform == Device.Android)
                resourcePrefix = "MyApp.Droid";
            else
                throw new Exception("Could not initialize Logger: Unknonw Platform");

            //var location = $"{assemblyName}.NLog.config";

            string location = $"{resourcePrefix}.NLog.config";
            Stream stream = assembly.GetManifestResourceStream(location);
            if (stream == null)
                throw new Exception($"The resource '{location}' was not loaded properly.");

            LogManager.Configuration = new XmlLoggingConfiguration(System.Xml.XmlReader.Create(stream), null);
        }
    }
}

Update Android MainActivity.cs

In your Android project, update MainActivity.cs and add a new sub InitializeNLog:

protected override void OnCreate(Bundle savedInstanceState)
        {
            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;

            base.OnCreate(savedInstanceState);

            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            global::Xamarin.Forms.Forms.Init(this, savedInstanceState);

            InitializeNLog();
            LoadApplication(new App());
        }
        
        private void InitializeNLog()
        {
            Assembly assembly = this.GetType().Assembly;
            string assemblyName = assembly.GetName().Name;
            new Helpers.LogService().Initialize(assembly, assemblyName);
        }

Add NLog.config to Android

Now it’s time to add the configuration. Name it “NLog.config” in your root Android folder and modify the config according to your needs, see NLog Wiki about configuration file. NLog Documentation also contains detailled information about all different log targets and the available options for each target.

Here is a sample file which works quite well on Android. It has 2 targets: Logfile and Console. You could set e.g. different log levels to your targets so you could log all debug messages to console, but only store info messages (and above) to your logfile.

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
      autoReload="true"
      throwExceptions="false">

  <targets>
    <target name="logfile"
        xsi:type="File"
        fileName="${specialfolder:folder=LocalApplicationData}/logs/nlog.csv"
        archiveFileName="${specialfolder:folder=LocalApplicationData}/logs/nlog-{#}.csv"
        archiveEvery="Hour"
        archiveNumbering="Date"
        maxArchiveFiles="5"
        archiveDateFormat="yyyy-MM-dd-HH-mm"
        encoding="UTF-8">
      <layout xsi:type="CSVLayout">
        <quoting>All</quoting>
        <withHeader>true</withHeader>
        <delimiter>Comma</delimiter>
        <column name="time" layout="${longdate}" />
        <column name="logger" layout="${logger}"/>
        <column name="level" layout="${level}"/>
        <column name="machinename" layout="${machinename}"/>
        <column name="windows-identity" layout="${windows-identity}"/>
        <column name="appdomain" layout="${appdomain}"/>
        <column name="processid" layout="${processid}"/>
        <column name="processname" layout="${processname}"/>
        <column name="threadid" layout="${threadid}"/>
        <column name="message" layout="${message}" />
        <column name="stacktrace" layout="${exception:format=Type,Message,StackTrace,Data:maxInnerExceptionLevel=5}" />
      </layout>
    </target>
    <target xsi:type="Console" name="console" layout="${longdate} ${uppercase:${level}} ${message}" />
  </targets>
  <rules>
    <!-- Available LogLevels: Trace, Debug, Info, Warn, Error and Fatal -->
    <logger rulename="logfilelogger" name="*" minlevel="Warn" writeTo="logfile" />
    <logger rulename="consolelogger" name="*" minlevel="Debug" writeTo="console" />
  </rules>

</nlog>

It’s important to check the Build Action afterwards on new NLog.config. Right-click on NLog.config, select “Build Action” and make sure “EmbeddedResource” is selected!

Update iOS AppDelegate.cs

Similar to Android, you also have to update your AppDelegate.cs in your iOS Project:

 [Register("AppDelegate")]
    public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
    {
        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            global::Xamarin.Forms.Forms.Init();
            InitializeNLog();
            LoadApplication(new App());

            return base.FinishedLaunching(app, options);
        }

        private void InitializeNLog()
        {
            Assembly assembly = this.GetType().Assembly;
            string assemblyName = assembly.GetName().Name;
            new Helpers.LogService().Initialize(assembly, assemblyName);
        }
    }

Add NLog.config to iOS Project

Also to your iOS Project you have to add a new NLog.config of course. Place it in the root folder of your iOS Project. As iOS has a slightly different directory structure, you could replace these 2 lines from the Android config and store your (archive) logfiles in the library folder of iOS:

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
      autoReload="true"
      throwExceptions="false">

  <targets>
    <target name="logfile"
        xsi:type="File"
        fileName="${specialfolder:folder=MyDocuments}/../Library/logs/nlog.csv"
        archiveFileName="${specialfolder:folder=MyDocuments}/../Library/logs/nlog-{#}.csv"
        archiveEvery="Hour"
        archiveNumbering="Date"
        maxArchiveFiles="5"
        archiveDateFormat="yyyy-MM-dd-HH-mm"
        encoding="UTF-8">
      <layout xsi:type="CSVLayout">
        <quoting>All</quoting>
        <withHeader>true</withHeader>
        <delimiter>Comma</delimiter>
        <column name="time" layout="${longdate}" />
        <column name="logger" layout="${logger}"/>
        <column name="level" layout="${level}"/>
        <column name="machinename" layout="${machinename}"/>
        <column name="windows-identity" layout="${windows-identity}"/>
        <column name="appdomain" layout="${appdomain}"/>
        <column name="processid" layout="${processid}"/>
        <column name="processname" layout="${processname}"/>
        <column name="threadid" layout="${threadid}"/>
        <column name="message" layout="${message}" />
        <column name="stacktrace" layout="${exception:format=Type,Message,StackTrace,Data:maxInnerExceptionLevel=5}" />
      </layout>
    </target>
    <target xsi:type="Console" name="console" layout="${longdate} ${uppercase:${level}} ${message}" />
  </targets>
  <rules>
    <!-- Available LogLevels: Trace, Debug, Info, Warn, Error and Fatal -->
    <logger rulename="logfilelogger" name="*" minlevel="Warn" writeTo="logfile" />
    <logger rulename="consolelogger" name="*" minlevel="Debug" writeTo="console" />
  </rules>

</nlog>

Select Build Action for iOS config

Again similar as the Android part, make sure that your iOS NLog.config file has Build Action “EmbeddedResource”.

Implement in Shared Code

Setup is done, so now we could add it to the code 🙂 Easiest way: Just add this line and you are ready to go:

private readonly NLog.ILogger Logger = NLog.LogManager.GetCurrentClassLogger();

For example in your MainPage.xaml.cs it would look like this:

public partial class MainPage : ContentPage
    {
        private readonly NLog.ILogger Logger = NLog.LogManager.GetCurrentClassLogger();
        public MainPage()
        {
            InitializeComponent();
            Logger.Info("Application Start.");
        }

Zip Logfiles

So now you have some logfiles locally on users’ smartphone. NLog has some FileTarget Archive Examples how to configure rolling logfiles based on size or date etc. However, you probably want to send the files to your server. To prepare them for sending, it might be helpful to zip them.

Zipping is split into 2 parts: First we have a general procedure which zips all files from a given directory into a zipfile. This general procedure is not only created for nlog, so you could use it also in other places if necessary. Second part is the nlog part.

During the zip process, I do not write anything into the logfiles in order to avoid problems with open files. NLog has a configuration option KeepFileOpen which is false by default so it should be safe to use logging also before zipping. Nevertheless for me it’s fine to not log the zip process.

Zip all files from directory

QuickZip is inspired by a post from Danny Cabrera but instead of adding files one by one, I’m zipping the whole directory. Therefore it’s important to have the actual logfile and all archives in the same (dedicated) directory.

The procedure needs 2 parameters: The directory to be zipped, and the new target filename of the zip.

bool QuickZip(string directoryToZip, string destinationZipFullPath)
{
    try
    {
        // Delete existing zip file if exists
        if (System.IO.File.Exists(destinationZipFullPath))
            System.IO.File.Delete(destinationZipFullPath);

        if (!System.IO.Directory.Exists(directoryToZip))
            return false;
        else
        {
            System.IO.Compression.ZipFile.CreateFromDirectory(directoryToZip, destinationZipFullPath, System.IO.Compression.CompressionLevel.Optimal, true);
            return System.IO.File.Exists(destinationZipFullPath);
        }
    }
    catch (Exception e)
    {
        Console.WriteLine($"Exception: {e.Message}");
        return false;
    }
}

Zip nlog files

Using above QuickZip, the next procedure sets the target zip filename and deletes all existing zips (if any). It zips all files from subdirectory “logs” (as defined in NLog.config) into a zip named by current date and time, e.g. “20200117-210536.zip”. The name of the zipfile is stored in a site variable. Advantage: I call this sub already in the “OnAppearing”-Event so that the zip is already created in the back while the user is on the Error Page and could enter some more details etc. so he does not need to wait for zipping when he presses “Send Logfile”.

void CreateZipFile()
{
    if (NLog.LogManager.IsLoggingEnabled())
    {
        string folder;

        if (Device.RuntimePlatform == Device.iOS)
            folder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "..", "Library");
        else if (Device.RuntimePlatform == Device.Android)
            folder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        else
            throw new Exception("Could not show log: Platform undefined.");

        //Delete old zipfiles (housekeeping)
        try
        {
            foreach (string fileName in System.IO.Directory.GetFiles(folder, "*.zip"))
            {
                System.IO.File.Delete(fileName);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error deleting old zip files: {ex.Message}");
        }

        string logFolder = System.IO.Path.Combine(folder, "logs");
        if (System.IO.Directory.Exists(logFolder))
        {
            zipFilename = $"{folder}/{DateTime.Now.ToString("yyyyMMdd-HHmmss")}.zip";
            int filesCount = System.IO.Directory.GetFiles(logFolder, "*.csv").Length;
            if (filesCount > 0)
            {
                if (!QuickZip(logFolder, zipFilename))
                    zipFilename = string.Empty;
            }
            else
                zipFilename = string.Empty;
        }
        else
            zipFilename = string.Empty;
    }
    else
        zipFilename = string.Empty;
}

So now you have all your nlog files in a zip file and could transfer them to your server as you like.

 

References

These information are based on several great articles out there. Here’s a list of these articles, I hope I did not forget one. Thanks to all of them!!

9 Comments

  1. Hello,
    First of all, awesome post. I find it very usefull.

    Second, I need to know if this code loggs warnings and exceptions automatically or if I need to log them manually, like Logger.Info(…) or Logger.Error(…).

    Thank you.

    1. Thanks Cristian! So far, the code does not log anything automatically. Such a mechanism might exist, but I prefer to log it all manually as I could then log more details, e.g. important variables or else.

  2. Hi,

    I have tried using this sample. but unfortunately, I am not able to see any log files in my android device. Though, I found no exception or err while debugging. Can you let me know if there is anything new or extra, I need to do.

    1. Hi, no, there is nothing new or extra since this blog post. I’m still using it in my projects and it’s logging fine. Easiest would be to check with your debugger, e.g. in the zipping file procedure whether this one finds the logs.

  3. Hello, this seems to work great other than trying to retrieve the directory
    folder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
    string logFolder = System.IO.Path.Combine(folder, “logs”);
    if (System.IO.Directory.Exists(logFolder))
    {
    // NEVER MAKES IT HERE…SAYS FIILE DOES NOT EXIST
    }

    The folder does not exist…..yet I can plainly see the log statements in the console

Leave a Comment