Skip to content

feat(workflow): trace step attempts with OpenTelemetry - #7635

Merged
FarhanAliRaza merged 5 commits into
mainfrom
claude/project-thread-5qo1s9
Oct 28, 2026
Merged

FarhanAliRaza merged 5 commits into
mainfrom
claude/project-thread-5qo1s9

Conversation

@masenf

@masenf masenf commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Requested by Masen · project thread

First PR for ENG-13483 (plan items A, B, C).

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update (READMEs of both packages updated here)

What and why

reflex-workflow had no OpenTelemetry code. Checked on main with the SDK, an in-memory exporter and ReflexInstrumentor().instrument():

  • a run started from a traced event handler produced steps that were invisible to the trace; anything a step traced itself started an unrelated trace;
  • every step task inherited whatever context was current when run_workflows was entered (asyncio.create_task copies contextvars for the runner loop and each step), so a worker entered inside a span parented every later step under that long-ended span.

This PR:

  • reflex_workflow.otel, on the reflex_base.otel pattern: an enabled flag the engine checks at each trace point, opentelemetry imports confined to enable(). reflex-workflow keeps its SQLAlchemy-only dependency list and imports nothing from opentelemetry until instrumented (a test pins that).
  • ReflexInstrumentor enables and disables the workflow trace points alongside the framework's when reflex_workflow.otel is importable, so one instrument() call, or opentelemetry-instrument, covers both.
  • One CONSUMER span per step attempt, named <table>.<step>, started from an explicit empty parent context so it is a trace root whatever the worker inherited. It links to the span that scheduled it rather than nesting under it: a step may run days after wake_in, on another worker after a lease expiry, or as one of many fanned-out children. Attributes: reflex.workflow.table, .step, .attempt, .lane, .outcome; error.type and the recorded exception on a failed attempt (status ERROR for retry/failed). Step arguments and row columns are never recorded.
  • Producer context carried in the row without a schema change. Call.encode() stamps the current W3C traceparent into the payload under a reserved top-level key of next_args, next to the claim's existing event_key, so start, run, deliver, chaining, schedules and fan_out all carry it. execute() strips it before the step sees its arguments; a retry re-stamps the failed attempt's span, so retries chain attempt → attempt.

Not in this PR (later items on the ticket): suppressing the engine's bookkeeping queries from DB instrumentations, and worker metrics.

Tests

  • tests/units/reflex_workflow/test_otel.py (no Postgres): disabled path imports no opentelemetry and changes nothing; stamp/encode carry the current span; the step span is a root even when entered under another span, with the link, kind and attributes; unusable traceparent values yield no link; failed attempts record the exception and status.
  • test_runner.py::test_steps_are_traced_as_linked_root_spans (Postgres): a chain started under a handler span links first → handler and second → first; a flaky step's three attempts are retry, retry, ok, each linked to the previous attempt; next_args is clean afterwards.
  • tests/units/reflex_otel/test_init.py: the instrumentor toggles the workflow trace points.

Ran locally: full tests/units/reflex_workflow against Postgres 16 plus tests/units/reflex_otel and tests/units/reflex_base/test_otel.py (1122 passed), ruff check/format, pyright on the touched packages and tests, codespell.


Generated by Claude Code

View guided diff Turn on auto-fix

reflex-workflow had no OpenTelemetry code: a run started from a traced
event handler produced steps invisible to the trace, anything a step
traced itself started an unrelated trace, and every step task inherited
whatever context was current when run_workflows was entered, so a worker
started inside a span parented every later step under that long-ended
span.

Add reflex_workflow.otel on the reflex_base.otel pattern: an `enabled`
flag the engine checks at each trace point, with the opentelemetry
imports confined to enable(), so the package keeps its SQLAlchemy-only
dependency list and imports nothing from opentelemetry until
instrumented. ReflexInstrumentor enables and disables it alongside the
framework trace points when reflex-workflow is importable.

Each step attempt gets a CONSUMER span named `<table>.<step>`, started
from an explicit empty parent context so it is a trace root whatever the
worker inherited. It links to the span that scheduled it rather than
nesting under it, since a step may run days after wake_in, on another
worker after a lease expiry, or as one of many fanned-out children.
Call.encode() stamps the current W3C traceparent into the payload under
a reserved key of next_args, next to the claim's event key, so start,
run, deliver, chaining, schedules and fan-out all carry it without a
schema change; execute() strips it before the step sees its arguments,
and a retry re-stamps the failed attempt's span so retries chain.
Attributes are the table, step, attempt number, lane and outcome, plus
error.type and the recorded exception on a failed attempt. Arguments
and row columns are never recorded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SjgvPv7AqSycFLeXwSfB6D
@masenf
masenf requested a review from a team as a code owner October 22, 2026 17:23
@masenf masenf self-assigned this Oct 22, 2026
@linear-code

linear-code Bot commented Oct 22, 2026

Copy link
Copy Markdown

ENG-13483

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 11 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/reflex-otel/README.md Resolved
@greptile-apps

greptile-apps Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issues remain.

Summary

Adds OpenTelemetry spans for workflow step attempts.

  • Workflow steps create linked spans for each attempt.
  • The Reflex instrumentor also controls workflow tracing.

No new actionable issues were found. The previous unnumbered finding is fixed: wake_on_schedule records the exception before converting it to text, and the regression test checks the exported error details.

Reviews (3) · Last reviewed commit: "refactor(otel): tidy workflow trace-poin..." · Reviewed by Greptile

wake_on_schedule turns the exception into the row's last_error, so the
span ended as failed with no exception or error.type on it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SjgvPv7AqSycFLeXwSfB6D
@codspeed

codspeed Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 10 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 148 untouched benchmarks


Comparing claude/project-thread-5qo1s9 (e08041b) with main (17370c2)

Open in CodSpeed

@masenf masenf added the on deck PRs lined up to review / merge next label Oct 27, 2026
Merge the doubled TYPE_CHECKING block in reflex_workflow.otel, drop the
stale/missing outcomes from record_outcome's docs (execute returns them
before the span opens), make run_attempt's arguments keyword-only, and
toggle the workflow trace points through one helper so _instrument keeps
its complexity.
@FarhanAliRaza
FarhanAliRaza merged commit c48a696 into main Oct 28, 2026
169 checks passed
@FarhanAliRaza
FarhanAliRaza deleted the claude/project-thread-5qo1s9 branch October 28, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

on deck PRs lined up to review / merge next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants