Metrics and tracing
Observability for workflows
To instrument a task, pass an OpenTelemetry meter with WithMeter. Without one, the instruments are no-ops.
wf, err := workflow.New("order-fulfillment",
workflow.WithMeter(meter),
workflow.WithLogger(log),
workflow.WithSteps( /* ... */ ),
)Metrics#
| Instrument | Kind | Attributes |
|---|---|---|
francis.workflow.instances.started | counter | workflow |
francis.workflow.instances.terminated | counter | workflow, status |
francis.workflow.instances.running | up-down counter | workflow |
francis.workflow.instance.duration | histogram (s) | workflow, status |
francis.workflow.step.duration | histogram (s) | workflow, step, outcome |
francis.workflow.task.attempts | counter | workflow, step, failed |
francis.workflow.task.transport_failures | counter | workflow, step or method |
francis.workflow.compensations.run | counter | workflow, step |
francis.workflow.compensations.failed | counter | workflow, step |
francis.workflow.instances.suspended | counter | workflow |
francis.workflow.children.started | counter | workflow, child |
francis.workflow.instances.purged | counter | workflow |
francis.workflow.turn.duration | histogram (s) | workflow |
francis.workflow.turns.duplicate_events | counter | workflow, event |
francis.workflow.definition.conflicts | counter | workflow, version |
Pay particular attention to:
francis.workflow.turn.durationis how long the engine spends deciding what an instance does next. Ideally, it should sit in single-digit milliseconds. If it climbs, instances are slower to advance and status reads queue behind it.francis.workflow.turns.duplicate_eventscounts results received more than once. A low, non-zero rate is healthy. A climbing rate means work is repeatedly failing to be handed off, so checktask.transport_failuresand the provider.francis.workflow.task.transport_failurescounts attempts that failed because the result could not be delivered, not because the handler failed. The work probably did happen and is about to happen again, so this is the counter that says your handlers’ idempotency is being exercised for real.
What to alert on#
instances.terminated{status="failed"}climbing.- A terminal
compensation: partialorcompensation: failedoutcome. These are the “money may be stranded” cases. Watchcompensations.failedper workflow, and readGetStatusfor the instance itself. definition.conflictsabove zero, at any rate: two hosts are serving different graphs under one version number. See deploying and versioning .turn.durationregressing past a few milliseconds.
Tracing#
A workflow instance is long-lived and spread across hosts, so it is not one span. Instead:
Startrecords the caller’s trace context on the instance, and it is carried through everything dispatched afterwards.- There is one span per attempt, and one each time the engine advances the instance, tagged with instance ID, workflow, version, step, index, and attempt. Each links back to the original trace context, so you can follow a run from the request that started it or from any single step.
- A child instance’s spans link to its parent’s trace context as well as its own.
Logs#
WithLogger gets you instance and task lifecycle events, every line tagged with the instance ID and, for a task, the step, index, and attempt.
A task’s worker is <instanceID>|<step>|<index>, and a child instance
uses the same shape, so a parent’s ID is a prefix of everything underneath it.