Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScript

ILog writes a message at a level, optionally carrying an exception.

MicrosoftLogging.create and MicrosoftLogging.fromFactory wrap each entry in a Microsoft ILogger.BeginScope dictionary containing the current explicitly set named FiberLocal values. FiberLogging.observer does the same for defect entries. Without named values, no scope is opened. Providers must enable scope capture to display these fields.

Show page setup
// Setup for the checked examples on this page.
open System
open Axial
open Axial.PlatformService
/// Fails the docs test when an example's result differs from the value shown.
let shouldEqual expected actual =
    if actual <> expected then failwithf "Expected %A but got %A" expected actual
Verified
open System
open Axial
open Axial.PlatformService
Verified
let recordAttempt name : Flow<#IHasLog, Never, unit> =
    Log.info $"Processing {name}"
Requires & types
Requires: open Axial; open Axial.PlatformService (packages: Axial, Axial.PlatformService)
val recordAttempt: 'a -> Flow<'b,Never,unit>

Each level has a helper, and two more carry an exception without flattening its stack trace into a string:

Verified
let everyLevel (error: exn) : Flow<#IHasLog, Never, unit> =
    flow {
        do! Log.trace "trace"
        do! Log.debug "debug"
        do! Log.info "info"
        do! Log.warning "warning"
        do! Log.error "error"
        do! Log.critical "critical"

        do! Log.errorExn error "error with exception"
        do! Log.criticalExn error "critical with exception"
        do! Log.log LogLevel.Information "at a chosen level"
        do! Log.logException LogLevel.Warning error "exception at a chosen level"
    }
Requires & types
Requires: open Axial; open Axial.PlatformService (packages: Axial, Axial.PlatformService)
val everyLevel: exn -> Flow<'a,Never,unit>

logException exists so the host logger receives the exception object itself, which is what preserves the stack trace in a structured logging backend.

Supplying the service

Log.live is a no-op logger, so a library does not write to the console unless the host configures a sink. Wire a real one with Log.fromSink:

Verified
let consoleLog = Log.fromSink (fun level message -> Console.Error.WriteLine $"[{level}] {message}")
Requires & types
Requires: open Axial.PlatformService; open System (packages: Axial.PlatformService)
val consoleLog: ILog

Log.fromSink appends the exception text to the message for logException. To hand the exception object to a real logging framework, implement ILog directly and forward both members.

Log.layer provides the no-op logger; supply your own layer when the application has a sink.

Logging against telemetry

A workflow uses ILog to emit messages. Spans, metrics, and OpenTelemetry export are covered separately, in observability. The two meet at the host: an ILog implementation can forward into the same backend the telemetry exporter writes to.

Choose ILog when the workflow itself should emit a message. Choose telemetry when you want the runtime's own execution structure recorded.

Microsoft.Extensions.Logging adapter

MicrosoftLogging and FiberLogging live in Axial.Hosting, not Axial.PlatformService; the types they adapt come from Microsoft.Extensions.Logging. MicrosoftLogging.fromFactory wraps a host ILoggerFactory as an ILog, opening a scope of named FiberLocal values per entry. FiberLogging.observe reports fiber defects through the same Microsoft logger:

Fragment
open Microsoft.Extensions.Logging
open Axial
open Axial.Hosting

// `factory` is the host's ILoggerFactory, supplied at composition time.
let logger : ILog = MicrosoftLogging.fromFactory "App" factory

// Report fiber defects through the same Microsoft logger.
let observed (application: Flow<'env, 'error, 'value>) =
    application |> FiberLogging.observe (factory.CreateLogger "App")

The .NET hosting guide wires both adapters into a Generic Host.

Testing

Assert on log output by collecting it:

Verified
type LogEnv =
    { Log: ILog }
    interface IHasLog with
        member this.Log = this.Log
Requires & types
Requires: open Axial.PlatformService (packages: Axial.PlatformService)
Verified
let gate = obj()
let mutable messages = []
let collecting = { Log = Log.fromSink (fun level message -> lock gate (fun () -> messages <- (level, message) :: messages)) }

recordAttempt "invoice 7" |> Flow.run collecting |> shouldEqual (Exit.Success())
lock gate (fun () -> messages) |> shouldEqual [ LogLevel.Information, "Processing invoice 7" ]

lock gate (fun () -> messages <- [])
everyLevel (InvalidOperationException "boom") |> Flow.run collecting |> ignore
lock gate (fun () -> List.length messages) |> shouldEqual 10
Requires & types
Requires: open Axial; open Axial.PlatformService (packages: Axial, Axial.PlatformService)
val gate: obj
val messages: (LogLevel * string) list
val collecting: LogEnv

Tests that do not assert on logging can supply Log.live.