Skip to content

Instantly share code, notes, and snippets.

@litetex
Last active September 18, 2024 02:25
Show Gist options
  • Save litetex/b88fe0531e5acea82df1189643fb1f79 to your computer and use it in GitHub Desktop.
Save litetex/b88fe0531e5acea82df1189643fb1f79 to your computer and use it in GitHub Desktop.
Serilog (C#): How to get current MethodName, FileName/Path and LineNumber without reflection

Serilog (C#): How to get the current MethodName, FileName/Path and LineNumber without reflection

This is a simple setup for reflectionless logging with serilog using caller information (and a single static class).

See also https://stackoverflow.com/a/46905798

Log.cs

Create your own Log.cs in your Root-Namespace (you can use the class below).

This class is required to detect where the call is coming from; it uses Caller-Information to speed up the program execution, because the attributes are resolved at compile-time.

You can now also remove all using Serilog;-imports - if you have any - in the classes where you want to log, because the Log.cs is everywhere in the namespace visble and easy accessible.

Serilog-Output Template

Now you can use the added properties (here: MemberName, FilePath, FileName, LineNumber) in your outputTemplate:

⚠️ Note: For more performance you can remove/uncomment unused properties, here e.g. FilePath and LineNumber in SetContext(...)

Final Example

OutputTemplate: {Timestamp:HH:mm:ss,fff} {Level:u3} {FileName} [{MemberName}] {Message:lj}{NewLine}{Exception}

namespace Demo
{
  public class TestClass
  {
    public void TestMethod()
    {
      Log.Info("Some test");
    }
  }
}

Produces the following output: 18:16:40,183 INFO TestClass [TestMethod] Some test

Changelog

  • 2020-03-21: Tuned docs a bit; updated and reformatted the Log.cs class
  • 2020-05-29: Tuned docs a bit; Reformatted the Log.cs; References to CoreFramework.Logging
// MIT License
// Copyright (c) 2020 litetex
// See also https://github.com/litetex/CoreFramework/blob/develop/CoreFramework.Logging/Log.cs
using System;
using System.IO;
using System.Runtime.CompilerServices;
namespace CoreFramework
{
public static class Log
{
private static string FormatForException(this string message, Exception ex)
{
return $"{message}: {(ex != null ? ex.ToString() : "")}";
}
private static string FormatForContext(
this string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var fileName = Path.GetFileNameWithoutExtension(sourceFilePath);
var methodName = memberName;
return $"{fileName} [{methodName}] {message}";
}
public static void Verbose(
string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Verbose(
message
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Verbose(
string message,
Exception ex,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Verbose(
message
.FormatForException(ex)
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Verbose(
Exception ex,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Verbose(
(ex != null ? ex.ToString() : "")
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Debug(
string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Debug(
message
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Debug(
string message,
Exception ex,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Debug(
message
.FormatForException(ex)
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Debug(
Exception ex,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Debug(
(ex != null ? ex.ToString() : "")
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Info(
string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Information(
message
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Info(
string message,
Exception ex,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Information(
message
.FormatForException(ex)
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Info(
Exception ex,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Information(
(ex != null ? ex.ToString() : "")
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Warn(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Warning(
message
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Warn(
string message,
Exception ex,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Warning(
message
.FormatForException(ex)
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Warn(
Exception ex,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Warning(
(ex != null ? ex.ToString() : "")
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Error(
string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Error(
message
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Error(
string message,
Exception ex,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Error(
message
.FormatForException(ex)
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Error(
Exception ex,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Serilog.Log.Error(
(ex != null ? ex.ToString() : "")
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Fatal(
string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
FatalAction();
Serilog.Log.Error(
message
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Fatal(
string message,
Exception ex,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
FatalAction();
Serilog.Log.Error(
message
.FormatForException(ex)
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
public static void Fatal(
Exception ex,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
FatalAction();
Serilog.Log.Error(
(ex != null ? ex.ToString() : "")
.FormatForContext(memberName, sourceFilePath, sourceLineNumber)
);
}
private static void FatalAction()
{
Environment.ExitCode = -1;
}
}
}
@morteng85
Copy link

I ended up using a different approach - when creating a custom Enricher, the Enricher-pipeline already have a hook for the "caller class that made the log statement" in the SourceContext property in the LogEvent passed to the custom ILogEventEnricher. Having that information, one can browse through the StackTrace, identify the caller StackFrame by class name and get relevant information.

public class InvocationContextEnricher : ILogEventEnricher
{   
	public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
	{
		if (logEvent.Properties.ContainsKey(Constants.LogProperties.SourceContext))
		{
			var sourceContext = ((ScalarValue)logEvent.Properties[Constants.LogProperties.SourceContext]).Value?.ToString();
			var callerFrame = GetCallerStackFrame(sourceContext);

			if (callerFrame != null)
			{
				var methodName = callerFrame.GetMethod()?.Name;
				var lineNumber = callerFrame.GetFileLineNumber();
				var fileName = callerFrame?.GetFileName();

				logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(Constants.LogProperties.InvocationContextClassName, sourceContext));
				logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(Constants.LogProperties.InvocationContextMethodName, methodName));
				logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(Constants.LogProperties.InvocationContextFilePath, fileName));
				logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(Constants.LogProperties.InvocationContextLineNumber, lineNumber));
			}
		}
	}

	private StackFrame GetCallerStackFrame(string className)
	{
		var trace = new StackTrace(true);
		var frames = trace.GetFrames();

		var callerFrame = frames.FirstOrDefault(f => f.GetMethod()?.DeclaringType?.FullName == className);

		return callerFrame;
	}
}

@danworley
Copy link

Nice job @morteng85 ! That works with the message templates! I just needed to implement the Constants, added .Enrich.With<InvocationContextEnricher>() to the LoggerConfiguration instatiation, and the appropriate outputTemplate in my Sink(s), and it all worked! 🙇

@NeverMorewd
Copy link

How to implement the Constants? Could upload a more complete code please, thank you very much

@marazattila
Copy link

The solution of @morteng85 uses reflection when calling GetMethod() in the GetCallerStackFrame function, so it is not a real solution for the original question!

@NeverMorewd CreateProperty() methods first parameter is the log property name (or key) and the second is the property value. Eg:
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("my_caller_method_name", methodName));

@NeverMorewd
Copy link

NeverMorewd commented Sep 18, 2024

@marazattila Thanks for your reply.
Eventually I found another similar method: https://gist.github.com/nblumhardt/0e1e22f50fe79de60ad257f77653c813.
It used reflection as well, and I can not find any one without reflection.
My app does not seems to support native aot anymore -_-

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment