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:
The builder below is the WebApplicationBuilder from an ASP.NET Core Program.fs; this block is a fragment of
that host setup, not a standalone snippet.
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:
Show page 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 actualopen 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 =
FlowStream.fromCallback 64 (fun emit ->
Flow.delay (fun () ->
let 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
emit.Value(activity.DisplayName, tags) |> ignore))
ActivitySource.AddActivityListener listener
workflow |> Flow.run () |> ignore
emit.End()
Flow.ok (listener :> IDisposable)))
|> FlowStream.runCollect
|> Flow.run ()
|> function Exit.Success spans -> spans | exit -> failwithf "Span collection failed: %A" exit
SystemDiagnosticsFsLiveDocsGeneratedPage1_F39FDF9DAE04.OrderOrderId: intintAn abbreviation for the CLI type . Basic Types
Total: decimaldecimalAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage1_F39FDF9DAE04.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`3unitThe 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.
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<obj,(string * Map<string,string>)>delay: (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)
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")]
Value: string * Map<string,string> -> boolOffers a value; false means it was dropped or the stream has ended.
DisplayName: stringGets or sets the display name of the activity. A string that represents the activity display name.
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 ()
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
End: unit -> unitEnds normally.
System.IDisposableProvides a mechanism for releasing unmanaged resources.
runCollect: FlowStream<'a,'b,'c> -> Flow<'a,'b,'c list>Collects all emitted values into a list. stream |> FlowStream.runCollect
Axial.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.
spans: (string * Map<string,string>) listexit: Exit<(string * Map<string,string>) list,obj>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.
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_F39FDF9DAE04.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
Capture applicationActivitySource once as an ActivityTracer, then call .Trace 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, without passing the source at each 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_F39FDF9DAE04.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_F39FDF9DAE04.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_F39FDF9DAE04.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_F39FDF9DAE04.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_F39FDF9DAE04.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_F39FDF9DAE04.CheckoutAttributestenantId: AttributeKey<string>exportedTenant: Flow<unit,CheckoutError,string option>Axial.Flow`3unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
FsLiveDocsGeneratedPage1_F39FDF9DAE04.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`3unitThe 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.
A MeterListener can sample the Axial meter to check the observed figures, and to confirm a queue stops being
reported after its scope closes:
open System.Diagnostics.Metrics
type QueueCapture() =
let gate = obj ()
let latest = Collections.Generic.Dictionary<string * string, int64>()
let listener = new MeterListener()
do
listener.InstrumentPublished <- fun instrument metricListener ->
if instrument.Meter.Name = "Axial" && instrument.Name.StartsWith "axial.queue." then
metricListener.EnableMeasurementEvents instrument
listener.SetMeasurementEventCallback<int64>(fun instrument value tags _ ->
let queueName =
tags.ToArray()
|> Array.tryFind (fun tag -> tag.Key = "axial.queue.name")
|> Option.map (fun tag -> string tag.Value)
|> Option.defaultValue ""
lock gate (fun () -> latest[(instrument.Name, queueName)] <- value))
listener.Start()
member _.Read(instrument: string, queueName: string) =
lock gate (fun () -> latest.Clear())
listener.RecordObservableInstruments()
lock gate (fun () ->
match latest.TryGetValue((instrument, queueName)) with
| true, value -> Some value
| _ -> None)
interface IDisposable with
member _.Dispose() = listener.Dispose()
let observeQueue (capture: QueueCapture) : Flow<unit, Never, (int64 option list) * int64 option> =
flow {
let! readings =
flow {
let! (display: Queue<int>) = Queue.sliding 2
do! display |> QueueMetrics.observe "queue-metrics-display"
do! display |> Queue.offerAll [ 1..5 ] |> Flow.ignore
return
[ for instrument in
[ "axial.queue.size"
"axial.queue.capacity"
"axial.queue.accepted"
"axial.queue.evicted"
"axial.queue.dropped" ] ->
capture.Read(instrument, "queue-metrics-display") ]
}
|> Flow.scoped
let afterScope = capture.Read("axial.queue.size", "queue-metrics-display")
return readings, afterScope
}
SystemDiagnosticsMetricsFsLiveDocsGeneratedPage1_F39FDF9DAE04.QueueCapturegate: obj``.ctor``: unit -> unitInitializes a new instance of the class.
latest: Collections.Generic.Dictionary<(string * string),int64>Collections``.ctor``: unit -> unitInitializes a new instance of the class that is empty, has the default initial capacity, and uses the default equality comparer for the key type.
GenericstringAn abbreviation for the CLI type . Basic Types
int64An abbreviation for the CLI type . Basic Types
listener: MeterListenerSystem.Diagnostics.Metrics.MeterListenerThe MeterListener is class used to listen to the metrics instrument measurements recording.
InstrumentPublished: Action<Instrument,MeterListener>Gets or sets the callback to get notified when an instrument is published. The callback to get notified when an instrument is published.
instrument: InstrumentmetricListener: MeterListenerName: stringGets the Meter name. The Meter name
Meter: MeterGets the Meter that created the instrument.
(=): '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
(&&): bool -> bool -> boolBinary 'and'. When used as a binary operator the right hand value is evaluated only on demand The first value. The second value. The result of the operation.
StartsWith: string -> boolDetermines whether the beginning of this string instance matches the specified string. The string to compare. if matches the beginning of this string; otherwise, . is .
Name: stringGets the instrument name.
EnableMeasurementEvents: Instrument * obj -> unitStarts listening to a specific instrument measurement recording. The instrument to listen to. A state object that will be passed back to the callback getting measurements events.
SetMeasurementEventCallback: MeasurementCallback<'T> -> unitSets a callback for a specific numeric type to get the measurement recording notification from all instruments which enabled listening and was created with the same specified numeric type. If a measurement of type T is recorded and a callback of type T is registered, that callback will be used. The callback which can be used to get measurement recording of numeric type T. The type of the numeric measurement.
value: int64tags: ReadOnlySpan<Collections.Generic.KeyValuePair<string,obj>>queueName: stringToArray: unit -> Collections.Generic.KeyValuePair<string,obj> array(|>): '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.ArrayModuleContains operations for working with arrays. See also F# Language Guide - Arrays.
tryFind: ('T -> bool) -> 'T array -> 'T optionReturns the first element for which the given function returns True. Return None if no such element exists. The function to test the input elements. The input array. The first element that satisfies the predicate, or None. Thrown when the input array is null. Try to find the first even number: let inputs = [| 1; 2; 3 |] inputs |> Array.tryFind (fun elm -> elm % 2 = 0) Evaluates to Some 2 Try to find the first even number: let inputs = [| 1; 5; 3 |] inputs |> Array.tryFind (fun elm -> elm % 2 = 0) Evaluates to None This is an O(n) operation in the worst case, where n is the length of the array.
tag: Collections.Generic.KeyValuePair<string,obj>Key: stringGets the key in the key/value pair. A that is the key of the .
Microsoft.FSharp.Core.OptionModuleContains operations for working with options. Options
map: ('T -> 'U) -> 'T option -> 'U optionmap f inp evaluates to match inp with None -> None | Some x -> Some (f x). A function to apply to the option value. The input option. An option of the input value after applying the mapping function, or None if the input is None. None |> Option.map (fun x -> x * 2) // evaluates to None Some 42 |> Option.map (fun x -> x * 2) // evaluates to Some 84
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 .
defaultValue: 'T -> 'T option -> 'TGets the value of the option if the option is Some, otherwise returns the specified default value. The specified default value. The input option. The option if the option is Some, else the default value. Identical to the built-in operator, except with the arguments swapped. (99, None) ||> Option.defaultValue // evaluates to 99 (99, Some 42) ||> Option.defaultValue // evaluates to 42
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
Item: int64Gets or sets the value associated with the specified key. The key of the value to get or set. The value associated with the specified key. If the specified key is not found, a get operation throws a , and a set operation creates a new element with the specified key. is . The property is retrieved and does not exist in the collection.
Start: unit -> unitEnables the listener to start listening to instruments measurement recording.
_: QueueCaptureRead: QueueCapture -> string * string -> int64 optioninstrument: stringClear: unit -> unitRemoves all keys and values from the .
RecordObservableInstruments: unit -> unitCalls all Observable instruments that the listener is listening to, and calls with every collected measurement.
TryGetValue: (string * string) * byref<int64> -> boolGets the value associated with the specified key. The key of the value to get. When this method returns, contains the value associated with the specified key, if the key is found; otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. if the contains an element with the specified key; otherwise, . is .
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
NoneThe representation of "No value"
System.IDisposableProvides a mechanism for releasing unmanaged resources.
Dispose: QueueCapture -> unit -> unitDispose: unit -> unitDisposes the listeners which will stop it from listening to any instrument.
observeQueue: QueueCapture -> Flow<unit,Never,(int64 option list * int64 option)>capture: QueueCaptureAxial.Flow`3unitThe 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.
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
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.
flow: FlowBuilderThe universal flow { } computation expression.
readings: int64 option listdisplay: Queue<int>Axial.Queue`1An asynchronous FIFO queue for handing values between fibers, with an overflow strategy chosen at creation. Create one with Queue.make or a shorthand such as Queue.bounded, offer with Queue.offer, and take with the Dequeue functions. A queue is meant for one logical consumer; use Hub when zero or many consumers each need every value. The type of the queued values.
intAn abbreviation for the CLI type . Basic Types
Axial.QueueModuleCreates queues and offers values to them. Take values, inspect, and shut a queue down with the Dequeue functions.
sliding: int -> Flow<'env,'error,Queue<'a>>Creates a queue whose offer evicts the oldest value when full and returns true. Shorthand for Queue.make (QueueStrategy.Sliding capacity).
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 }
offerAll: 'a seq -> Queue<'a> -> Flow<'env,'error,bool>Adds values in order according to the queue's strategy. true when every value was accepted. A dropping queue accepts values while it has room and returns false if it discarded any. A bounded queue suspends as needed; values from other producers may be interleaved between the batch's values, but the batch's own order is kept. If the flow is interrupted part way, the values already accepted stay in the queue. flow { let! (samples: Queue<float>) = Queue.unbounded () do! samples |> Queue.offerAll [ 1.0; 2.0; 3.0 ] |> Flow.ignore return! samples |> Dequeue.takeUpTo 500 }
(..): ^T -> ^T -> ^T seqThe standard overloaded range operator, e.g. [n..m] for lists, seq {n..m} for sequences The start value of the range. The end value of the range. The sequence spanning the range. [1..4] // Evaluates to [1; 2; 3; 4] [1.5..4.4] // Evaluates to [1.5; 2.5; 3.5] ['a'..'d'] // Evaluates to ['a'; 'b'; 'c'; 'd'] [|1..4|] // Evaluates to an array [|1; 2; 3; 4|] { 1..4 } // Evaluates to a sequence [1; 2; 3; 4])
Axial.Flowignore: Flow<'env,'error,'value> -> Flow<'env,'error,unit>Maps the successful value of a synchronous flow to unit. The source flow. A flow that succeeds with unit instead of the original value. let flow = Flow.succeed 42 |> Flow.ignore
Read: string * string -> int64 optionscoped: 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.
afterScope: int64 optionuse capture = new QueueCapture()
observeQueue capture
|> Flow.run ()
|> shouldEqual
(Exit.Success([ Some 2L; Some 2L; Some 5L; Some 3L; Some 0L ], None)
: Exit<(int64 option list) * int64 option, Never>)
capture: QueueCaptureFsLiveDocsGeneratedPage1_F39FDF9DAE04.QueueCaptureobserveQueue: QueueCapture -> Flow<unit,Never,(int64 option list * int64 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.
NoneThe representation of "No value"
int64An 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
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.
Axial.NeverRepresents an error channel that cannot occur.
The sliding queue has capacity 2, accepted all five offers, evicted the first three, and ended with two values
buffered; after its scope closed, axial.queue.size no longer reports the queue (None).
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`3Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
FsLiveDocsGeneratedPage1_F39FDF9DAE04.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.
registry.DumpAt and FiberDumpTelemetry.record both take an IClock to timestamp the snapshot. Inside a workflow,
read the environment's injected clock (Flow.envWith (fun (env: ClockEnvironment) -> env.Clock)) so tests and
repeated runs get a deterministic timestamp. At the edge there is no environment to read: call
registry.DumpAt(Clock.live) from a protected diagnostics endpoint, write it during a stuck shutdown, or attach it
to the current trace, all with the live clock:
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. The event records which fibers 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.
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.

