Skip to content

Observability

Mikołaj Milewski edited this page Oct 27, 2026 · 1 revision

Observability

Stateflows exposes four kinds of extension points for inspecting or influencing behavior execution and registration:

Extension point When it runs Main purpose
Observer Around an internal behavior operation Measure, log, or otherwise observe the operation while allowing it to proceed
Interceptor Around incoming event processing Apply cross-cutting policy, short-circuit processing, or adjust its status
Exception handler After behavior code throws Inspect and acknowledge an exception; this does not recover the failed execution
Visitor While a registered behavior definition is traversed Inspect its structure and build derived functionality

These APIs exist for State Machines, Activities, Actions, and Entities, though their hooks and convenience base classes differ by behavior type.

Registering observers, interceptors, and exception handlers

Each behavior family has registration methods for global hooks and hooks scoped to one behavior definition. Global hooks apply to every behavior of that family; local hooks apply only to the definition where they are registered. For example:

using Stateflows;

services.AddStateflows(stateflows => stateflows
    .AddStateMachines(machines => machines
        .AddObserver<AuditObserver>()
        .AddStateMachine<Order>(
            buildAction: machine => machine
                .AddObserver<OrderObserver>()
                .AddInterceptor<OrderEventInterceptor>()
                .AddExceptionHandler<OrderExceptionHandler>())));

The family-specific builders follow the same pattern: AddActivities, AddActions, and AddEntities accept global hooks, while AddActivity, AddAction, and AddEntity configure hooks on an individual definition. AddObserver<T>, AddInterceptor<T>, and AddExceptionHandler<T> are available where that hook type is supported. The registration APIs also accept factories; State Machine and Entity factories receive the service provider and a behavior action context, while Activity and Action factories receive the service provider.

For observers and interceptors, global hooks wrap local hooks. Within either group, the first registered hook is the outermost wrapper. Exception-handler order is not identical across the families: State Machine, Activity, and Action definitions append their local handlers before global handlers, whereas Entity processing runs global handlers before Entity-local handlers. Handlers are all awaited sequentially; a handler returning true acknowledges the exception but does not change the failed outcome.

Typed registrations create hook instances through Stateflows' model-element activator, allowing constructor dependencies to be supplied by the service provider. Use a factory when construction needs explicit logic or execution context.

Observers

An observer wraps one of the behavior's internal operations and receives a next continuation for the operation being observed. Use try/finally for measurements or cleanup that must also happen when the operation fails:

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Stateflows.StateMachines;
using Stateflows.StateMachines.Context.Interfaces;

public sealed class StateEntryTimingObserver : StateMachineObserver
{
    private readonly ILogger<StateEntryTimingObserver> logger;

    public StateEntryTimingObserver(ILogger<StateEntryTimingObserver> logger)
        => this.logger = logger;

    public override async Task OnStateEntryAsync(
        IStateActionContext context,
        Func<Task> next)
    {
        var timer = Stopwatch.StartNew();
        try
        {
            await next();
        }
        finally
        {
            logger.LogInformation("State entry took {Elapsed}", timer.Elapsed);
        }
    }
}

Observers are observational hooks, not alternate implementations of the operation:

  • Call and await next exactly once. Skipping it, invoking it more than once, or returning before it finishes fails execution.
  • For operations that produce a result, the result is available through the continuation, but must not be replaced. Do not alter behavior state, Activity tokens, or returned values from an observer.
  • Do not catch and suppress a failure from next. If failure-safe work is needed, put it in finally and allow the failure to propagate.

The observer callbacks cover different internal operations in each family:

Behavior Observer callbacks
State Machine Initialization and finalization; State initialization, finalization, entry, and exit; transition guards and effects
Activity Initialization and finalization; node initialization, finalization, activation, and execution; flow activation, guards, and transformations
Action Action initialization and finalization
Entity Mutation execution; computed-field evaluation; projection evaluation

Entity observers run when a computed field or projection is evaluated, including recalculation. They do not wrap reads of cached projection values. The Entity observer context exposes operation-specific information for inspection; it is not permission to mutate Entity state or substitute a result.

Interceptors

An interceptor wraps processing of an incoming event, including initialization events. Its continuation represents the rest of event processing. Unlike an observer, an interceptor is allowed not to invoke next: it can short-circuit by returning an EventStatus. It can also inspect and replace the resulting status, or catch an exception from the continuation when that is an intentional policy.

using System;
using System.Threading.Tasks;
using Stateflows.Common;
using Stateflows.StateMachines;
using Stateflows.StateMachines.Context.Interfaces;

public sealed class EventAuditInterceptor : StateMachineInterceptor
{
    public override async Task<EventStatus> ProcessEventAsync<TEvent>(
        IEventContext<TEvent> context,
        Func<Task<EventStatus>> next)
    {
        var status = await next();
        return status;
    }
}

Call and await the continuation at most once. Repeated calls are rejected, and returning while the continuation is still running is an invalid interceptor implementation. Returning without calling it is valid for a deliberate short-circuit. An interceptor that catches a downstream exception is responsible for deciding what EventStatus to return.

Exception handlers

Exception handlers are notified when an operation in behavior code throws. They receive the relevant context and exception and return true to acknowledge it. Acknowledgement is diagnostic/handling information, not recovery: the interrupted operation still fails, no success result is synthesized, and the failed behavior execution does not commit its pending state changes. Entity execution rolls back. Exception handlers should therefore report, log, or perform other appropriate failure-side work—not assume they can resume the interrupted operation.

The hooks are specific to each behavior:

Behavior Exception sources exposed to handlers
State Machine State Machine initialization/finalization; transition guards/effects; State initialization/finalization/entry/exit
Activity Activity initialization/finalization; node initialization/finalization/execution; flow guards/transformations
Action Action delegate execution
Entity Entity initialization; mutations; computed fields; projections

Each registered handler is awaited in sequence. A true result acknowledges the exception, but does not prevent later handlers from running. A failure inside a handler is recorded/logged by the framework and does not convert the original failed operation into success. IDE break-on-thrown settings are independent: a debugger may stop at the original throw before handlers run.

Activity exception-handler nodes are a separate modeling feature: a matching graph handler can take an alternate path and handle the Activity failure. IActivityExceptionHandler is a runtime hook; it does not recover an Activity execution. See Activity Exception Handling for graph-handler matching, scope, and examples.

Visitors

Visitors inspect the registered definition, not a live execution. The registration interfaces expose VisitStateMachinesAsync, VisitActivitiesAsync, VisitActionsAsync, and VisitEntitiesAsync (as well as methods to visit an individual named/versioned definition). A visitor receives asynchronous callbacks as the framework traverses the model:

Visitor Definition details exposed
IStateMachineVisitor Vertices, transitions, attached behaviors, initializers/finalizers, and accepted/custom events
IActivityVisitor Nodes, control/data flows, event nodes, initializers/finalizers, and custom events
IActionVisitor Action type and consumed/custom events
IEntityVisitor Entity type, fields and access, initializers, mutations, and projections with publish scopes

Use visitors to build derived functionality from a model's structure rather than instrumenting event execution. For example, the Minimal APIs extension visits registered behavior definitions to discover accepted events and generate endpoints.

Convenience base classes

Several public abstract base classes supply default implementations, so a derived class can override only the callbacks it needs:

Family Observer Interceptor Exception handler Visitor
State Machine StateMachineObserver — calls next StateMachineInterceptor — calls next StateMachineExceptionHandler — returns false StateMachineVisitor — completes callbacks as no-ops
Activity ActivityObserver — calls next ActivityInterceptor — calls next ActivityExceptionHandler — returns false ActivityVisitor — completes callbacks as no-ops
Action ActionObserver — calls next ActionInterceptor — calls next No base class; implement IActionExceptionHandler ActionVisitor — completes callbacks as no-ops
Entity No base class; implement IEntityObserver No base class; implement IEntityInterceptor No base class; implement IEntityExceptionHandler EntityVisitor — completes callbacks as no-ops

The default observer implementations preserve the required continuation behavior; the interceptor defaults transparently pass through to event processing. The exception-handler defaults do not acknowledge exceptions. Visitor defaults do nothing. If no base class is listed, implement the interface methods directly.

Related instrumentation

The OpenTelemetry extension uses Stateflows observers and interceptors to emit traces and metrics. Visitors are used by structural integrations such as Minimal APIs; they are not runtime telemetry hooks.

Overview
Installation
Runtimes
   Orleans runtime
Concepts
   Events and Structural Events
   Messaging Patterns
   Validation
   Scheduling
   Observability
Behaviors
   Behavior Values
   Dependency Injection
   Behavioral Groups
   State Machines
       Concepts
           State Identity
           In-State Event Deferrals
           Evaluation of Transitions
           Exception Handling
       Building blocks
           Regions
           States
               State
               Composite State
               Orthogonal State
               Final State
           Pseudostates
               Choice
               Junction
               Fork
               Join
               History
           Transitions
               Transition
               Default Transition
               Internal Transition
   Activities
       Concepts
           Exception Handling
           Implicit Fork and Join
           Tokens
       Building blocks
           Nodes
               Action Node
               Decision Node
               Merge Node
               Initial Node
               Final Node
               Input Node
               Output Node
               Fork Node
               Join Node
               Accept Event Action Node
               Send Event Action Node
               Publish Event Action Node
               Time Event Action Node
               Data Store Node
               Structured Activity Node
               Iterative Activity Node
               Parallel Activity Node
           Flows
               Data Flow
               Control Flow
   Actions
   Entities
       Fields
       Mutations
       Projections
Extensions
   ClearScript
   FluentValidation
   JSON definitions
   Minimal APIs
   OneOf
   OpenTelemetry
   PlantUML
   Scheduling

Clone this wiki locally