4

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
Coding Edgar
  • 1,285
  • 1
  • 8
  • 22
  • 2
    Consider using `typeof` operator. – ndogac Sep 04 '20 at 18:02
  • Could you elaborate? I've tried but cannot figure how to use `typeof` in this case. – Coding Edgar Sep 04 '20 at 18:05
  • 1
    Is there a ForContext(Type) overload? It might just be using ToString if there is only an object overload.. if so, x.GetType().GetName() might show a more equivalent result. IIRC Type.ToString() shows the FullName which is not assembly-qualified. – user2864740 Sep 04 '20 at 18:10
  • it does exists, it complains with `Serilog.Log.ForContext(typeof(f))`: A unique overload for method 'ForContext' could not be determined based on type information prior to this program point. A type annotation may be needed. Known type of argument: 'a Candidates: - Serilog.Log.ForContext(enricher: Serilog.Core.ILogEventEnricher) : Serilog.ILogger - Serilog.Log.ForContext(enrichers: Serilog.Core.ILogEventEnricher []) : Serilog.ILogger - Serilog.Log.ForContext(source: Type) : Serilog.ILogger The generic function 'typeof' must be given explicit type argument(s) – Coding Edgar Sep 04 '20 at 18:13

2 Answers2

5

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.

Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
rob.earwaker
  • 1,244
  • 1
  • 7
  • 14
  • I tend to refactor a lot, so I was looking for a more "refactor proof approach", I also tried to do something with Quotations but cannot refer to the function inside its body unless is `rec` (which is not a problem with Classes) – Coding Edgar Sep 04 '20 at 18:37
  • 1
    @CodingEdgar - I've edited to include options that are robust to rename operations. – rob.earwaker Sep 04 '20 at 20:10
1

The key complication in F# is that you cannot use a module identifier as a type literal, i.e. while you can say Log.ForContext<TypeName>(), you cannot do Log.ForContext<ModuleName>().

The best workaround until the language supports using a module identifier as a Type and/or a typeof(ModuleName) facility is added is unfortunately to plonk this ugly boilerplate at the top of your module definition:

type private Marker = interface end
let private moduleType = typeof<Marker>.DeclaringType
let log = Log.ForContext(moduleType)

The other answer obviously answers the question as posed in full. This one answers a different question as IMO:

As with C#, including the member/function name in the log context is not appropriate idiomatic use of structured logging - the message should make sense independent of you needing to know what function/member it's embedded in.

Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249