Skip to content

Instantly share code, notes, and snippets.

@TheAngryByrd
Last active August 21, 2025 14:17
Show Gist options
  • Save TheAngryByrd/bcd07468ba191c8e7e6cc4b614cef7d3 to your computer and use it in GitHub Desktop.
Save TheAngryByrd/bcd07468ba191c8e7e6cc4b614cef7d3 to your computer and use it in GitHub Desktop.
F# Collecting logging metadata like namespace, function name, filepath, and line number
open System.Runtime.CompilerServices
type Log() =
static member inline GatherLogMetaStackFrame
(?name_space: string, [<CallerMemberName>] ?cmb: string, [<CallerLineNumber>] ?cln: int, [<CallerFilePath>] ?cfp: string)
=
let name_space =
match name_space with
| Some ns -> ns
| _ ->
let stackFrame = System.Diagnostics.StackFrame(0)
let method = stackFrame.GetMethod()
method.DeclaringType.FullName.Split('+')
|> String.concat "."
let cmb = defaultArg cmb ""
let cln = defaultArg cln 0
let cfp = defaultArg cfp ""
printfn $"{name_space}.{cmb} was called {cfp}:{cln}" // Filepath:LineNumber is a format many tools know how to use for navigation to the source code
static member inline GatherLogMetaReflection
(?name_space: string, [<CallerMemberName>] ?cmb: string, [<CallerLineNumber>] ?cln: int, [<CallerFilePath>] ?cfp: string)
=
let name_space =
match name_space with
| Some ns -> ns
| _ ->
System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName.Split('+')
|> String.concat "."
let cmb = defaultArg cmb ""
let cln = defaultArg cln 0
let cfp = defaultArg cfp ""
printfn $"{name_space}.{cmb} was called {cfp}:{cln}" // Filepath:LineNumber is a format many tools know how to use for navigation to the source code
module Log =
open Microsoft.FSharp.Quotations.Patterns
let rec getModuleNamespace =
function
| PropertyGet(_, propertyInfo, _) ->
propertyInfo.DeclaringType.FullName.Split('+')
|> String.concat "."
| x -> failwithf "Expression is not a property. %A" x
module MyTestModule1 =
// this will use reflection to get the module namespace
let functionInlineReflection () = Log.GatherLogMetaReflection()
// this will use the stack frame to get the module namespace
[<MethodImpl(MethodImplOptions.AggressiveInlining)>]
let functionInlineStackFrame () = Log.GatherLogMetaStackFrame()
// To avoid using reflection or stack frame, we have a few boilerplate options:
// 0 - Hardcoding the module namespace works but is not as maintainable if the code moves around
// 1 - use a reflection/expression to get the module namespace
let rec moduleLocation = lazy Log.getModuleNamespace <@ moduleLocation @>
let inline functionRecMarker () =
Log.GatherLogMetaReflection(name_space = moduleLocation.Value)
// However, you can't "reuse" functionRecMarker in other functions, it will always print "functionRecMarker" as the CallerMemberName.
// This _can be_ addressed below with LogHelper
let indirectMarker () = functionRecMarker ()
// 2 - use a marker type to get the module namespace
type internal Marker = interface end
let moduleLocation2 =
lazy
(typeof<Marker>.DeclaringType.FullName.Split('+')
|> String.concat ".")
let functionWithTypeOfMarker () =
Log.GatherLogMetaReflection(name_space = moduleLocation2.Value)
// 3 - use a marker type with inline gathering of the module namespace
type internal LogHelper() =
static let name_space =
lazy
(typeof<LogHelper>.DeclaringType.FullName.Split('+')
|> String.concat ".")
static member inline GatherLogMetaReflection
([<CallerMemberName>] ?cmb: string, [<CallerLineNumber>] ?cln: int, [<CallerFilePath>] ?cfp: string)
=
Log.GatherLogMetaReflection(name_space = name_space.Value, ?cmb = cmb, ?cln = cln, ?cfp = cfp)
// This does allow you to reuse the namespace for other functions but it's clearly the largest boilerplate.
let helperClassMarker () = LogHelper.GatherLogMetaReflection()
do MyTestModule1.functionInlineReflection () // FSI_0053.MyTestModule1.functionInlineReflection was called c:\Users\jimmy\Repositories\LogMetaData.fsx:57
do MyTestModule1.functionInlineStackFrame () // FSI_0053.MyTestModule1.functionInlineStackFrame was called c:\Users\jimmy\Repositories\LogMetaData.fsx:58
do MyTestModule1.functionRecMarker () // FSI_0053.MyTestModule1.functionRecMarker was called c:\Users\jimmy\Repositories\LogMetaData.fsx:62
do MyTestModule1.indirectMarker () // this doesn't give you "indirectMarker" see above : FSI_0053.MyTestModule1.functionRecMarker was called c:\Users\jimmy\Repositories\LogMetaData.fsx:62
do MyTestModule1.functionWithTypeOfMarker () // FSI_0053.MyTestModule1.functionWithTypeOfMarker was called c:\Users\jimmy\Repositories\LogMetaData.fsx:72
do MyTestModule1.helperClassMarker () // FSI_0053.MyTestModule1.helperClassMarker was called c:\Users\jimmy\Repositories\LogMetaData.fsx:83
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment