So… my first, slightly annoying answer is that I try not to. Mark Seeman has written about this in a great series of blog posts which I won't try and repeat here.
Still, there are occasions where you want to quickly and easily do… something.. with a dependency making use of the context that being inside a Freya workflow provides. Let's quickly walk through how I inject a logger into a Freya workflow which "knows" about things like the request ID Kestrel has assigned to the current request.
I'm going to use Serilog as an example below, but you could also use any other structured logging library (I like Logary, but there isn't a .NET Core release at time of writing).
I'll annotate the code inline to give you an idea what it's doing.
So; our first module is shared code which you'll probably want to reuse across all of your Freya services. Put it in a separate .fs file (it assumes Serilog has been taken as a dependency).
moduleLoggingopenAether.OperatorsopenFreya.CoreopenFreya.OpticsopenSerilogopenSerilog.ContextopenSerilog.Configuration// We'll expand the Request module with two news Optics;// one uses the "RequestId" constant defined by the Owin// specification to extract the ID assigned to this request.// The other we'll define in the "serilog" name space in the// Freya context Dictionary (all owin keys start "owin.")[<RequireQualifiedAccess>]moduleRequest=letrequestId_=State.value_<string>Constants.RequestId>->Option.unsafe_// An optic for focussing on an ILogger in the Freya// state. That's great, but how does the ILogger get// there? Read on...letlogger_=State.value_<ILogger>"serilog.logger">->Option.unsafe_// As a structured logging library, you can attach an// array of "values" to a Serilog event - we'll use this// helper to give us a more "F#ish" APItypeSerilogContext={Template:stringValues:objlist}[<RequireQualifiedAccess>]moduleLog=// Extract the request ID once per requestletprivaterid=Freya.Optic.getRequest.requestId_|>Freya.memo// Extract the ILogger once per requestletprivateilogger=Freya.Optic.getRequest.logger_|>Freya.memo// A method to inject an ILogger *into* the Freya// stateletinjectLogger(config:LoggerConfiguration)=letlogger=config.Enrich.FromLogContext().CreateLogger():>ILoggerfreya{do!Freya.Optic.setRequest.logger_loggerreturnNext}// From here on in is just an F# friendly wrapper// around Serilog.// Start building up a new log message with a// message templateletmessagetemplate={Template=templateValues=[]}// Add a value to the message contextletaddvaluecontext={contextwithValues=(boxvalue)::context.Values}// Function that knows how to send a message with all of the// values correctly associated, and the requestId setletprivatesendfcontext=freya{let!requestId=ridlet!logger=iloggerusing(LogContext.PushProperty("RequestId",requestId))(fun_->letvalues=context.Values|>List.toArray|>Array.revfloggercontext.Templatevalues)}// The four standard log levelsletdebugcontext=letf(logger:ILogger)template(values:obj[])=logger.Debug(template,values)sendfcontextletinfocontext=letf(logger:ILogger)template(values:obj[])=logger.Information(template,values)sendfcontextletwarncontext=letf(logger:ILogger)template(values:obj[])=logger.Warning(template,values)sendfcontextleterrorcontext=letf(logger:ILogger)template(values:obj[])=logger.Error(template,values)sendfcontext
So that's great and all… but how and where do we actually call that injectLogger function?
Well, that goes in your application root where you build your final Freya app.
Mine normally ends up looking something like this:
123456
letrootlogConfig=letroutes=freyaRouter{(* My resources here *)}Log.injectLoggerlogConfig|>(flipPipeline.compose)routes|>(flipPipeline.compose)notFound
Because injectLogger returns a Freya Pipeline type which always passes handling onto the next step in the pipeline, all that first step does is add in a newly initialized ILogger to the Freya state, and then passes things on down the chain as normal.
In your Freya code, logging looks like this:
123456789
letnotFoundResponse=freya{let!path=Freya.Optic.getRequest.path_do!Log.message"Why am I logging a GUID like this one {guid} on requests to {path}?"|>Log.add(Guid.NewGuid())|>Log.addpath|>Log.inforeturnrepresentJson"We couldn't find that"}
Notice that do! is required for logging now, as our log methods have type Freya<unit>. This is what allows us to add the request specific context to our logs without explicitly having to append it ourselves every time.
I'm not sure if this strictly answers Eugene's question, but I hope all you (potential) Freya users out there find it helpful regardless.