Generated Python clients should emit a shared tracing module + auto-span every RPC call #30
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_rpc#30
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Companion to #29
#29 surfaces service methods in generated Python clients (currently only rootobject CRUD is emitted). This issue is about a second capability: generating the tracing helpers that flow authors need.
Important revision (after discussion): the original draft of this issue proposed auto-wrapping every generated client method in a span. That was the wrong answer — it couples tracing into every client, pollutes standalone-script use, and leaks observability concerns into plain RPC code. The cleaner design, reflected below, is that clients stay pure and tracing is a wrapping concern applied explicitly via
instrument(client).Problem
Hero is moving toward Python flows as the primary authoring artifact (see hero_logic
docs/flows-as-python.md, hero_logic#10). Flow authors need:@flow(...)that registers a function as a flow with declared inputswith flow.step(name):that marks a named stepflow.Failedfor typed failuresasynciotasks and across sub-flow spawnsinstrument(client)helper for flow authors who want per-RPC spansNone of this exists today in generated Python clients. Every hero_logic template re-invents JSON-RPC plumbing by hand.
What to generate
Per repo that calls
hero_rpcclient codegen, emit two Python artifacts:1. The service client (extended, per #29)
One per service, as today. Pure RPC client — no tracing code. Includes every service method (not just CRUD). Zero runtime imports beyond stdlib. Works identically whether called from a standalone script, a Rhai-like sandbox, a test, or a
@flow-decorated Python function.2. A shared
hero_tracing.pymodule (new — identical bytes per repo)Boilerplate generator output; same source code every time. Imports stdlib only. Contains:
The three tiers of use
Tier 1 — standalone script (plain RPC, no tracing)
No
hero_tracingimport. No sockets. No spans. Just RPC.Tier 2 — flow with step-level tracing
One span per
with flow.stepblock. RPC calls inside are opaque to the viewer. Fine for most flows.Tier 3 — flow with step + RPC tracing (opt-in)
One line of opt-in. Generic
instrument()works on any client.Wire protocol — span events (JSONL over UDS)
Stable, forward-compatible. No schema version needed; add fields optionally.
Cross-process span propagation
When a parent flow calls
HeroLogicClient.play_run_async(flow_name='X', inputs=...), the parent flow author is expected toinstrument()the client if they want the spawn site traced. If instrumented:flow.current_span.idfrom contextvarsX-Hero-Parent-Spanheader with the JSON-RPC request@flowroot span uses it asparent_idResult: nested tree in the viewer. Same mechanism whether the child is another Python flow or a generated client call.
Scope / definition of done
hero_tracing.pyemitted by the Python generator, identical content per repoinstrument(client)helper wraps any client with per-method spans via__getattr__proxyHERO_FLOW_SPAN_SOCKis unset, all tracing helpers are silent no-ops (no stderr, no stdout)asyncio.create_task/gatherX-Hero-Parent-Spanheader in outgoing RPCwith flow.step(...)blocks and 1instrumented client making 6 RPC calls produces a 9-span tree rooted at the@flowrootDownstream
hero_logic gets:
Spanrootobject (parent_id, start/end, name, status, tags, logs)Play.spans: [Span](replacesnode_runs)HERO_FLOW_SPAN_SOCKper-PlayThese land in hero_logic#11.
Related
docs/flows-as-python.mdClosing — scope moves to hero_logic.
After further design review: the tracing helpers (
@flow,flow.step,flow.Failed,instrument(), socket plumbing, contextvars propagation) are service-agnostic generic code. Having hero_rpc emit them alongside every service's client would mean identical-bytes duplication across every Hero repo.The cleaner architecture: hero_tracing.py is a hand-written module in hero_logic (the service that owns the flow concept), embedded in the hero_logic_server binary, staged at
~/.hero/var/flows/sdk/hero_tracing.pyon startup, and PYTHONPATH-injected into flow subprocesses by the executor.hero_rpc stays focused on its actual job: typed RPC client codegen. Pure clients, per service, that work standalone without any tracing knowledge. Issue #29 is the only hero_rpc work needed.
Scope moved to hero_logic#11 (Foundation story), which now includes:
hero_tracing.py(generic module)