Skip to content

Instantly share code, notes, and snippets.

@litetex
Last active March 2, 2024 03:12
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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;
}
}
}
@sommmen
Copy link

sommmen commented Sep 25, 2019

Hi, thanks this script is quite usefull. Just a question though, what does this do in a multithreaded scenario? I could imagine since SetContext is not thread safe there could be an issue where one thread injects the context into the global Log.Logger and then another thread does a writeline, and gets the wrong data.

@sommmen
Copy link

sommmen commented Sep 25, 2019

Hi, thanks this script is quite usefull. Just a question though, what does this do in a multithreaded scenario? I could imagine since SetContext is not thread safe there could be an issue where one thread injects the context into the global Log.Logger and then another thread does a writeline, and gets the wrong data.

public static void Information(string messageTemplate, [CallerMemberName] string memberName = "",
            [CallerFilePath] string sourceFilePath = "",
            [CallerLineNumber] int sourceLineNumber = 0)
        {
            Serilog.Log.Logger
                .ForContext("MemberName", memberName)
                .ForContext("FilePath", sourceFilePath)
                .ForContext("FileName", System.IO.Path.GetFileNameWithoutExtension(sourceFilePath))
                .ForContext("LineNumber", sourceLineNumber)
                .Information(messageTemplate);

            //SetContext(memberName, sourceFilePath, sourceLineNumber);
            //Serilog.Log.Information(messageTemplate);
        }

Would you think this is more thread safe because it is creating a new logger object rather than putting it in the global Log.Logger field?
I kind of don't want to create a new logger object every time, but i think the logger.ForContext does a good deal of optimizing this. Maybe tomorrow ill run a benchmark - wanting to try a benchmark libray out anyways :)

@litetex
Copy link
Author

litetex commented Oct 11, 2019

I didn't test the sample for multi threading safety 😅

Yes technically the ForContext could be called at the wrong time.
Your solution makes it a bit better, but it's still not completely thread safe.

For thread safety, you should use in each "Logging"-method (Info, Debug, Error, etc.) a lock on a static lock-Object.

@litetex
Copy link
Author

litetex commented Oct 11, 2019

I updated the the class so that it doesn't use ForContext anymore, because there were problems when a parameter (e.g. MemberName) could get stuck and would be written everytime, when you call Serilog directly on another location.

e.g when calling

public class TestClass
  {
    public void A()
    {
      Log.Info("Some test");
    }
  }

18:16:40,183 INFO TestClass [A] Some test ✔️
and then calling

public class AnotherTestClass
  {
    public void B()
    {
      Serilog.Log.Verbose("Some verbose stuff");
    }
  }

18:16:40,183 Verbose TestClass [A] Some verbose stuff

@sommmen
Copy link

sommmen commented Oct 14, 2019

I didn't test the sample for multi threading safety 😅

Yes technically the ForContext could be called at the wrong time.
Your solution makes it a bit better, but it's still not completely thread safe.

For thread safety, you should use in each "Logging"-method (Info, Debug, Error, etc.) a lock on a static lock-Object.

Well for your 2 remarks (thread saf. and Context getting stuck) you could also create a new logger object everytime like in my sample snippet. This sets the context of a differtent logger object not on the root one - so you should not get any problems if i read the docs right. I have not tested this however and if i get time (which i most likely dont) i will perform a test. I am also curious regarding the performance. Locking would also bring a nasty performance hit, certainly when using async sinks.

@sommmen
Copy link

sommmen commented Oct 14, 2019

I do also like the approach you took right now with just injecting the string. It is however not really configurable this way. Also not compatible with the outputformat of the sinks.

Maybe we can use an enricher or something else in Serilog to get a slightly better/more configurable result?

@akovac35
Copy link

akovac35 commented Mar 15, 2020

Hi, thanks this script is quite usefull. Just a question though, what does this do in a multithreaded scenario? I could imagine since SetContext is not thread safe there could be an issue where one thread injects the context into the global Log.Logger and then another thread does a writeline, and gets the wrong data.

public static void Information(string messageTemplate, [CallerMemberName] string memberName = "",
            [CallerFilePath] string sourceFilePath = "",
            [CallerLineNumber] int sourceLineNumber = 0)
        {
            Serilog.Log.Logger
                .ForContext("MemberName", memberName)
                .ForContext("FilePath", sourceFilePath)
                .ForContext("FileName", System.IO.Path.GetFileNameWithoutExtension(sourceFilePath))
                .ForContext("LineNumber", sourceLineNumber)
                .Information(messageTemplate);

            //SetContext(memberName, sourceFilePath, sourceLineNumber);
            //Serilog.Log.Information(messageTemplate);
        }

Would you think this is more thread safe because it is creating a new logger object rather than putting it in the global Log.Logger field?
I kind of don't want to create a new logger object every time, but i think the logger.ForContext does a good deal of optimizing this. Maybe tomorrow ill run a benchmark - wanting to try a benchmark libray out anyways :)

Try using enricher object instead:

InvocationContextEnricher enricher = new InvocationContextEnricher(typeof(T).FullName, callerMemberName, callerFilePath, callerLineNumber);
            return Log.Logger.ForContext(enricher);

Where InvocationContextEnricher is:

public class InvocationContextEnricher : ILogEventEnricher
    {   
        public InvocationContextEnricher(string sourceContext, string callerMemberName, string callerFilePath, int callerLineNumber)
        {
            SourceContext = sourceContext;
            CallerMemberName = callerMemberName;
            CallerFilePath = callerFilePath;
            CallerLineNumber = callerLineNumber;
        }

        public string SourceContext { get; protected set; }
        public string CallerMemberName { get; protected set; }
        public string CallerFilePath { get; protected set; }
        public int CallerLineNumber { get; protected set; }
                
        public static string SourceContextPropertyName {get;} = Constants.SourceContextPropertyName;
        public static string CallerMemberNamePropertyName {get;} = "CallerMemberName";

        public static string CallerFilePathPropertyName {get;} = "CallerFilePath";
        public static string CallerLineNumberPropertyName {get;} = "CallerLineNumber";
        
        public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
        {
            logEvent.AddPropertyIfAbsent(new LogEventProperty(SourceContextPropertyName, new ScalarValue(SourceContext)));
            logEvent.AddPropertyIfAbsent(new LogEventProperty(CallerMemberNamePropertyName, new ScalarValue(CallerMemberName)));
            logEvent.AddPropertyIfAbsent(new LogEventProperty(CallerFilePathPropertyName, new ScalarValue(CallerFilePath)));
            logEvent.AddPropertyIfAbsent(new LogEventProperty(CallerLineNumberPropertyName, new ScalarValue(CallerLineNumber)));
        }
    }

@PlexRipper
Copy link

Awesome work @litetex! Thank you very much!

@ppmetz
Copy link

ppmetz commented Aug 10, 2022

Hi, is there a running example?

@JasonLandbridge
Copy link

Warning for those coming here!

Serilog works with message templates: message-template-recommendations

And with the above samples, you don't have that message template anymore as you can only pass a single string, which kinda forces you to do string interpolation, which is not recommended.

@hrithik1996
Copy link

InvocationContextEnricher enricher = new InvocationContextEnricher(typeof(T).FullName, callerMemberName, callerFilePath, callerLineNumber);
return Log.Logger.ForContext(enricher);

Where to use this one?

@hrithik1996
Copy link

And How can this enricher add in serilog?

@morteng85
Copy link

@akovac35 A piece of the puzzle is missing - how to register the Enricher? It doesnt have an empty constructor

@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! 🙇

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