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.JavaScriptSupervision and Fiber Observability
A forked fiber whose handle is discarded can die silently.
Flow.fork returns a Fiber handle, and nothing stops a caller writing |> Flow.map ignore or let! _ = ... and dropping it. When such a fiber hits an unhandled exception, the runtime contains it as Exit.Failure (Cause.Die _), but nobody is awaiting that exit. Because Axial converts every exception into an Exit value, the underlying task never faults, so even .NET's TaskScheduler.UnobservedTaskException net never fires. Without help, that is a production failure with no log line.
Axial answers this with two pieces: Flow.supervise restarts background work that dies with defects, and the fiber observer reports the defects that still escape.
Both stay inside Axial's error model:
- Typed errors (
Cause.Fail) are untouched. They are domain values in yourFlow<'env, 'error, 'value>signature, not diagnostics. Supervision and observation apply only to defects (Cause.Die): bugs that escaped the typed channel. - Joining is the opt-out. A fiber whose outcome someone consumed (
Fiber.join,Fiber.interrupt) belongs to that caller; the runtime says nothing about it.
Restarting defects: Flow.supervise
supervise is the defect-channel sibling of Flow.retry:
retryre-runs typedCause.Failerrors and never touches defects.supervisere-runsCause.Diedefects and never touches typed errors or interruptions.
Show page setup
// Setup for the checked examples on this page.
open System
open Axial
open Axial.PlatformService
open Axial.State
open Axial.Telemetry.JavaScript
/// 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/// Crashes on its first two runs, as a worker does on a poison message, then succeeds.
let pollQueue (polls: Ref<int>) : Flow<ClockEnvironment, string, int> =
flow {
let! run = polls |> Ref.updateAndGet ((+) 1)
if run <= 2 then return! Flow.die (InvalidOperationException "poison message") else return run
}
let restartPolicy =
Retry.schedule
{ Retry.defaults with
Retries = 4
Backoff = Backoff.Exponential(TimeSpan.FromMilliseconds 1.0, TimeSpan.FromMilliseconds 10.0) }
let reliableWorker (polls: Ref<int>) : Flow<ClockEnvironment, string, int> = pollQueue polls |> Flow.supervise restartPolicy
pollQueue: Ref<int> -> Flow<ClockEnvironment,string,int>polls: Ref<int>Axial.State.Ref`1Represents a handle to a mutable reference that can be updated atomically. The type of the value stored in the reference.
intAn abbreviation for the CLI type . Basic Types
Axial.Flow`3Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
stringAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
run: int(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.State.RefModuleupdateAndGet: ('T -> 'T) -> Ref<'T> -> Flow<'env,'none,'T>Updates the value using the supplied function and returns the value after the update. The update function of type 'T -> 'T. The to update. A flow that returns the updated value. Ref.updateAndGet (fun x -> x + 1) myRef
(+): ^T1 -> ^T2 -> ^T3Overloaded addition operator The first parameter. The second parameter. The result of the operation. 2 + 2 // Evaluates to 4 "Hello " + "World" // Evaluates to "Hello World"
(<=): 'T -> 'T -> boolStructural less-than-or-equal comparison The first parameter. The second parameter. The result of the comparison. 5 <= 1 // Evaluates to false 5 <= 5 // Evaluates to true [1; 5] <= [1; 6] // Evaluates to true
Axial.Flowdie: exn -> Flow<'env,'error,'value>Creates a defective flow that fails with an exception. The exception representing the defect. A flow that always dies with the provided exception. This is the public constructor for non-domain defects. Use fail for expected typed failures and die when the workflow should surface a bug or panic.
``.ctor``: string -> unitInitializes a new instance of the class with a specified error message. The message that describes the error.
restartPolicy: Schedule<ClockEnvironment,exn,int>Axial.RetryModuleConstructors for .
schedule: Retry<'input> -> Schedule<'env,'input,int>Builds the schedule a retry record describes. The retry description. A schedule that emits the retry number, starting at 0. Thrown when Retries is negative or a delay is negative.
Axial.Retry`1A retry described with named fields: how many retries, how long to wait, and which inputs to retry. Build one from Retry.defaults with record update syntax and pass it to Flow.retry through Retry.schedule. It is a shorthand for the common case; anything a record cannot express (jitter, elapsed-time limits, fixed-rate runs) is written as a Schedule directly. The input the retry decision sees: the typed error for Flow.retry, the defect for Flow.supervise.
defaults: Retry<'input>Three retries with exponential backoff from 100 ms, capped at 10 s, retrying every input. fetch |> Flow.retry (Retry.schedule { Retry.defaults with Retries = 5; When = HttpError.isTransient })
Retries: intThe maximum number of retries after the first attempt; 3 allows 4 executions in total.
Backoff: BackoffThe delay before each retry.
Axial.BackoffHow long a waits between attempts.
ExponentialDoubles the delay before each retry, starting at initial and never exceeding max.
System.TimeSpanRepresents a time interval.
FromMilliseconds: float -> TimeSpanReturns a that represents a specified number of milliseconds. A number of milliseconds. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
reliableWorker: Ref<int> -> Flow<ClockEnvironment,string,int>supervise: Schedule<'env,exn,'output> -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Restarts a flow that terminates with an unexpected defect, according to a schedule. The defect-channel sibling of retry: retry re-runs typed Cause.Fail errors and never touches defects, while supervise re-runs Cause.Die defects and never touches typed errors or interruptions. The schedule sees the first defect of each failed run. Each attempt runs in its own child scope, closed before the next attempt starts, so finalizers registered by a failed attempt are released instead of accumulating. Re-evaluation only resets state that lives inside the flow itself; mutable state in the environment is not restored. When the schedule stops, the final defect propagates as the flow's exit. Decides, from each defect, whether to restart and after what delay. The flow to supervise. A flow that re-evaluates Cause.Die outcomes while the schedule allows it. worker |> Flow.supervise (Retry.schedule { Retry.defaults with Retries = 5 })
Requires & types
Requires: open Axial; open Axial.State; open System (packages: Axial)val pollQueue: Ref<int> -> Flow<ClockEnvironment,string,int>val restartPolicy: Schedule<ClockEnvironment,exn,int>val reliableWorker: Ref<int> -> Flow<ClockEnvironment,string,int>flow {
let! polls = Ref.make 0
let! fiber = Flow.fork (reliableWorker polls)
return! Fiber.join fiber
}
|> Flow.run (ClockEnvironment Clock.live)
|> shouldEqual (Exit.Success 3)
flow: FlowBuilderThe universal flow { } computation expression.
polls: Ref<int>Axial.State.RefModulemake: 'T -> Flow<'env,'none,Ref<'T>>Creates a new with the initial value. The initial value of the reference. A flow that creates and returns the reference. (Ref.make 10).RunSynchronously(())
fiber: Fiber<string,int>Axial.Flowfork: Flow<'env,'error,'value> -> Flow<'env,'none,Fiber<'error,'value>>Starts a flow in a new fiber without waiting for it to complete. Forking turns a cold flow description into hot child work and returns a handle that can later be joined or interrupted. Prefer zipPar or race when the caller only needs a simple parallel composition. Wait for the handle with Fiber.join or Fiber.await, and stop it with Fiber.interrupt. The fiber runs in its own scope and releases its scoped resources (anything registered with a scope, such as scopeResource, scopeFinalizer, or a hub subscription) when it settles, whether or not anyone joins it. A value the fiber returns therefore outlives the scoped resources the fiber acquired: a resource returned through Fiber.join has already been released. Use forkResource to acquire a resource in the background and keep it, or zipPar and sequencePar to acquire several in parallel. The flow to fork. A flow that produces a handle.
reliableWorker: Ref<int> -> Flow<ClockEnvironment,string,int>Axial.FiberModuleOperations on a running , the handle returned by Flow.fork. Every operation returns a flow; nothing waits or interrupts until that flow runs. Reading a fiber's outcome through join, await, or interrupt marks it observed, so a defect it died with is not also reported as unobserved.
join: Fiber<'error,'value> -> Flow<'env,'error,'value>Waits for a fiber and returns its value, failing the same way the fiber failed. Joining preserves the child's error channel: a Cause.Fail becomes the same typed error, and interruption and defects remain interruption and defects. Use await to inspect the outcome instead. The fiber to join. A flow that completes with the fiber's value. let loadProfile : Flow<unit, string, string> = Flow.ok "profile" let loadOrders : Flow<unit, string, int list> = Flow.ok [ 1; 2 ] let page = flow { let! fiber = Flow.fork loadProfile let! orders = loadOrders let! profile = Fiber.join fiber return profile, orders }
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
run: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
``.ctor``: IClock -> ClockEnvironmentAxial.PlatformService.ClockHelpers for the clock service.
live: IClockCreates a live clock backed by and a monotonic timer.
shouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
Requires & types
Requires: open Axial; open Axial.PlatformService; open Axial.State (packages: Axial, Axial.PlatformService)supervise takes the same Schedule as retry, but its input is the defect exception, so Schedule.whileInput or a Retry record's When can decide which crashes are worth a restart. Bound the restarts with Retries or Schedule.recursAtMost (or a time budget with Schedule.upTo) so a crash loop eventually surfaces. When the schedule stops, the final defect propagates as the flow's exit.
Two semantics worth knowing:
- Each attempt runs in its own child scope. Finalizers registered by a failed attempt run before the next attempt starts, so a supervised worker that acquires resources does not leak one acquisition per restart. The successful attempt's scope stays open until the enclosing scope closes, so a value it returns is still usable.
- Restart is not an Erlang restart. Re-evaluating the cold flow resets state that lives inside the flow. If your environment holds mutable state that the crashed attempt corrupted, restarting does not heal it.
Deliberate fire-and-forget: Flow.forkDetached
If a background fiber's outcome genuinely does not matter, say so at the call site:
let bestEffortCacheWarmup : Flow<ClockEnvironment, string, unit> = Flow.die (InvalidOperationException "cache warmup failed")
let startWarmup : Flow<ClockEnvironment, string, unit> =
flow {
let! _fiber = Flow.forkDetached bestEffortCacheWarmup
return ()
}
bestEffortCacheWarmup: Flow<ClockEnvironment,string,unit>Axial.Flow`3Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
stringAn abbreviation for the CLI type . Basic Types
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
Axial.Flowdie: exn -> Flow<'env,'error,'value>Creates a defective flow that fails with an exception. The exception representing the defect. A flow that always dies with the provided exception. This is the public constructor for non-domain defects. Use fail for expected typed failures and die when the workflow should surface a bug or panic.
``.ctor``: string -> unitInitializes a new instance of the class with a specified error message. The message that describes the error.
startWarmup: Flow<ClockEnvironment,string,unit>flow: FlowBuilderThe universal flow { } computation expression.
_fiber: Fiber<string,unit>forkDetached: Flow<'env,'error,'value> -> Flow<'env,'none,Fiber<'error,'value>>Starts a flow in a new fiber that is deliberately never awaited. The explicit fire-and-forget: the fiber counts as observed from birth, so a defect it dies with is never reported as an unobserved defect through the runtime's fiber observer. Use this instead of discarding a Flow.fork handle when silence is intended; a discarded fork handle whose fiber dies of a defect is reported. The flow to fork. A flow that produces a handle that can still be joined or interrupted.
Requires & types
Requires: open Axial (packages: Axial)val bestEffortCacheWarmup: Flow<ClockEnvironment,string,unit>val startWarmup: Flow<ClockEnvironment,string,unit>A detached fiber counts as observed from birth, so a defect it dies with is never reported as unobserved. Use it instead of discarding a Flow.fork handle: a discarded fork handle whose fiber dies of a defect is reported.
The safety net: FiberObserver
FiberObserver is a record of lifecycle hooks installed once at the application edge and carried implicitly to every descendant fork:
let defectStream (runApplication: FiberObserver -> Exit<unit, string>) =
FlowStream.fromCallback 16 (fun emit ->
Flow.delay (fun () ->
let observer =
{ FiberObserver.none with
OnUnobservedDefect = fun _ defect -> emit.Value defect.Message |> ignore }
runApplication observer |> ignore
emit.End()
Flow.ok ({ new IDisposable with member _.Dispose() = () })))
/// Detaches one failing fiber and discards the handle of another.
let application : Flow<ClockEnvironment, string, unit> =
flow {
do! startWarmup
let! _ = Flow.fork (Flow.die (InvalidOperationException "lost order") : Flow<ClockEnvironment, string, unit>)
do! Flow.sleep (TimeSpan.FromMilliseconds 20.0)
}
|> Flow.scoped
defectStream: (FiberObserver -> Exit<unit,string>) -> FlowStream<'a,'b,string>runApplication: FiberObserver -> Exit<unit,string>Axial.FiberObserverRuntime hooks observing fiber lifecycle events for diagnostics and telemetry. Installed once at the application edge with Flow.withFiberObserver and carried implicitly to every descendant fork. All hooks default to no-ops, receive only diagnostic data (FiberMetadata and defect exceptions, never typed exits), and must not throw; exceptions raised by hooks are swallowed so a diagnostics hook can never alter a fiber's outcome.
Axial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
stringAn abbreviation for the CLI type . Basic Types
Axial.FlowStreamModulefromCallback: int -> (Emit<'error,'value> -> Flow<'env,'error,IDisposable>) -> FlowStream<'env,'error,'value>Bridges a synchronous callback into a bounded, scope-owned stream. When full, the newest value is dropped; Value returns false. The subscription is disposed on completion, failure, interruption, or early termination. Subscribe should register cleanup with Flow.scopeFinalizer for anything it attaches before a later step can fail or be interrupted. Callbacks after completion are ignored.
emit: Emit<'b,string>Axial.Flowdelay: (unit -> Flow<'env,'error,'value>) -> Flow<'env,'error,'value>Defers flow construction until execution time. A function that returns the flow to execute. Recursive calls deferred here are consumed by the executor without growing a chain of parent tasks. Work that must run after the recursive call still needs storage until that call finishes. A flow that lazily evaluates the factory when executed. let flow = Flow.delay (fun () -> Flow.succeed 42)
observer: FiberObserverAxial.FiberObserverModuleStandard fiber observers.
none: FiberObserverThe default observer: every hook is a no-op.
OnUnobservedDefect: FiberMetadata option -> exn -> unitA Cause.Die defect became unobservable: a forked fiber died unobserved and no observation can happen anymore, or the runtime discarded a race/timeout loser's exit. The metadata is absent for discarded race/timeout losers, which are executions rather than fibers.
defect: exnValue: string -> boolOffers a value; false means it was dropped or the stream has ended.
Message: stringGets a message that describes the current exception. The error message that explains the reason for the exception, or an empty string ("").
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
ignore: 'T -> unitIgnore the passed value. This is often used to throw away results of a computation. The value to ignore. ignore 55555 // Evaluates to ()
End: unit -> unitEnds normally.
ok: 'value -> Flow<'env,'error,'value>Creates a successful synchronous flow. The value to wrap in a successful flow. A flow that always succeeds with the provided value.
System.IDisposableProvides a mechanism for releasing unmanaged resources.
_: IDisposableDispose: unit -> unitPerforms application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
application: Flow<ClockEnvironment,string,unit>Axial.Flow`3Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
flow: FlowBuilderThe universal flow { } computation expression.
startWarmup: Flow<ClockEnvironment,string,unit>fork: Flow<'env,'error,'value> -> Flow<'env,'none,Fiber<'error,'value>>Starts a flow in a new fiber without waiting for it to complete. Forking turns a cold flow description into hot child work and returns a handle that can later be joined or interrupted. Prefer zipPar or race when the caller only needs a simple parallel composition. Wait for the handle with Fiber.join or Fiber.await, and stop it with Fiber.interrupt. The fiber runs in its own scope and releases its scoped resources (anything registered with a scope, such as scopeResource, scopeFinalizer, or a hub subscription) when it settles, whether or not anyone joins it. A value the fiber returns therefore outlives the scoped resources the fiber acquired: a resource returned through Fiber.join has already been released. Use forkResource to acquire a resource in the background and keep it, or zipPar and sequencePar to acquire several in parallel. The flow to fork. A flow that produces a handle.
die: exn -> Flow<'env,'error,'value>Creates a defective flow that fails with an exception. The exception representing the defect. A flow that always dies with the provided exception. This is the public constructor for non-domain defects. Use fail for expected typed failures and die when the workflow should surface a bug or panic.
``.ctor``: string -> unitInitializes a new instance of the class with a specified error message. The message that describes the error.
sleep: TimeSpan -> Flow<'env,'error,unit>Suspends the flow for the specified duration, observing cancellation. The duration to sleep. A flow that completes after the specified delay, or is interrupted if cancelled first.
System.TimeSpanRepresents a time interval.
FromMilliseconds: float -> TimeSpanReturns a that represents a specified number of milliseconds. A number of milliseconds. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
scoped: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Runs a flow in a child scope and closes that scope before returning. Resources and fibers acquired inside the flow are released after success, typed failure, defect, or interruption without waiting for the surrounding application scope to close.
Requires & types
Requires: open Axial; open System (packages: Axial)val defectStream: (FiberObserver -> Exit<unit,string>) -> FlowStream<'a,'b,string>val application: Flow<ClockEnvironment,string,unit>defectStream (fun observer ->
application |> Flow.withFiberObserver observer |> Flow.run (ClockEnvironment Clock.live))
|> FlowStream.runCollect
|> Flow.run (ClockEnvironment Clock.live)
|> shouldEqual (Exit.Success [ "lost order" ])
defectStream: (FiberObserver -> Exit<unit,string>) -> FlowStream<'a,'b,string>observer: FiberObserverapplication: Flow<ClockEnvironment,string,unit>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.FlowwithFiberObserver: FiberObserver -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Installs runtime fiber-lifecycle hooks for diagnostics and telemetry. The observer is carried implicitly to every fiber forked inside , so installing it once at the application edge covers all descendant background work. Hooks receive diagnostic data only and cannot alter any fiber's outcome; exceptions they throw are swallowed. The lifecycle hooks. Start from FiberObserver.none and override what you need. The source flow. A flow that runs with the supplied observer in the ambient runtime context.
run: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
``.ctor``: IClock -> ClockEnvironmentAxial.PlatformService.ClockHelpers for the clock service.
live: IClockCreates a live clock backed by and a monotonic timer.
Axial.FlowStreamModulerunCollect: FlowStream<'a,'b,'c> -> Flow<'a,'b,'c list>Collects all emitted values into a list. stream |> FlowStream.runCollect
shouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
Requires & types
Requires: open Axial; open Axial.PlatformService (packages: Axial, Axial.PlatformService)The discarded fork's defect was reported when its scope closed. The detached warmup's was not.
The hooks:
OnStart: a fiber was forked; receives the child'sFiberMetadata.OnEnd: a fiber settled;FiberMetadata.StatusdistinguishesSucceeded/Failed/Interrupted, and the defect exception (if the fiber died of one) is passed alongside. This fires for every fiber, observed or not, so use it for metrics.OnUnobservedDefect: a defect became unobservable: a forked fiber died and nobody ever consumed its outcome, or the runtime itself discarded aFlow.race/ timeout loser's exit (those never had a handle at all, so their metadata isNone).
All hooks default to no-ops, receive diagnostic data only, and cannot alter any fiber's outcome; exceptions they throw are swallowed.
When does "unobserved" fire?
Whether a fiber will ever be joined is only knowable retroactively, so the runtime reports at three moments:
- Immediately, for race/timeout losers: the runtime knows at the discard site that no one can ever see that exit.
- When the forking scope closes, for fibers that settled with a defect and were never observed. Scope close is a fixed point in the program, so the report arrives at a predictable time.
- When a discarded handle is garbage-collected, as a best-effort net for forks made inside long-lived scopes (the same mechanism family as
UnobservedTaskException; timing depends on GC).
Each defect is reported at most once, whichever mechanism gets there first.
Note the interaction with supervise: a supervised flow that exhausts its restarts still settles with Cause.Die, so a discarded supervised fiber still reaches the net. Supervision reduces how often the net is needed; it does not replace it.
Telemetry integration
Axial.Telemetry ships a ready-made observer that records defects on the Axial activity source:
let tracedApplication : Flow<ClockEnvironment, string, unit> =
application |> FiberTelemetry.observe // = Flow.withFiberObserver FiberTelemetry.observer
tracedApplication: Flow<ClockEnvironment,string,unit>Axial.Flow`3Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
stringAn abbreviation for the CLI type . Basic Types
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
application: Flow<ClockEnvironment,string,unit>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Telemetry.JavaScript.FiberTelemetryFiber-lifecycle observability on the installed OpenTelemetry tracer: the JavaScript counterpart of the .NET package's FiberTelemetry, with the same span names and attribute vocabulary.
observe: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Installs the defect-only telemetry fiber observer, typically once at the application edge. The source flow. A flow whose forked fibers report defects through the installed tracer.
Requires & types
Requires: open Axial; open Axial.Telemetry.JavaScript (packages: Axial, Axial.Telemetry.JavaScript)val tracedApplication: Flow<ClockEnvironment,string,unit>Every fiber that settles with a defect produces an axial.flow.fiber.defect error span, and every unobservable defect produces an axial.flow.fiber.unobserved_defect error span, tagged with fiber id, parent id, status, and OpenTelemetry-convention exception tags.
Logging
Axial.Hosting ships the Microsoft.Extensions.Logging wiring: FiberLogging.observe logger logs
fiber defects as errors and unobserved defects as critical entries, with the exception attached. Observers
compose, so telemetry and logging stack from one edge install:
let observeWithLogging (logger: Microsoft.Extensions.Logging.ILogger) (workflow: Flow<ClockEnvironment, string, unit>) =
workflow
|> Flow.withFiberObserver (FiberObserver.compose FiberTelemetry.observer (FiberLogging.observer logger))Platform notes
Supervision and the observer hooks are pure F# and behave identically under Fable. The detection mechanisms differ slightly by platform: the scope-close sweep and timeout-loser reporting work everywhere, and the GC net is .NET-only.

