Skip to content

Instantly share code, notes, and snippets.

@DamianSuess
Last active October 17, 2025 14:38
Show Gist options
  • Save DamianSuess/df8798838b9c1af47d5ba8dee9835f1e to your computer and use it in GitHub Desktop.
Save DamianSuess/df8798838b9c1af47d5ba8dee9835f1e to your computer and use it in GitHub Desktop.
NLog with Prism

Using NLog with Prism Library

Supports NLog v6.0 and above.

Overview

This is a simple example for how to implement NLog with your Prism application by simply creating an extension method for Prism's IContainerRegistry.

Using this implementation method is very simple and aims to use very little moving parts.

Alternative Approach

As an alternative approach, you can create your own custom LogService and wire-in class lookups etc.

The following snippet was created for a Prism.Avalonia application using NLog v5. It is long and adds overhead that must be maintained.

Program.cs

using Avalonia;
using Avalonia.Logging;
using NLog;
using NLog.Layouts;

public class Program
{
  // Avalonia configuration, don't remove; also used by visual designer.
  public static AppBuilder BuildAvaloniaApp() => AppBuilder
    .Configure<App>()
    .UsePlatformDetect()
    .With(new X11PlatformOptions { EnableMultiTouch = true, UseDBusMenu = true, })
    .With(new Win32PlatformOptions())
    .WithInterFont()
    .UseSkia()
    .UseReactiveUI()
    //// NOTE: The extra logging will slow things down. Use ".LogToTrace()"
    ////.LogToTrace(LogEventLevel.Debug, LogArea.Property, LogArea.Layout, LogArea.Binding);
    .LogToTrace(LogEventLevel.Debug, LogArea.Property, LogArea.Binding);
  
    [ExcludeFromCodeCoverage]
    public static void Main(string[] args)
    {
        ////if (CheckPreviousInstance())
        ////{
        ////    Console.WriteLine("Previous instance already running, closing app.");
        ////    return;
        ////}

        var nlogConfig = "diagnostics.nlog.config";
        try
        {
            NLog.LogManager.LoadConfiguration(nlogConfig);
        }
        catch
        {
            // File not found, manually configure
            var config = LogManager.Configuration ?? new NLog.Config.LoggingConfiguration();
            Layout minLevel;
            if (config.Variables.ContainsKey("minlevel"))
                minLevel = config.Variables["minlevel"];
            else
                minLevel = "Warn";

            string minLevelString = minLevel.Render(LogEventInfo.CreateNullEvent());
            LogLevel minLevelValue = LogLevel.FromString(minLevelString);

            // Targets where to log to: File and Console
            var logfile = new NLog.Targets.FileTarget(nlogConfig) { FileName = "Log/Diagnostics.log" };
            var logfileError = new NLog.Targets.FileTarget(nlogConfig) { FileName = "Log/Diagnostics-Error.log" };
            var logconsole = new NLog.Targets.ConsoleTarget("logconsole");

            // Rules for mapping loggers to targets
            config.AddRule(minLevelValue, LogLevel.Info, logconsole);
            config.AddRule(minLevelValue, LogLevel.Trace, logfile);
            config.AddRule(minLevelValue, LogLevel.Error, logfileError);

            // Re-Apply config
            NLog.LogManager.Configuration = config;
        }

        BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
    }
}

LogService

public enum LogLevel
{
    Trace,
    Debug,
    Info,
    Warn,
    Error,
    Fatal,
}

public class LogService : ILogService
{
    private static readonly Assembly LogAssembly = typeof(LogService).GetAssembly();
    private static readonly Assembly MscorlibAssembly = typeof(string).GetAssembly();
    private static readonly Assembly SystemAssembly = typeof(Debug).GetAssembly();

    private readonly IEventAggregator _eventAggregator;

    public LogService(IEventAggregator ea)
    {
        _eventAggregator = ea;
    }

    /// <summary>Gets the NLog Logger. Default config set by, 'nlog.config'.</summary>
    /// <remarks>Override default configuration file name via, 'Configure(..)' method.</remarks>
    public NL.Logger NLogger { get; private set; } = NL.LogManager.GetCurrentClassLogger();

    private static string FormattedTime
    {
        get
        {
            string date = $"{DateTime.Now.ToString("yyyy-MM-dd")} ";
            return date + $"{DateTime.Now.Hour:00}:{DateTime.Now.Minute:00}:{DateTime.Now.Second:00}.{DateTime.Now.Millisecond:000}";
        }
    }

    /// <summary>Alternative configuration.</summary>
    /// <code>
    /// <![CDATA[
    ///     // Set using DI
    ///     var logService = Container.Resolve<ILogService>();
    ///     logService.Configure("diagnostics.nlog.config");
    /// ]]>
    /// </code>
    /// <param name="configFile">Configuration file name.</param>
    public void Configure(string configFile)
    {
        NLogger = NL.LogManager.LoadConfiguration(configFile).GetCurrentClassLogger();
    }

    public void Debug(string message)
    {
        Log(LogLevel.Debug, message);
    }

    public void Error(string message)
    {
        Log(LogLevel.Error, message);
    }

    public void Fatal(string message)
    {
        Log(LogLevel.Fatal, message);
    }

    public void Info(string message)
    {
        Log(LogLevel.Info, message);
    }

    /// <summary>Log to Shell Window's LogOutput module and log as, Trace.</summary>
    /// <param name="message">Log message.</param>
    public void Status(string message)
    {
        Status(string.Empty, message);
    }

    /// <summary>Log to Shell Window's LogOutput module and log as, Trace.</summary>
    /// <param name="customSeverity">Severity level.</param>
    /// <param name="message">Log message.</param>
    public void Status(string customSeverity, string message)
    {
        // TODO: Make a different service for logging to LogOutput Module
        try
        {
            // Inject our custom "[${logger}]"
            Log(LogLevel.Trace, $"{customSeverity} {message}");

            // Send out for listeners to pick it up
            _eventAggregator
              .GetEvent<LogEvent>()
              .Publish(new LogInfo(customSeverity, message));
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine($">> LogService.Status could not parse calling method. Exception: {ex.Message}");
        }
    }

    public void Warn(string message)
    {
        Status(LogLevel.Warn.ToString(), message);
    }

    /// <summary>Extracts calling frame namespace and outputs.</summary>
    /// <param name="logLevel">Custom LogLevel.</param>
    /// <param name="message">User message.</param>
    private void Log(LogLevel logLevel, string message)
    {
        var nLogLevel = ToLogLevel(logLevel);

        var stackTrace = new System.Diagnostics.StackTrace();
        var loggerName = string.Empty;      // = (loggerName ?? Name) ?? string.Empty;
        var userFrameIndex = -1;

        // NLogTraceListener and StackTraceUsageUtils.LookupClassNameFromStackFrame(..)
        for (int i = 0; i < stackTrace.FrameCount; ++i)
        {
            var frame = stackTrace.GetFrame(i);
            loggerName = LookupClassNameFromStackFrame(frame);
            if (!string.IsNullOrEmpty(loggerName))
            {
                userFrameIndex = i;
                break;
            }
        }

        System.Diagnostics.Debug.WriteLine($">> [{FormattedTime}] [{logLevel}] [{loggerName}] [{message}");

        if (userFrameIndex >= 0)
        {
            NLogger.Log(nLogLevel, $"{loggerName}] [{message}");
        }
    }

    /// <summary>Returns the classname from the provided StackFrame (If not from internal assembly).</summary>
    /// <param name="stackFrame">StackFrame.</param>
    /// <returns>Valid class name, or empty string if assembly was internal.</returns>
    private string LookupClassNameFromStackFrame(StackFrame stackFrame)
    {
        var method = stackFrame.GetMethod();
        if (method != null && LookupAssemblyFromStackFrame(stackFrame) != null)
        {
            string className = GetStackFrameMethodClassName(method, true, true, true);
            if (!string.IsNullOrEmpty(className))
            {
                if (!className.StartsWith("System.", StringComparison.Ordinal))
                    return className;
            }
            else
            {
                className = method.Name ?? string.Empty;
                if (className != "lambda_method" && className != "MoveNext")
                    return className;
            }
        }

        return string.Empty;
    }

    /// <summary>Returns the assembly from the provided StackFrame (If not internal assembly).</summary>
    /// <returns>Valid assembly, or null if assembly was internal.</returns>
    private Assembly LookupAssemblyFromStackFrame(StackFrame stackFrame)
    {
        var method = stackFrame.GetMethod();
        if (method is null)
        {
            return null;
        }

        var assembly = method.DeclaringType?.GetAssembly() ?? method.Module?.Assembly;

        // skip stack frame if the method declaring type assembly is from hidden assemblies list
        if (assembly == LogAssembly)
        {
            return null;
        }

        if (assembly == MscorlibAssembly)
        {
            return null;
        }

        if (assembly == SystemAssembly)
        {
            return null;
        }

        return assembly;
    }

    private string GetStackFrameMethodClassName(MethodBase method, bool includeNameSpace, bool cleanAsyncMoveNext, bool cleanAnonymousDelegates)
    {
        if (method is null)
            return null;

        var callerClassType = method.DeclaringType;
        if (cleanAsyncMoveNext && method.Name == "MoveNext" && callerClassType?.DeclaringType != null && callerClassType.Name.IndexOf('<') == 0)
        {
            // NLog.UnitTests.LayoutRenderers.CallSiteTests+<CleanNamesOfAsyncContinuations>d_3'1
            int endIndex = callerClassType.Name.IndexOf('>', 1);
            if (endIndex > 1)
            {
                callerClassType = callerClassType.DeclaringType;
            }
        }

        if (!includeNameSpace
            && callerClassType?.DeclaringType != null
            && callerClassType.IsNested
            && callerClassType.GetFirstCustomAttribute<CompilerGeneratedAttribute>() != null)
        {
            return callerClassType.DeclaringType.Name;
        }

        string className = includeNameSpace ? callerClassType?.FullName : callerClassType?.Name;

        if (cleanAnonymousDelegates && className != null)
        {
            // NLog.UnitTests.LayoutRenderers.CallSiteTests+<>c__DisplayClassa
            int index = className.IndexOf("+<>", StringComparison.Ordinal);
            if (index >= 0)
            {
                className = className.Substring(0, index);
            }
        }

        return className;
    }

    /// <summary>Converts custom logging level to NLog.LogLevel.</summary>
    /// <param name="logLevel">Custom Log Level.</param>
    /// <returns>NLog.LogLevel.</returns>
    private NL.LogLevel ToLogLevel(LogLevel logLevel)
    {
        NL.LogLevel level = logLevel switch
        {
            LogLevel.Trace => NL.LogLevel.Trace,
            LogLevel.Debug => NL.LogLevel.Debug,
            LogLevel.Info => NL.LogLevel.Info,
            LogLevel.Error => NL.LogLevel.Error,
            LogLevel.Warn => NL.LogLevel.Warn,
            LogLevel.Fatal => NL.LogLevel.Fatal,
            _ => NL.LogLevel.Debug,
        };

        return level;
    }
}

ILogService

public interface ILogService
{
    void Configure(string configFile);

    void Debug(string message);

    void Error(string message);

    void Fatal(string message);

    void Info(string message);

    /// <summary>Log trace message and output send LogEvent message to (GUI) listeners.</summary>
    /// <param name="message">Message to log.</param>
    /// <remarks>TODO: Make a different service for logging to LogOutput Module.</remarks>
    void Status(string message);

    /// <summary>Log trace message and output send LogEvent message to (GUI) listeners.</summary>
    /// <param name="customSeverity">Custom status message.</param>
    /// <param name="message">Message to log.</param>
    /// <remarks>TODO: Make a different service for logging to LogOutput Module.</remarks>
    void Status(string customSeverity, string message);

    void Warn(string message);
}
public partial class App : PrismApplication
{
/// <summary>Register you Services, Views, Dialogs, etc.</summary>
/// <param name="containerRegistry">container</param>
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.AddLogger();
// ...
}
}
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
using Prism.Ioc;
namespace TestApp.Extensions;
public static class ContainerRegistryExtension
{
public static IContainerRegistry AddLogger(this IContainerRegistry services)
{
var loggerFactory = new NLogLoggerFactory();
services.RegisterInstance<ILoggerFactory>(loggerFactory);
services.Register(typeof(ILogger<>), typeof(Logger<>));
return services;
}
}
using Microsoft.Extensions.Logging;
using Prism.Commands;
using Prism.Navigation.Regions;
namespace TestApp.ViewModels;
public class DashboardViewModel : ViewModelBase
{
private readonly ILogger<DashboardViewModel> _log;
public DashboardViewModel(ILogger<DashboardViewModel> logger)
{
_log = logger;
_log.LogDebug("Loaded");
Title = "Dashboard";
}
}
<?xml version="1.0" encoding="utf-8" ?>
<!--
Logging Help:
Base log level is set by, 'internalLogLevel'. ASP.NET can override via, 'appsettings.json'
Log Levels: Off|Trace|Debug|Info|Warn|Error|Fatal
-->
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogLevel="Trace">
<!-- Optional: Enable asp.net core layout renderers
<extensions>
<add assembly="NLog.Web.AspNetCore" />
</extensions>
-->
<!-- Output Targets -->
<targets>
<target name="logFile"
xsi:type="File"
fileName="Log/Diagnostics.log"
archiveFileName="Log/Diagnostics.{##}.log"
archiveAboveSize="10000000"
archiveNumbering="Rolling"
maxArchiveFiles="4"
openFileCacheTimeout="30"
layout="[${longdate}] [${event-properties:item=EventId_Id:whenEmpty=0}] [${uppercase:${level}}] [${callsite:className=true:methodName=false}] [${message:exceptionSeparator=\r\n:withException=true}]" />
<target name="logError"
xsi:type="File"
fileName="Log/Diagnostics-Error.log"
archiveFileName="Log/Diagnostics-Error.{##}.log"
archiveAboveSize="10000000"
archiveNumbering="Rolling"
maxArchiveFiles="4"
layout="[${longdate}] [${event-properties:item=EventId_Id:whenEmpty=0}] [${uppercase:${level}}] [${callsite:className=true:methodName=false}] [${message} ${exception:format=ToString}]" />
<target name="logConsole"
xsi:type="Console" />
</targets>
<!-- Target Rules -->
<rules>
<logger name="*" minlevel="Trace" writeTo="logFile" />
<logger name="*" minlevel="Error" writeTo="logError" />
<logger name="*" minlevel="Info" writeTo="logConsole" />
<!-- Output hosting lifetime messages to console target for faster startup detection -->
<logger name="Microsoft.Hosting.Lifetime" minlevel="Info" writeTo="logconsole" final="true" />
<!-- Skip non-critical Microsoft logs and so log only own logs (BlackHole) -->
<logger name="Microsoft.*" maxlevel="Debug" final="true" />
<logger name="System.Net.Http.*" maxlevel="Info" final="true" />
</rules>
</nlog>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment