Search code examples
c#f#serilog

How to use Serilog.Log.ForContext with F# function or C# Method


Using Serilog and F# how to use .ForContext with functions? it appears it only accepts type classes:

type A()=
  log = Serilog.Log.ForContext<A>() // ✅
let f _ =
  log = Serilog.Log.ForContext<f>() // compile error
  log = Serilog.Log.ForContext(f.GetType()) // the information is not the same as A where it gives module + type path

What would be the right way to add the context of a F# function or a C# method (which I presume should be the same problem)?


So far my solution has been using this function which return SourceContext exactly as using a class (moduleFullName+className):

let logFromFn _ =
  Serilog.Log.ForContext
    ("SourceContext",
     StackTrace().GetFrame(1).GetMethod().DeclaringType
     |> fun dt -> dt.FullName + "+" + dt.Name)
  1. As stated in this answer, it might be expensive, I use it in places where it would be called once in the whole program.
  2. It works well in F# functions inside modules, not so good in for example, the entry point file, where it returns program+program

Solution

  • Methods don't have a Type associated with them so you can't use either of the ForContext<T>() or ForContext(Type type) overloads. Since functions in an F# module are compiled to static methods inside a static class (the module), the same applies to them. As a simple alternative, you could set the SourceContext property yourself:

    let propName = Serilog.Core.Constants.SourceContextPropertyName
    Log.ForContext(propName, "App.Class.Method")
    Log.ForContext(propName, "App.Module.function")
    

    If you want to be robust to rename operations, you could do something like the following:

    class Class
    {
        public void Method()
        {
            var source = $"{typeof(Class).FullName}.{nameof(Method)}";
            var logger = Log.ForContext(Core.Constants.SourceContextPropertyName, source);
            // ...
        }
    }
    
    module Module =
        // It's a bit fiddly to get the module type in F#, but here's one
        // approach (credit https://stackoverflow.com/a/14706890/7694577).
        type private Marker = interface end
        let private moduleType = typeof<Marker>.DeclaringType
    
        let loggerFor name =
            let source = sprintf "%s.%s" moduleType.FullName name
            let propName = Serilog.Core.Constants.SourceContextPropertyName
            Log.ForContext(propName, source)
        
    
        // Requires the `nameof` operator from F# 4.7 preview.
        // The function needs to be recursive since it is referenced when calling `nameof`.
        let rec func () =
            let logger = loggerFor (nameof func)
            // ...
    

    I'd add that usually the class/module name is enough context to figure out where a message has originated from, so I would suggest leaving out the method/function name from the source context. This would allow you to use the ForContext<T>() or ForContext(Type type) overloads, and would therefore simplify things a great deal.