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.
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);
}
}
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;
}
}
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);
}