Home

Awesome

FsLibLog

What is this?

FsLibLog is a single file you can copy paste or add through Paket Github dependencies to provide your F# library with a logging abstraction. This is a port of the C# LibLog.

Why does this exist?

When creating a library for .NET, you typically do not want to depend on a logging framework or abstraction. Depending on a logging framework forces your consumers to use that framework, which is not ideal. Depending on an abstraction can work but you can run into the diamond dependency problem. Since this is just a file you compile into your library, no dependency is taken and is transparent to your consumers.

Additionally, loggers aren't particularly friendly for F#, this sets out to resolve that.

How to get started

1. Put the file into your project

Option 1

Copy/paste FsLibLog.fs into your library.

Option 2

Read over Paket Github dependencies.

Add the following line to your paket.depedencies file.

github TheAngryByrd/FsLibLog src/FsLibLog/FsLibLog.fs

Then add the following line to projects with paket.references file you want FsLibLog to be available to.

File: FsLibLog.fs

2. Replace its namespace with yours

To alleviate potential naming conflicts, it's best to replace FsLibLog namespace with your own.

Here is an example with FAKE 5:

Target.create "Replace" <| fun _ ->
  Shell.replaceInFiles
    [ "FsLibLog", "MyLib.Logging" ]
    (!! "paket-files/TheAngryByrd/FsLibLog/src/FsLibLog/FsLibLog.fs")

3. Setup a LogProvider

Using in your library

Open namespaces

open FsLibLog
open FsLibLog.Types

Get a logger

There are currently six ways to get a logger.

Fable libraries can only use the following:

The other functions rely reflection and thus are only available when compiling on dotnet.

Set the loglevel, message, exception and parameters

Choose a LogLevel. (Fatal|Error|Warn|Info|Debug|Trace).

There are helper methods on the logger instance, such as logger.warn.

These helper functions take a (Log -> Log) which allows you to amend the log record easily with functions in the Log module. You can use function composition to set the fields much easier.

logger.warn(
    Log.setMessage "{name} Was said hello to"
    >> Log.addParameter name
)

The set of functions to augment the Log record are

Full Example

namespace SomeLib
open FsLibLog
open FsLibLog.Types
open FsLibLog.Operators


module Say =
    let logger = LogProvider.getCurrentLogger()

    type AdditionalData = {
        Name : string
    }


    // Example Log Output:
    // 16:23 [Information] <SomeLib.Say> () "Captain" Was said hello to - {"UserContext": {"Name": "User123", "$type": "AdditionalData"}, "FunctionName": "hello"}
    let hello name  =
        // Starts the log out as an Informational log
        logger.info(
            Log.setMessage "{name} Was said hello to"
            // MessageTemplates require the order of parameters to be consistent with the tokens to replace
            >> Log.addParameter name
            // This adds additional context to the log, it is not part of the message template
            // This is useful for things like MachineName, ProcessId, ThreadId, or anything that doesn't easily fit within a MessageTemplate
            // This is the same as calling `LogProvider.openMappedContext` right before logging.
            >> Log.addContext "FunctionName" "hello"
            // This is the same as calling `LogProvider.openMappedContextDestucturable`  right before logging.
            >> Log.addContextDestructured "UserContext"  {Name = "User123"}
        )
        sprintf "hello %s." name


    // Example Log Output:
    // 16:23 [Debug] <SomeLib.Say> () In nested - {"DestructureTrue": {"Name": "Additional", "$type": "AdditionalData"}, "DestructureFalse": "{Name = \"Additional\";}", "Value": "bar"}
    // [Information] <SomeLib.Say> () "Commander" Was said hello to - {"UserContext": {"Name": "User123", "$type": "AdditionalData"}, "FunctionName": "hello", "DestructureTrue": {"Name": "Additional", "$type": "AdditionalData"}, "DestructureFalse": "{Name = \"Additional\";}", "Value": "bar"}
    let nestedHello name =
        // This sets additional context to any log within scope
        // This is useful if you want to add this to all logs within this given scope
        use x = LogProvider.openMappedContext "Value" "bar"
        // This doesn't destructure the record and calls ToString on it
        use x = LogProvider.openMappedContext "DestructureFalse" {Name = "Additional"}
        // This does destructure the record,  Destructuring can be expensive depending on how big the object is.
        use x = LogProvider.openMappedContextDestucturable "DestructureTrue" {Name = "Additional"} true

        logger.debug(
            Log.setMessage "In nested"
        )
        // The log in `hello` should also have these additional contexts added
        hello name


    // Example Log Output:
    // 16:23 [Error] <SomeLib.Say> () "DaiMon" was rejected. - {}
    // System.Exception: Sorry DaiMon isnt valid
    //    at Microsoft.FSharp.Core.PrintfModule.PrintFormatToStringThenFail@1647.Invoke(String message)
    //    at SomeLib.Say.fail(String name) in /Users/jimmybyrd/Documents/GitHub/FsLibLog/examples/SomeLib/Library.fs:line 57
    let fail name =
        try
            failwithf "Sorry %s isnt valid" name
        with e ->
            // Starts the log out as an Error log
            logger.error(
                Log.setMessage "{name} was rejected."
                // MessageTemplates require the order of parameters to be consistent with the tokens to replace
                >> Log.addParameter name
                // Adds an exception to the log
                >> Log.addException  e
            )

    // Example Log Output:
    // 2021-09-15T20:34:14.9060810-04:00 [Information] <SomeLib.Say> () The user {"Name": "Ensign Kim", "$type": "AdditionalData"} has requested a reservation date of "2021-09-16T00:34:14.8853360+00:00"
    let interpolated (person : AdditionalData) (reservationDate : DateTimeOffset) =
        // Starts the log out as an Info log
        logger.info(
            // Generates a message template via a specific string intepolation syntax.
            // Add the name of the property after the expression
            // for example: "person" will be logged as "User" and "reservationDate" as "ReservationDate"
            Log.setMessageI $"The user {person:User} has requested a reservation date of {reservationDate:ReservationDate} "
        )

    // Has the same logging output as `hello`, above, but uses the Operators module.
    let helloWithOperators name =
        // Initiate a log with a message
        !!! "{name} Was said hello to"
        // Add a parameter
        >>! name
        // Adds a value, but does not destructure it.
        >>!- ("FunctionName", "operators")
        // Adds a value & destructures it.
        >>!+ ("UserContext", {Name = "User123"})
        // Logs at the Info level.
        |> logger.info
        sprintf "hello %s." name

Log Providers

Providers are the actual logging framework that sends the logs to some destination (console, file, logging service). FsLibLog uses reflection to inspect the running application and wire these up telling FsLibLog to do it.

Currently supported provider

Setting up Serilog

  1. Install Serilog

  2. Install Serilog.Sinks.ColoredConsole (or any other Sink)

  3. Create your Logger

        let log =
            LoggerConfiguration()
                .MinimumLevel.Verbose()
                .WriteTo.Console(outputTemplate= "{Timestamp:o} [{Level}] <{SourceContext}> ({Name:l}) {Message:j} - {Properties:j}{NewLine}{Exception}")
                .Enrich.FromLogContext()
                .CreateLogger();
        Log.Logger <- log
    
  4. FsLibLog will pick up Serilog automatically, no need to tell FsLibLog about it

Setting up Microsoft.Extensions.Logging

  1. Install Microsoft.Extensions.Logging

  2. Install Microsoft.Extensions.Logging.Console (or any other Provider)

  3. Create your ILoggerFactory

    let microsoftLoggerFactory = LoggerFactory.Create(fun builder ->
            builder
                .SetMinimumLevel(LogLevel.Debug)
                .AddSimpleConsole(fun opts -> opts.IncludeScopes <-true)
                // .AddJsonConsole(fun opts -> opts.IncludeScopes <- true)
            |> ignore
    
        )
    
  4. Tell FsLibLog to use this factory

    FsLibLog.Providers.MicrosoftExtensionsLoggingProvider.setMicrosoftLoggerFactory microsoftLoggerFactory
    
    1. One downside to this is you need to do this for every library your application consumes that uses FsLiblog.

Custom Providers

You can implement and teach FsLibLog about your own custom provider if one is not listed. You have to do 2 things:

  1. You have to implement the ILogProvider interface. Example Implemenation
  2. You have to tell FsLibLog to use it. Example calling FsLibLog.LogProvider.setLoggerProvider
    1. One downside to this is you need to do this for every library your application consumes that uses FsLiblog.

Using the Example JsConsoleProvider

This can look something like JsConsoleProvider, with simple & direct logging to console. Other log providers, such as one to ship front-end logs to a back-end service, are left as an exercise for the reader.

Option 1

Copy/paste JsConsoleProvider.fs into your library.

Option 2

Read over Paket Github dependencies.

Add the following line to your paket.depedencies file.

github TheAngryByrd/FsLibLog examples/JsConsoleProvider.JsConsoleProvider.fs

Then add the following line to projects with paket.references file you want the console provider to be available to.

File: JsConsoleProvider.fs

Register the Log Provider

Note that the log provider must be registered manually in a Fable application, like so:

LogProvider.setLoggerProvider <| JsConsoleProvider.create()

From there, the logger can be used as normal.


String Interpolation

This allows for string interpolation with a special syntax to be convertable to Message Templates used in underlying providers (such as Serilog).

Typically when one uses string interpolation it works as such:

let favoriteCartoon = "Captain Planet"
let dayItsOn = "Saturday"
printfn $"My favorite cartoon is {favoriteCartoon} and airs on {dayItsOn}"

F# compiler will create a FormattableString where it's Format property looks like My favorite cartoon is {0} and airs on {1} and the GetArguments() are [| "Captain Planet"; "Saturday" |]. As you can see, FormattableString doesn't have the named parameters that Message Templates would want. To make this work the way we want we need to introduce a specific syntax.

let favoriteCartoon = "Captain Planet"
let dayItsOn = "Saturday"
printfn $"My favorite cartoon is {favoriteCartoon:CartoonShow} and airs on {dayItsOn:DayAired}"

setMessageInterpolated will make the template look like My favorite cartoon is {CartoonShow} and airs on {DayAired}. This will replace the number arguments with the names after the colon within the interpolated string. This makes a usable message template.

Builds

GitHub Actions
GitHub Actions
Build History

Building

Make sure the following requirements are installed in your system:

> build.cmd // on windows
$ ./build.sh  // on unix

Environment Variables

Watch Tests

The WatchTests target will use dotnet-watch to watch for changes in your lib or tests and re-run your tests on all TargetFrameworks

./build.sh WatchTests

Releasing

git add .
git commit -m "Scaffold"
git remote add origin origin https://github.com/user/MyCoolNewLib.git
git push -u origin master
paket config add-token "https://www.nuget.org" 4003d786-cc37-4004-bfdf-c4f3e8ef9b3a
#### 0.2.0 - 2017-04-20
* FEATURE: Does cool stuff!
* BUGFIX: Fixes that silly oversight
./build.sh Release

Code formatting

To format code run the following target

./build.sh FormatCode

This uses Fantomas to do code formatting. Please report code formatting bugs to that repository.