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.HttpClient
open Axial.PlatformService
open Axial.Process
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:
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)
|> ignorecheckout-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:
open Axial.Telemetry
checkout order
|> Activity.traceWithSource applicationActivitySource CheckoutError.describe "checkout.submit"checkout order
|> Activity.traceOn applicationActivitySource "checkout.submit"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
checkout order
|> checkoutTracer.Trace "checkout.submit"application
|> Activity.withTracer checkoutTracer
// deep inside the workflow tree, in code that has no reference to checkoutTracer:
checkout order |> Activity.trace "checkout.submit"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:
checkout order
|> Context.withEndUserId user.Id
|> Activity.traceWithSource applicationActivitySource CheckoutError.describe "checkout.submit"Define typed keys for application attributes:
module CheckoutAttributes =
let tenantId = AttributeKey.string "example.tenant.id"
let retryCount = AttributeKey.int64 "example.checkout.retry_count"
11-observability_01-telemetry__index.md_page.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.
checkout order
|> Context.withAttributes [
Context.attribute CheckoutAttributes.tenantId tenantId
Context.attribute CheckoutAttributes.retryCount 2L
]
|> Activity.traceWithSource applicationActivitySource CheckoutError.describe "checkout.submit"Build a context once when several workflows share the same metadata:
let requestContext =
Context.empty
|> Context.addEndUserId user.Id
|> Context.add (Context.attribute CheckoutAttributes.tenantId tenantId)
application
|> Context.withContext requestContextflow {
let! telemetryContext = Context.current
return exportContext telemetryContext
}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:
application
|> FiberTelemetry.observeapplication
|> FiberTelemetry.observeWithSpansAdd runtime metrics independently:
application
|> FiberMetrics.observe
|> FiberTelemetry.observeCapture 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.observeFlow.forkNamed "outbox-poller" pollOutboxFiberDumpTelemetry.record registryView 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)
|> ignore- 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.
This is the intended feedback loop: 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.

