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.JavaScriptTrace workflows and inspect them in Aspire
Telemetry answers three different questions:
- What happened? A trace shows the spans that ran and how long each one took.
- Why did it fail? Span status and attributes distinguish typed failures, defects, and interruption.
- What was still running? Fiber metrics and dumps expose background work that an ordinary task trace can miss.
Axial publishes standard .NET ActivitySource spans and Meter instruments. OpenTelemetry collects those signals and
exports them to a backend. The .NET Aspire dashboard is the fastest way to see the
result locally.
Use Axial.Telemetry on .NET. For Node and browser applications compiled with Fable, use
Axial.Telemetry.JavaScript. Both adapters read the same ambient Context and emit the same
axial.flow.* vocabulary.
Before you begin
Install the .NET adapter and the OpenTelemetry host packages:
dotnet add package Axial.Telemetry
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
You need an OTLP receiver such as the Aspire dashboard, an OpenTelemetry Collector, Jaeger, Tempo, or a hosted observability service. Axial does not choose an exporter or send telemetry by itself. The runnable Axial.ReferenceApp configures the SDK and OTLP exporters, launches a local Aspire dashboard, and generates traces, metrics, logs, and a fiber dump from one endpoint.
Configure OpenTelemetry once
Create an application-owned ActivitySource, then subscribe to both that source and Axial's runtime source at the host
boundary:
Shared setup
// Setup for the checked examples on this page.
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
/// 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
SystemIOThreadingTasksAxialLayersConsoleFileSystemHostingBrowserNodePlatformServiceStateTelemetryJavaScriptshouldEqual: 'a -> 'a -> unitexpected: 'aactual: 'a(<>): 'T -> 'T -> boolStructural inequality The first parameter. The second parameter. The result of the comparison. 5 <> 5 // Evaluates to false 5 <> 6 // Evaluates to true [1; 2] <> [1; 2] // Evaluates to false
failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
open System.Diagnostics
let applicationActivitySource = new ActivitySource("Checkout.Api")
builder.Services
.AddOpenTelemetry()
.ConfigureResource(fun resource ->
resource.AddService("checkout-api") |> ignore)
.WithTracing(fun tracing ->
tracing
.AddSource(applicationActivitySource.Name, "Axial")
.AddAspNetCoreInstrumentation()
.AddOtlpExporter()
|> ignore)
.WithMetrics(fun metrics ->
metrics
.AddMeter("Axial")
.AddOtlpExporter()
|> ignore)
|> ignoreThese names answer different questions:
checkout-apiis the service name. Aspire groups telemetry by the deployed application or service that produced it.Checkout.Apiis the application's instrumentation scope. Workflow spans describing checkout behavior should use this source rather than appearing to be operations owned by Axial.Axialis Axial's runtime instrumentation scope. Automatic fiber spans and fiber metrics remain here because they describe the Flow runtime.checkout.submitis the span name for one operation.
An ActivitySource only publishes spans. The OpenTelemetry SDK listens, samples, and exports them. Register every source
used by Activity.traceOn, Activity.traceWithSource, or captured in an ActivityTracer; otherwise .NET returns
null from StartActivity and the workflow runs without a span.
Trace a workflow
Wrap a workflow at a boundary that has an operational meaning. Pass the application source because this span describes user code; Axial supplies the Flow-aware tracing behavior around it:
The examples below use this checkout workflow, and a listener that records each finished span with its tags, as the OpenTelemetry SDK would export them:
open System.Diagnostics
type Order = { OrderId: int; Total: decimal }
type CheckoutError =
| CardDeclined
static member describe(error: CheckoutError) =
match error with
| CardDeclined -> "card declined"
let applicationActivitySource = new ActivitySource("Checkout.Api")
let checkout (order: Order) : Flow<unit, CheckoutError, int> =
if order.Total > 0m then Flow.ok order.OrderId else Flow.fail CardDeclined
let order = { OrderId = 42; Total = 19.95m }
let userId = "user-7"
let tenantId = "acme"
/// Runs a flow and returns the spans that finished on the application source, with their tags.
let spansOf (workflow: Flow<unit, CheckoutError, int>) : (string * Map<string, string>) list =
let spans = ResizeArray<string * Map<string, string>>()
use listener =
new ActivityListener(
ShouldListenTo = (fun source -> source.Name = "Checkout.Api"),
Sample = SampleActivity<ActivityContext>(fun _ -> ActivitySamplingResult.AllData),
ActivityStopped =
(fun activity ->
let tags = activity.TagObjects |> Seq.map (fun tag -> tag.Key, string tag.Value) |> Map.ofSeq
lock spans (fun () -> spans.Add(activity.DisplayName, tags))))
ActivitySource.AddActivityListener listener
workflow |> Flow.run () |> ignore
List.ofSeq spans
SystemDiagnosticsFsLiveDocsGeneratedPage1_566144137446.OrderOrderId: intintAn abbreviation for the CLI type . Basic Types
Total: decimaldecimalAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage1_566144137446.CheckoutErrorCardDeclineddescribe: CheckoutError -> stringerror: CheckoutErrorapplicationActivitySource: ActivitySourceSystem.Diagnostics.ActivitySourceProvides APIs to create and start objects and to register objects to listen to the events.
checkout: Order -> Flow<unit,CheckoutError,int>order: OrderAxial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
(>): 'T -> 'T -> boolStructural greater-than The first parameter. The second parameter. The result of the comparison. 5 > 1 // Evaluates to true 5 > 5 // Evaluates to false (1, "a") > (1, "z") // Evaluates to false
Axial.Flowok: '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.
fail: 'error -> Flow<'env,'error,'value>Same as error. The error value to wrap in a failing flow. A flow that always fails with the provided error. let result = Flow.fail "error" |> Flow.run () // result = Failure (Cause.Fail "error")
userId: stringtenantId: stringspansOf: Flow<unit,CheckoutError,int> -> (string * Map<string,string>) listworkflow: Flow<unit,CheckoutError,int>stringAn abbreviation for the CLI type . Basic Types
Microsoft.FSharp.Collections.FSharpMap`2Immutable maps based on binary trees, where keys are ordered by F# generic comparison. By default comparison is the F# structural comparison function or uses implementations of the IComparable interface on key values. See the module for further operations on maps. All members of this class are thread-safe and may be used concurrently from multiple threads.
listThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
spans: ResizeArray<string * Map<string,string>>``.ctor``: unit -> unitInitializes a new instance of the class that is empty and has the default initial capacity.
listener: ActivityListenerSystem.Diagnostics.ActivityListenerAllows listening to the start and stop activity events and gives the opportunity to decide creating an activity for sampling scenarios.
ShouldListenTo: Func<ActivitySource,bool>Gets or sets the callback that allows deciding if activity object events that were created using the activity source object should be listened or not. to listen events; otherwise.
source: ActivitySourceName: stringReturns the activity source name. A string that represents the activity source name.
(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
Sample: SampleActivity<ActivityContext>Gets or sets the callback that is used to decide if creating objects with a specific data state is allowed. A sample activity instance.
System.Diagnostics.SampleActivity`1A delegate that defines the signature of the callbacks used in the sampling process. The Activity creation options used by callbacks to decide creating the Activity object or not. The type of the requested parent to create the Activity object with. Should be either a string or an instance. An object containing the sampling results, which indicate the amount of data to collect for the related .
System.Diagnostics.ActivityContextA representation that conforms to the W3C TraceContext specification. It contains two identifiers: a TraceId and a SpanId, along with a set of common TraceFlags and system-specific TraceState values.
System.Diagnostics.ActivitySamplingResultEnumeration values used by to indicate the amount of data to collect for the related . Requesting more data causes a greater performance overhead.
AllData: ActivitySamplingResultThe activity object should be populated with all the propagation information and also all other properties such as Links, Tags, and Events. Using this value causes to return .
ActivityStopped: Action<Activity>Gets or sets the callback used to listen to the activity stop event. An activity callback instance used to listen to the activity stop event.
activity: Activitytags: Map<string,string>TagObjects: Collections.Generic.KeyValuePair<string,obj> seqGets the list of tags that represent information to log along with the activity. This information is not passed on to the children of this activity. A key-value pair enumeration of tags and objects.
(|>): '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
Microsoft.FSharp.Collections.SeqModuleContains operations for working with values of type .
map: ('T -> 'U) -> 'T seq -> 'U seqBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The given function will be applied as elements are demanded using the MoveNext method on enumerators retrieved from the object. The returned sequence may be passed between threads safely. However, individual IEnumerator values generated from the returned sequence should not be accessed concurrently. Sequence construction is O(1). Enumeration is O(n), where n is the length of the sequence. A function to transform items from the input sequence. The input sequence. The result sequence. Thrown when the input sequence is null. let inputs = ["a"; "bbb"; "cc"] inputs |> Seq.map (fun x -> x.Length) Evaluates to a sequence yielding the same results as seq { 1; 3; 2 }
tag: Collections.Generic.KeyValuePair<string,obj>Key: stringGets the key in the key/value pair. A that is the key of the .
string: 'T -> stringConverts the argument to a string using ToString. For standard integer and floating point values and any type that implements IFormattable, ToString conversion uses CultureInfo.InvariantCulture. The input value. The converted string. string 'A' // evaluates to "A" string 0xff // evaluates to "255" string -10 // evaluates to "-10"
Value: objGets the value in the key/value pair. A that is the value of the .
Microsoft.FSharp.Collections.MapModuleContains operations for working with values of type .
ofSeq: ('Key * 'T) seq -> Map<'Key,'T>Returns a new map made from the given bindings. The input sequence of key/value pairs. The resulting map. This is an O(n log n) operation, where n is the number of elements in the sequence. let input = seq { (1, "a"); (2, "b") } input |> Map.ofSeq // evaluates to map [(1, "a"); (2, "b")]
lock: 'Lock -> (unit -> 'T) -> 'TExecute the function as a mutual-exclusion region using the input value as a lock. The object to be locked. The action to perform during the lock. The resulting value. open System.Linq /// A counter object, supporting unlocked and locked increment type TestCounter () = let mutable count = 0 /// Increment the counter, unlocked member this.IncrementWithoutLock() = count <- count + 1 /// Increment the counter, locked member this.IncrementWithLock() = lock this (fun () -> count <- count + 1) /// Get the count member this.Count = count let counter = TestCounter() // Create a parallel sequence to that uses all our CPUs (seq {1..100000}).AsParallel() .ForAll(fun _ -> counter.IncrementWithoutLock()) // Evaluates to a number between 1-100000, non-deterministically because there is no locking counter.Count let counter2 = TestCounter() // Create a parallel sequence to that uses all our CPUs (seq {1..100000}).AsParallel() .ForAll(fun _ -> counter2.IncrementWithLock()) // Evaluates to 100000 deterministically because the increment to the counter object is locked counter2.Count
Add: string * Map<string,string> -> unitAdds an object to the end of the . The object to be added to the end of the . The value can be for reference types.
DisplayName: stringGets or sets the display name of the activity. A string that represents the activity display name.
AddActivityListener: ActivityListener -> unitAdds a listener to the activity starting and stopping events. The activity listener object to use for listening to the activity events.
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
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 ()
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
ofSeq: 'T seq -> 'T listBuilds a new list from the given enumerable object. The input sequence. The list of elements from the sequence. let inputs = seq { 1; 2; 5 } inputs |> List.ofSeq Evaluates to [ 1; 2; 5 ]. This is an O(n) operation, where n is the length of the sequence.
let submitted =
checkout order
|> Activity.traceWithSource applicationActivitySource CheckoutError.describe "checkout.submit"
submitted: Flow<unit,CheckoutError,int>checkout: Order -> Flow<unit,CheckoutError,int>order: Order(|>): '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.ActivitytraceWithSource: ActivitySource -> ('error -> string) -> string -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Wraps a flow in a new activity from the supplied application-owned source, exports the ambient telemetry context, and stamps the final exit onto the span. The activity stops when the workflow settles, so span duration covers asynchronous work. On settle the span receives ActivityStatusCode from the exit, axial.flow.outcome (success/fail/die/interrupt), axial.flow.error for typed errors, OpenTelemetry exception.* tags for defects, axial.flow.interrupted for cancellation, and axial.flow.cause with the pretty-printed tree for composite causes. Register the source name with the host's OpenTelemetry tracing pipeline. Axial's automatic fiber spans continue to use the Axial source because they describe Axial runtime behavior. The application-owned activity source that emits the span. Renders typed errors for the axial.flow.error attribute. The name of the activity. The flow to trace. A flow that executes within the activity span.
applicationActivitySource: ActivitySourceFsLiveDocsGeneratedPage1_566144137446.CheckoutErrordescribe: CheckoutError -> stringspansOf submitted |> List.map fst |> shouldEqual [ "checkout.submit" ]
spansOf: Flow<unit,CheckoutError,int> -> (string * Map<string,string>) listsubmitted: Flow<unit,CheckoutError,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
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
map: ('T -> 'U) -> 'T list -> 'U listBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The function to transform elements from the input list. The input list. The list of transformed elements. let inputs = [ "a"; "bbb"; "cc" ] inputs |> List.map (fun x -> x.Length) Evaluates to [ 1; 3; 2 ] This is an O(n) operation, where n is the length of the list.
fst: 'T1 * 'T2 -> 'T1Return the first element of a tuple, fst (a,b) = a. The input tuple. The first value. fst ("first", 2) // Evaluates to "first"
shouldEqual: 'a -> 'a -> unitActivity.traceWithSource accepts the source and a renderer for the workflow's typed error. Use Activity.traceOn when
string is an acceptable representation:
let submittedOn =
checkout order
|> Activity.traceOn applicationActivitySource "checkout.submit"
submittedOn: Flow<unit,CheckoutError,int>checkout: Order -> Flow<unit,CheckoutError,int>order: Order(|>): '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.ActivitytraceOn: ActivitySource -> string -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Wraps a flow in an application-owned activity source, rendering typed errors without reflection. Errors are rendered with their own ToString, or by type name when that ToString needs reflection NativeAOT removed. Use traceWith to supply a renderer. The application-owned activity source that emits the span. The name of the activity. The flow to trace. A flow that executes within the activity span.
applicationActivitySource: ActivitySourceTrace without repeating the source at every call site
Passing applicationActivitySource into every traceOn call gets repetitive once tracing spreads across a codebase.
Capture the source once as an ActivityTracer, and call .Trace on it wherever you need a span:
let checkoutTracer = ActivityTracer.create applicationActivitySource
let submittedWithTracer =
checkout order
|> checkoutTracer.Trace "checkout.submit"
checkoutTracer: ActivityTracerAxial.Telemetry.ActivityTracerModuleCreates values.
create: ActivitySource -> ActivityTracerCaptures an application-owned activity source. The application-owned activity source that emits application spans.
applicationActivitySource: ActivitySourcesubmittedWithTracer: Flow<unit,CheckoutError,int>checkout: Order -> Flow<unit,CheckoutError,int>order: Order(|>): '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
Trace: string -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Wraps a flow in a span from this tracer's activity source, rendering typed errors with string. The name of the activity.
For a workflow tree that traces from many places, install the tracer once near the composition root instead, and call
the ambient Activity.trace/Activity.traceWith from anywhere underneath it, with no source to pass or capture at the
call site:
// deep inside the workflow tree, in code that has no reference to checkoutTracer:
let innerStep = checkout order |> Activity.trace "checkout.submit"
let application = innerStep |> Activity.withTracer checkoutTracer
innerStep: Flow<unit,CheckoutError,int>checkout: Order -> Flow<unit,CheckoutError,int>order: Order(|>): '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.Activitytrace: string -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Wraps a flow in the ambient application tracer installed by withTracer, and renders typed errors with string. Throws if no ambient tracer is installed. Use traceOn to supply a source explicitly instead of relying on the ambient one.
application: Flow<unit,CheckoutError,int>withTracer: ActivityTracer -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Installs an application-owned as the ambient tracer for the scope of . trace and traceWith read this ambient tracer; child fibers forked within the scope inherit it. The application-owned tracer to install. The flow to run with the tracer installed.
checkoutTracer: ActivityTracerspansOf application |> List.map fst |> shouldEqual [ "checkout.submit" ]
spansOf: Flow<unit,CheckoutError,int> -> (string * Map<string,string>) listapplication: Flow<unit,CheckoutError,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
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
map: ('T -> 'U) -> 'T list -> 'U listBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The function to transform elements from the input list. The input list. The list of transformed elements. let inputs = [ "a"; "bbb"; "cc" ] inputs |> List.map (fun x -> x.Length) Evaluates to [ 1; 3; 2 ] This is an O(n) operation, where n is the length of the list.
fst: 'T1 * 'T2 -> 'T1Return the first element of a tuple, fst (a,b) = a. The input tuple. The first value. fst ("first", 2) // Evaluates to "first"
shouldEqual: 'a -> 'a -> unitActivity.trace and Activity.traceWith read the tracer installed by the nearest enclosing Activity.withTracer;
forked child fibers inherit it too. There is no default source to fall back to: calling Activity.trace outside a
withTracer scope throws immediately, so a workflow can never end up silently tagged under the wrong instrumentation
scope. Use the ambient form for workflow trees under one application source, and traceOn/traceWithSource (or a
second ActivityTracer) when a call site needs a different source than the one currently installed.
The span starts when the workflow runs and stops when its asynchronous execution settles. Axial records:
| Exit | Span status | Attributes |
|---|---|---|
| success | Ok |
axial.flow.outcome = success |
| typed failure | Error |
axial.flow.outcome = fail, axial.flow.error |
| defect | Error |
axial.flow.outcome = die, exception.* |
| interruption | unset | axial.flow.outcome = interrupt, axial.flow.interrupted = true |
| composite cause | dominant outcome | axial.flow.cause with the rendered cause tree |
Do not trace every combinator. Trace operations that you would search for in an incident: checkout.submit,
invoice.generate, or outbox.deliver.
Add attributes
An attribute adds searchable context to the active span. Axial stores attributes in an immutable ambient Context.
The context is separate from the workflow environment because it is execution metadata, not an application service.
Nested scopes restore the previous value when they finish, and forked fibers inherit the context present at the fork.
Use a curated OpenTelemetry helper for a common semantic attribute:
let submittedByUser =
checkout order
|> Context.withEndUserId userId
|> Activity.traceWithSource applicationActivitySource CheckoutError.describe "checkout.submit"
submittedByUser: Flow<unit,CheckoutError,int>checkout: Order -> Flow<unit,CheckoutError,int>order: Order(|>): '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.ContextBuilds and scopes ambient telemetry attributes for Axial workflows. Context is immutable and lexically scoped. Nested attributes override matching outer keys only for the wrapped workflow, and child fibers inherit the context present when they are forked.
withEndUserId: string -> Flow<'a,'b,'c> -> Flow<'a,'b,'c>Scopes the OpenTelemetry enduser.id span attribute around a workflow.
userId: stringAxial.Telemetry.ActivitytraceWithSource: ActivitySource -> ('error -> string) -> string -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Wraps a flow in a new activity from the supplied application-owned source, exports the ambient telemetry context, and stamps the final exit onto the span. The activity stops when the workflow settles, so span duration covers asynchronous work. On settle the span receives ActivityStatusCode from the exit, axial.flow.outcome (success/fail/die/interrupt), axial.flow.error for typed errors, OpenTelemetry exception.* tags for defects, axial.flow.interrupted for cancellation, and axial.flow.cause with the pretty-printed tree for composite causes. Register the source name with the host's OpenTelemetry tracing pipeline. Axial's automatic fiber spans continue to use the Axial source because they describe Axial runtime behavior. The application-owned activity source that emits the span. Renders typed errors for the axial.flow.error attribute. The name of the activity. The flow to trace. A flow that executes within the activity span.
applicationActivitySource: ActivitySourceFsLiveDocsGeneratedPage1_566144137446.CheckoutErrordescribe: CheckoutError -> stringspansOf submittedByUser |> List.map (fun (_, tags) -> Map.tryFind "enduser.id" tags) |> shouldEqual [ Some "user-7" ]
spansOf: Flow<unit,CheckoutError,int> -> (string * Map<string,string>) listsubmittedByUser: Flow<unit,CheckoutError,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
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
map: ('T -> 'U) -> 'T list -> 'U listBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The function to transform elements from the input list. The input list. The list of transformed elements. let inputs = [ "a"; "bbb"; "cc" ] inputs |> List.map (fun x -> x.Length) Evaluates to [ 1; 3; 2 ] This is an O(n) operation, where n is the length of the list.
tags: Map<string,string>Microsoft.FSharp.Collections.MapModuleContains operations for working with values of type .
tryFind: 'Key -> Map<'Key,'T> -> 'T optionLookup an element in the map, returning a Some value if the element is in the domain of the map and None if not. The input key. The input map. The found Some value or None. Maps are represented as binary trees so this is an O(log n) operation, where n is the number of bindings in the map. let sample = Map [ (1, "a"); (2, "b") ] sample |> Map.tryFind 1 // evaluates to Some "a" sample |> Map.tryFind 3 // evaluates to None
shouldEqual: 'a -> 'a -> unitSomeThe representation of "Value of type 'T" The input value. An option representing the value.
enduser.id is an OpenTelemetry semantic-convention attribute, currently marked development by OpenTelemetry. It
can contain identifying information; use a stable opaque identifier only when your privacy policy permits exporting it.
Axial documents the stability and scope of every convention for which it provides a convenience helper. Configure resource attributes such as service identity in the OpenTelemetry SDK instead of
copying them onto every span.
Define typed keys for application attributes:
module CheckoutAttributes =
let tenantId = AttributeKey.string "example.tenant.id"
let retryCount = AttributeKey.int64 "example.checkout.retry_count"
FsLiveDocsGeneratedPage1_566144137446.CheckoutAttributestenantId: AttributeKey<string>Axial.Telemetry.AttributeKeyCreates typed keys for application-defined telemetry attributes.
string: string -> AttributeKey<string>Creates a string-valued attribute key.
retryCount: AttributeKey<int64>int64: string -> AttributeKey<int64>Creates a 64-bit integer-valued attribute key.
Attach values without boxing or runtime conversion:
let submittedForTenant =
checkout order
|> Context.withAttributes [
Context.attribute CheckoutAttributes.tenantId tenantId
Context.attribute CheckoutAttributes.retryCount 2L
]
|> Activity.traceWithSource applicationActivitySource CheckoutError.describe "checkout.submit"
submittedForTenant: Flow<unit,CheckoutError,int>checkout: Order -> Flow<unit,CheckoutError,int>order: Order(|>): '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.ContextBuilds and scopes ambient telemetry attributes for Axial workflows. Context is immutable and lexically scoped. Nested attributes override matching outer keys only for the wrapped workflow, and child fibers inherit the context present when they are forked.
withAttributes: Attribute seq -> Flow<'a,'b,'c> -> Flow<'a,'b,'c>Scopes telemetry attributes around a workflow.
attribute: AttributeKey<'value> -> 'value -> AttributeCreates an application-defined attribute by pairing a typed key with a value.
FsLiveDocsGeneratedPage1_566144137446.CheckoutAttributestenantId: AttributeKey<string>tenantId: stringretryCount: AttributeKey<int64>Axial.Telemetry.ActivitytraceWithSource: ActivitySource -> ('error -> string) -> string -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Wraps a flow in a new activity from the supplied application-owned source, exports the ambient telemetry context, and stamps the final exit onto the span. The activity stops when the workflow settles, so span duration covers asynchronous work. On settle the span receives ActivityStatusCode from the exit, axial.flow.outcome (success/fail/die/interrupt), axial.flow.error for typed errors, OpenTelemetry exception.* tags for defects, axial.flow.interrupted for cancellation, and axial.flow.cause with the pretty-printed tree for composite causes. Register the source name with the host's OpenTelemetry tracing pipeline. Axial's automatic fiber spans continue to use the Axial source because they describe Axial runtime behavior. The application-owned activity source that emits the span. Renders typed errors for the axial.flow.error attribute. The name of the activity. The flow to trace. A flow that executes within the activity span.
applicationActivitySource: ActivitySourceFsLiveDocsGeneratedPage1_566144137446.CheckoutErrordescribe: CheckoutError -> stringspansOf submittedForTenant
|> List.map (fun (_, tags) -> Map.tryFind "example.tenant.id" tags, Map.tryFind "example.checkout.retry_count" tags)
|> shouldEqual [ Some "acme", Some "2" ]
spansOf: Flow<unit,CheckoutError,int> -> (string * Map<string,string>) listsubmittedForTenant: Flow<unit,CheckoutError,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
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
map: ('T -> 'U) -> 'T list -> 'U listBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The function to transform elements from the input list. The input list. The list of transformed elements. let inputs = [ "a"; "bbb"; "cc" ] inputs |> List.map (fun x -> x.Length) Evaluates to [ 1; 3; 2 ] This is an O(n) operation, where n is the length of the list.
tags: Map<string,string>Microsoft.FSharp.Collections.MapModuleContains operations for working with values of type .
tryFind: 'Key -> Map<'Key,'T> -> 'T optionLookup an element in the map, returning a Some value if the element is in the domain of the map and None if not. The input key. The input map. The found Some value or None. Maps are represented as binary trees so this is an O(log n) operation, where n is the number of bindings in the map. let sample = Map [ (1, "a"); (2, "b") ] sample |> Map.tryFind 1 // evaluates to Some "a" sample |> Map.tryFind 3 // evaluates to None
shouldEqual: 'a -> 'a -> unitSomeThe representation of "Value of type 'T" The input value. An option representing the value.
A typed key prevents attaching an integer to a string attribute. Supported values are strings, Booleans, 64-bit integers, floating-point values, and homogeneous lists of those types.
Build a context once when several workflows share the same metadata:
let requestContext =
Context.empty
|> Context.addEndUserId userId
|> Context.add (Context.attribute CheckoutAttributes.tenantId tenantId)
let inRequest = application |> Context.withContext requestContext
requestContext: TelemetryContextAxial.Telemetry.ContextBuilds and scopes ambient telemetry attributes for Axial workflows. Context is immutable and lexically scoped. Nested attributes override matching outer keys only for the wrapped workflow, and child fibers inherit the context present when they are forked.
empty: TelemetryContextAn empty telemetry context.
(|>): '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
addEndUserId: string -> TelemetryContext -> TelemetryContextAdds the OpenTelemetry enduser.id span attribute to a context.
userId: stringadd: Attribute -> TelemetryContext -> TelemetryContextAdds or replaces one attribute.
attribute: AttributeKey<'value> -> 'value -> AttributeCreates an application-defined attribute by pairing a typed key with a value.
FsLiveDocsGeneratedPage1_566144137446.CheckoutAttributestenantId: AttributeKey<string>tenantId: stringinRequest: Flow<unit,CheckoutError,int>application: Flow<unit,CheckoutError,int>withContext: TelemetryContext -> Flow<'a,'b,'c> -> Flow<'a,'b,'c>Scopes a telemetry context around a workflow.
Read the currently scoped context from a workflow when an integration needs to inspect it:
let exportContext (context: TelemetryContext) = Context.tryFind CheckoutAttributes.tenantId context
let exportedTenant : Flow<unit, CheckoutError, string option> =
flow {
let! telemetryContext = Context.current
return exportContext telemetryContext
}
|> Context.withContext requestContext
exportContext: TelemetryContext -> string optioncontext: TelemetryContextAxial.Telemetry.TelemetryContextAn immutable set of telemetry attributes propagated with a running workflow.
Axial.Telemetry.ContextBuilds and scopes ambient telemetry attributes for Axial workflows. Context is immutable and lexically scoped. Nested attributes override matching outer keys only for the wrapped workflow, and child fibers inherit the context present when they are forked.
tryFind: AttributeKey<'value> -> TelemetryContext -> 'value optionReads a typed attribute from a context.
FsLiveDocsGeneratedPage1_566144137446.CheckoutAttributestenantId: AttributeKey<string>exportedTenant: Flow<unit,CheckoutError,string option>Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
FsLiveDocsGeneratedPage1_566144137446.CheckoutErrorstringAn abbreviation for the CLI type . Basic Types
optionThe type of optional values. When used from other CLI languages the empty option is the null value. Use the constructors Some and None to create values of this type. Use the values in the Option module to manipulate values of this type, or pattern match against the values directly. 'None' values will appear as the value null to other CLI languages. Instance methods on this type will appear as static methods to other CLI languages due to the use of null as a value representation. Options
flow: FlowBuilderThe universal flow { } computation expression.
telemetryContext: TelemetryContextcurrent: Flow<'env,'error,TelemetryContext>Reads the currently scoped telemetry context.
(|>): '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
withContext: TelemetryContext -> Flow<'a,'b,'c> -> Flow<'a,'b,'c>Scopes a telemetry context around a workflow.
requestContext: TelemetryContextexportedTenant |> Flow.run () |> shouldEqual (Exit.Success(Some "acme"))
exportedTenant: Flow<unit,CheckoutError,string option>(|>): '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.Flowrun: '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
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.
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
Most workflows should scope attributes rather than read the whole context. Adapters use Context.current at integration
boundaries; application dependencies still belong in the Flow environment.
Attribute names are contracts with dashboards and alerts. Follow OpenTelemetry semantic conventions when one applies. Use an application-owned prefix otherwise. Do not attach secrets, unrestricted personal information, or high-cardinality values to metrics. A correlation value that must cross process boundaries may belong in OpenTelemetry baggage; trace identity itself belongs in trace and span context, not in a duplicate attribute.
Observe fibers
A successful root workflow can still have failed background work. Install fiber telemetry once around the application:
let withDefectSpans = application |> FiberTelemetry.observe
withDefectSpans: Flow<unit,CheckoutError,int>application: Flow<unit,CheckoutError,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.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.
This records defect spans for failed fibers and for defects that no code can observe. To create a span for every forked
fiber, use FiberTelemetry.observeWithSpans:
let withFiberSpans = application |> FiberTelemetry.observeWithSpans
withFiberSpans: Flow<unit,CheckoutError,int>application: Flow<unit,CheckoutError,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.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.
observeWithSpans: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Installs the span-per-fiber telemetry observer: every forked fiber becomes an axial.flow.fiber span covering fork to settle. See observerWithSpans. The source flow. A flow whose forked fibers each produce a span on the installed tracer.
Span-per-fiber mode provides more detail and more data. Use defect-only observation by default, and enable span-per-fiber when you need fork-to-settle timing or a complete concurrency tree.
Add runtime metrics independently:
let withMetrics =
application
|> FiberMetrics.observe
|> FiberTelemetry.observe
withMetrics: Flow<unit,CheckoutError,int>application: Flow<unit,CheckoutError,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.Telemetry.FiberMetricsOpenTelemetry-compatible fiber runtime metrics on the Axial meter. Register the meter with your OpenTelemetry pipeline (AddMeter("Axial")) and every metric appears in any OTLP backend, including the Aspire dashboard's metrics view. Instruments: axial.flow.fibers.started and axial.flow.fibers.settled (counters; settled is tagged with axial.flow.fiber.status), axial.flow.fibers.live (up-down counter), axial.flow.fiber.duration (histogram, seconds, tagged with status), and axial.flow.fibers.unobserved_defects (counter).
observe: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Installs the metrics fiber observer on a flow, composing with any observer already installed, typically once at the application edge, stacked with FiberTelemetry.observe or a FiberRegistry. The source flow. A flow whose forked fibers report runtime metrics on the Axial meter.
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.
The Axial meter records starts, live fibers, settlements, duration, and unobserved defects. A rising live-fiber count
without matching settlements indicates stuck or leaked work.
Watch queues and hub subscriptions
Report a queue or a hub subscription under a
name with QueueMetrics.observe. It is reported until the scope that registered it closes, so register a subscription
in the fiber that consumes it:
let historian (readings: Hub<float>) (record: float -> Flow<unit, Never, unit>) : Flow<unit, Never, unit> =
flow {
let! history = readings |> Hub.subscribe (QueueStrategy.BackPressure 10_000)
do! history |> QueueMetrics.observe "historian"
do! history |> FlowStream.fromDequeue |> FlowStream.runForEachFlow record
}
historian: Hub<float> -> (float -> Flow<unit,Never,unit>) -> Flow<unit,Never,unit>readings: Hub<float>Axial.Hub`1Broadcasts every published value to every current subscription. Create one with Hub.make. Use a hub when there can be zero or many consumers. The type of the published values.
floatAn abbreviation for the CLI type . Basic Types
record: float -> Flow<unit,Never,unit>Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
Axial.NeverRepresents an error channel that cannot occur.
flow: FlowBuilderThe universal flow { } computation expression.
history: Dequeue<float>(|>): '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.HubModuleCreates hubs, publishes to them, and subscribes to them. A publish reaches every subscription that exists when it starts; a subscriber sees only values published after its subscribe completes, so a late joiner that needs current state must get it elsewhere, for example from a SubscriptionRef. Publishes are serialized, so every subscriber observes values in the same order even with concurrent publishers. Dropping, Sliding, and Unbounded subscriptions never delay the publisher. A BackPressure subscription with a full buffer suspends publish until it has room or its subscription ends. While the publisher waits, no later value reaches any subscriber, lossy ones included: keeping one order for every subscriber means a stalled lossless subscriber stalls the feed. Size a lossless subscriber's buffer for the bursts you expect, watch Dequeue.size to raise an alarm before it fills, and use Hub.tryPublish where the publisher must never wait. Shutting a hub down shuts every subscription down with Dequeue.shutdown semantics: subscribers drain their backlog, streams over subscriptions end normally, and later publishes are interrupted.
subscribe: QueueStrategy -> Hub<'a> -> Flow<'env,'error,Dequeue<'a>>Subscribes to the hub with a buffering strategy. The subscription ends when the current scope closes. Run subscribe inside Flow.scoped, a forked fiber, or an application root: a forked fiber's subscription ends with the fiber. Ending the subscription, by closing its scope or with Dequeue.shutdown, removes it from the hub and releases a publisher waiting on it. To subscribe for the life of a stream, use FlowStream.fromHub. flow { let! (readings: Hub<float>) = Hub.make () let! latest = readings |> Hub.subscribe (QueueStrategy.Sliding 1) do! readings |> Hub.publishAll [ 20.0; 21.5 ] |> Flow.ignore return! Dequeue.takeAll latest } |> Flow.scoped
Axial.QueueStrategyWhat a queue, or a hub subscription, does with a value offered while it is full. Lossless delivery needs either back-pressure or unbounded memory. BackPressure keeps every value by making the producer wait; Unbounded keeps every value by growing without limit; Dropping and Sliding never make the producer wait and lose values instead. A non-positive capacity fails the flow that uses the strategy with a defect.
BackPressureLossless. A full buffer suspends the producer until a value is taken.
Axial.Telemetry.QueueMetricsOpenTelemetry-compatible queue and hub-subscription metrics on the Axial meter. Register a queue, or a hub subscription, under a name with QueueMetrics.observe. Its figures are read only when the metrics pipeline collects, so observing a queue adds no cost to offers and takes. Every measurement is tagged with axial.queue.name. Instruments: axial.queue.size, axial.queue.capacity (bounded queues only), axial.queue.waiting_takers, and axial.queue.waiting_offerers (gauges); and axial.queue.accepted, axial.queue.dropped, and axial.queue.evicted (counters). Alert on axial.queue.size approaching axial.queue.capacity for a lossless subscriber, before it fills and holds up the publisher, and on the rate of axial.queue.dropped or axial.queue.evicted for a lossy one.
observe: string -> Dequeue<'a> -> Flow<'env,'error,unit>Reports a queue's metrics under until the current scope closes. Works for a Queue and for a hub subscription, which is a Dequeue. Run it in the scope that owns the queue, such as the fiber that consumes a subscription, so the queue stops being reported when that scope ends. The value of the axial.queue.name tag. The queue or subscription to report. flow { let! history = readings |> Hub.subscribe (QueueStrategy.BackPressure 10_000) do! history |> QueueMetrics.observe "historian" do! history |> FlowStream.fromDequeue |> FlowStream.runForEachFlow record }
Axial.FlowStreamModulefromDequeue: Dequeue<'value> -> FlowStream<'env,'error,'value>Creates a stream that takes values from a queue or hub subscription until it is shut down and drained. Shutdown is the normal end of the stream, not a failure: after Dequeue.shutdown or Hub.shutdown the stream emits the remaining backlog and then completes. Each pull suspends while the queue is empty. Interrupting a pull leaves any value it would have received in the queue. flow { let! (jobs: Queue<string>) = Queue.bounded 8 do! jobs |> Queue.offerAll [ "a"; "b" ] |> Flow.ignore do! Dequeue.shutdown jobs return! jobs |> FlowStream.fromDequeue |> FlowStream.runCollect }
runForEachFlow: ('value -> Flow<'env,'error,unit>) -> FlowStream<'env,'error,'value> -> Flow<'env,'error,unit>Runs an effectful action for every stream value. stream |> FlowStream.runForEachFlow save
The figures are read only when the metrics pipeline collects, so an observed queue costs nothing extra per value. Each
measurement carries an axial.queue.name tag. axial.queue.size, axial.queue.capacity,
axial.queue.waiting_takers, and axial.queue.waiting_offerers are gauges; axial.queue.accepted,
axial.queue.dropped, and axial.queue.evicted are counters. Alert when a lossless subscriber's size approaches its
capacity, before it fills and holds up the publisher, and on the rate of drops or evictions for a lossy one.
Capture a fiber dump
A trace explains completed and timed operations. A fiber dump shows the work that is live now.
Install a registry at the application edge:
let registry = FiberRegistry()
let observedApplication =
application
|> Flow.withFiberRegistry registry
|> FiberMetrics.observe
|> FiberTelemetry.observe
registry: FiberRegistry``.ctor``: unit -> FiberRegistryCreates a registry that remembers the last 200 settled fibers and unobserved defects.
observedApplication: Flow<unit,CheckoutError,int>application: Flow<unit,CheckoutError,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.FlowwithFiberRegistry: FiberRegistry -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Tracks every fiber forked inside the flow in . The registry's observer is composed with any observer already installed, so telemetry hooks and the registry can coexist from separate installs. Install once at the application edge, keep the registry, and call registry.DumpAt(clock) (or registry.Snapshot()) whenever a live fiber tree is needed. The registry that receives fiber lifecycle events. The source flow. A flow whose forked fibers are tracked in the registry.
Axial.Telemetry.FiberMetricsOpenTelemetry-compatible fiber runtime metrics on the Axial meter. Register the meter with your OpenTelemetry pipeline (AddMeter("Axial")) and every metric appears in any OTLP backend, including the Aspire dashboard's metrics view. Instruments: axial.flow.fibers.started and axial.flow.fibers.settled (counters; settled is tagged with axial.flow.fiber.status), axial.flow.fibers.live (up-down counter), axial.flow.fiber.duration (histogram, seconds, tagged with status), and axial.flow.fibers.unobserved_defects (counter).
observe: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Installs the metrics fiber observer on a flow, composing with any observer already installed, typically once at the application edge, stacked with FiberTelemetry.observe or a FiberRegistry. The source flow. A flow whose forked fibers report runtime metrics on the Axial meter.
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.
Name long-lived work so the dump is readable:
let pollOutbox : Flow<ClockEnvironment, CheckoutError, unit> = Flow.sleep (TimeSpan.FromSeconds 5.0)
let startPoller : Flow<ClockEnvironment, CheckoutError, Fiber<CheckoutError, unit>> = Flow.forkNamed "outbox-poller" pollOutbox
pollOutbox: Flow<ClockEnvironment,CheckoutError,unit>Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
FsLiveDocsGeneratedPage1_566144137446.CheckoutErrorunitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
Axial.Flowsleep: 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.
FromSeconds: float -> TimeSpanReturns a that represents a specified number of seconds, where the specification is accurate to the nearest millisecond. A number of seconds, accurate to the nearest millisecond. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
startPoller: Flow<ClockEnvironment,CheckoutError,Fiber<CheckoutError,unit>>Axial.Fiber`2Represents a handle to a workflow that has already been started. A fiber is the hot counterpart to a cold Flow, returned by Flow.fork. Wait for it with Fiber.join (its value, re-raising its failure) or Fiber.await (its Exit), check it with Fiber.poll, and stop it with Fiber.interrupt. The failure type of the running workflow. The success type of the running workflow.
forkNamed: string -> Flow<'env,'error,'value> -> Flow<'env,'none,Fiber<'error,'value>>Starts a flow in a new fiber carrying a diagnostic name. The name appears in FiberDump snapshots, FiberRegistry dumps, and telemetry fiber spans, so long-lived background fibers are recognizable in diagnostics instead of showing as bare ids. The diagnostic name recorded in the fiber's metadata. The flow to fork. A flow that produces a handle.
Return registry.DumpAt(Clock.live) from a protected diagnostics endpoint, write it during a stuck shutdown, or attach it to the
current trace:
let recordDump () = FiberDumpTelemetry.record Clock.live registry
recordDump: unit -> unitAxial.Telemetry.FiberDumpTelemetryExports structured fiber dumps into traces.
record: IClock -> FiberRegistry -> unitRecords the registry's current live-fiber tree on the active trace: as an axial.flow.fiber.dump event on the current activity when one exists, otherwise as a standalone axial.flow.fiber.dump span. The event carries axial.flow.fibers.live and the rendered tree in axial.flow.fiber.dump.tree, so dumps land next to the traces they explain in any OTLP backend, including the Aspire dashboard. The registry to snapshot.
Axial.PlatformService.ClockHelpers for the clock service.
live: IClockCreates a live clock backed by and a monotonic timer.
registry: FiberRegistryFiberDumpTelemetry.record adds an axial.flow.fiber.dump event to the current activity. If no activity is current, it
creates a standalone span. This connects a slow or failed trace to the exact fibers that were running when you captured
the dump. See Supervision and fiber observability for registry and dump details.
View Axial in the Aspire dashboard
Aspire's dashboard accepts OTLP telemetry. The quickest complete example is
Axial.ReferenceApp: run
dotnet run --file apphost.cs from its directory, then call its /observability/demo endpoint as described in the
example README.
If your Aspire service uses AddServiceDefaults(), keep that setup and add the application source, Axial's runtime
source, and Axial's meter:
builder.Services
.AddOpenTelemetry()
.WithTracing(fun tracing ->
tracing.AddSource(applicationActivitySource.Name, "Axial") |> ignore)
.WithMetrics(fun metrics -> metrics.AddMeter("Axial") |> ignore)
|> ignoreRun the application and open the dashboard:
- Open Traces and select an incoming request.
- Expand the request span to find
checkout.submitor anotherActivity.tracespan. - Inspect its status and attributes, including
enduser.id, application attributes, andaxial.flow.outcome. - Enable
FiberTelemetry.observeWithSpansto see named child fibers in the same trace. - Trigger
FiberDumpTelemetry.recordand inspect theaxial.flow.fiber.dumpevent on the active span. - Open Metrics and chart
axial.flow.fibers.liveandaxial.flow.fibers.unobserved_defects.
Used together, traces identify the slow or failed operation, attributes identify its application context, metrics show whether the runtime is degrading, and a fiber dump shows the work still in flight.
Choose attributes, annotations, or logs
- Use telemetry context attributes for values intentionally exported to tracing backends and queried across spans.
- Use
Flow.annotatefor Axial runtime diagnostics. Telemetry exports these underaxial.flow.annotation.*, preserving their separate namespace. - Use
ILogfor messages and exceptions that belong in the host logging pipeline.
The mechanisms share ambient scoping, but they are different operational contracts.

