From 830f8370be09d97e014c964d46b24b3b5a2fef24 Mon Sep 17 00:00:00 2001 From: AymenFJA Date: Fri, 8 May 2026 03:06:09 +0000 Subject: [PATCH 01/10] refactor(telemetry): wire workflow-aware OTel span hierarchy via ContextVar propagation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add _workflow_id_ctx module-level ContextVar — asyncio copies it into every create_task/gather branch automatically, so concurrent workflows remain isolated without any explicit passing - Add workflow_scope() async context manager — sets the ContextVar, opens an OTel workflow span via span_scope() (or nullcontext() when telemetry is off), and yields the workflow_id; task spans created inside become structural OTel children - Add execute_block() span scope — wraps each block execution in an OTel block span using its uid as asyncflow.workflow_id; removes dead run_in_executor branch (all block functions are async) - Register a span enricher on TelemetryManager at start_telemetry() — copies asyncflow.workflow_id from event attributes into the OTel task span at creation time, decoupling RHAPSODY from this domain attribute - Propagate workflow_id through all _emit() call sites (TaskCreated, TaskStarted, TaskCompleted, TaskFailed, TaskCanceled, TaskQueued, TaskSubmitted, asyncflow.TaskResolved) by stamping comp_desc[workflow_id] at registration time from the active ContextVar - Add workflow_id kwarg to _emit() — injects asyncflow.workflow_id into event attributes when present, leaving the existing attribute dict untouched otherwise - Replace contextmanager + custom _null_context() with stdlib nullcontext from contextlib --- src/radical/asyncflow/workflow_manager.py | 81 +++++++++++++++++++++-- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/src/radical/asyncflow/workflow_manager.py b/src/radical/asyncflow/workflow_manager.py index 8ccf956..c18ee5d 100644 --- a/src/radical/asyncflow/workflow_manager.py +++ b/src/radical/asyncflow/workflow_manager.py @@ -10,6 +10,8 @@ import time import uuid from collections import defaultdict, deque +from contextlib import asynccontextmanager, nullcontext +from contextvars import ContextVar from functools import wraps from itertools import count from pathlib import Path @@ -33,6 +35,12 @@ logger = logging.getLogger(__name__) +# Per-coroutine workflow scope: asyncio copies this context into every create_task/gather +# branch, so concurrent workflows remain isolated from each other automatically. +_workflow_id_ctx: ContextVar[str | None] = ContextVar( + "asyncflow_workflow_id", default=None +) + class WorkflowEngine: """An asynchronous workflow manager that uses asyncio event loops and coroutines to @@ -177,6 +185,15 @@ async def start_telemetry( await self._telemetry.start() + self._telemetry.register_span_enricher( + lambda event: ( + {"asyncflow.workflow_id": event.attributes["asyncflow.workflow_id"]} + if isinstance(event.attributes, dict) + and "asyncflow.workflow_id" in event.attributes + else {} + ) + ) + from rhapsody.telemetry.events import ( # noqa: PLC0415 TaskCanceled, TaskCompleted, @@ -208,7 +225,13 @@ async def start_telemetry( return self._telemetry def _emit( - self, event_name: str, *, task_id: str, backend: str = None, **kwargs + self, + event_name: str, + *, + task_id: str, + backend: str = None, + workflow_id: str | None = None, + **kwargs, ) -> None: """Emit a telemetry event by name. @@ -218,6 +241,8 @@ def _emit( return if "event_time" not in kwargs: kwargs["event_time"] = time.time() + if workflow_id is not None: + kwargs.setdefault("attributes", {})["asyncflow.workflow_id"] = workflow_id self._telemetry.emit( self._tel_make_event( self._tel_events[event_name], @@ -228,6 +253,33 @@ def _emit( ) ) + @asynccontextmanager + async def workflow_scope(self, workflow_id: str | None = None): + """Tag all tasks registered inside this scope with a shared workflow_id. + + Also creates an OTel workflow span (when telemetry is active) so that task spans + registered inside become structural children in the trace hierarchy. + + Args: + workflow_id: Explicit id for this workflow instance. + If omitted, a short UUID is generated automatically. + + Yields: + The workflow_id string in use. + """ + wid = workflow_id or f"wf-{uuid.uuid4().hex[:8]}" + token = _workflow_id_ctx.set(wid) + try: + scope = ( + self._telemetry.span_scope("workflow", {"asyncflow.workflow_id": wid}) + if self._telemetry is not None + else nullcontext() + ) + with scope: + yield wid + finally: + _workflow_id_ctx.reset(token) + def _setup_signal_handlers(self): """Register signal handlers for graceful shutdown on SIGHUP, SIGTERM, and SIGINT.""" @@ -629,6 +681,7 @@ def _register_component( # make sure not to specify both func and executable at the same time comp_desc["name"] = comp_desc["function"].__name__ comp_desc["uid"] = self._assign_uid(prefix=comp_type) + comp_desc["workflow_id"] = _workflow_id_ctx.get() if task_type == EXECUTABLE: comp_desc[FUNCTION] = None # Clear function since we're using executable @@ -691,6 +744,7 @@ def _register_component( self._emit( "TaskCreated", task_id=comp_desc["uid"], + workflow_id=comp_desc.get("workflow_id"), attributes={ "executable": comp_desc["_tel_executable"], "task_type": comp_desc["_tel_task_type"], @@ -704,6 +758,7 @@ def _register_component( self._emit( "asyncflow.TaskResolved", task_id=comp_desc["uid"], + workflow_id=comp_desc.get("workflow_id"), attributes={ "executable": comp_desc["_tel_executable"], "task_type": comp_desc["_tel_task_type"], @@ -865,6 +920,7 @@ def _notify_dependents(self, comp_uid: str): self._emit( "asyncflow.TaskResolved", task_id=dependent_uid, + workflow_id=dep_desc.get("workflow_id"), attributes={ "executable": dep_desc["_tel_executable"], "task_type": dep_desc["_tel_task_type"], @@ -1108,6 +1164,7 @@ async def run(self): "TaskQueued", task_id=comp_uid, backend=comp_desc.get("target_backend"), + workflow_id=comp_desc.get("workflow_id"), attributes={ "executable": comp_desc["_tel_executable"], "task_type": comp_desc["_tel_task_type"], @@ -1216,6 +1273,7 @@ async def submit(self, objects: list) -> None: "TaskSubmitted", task_id=t["uid"], backend=t.get("target_backend"), + workflow_id=t.get("workflow_id"), event_time=now, attributes={ "executable": t["_tel_executable"], @@ -1304,18 +1362,25 @@ async def execute_block( None """ + block_uid = block_fut.block["uid"] + token = _workflow_id_ctx.set(block_uid) try: - if asyncio.iscoroutinefunction(func): + scope = ( + self._telemetry.span_scope( + "block", {"asyncflow.workflow_id": block_uid} + ) + if self._telemetry is not None + else nullcontext() + ) + with scope: result = await func(*args, **kwargs) - else: - # Run sync function in executor - result = await self.loop.run_in_executor(None, func, *args, **kwargs) - if not block_fut.done(): block_fut.set_result(result) except Exception as e: if not block_fut.done(): block_fut.set_exception(e) + finally: + _workflow_id_ctx.reset(token) def handle_task_success(self, task: dict, task_fut: asyncio.Future) -> None: """Handles successful task completion and updates the associated future. @@ -1499,6 +1564,7 @@ def wait_and_set(): self._emit( "TaskCompleted", task_id=uid, + workflow_id=task_dct.get("workflow_id"), event_time=now, duration_seconds=now - start, attributes=tel_attrs, @@ -1514,6 +1580,7 @@ def wait_and_set(): self._emit( "TaskStarted", task_id=uid, + workflow_id=task_dct.get("workflow_id"), event_time=now, attributes=tel_attrs, ) @@ -1528,6 +1595,7 @@ def wait_and_set(): self._emit( "TaskCanceled", task_id=uid, + workflow_id=task_dct.get("workflow_id"), event_time=now, duration_seconds=now - start, attributes=tel_attrs, @@ -1543,6 +1611,7 @@ def wait_and_set(): self._emit( "TaskFailed", task_id=uid, + workflow_id=task_dct.get("workflow_id"), event_time=now, duration_seconds=now - start, error_type="unknown", From 4cb9ea79b20dedc0c27c9aca630db879282902b0 Mon Sep 17 00:00:00 2001 From: AymenFJA Date: Mon, 11 May 2026 20:36:04 +0000 Subject: [PATCH 02/10] allows user to stream OTEL events/metrics as OTLP to online service --- docs/telemetry.md | 72 ++++++++++++++++++++++- src/radical/asyncflow/workflow_manager.py | 21 +++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/docs/telemetry.md b/docs/telemetry.md index c36581a..5d80f3f 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -21,8 +21,8 @@ backend = await ConcurrentExecutionBackend(ProcessPoolExecutor()) flow = await WorkflowEngine.create(backend) telemetry = await flow.start_telemetry( - resource_poll_interval=5.0, # node CPU/memory/GPU every 5 s - checkpoint_path="./telemetry/", # write a JSONL file (optional) + resource_poll_interval=5.0, # node CPU/memory/GPU every 5 s + checkpoint_path="./telemetry/", # write a JSONL file (optional) ) ``` @@ -34,6 +34,74 @@ await flow.shutdown() # also stops telemetry await telemetry.stop() ``` +### Forwarding to an external backend + +Pass pre-built OTel `SpanProcessor` and/or `MetricReader` instances to forward data to Jaeger, Grafana Tempo, Honeycomb, Prometheus, or any other OTel-compatible backend: + +```python +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader +from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter + +telemetry = await flow.start_telemetry( + span_processors=[BatchSpanProcessor(OTLPSpanExporter())], + metric_readers=[PeriodicExportingMetricReader(OTLPMetricExporter())], +) +``` + +Exporters read `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS`, and `OTEL_SERVICE_NAME` from the environment. See [RHAPSODY Integrations](https://radical-cybertools.github.io/rhapsody/telemetry/integrations/) for the full parameter reference. + +--- + +## Workflow grouping with `workflow_scope()` + +By default all tasks share the same `session_id`. Use `workflow_scope()` to tag every task submitted inside the scope with a `asyncflow.workflow_id` attribute — enabling per-workflow filtering in Jaeger, Tempo, and the JSONL checkpoint: + +```python +async with flow.workflow_scope("etl-run-42"): + raw = ingest("source.csv") + val = validate(raw) + result = await report(val) +``` + +Every span and JSONL event emitted inside the scope carries `asyncflow.workflow_id = "etl-run-42"`. The scope maps directly onto a named OTel span (`name="workflow"`), so the flame graph in Tempo shows `session → workflow(etl-run-42) → task` hierarchy. + +If no `workflow_id` is passed, one is auto-generated (`wf-`). + +```python +# Auto-ID +async with flow.workflow_scope() as wid: + print(wid) # e.g. "wf-3a1b9c2d" +``` + +`@flow.block`-decorated functions are automatically tagged with the block's UID as the `workflow_id` — no explicit `workflow_scope()` needed inside a block body. + +--- + +## OTel span hierarchy + +When telemetry is enabled, AsyncFlow produces a four-level span tree: + +``` +Trace (one per session) +└── session span [SessionStarted … SessionEnded] + │ + ├── workflow span [workflow_scope("etl-run-42")] + │ ├── task span task_id=ingest-001 + │ ├── task span task_id=validate-001 + │ └── task span task_id=report-001 + │ + ├── block span [execute_block("pipeline_block")] + │ ├── task span task_id=preprocess-001 + │ ├── task span task_id=compute-001 + │ └── task span task_id=store-001 + │ + └── task span (ungrouped — submitted outside any scope) +``` + +The `asyncflow.workflow_id` attribute is stamped on every task span and every JSONL lifecycle event inside a scope. This makes per-workflow Gantt views, performance comparison across workflow types, and span filtering all possible without any post-processing join. + --- ## AsyncFlow-specific events diff --git a/src/radical/asyncflow/workflow_manager.py b/src/radical/asyncflow/workflow_manager.py index c18ee5d..6fb9836 100644 --- a/src/radical/asyncflow/workflow_manager.py +++ b/src/radical/asyncflow/workflow_manager.py @@ -156,6 +156,9 @@ async def start_telemetry( resource_poll_interval: float = 5.0, checkpoint_interval: float | None = None, checkpoint_path: str | None = None, + span_processors: list | None = None, + metric_readers: list | None = None, + resource: object | None = None, ) -> Any: """Create and start a RHAPSODY TelemetryManager for this workflow engine. @@ -164,6 +167,21 @@ async def start_telemetry( NoopExecutionBackend which have no adapter), starts the async dispatch loop, and returns the manager. + Args: + resource_poll_interval: Seconds between resource metric polls (default: 5.0). + checkpoint_interval: Seconds between periodic metric+span flushes to disk. + None = no periodic flush (file still written at stop). + checkpoint_path: Directory for the JSONL checkpoint file. + None = no file output. + span_processors: Optional list of OTel SpanProcessor instances added to + RHAPSODY's TracerProvider alongside the internal + SpanBuffer. Callers own exporter construction. + metric_readers: Optional list of OTel MetricReader instances added to + RHAPSODY's MeterProvider alongside InMemoryMetricReader. + resource: Optional ``opentelemetry.sdk.resources.Resource``. + When None, ``Resource.create()`` reads + ``OTEL_SERVICE_NAME`` from the environment automatically. + Returns: The active TelemetryManager instance. """ @@ -173,6 +191,9 @@ async def start_telemetry( session_id=self.uid, checkpoint_interval=checkpoint_interval, checkpoint_path=checkpoint_path, + span_processors=span_processors, + metric_readers=metric_readers, + resource=resource, ) for backend in self._backends.values(): From 077adff5831a675eb16fb8b718f0662e06a9220a Mon Sep 17 00:00:00 2001 From: AymenFJA Date: Mon, 11 May 2026 21:47:57 +0000 Subject: [PATCH 03/10] update changelog --- CHANGELOG.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6606d6..6caa36b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,56 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added +- **`WorkflowEngine.workflow_scope(workflow_id=None)`** — new async context + manager that groups all tasks registered inside under a shared + `asyncflow.workflow_id`. Internally sets `_workflow_id_ctx` (a module-level + `ContextVar`), which asyncio copies into every `create_task` / `gather` + branch automatically so concurrent workflow instances remain isolated. When + telemetry is active, also opens an OTel `"workflow"` span, making all task + spans inside structural children in the trace hierarchy. Auto-generates a + short UUID if `workflow_id` is `None`. + +- **`_workflow_id_ctx` ContextVar** — module-level `ContextVar` (default + `None`) that carries the active workflow ID across asyncio task boundaries + without explicit argument passing. `@flow.block` execution sets it to the + block's UID so tasks inside a block inherit the workflow ID automatically. + +- **`WorkflowEngine.start_telemetry()` new parameters** — `span_processors`, + `metric_readers`, `resource` — forwarded to `TelemetryManager.__init__()`, + mirroring `Session.start_telemetry()`. Enables passing pre-built OTel + exporters (OTLP, Prometheus, Jaeger) without any RHAPSODY code changes. + +- **Span enricher for `asyncflow.workflow_id`** — registered automatically by + `start_telemetry()` via `register_span_enricher()`. Stamps + `asyncflow.workflow_id` onto every task OTel span when the task was created + inside a `workflow_scope()` or `@flow.block`. Enables per-workflow Gantt + views and span filtering in Jaeger / Grafana Tempo. + +- **`_emit()` `workflow_id` kwarg** — when `workflow_id` is set, injects + `asyncflow.workflow_id` into the event's `attributes` dict. All task lifecycle + events emitted by `WorkflowEngine` (TaskCreated, asyncflow.TaskResolved, + TaskSubmitted) carry the active workflow ID. + +- **Example `01-workflow_grouping.py`** — complete rewrite as an HPC Campaign + Manager simulation. Models 4 workflow types with resource tracking and + dependency chains: + - `simulate` (4 tasks, GPU, no deps) — molecular dynamics runs + - `analyze` (4 tasks, GPU, deps=simulate) — post-processing per simulation + - `train` (8 tasks, GPU, no deps) — distributed ML training + - `evaluate` (8 tasks, CPU, deps=train) — lightweight model evaluation + Uses `ResourcePool` (asyncio-queue-based GPU/CPU slot tracking) and emits + `campaign.ResourceAssigned` custom events to record per-instance resource + assignments in the JSONL checkpoint. + +- **`plot_campaign.py`** — new plotting script producing a two-panel Campaign + Manager timeline figure: + - **Top panel**: Gantt chart with rows per workflow instance, bars coloured by + workflow type and labelled with the assigned resource (`gpu:N` / `cpu:N`). + Right-margin annotations show priority / cpu / gpu per type. Right column + renders a config table, scheduler description box, and dependency graph. + - **Bottom panel**: GPU (left axis) and CPU (right axis) utilisation as step + functions over elapsed time, with total-capacity dashed reference lines. + - **`capture_stdio` decorator parameter** — `@flow.executable_task(capture_stdio=True)` redirects stdout/stderr from executable tasks directly to files instead of collecting them in memory. The awaited future resolves to the stdout **file path** (`{work_dir}/{uid}/{task_uid}.stdout`) @@ -25,6 +75,43 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Unit tests for `capture_stdio` field placement, default value, `_work_dir` authority, and end-to-end file I/O with `ConcurrentExecutionBackend`. +### Fixed + +- **Block spans parented correctly** — `execute_block()` now benefits from the + `span_scope()` session-span fallback added in RHAPSODY: block spans that run + in a context with no active OTel span (e.g. spawned from `run()` before + `start_telemetry()` was awaited) now correctly nest under the session root + span instead of floating as unrooted traces. + +- **`execute_block()` dead branch removed** — the `run_in_executor` code path + (used when the wrapped function was sync) is removed. `WorkflowEngine` + enforces a strict async API; all block functions must be `async def`. + +### Changed + +- **`execute_block()` uses `nullcontext`** — the no-telemetry code path uses + stdlib `nullcontext` (Python >= 3.7) instead of the former custom + `_null_context()` helper, which is removed. + +- **`execute_block()` sets `_workflow_id_ctx`** — the block's UID is set as the + active `_workflow_id_ctx` for the duration of block execution so every task + registered inside the block inherits it as `asyncflow.workflow_id` without + requiring an explicit `workflow_scope()` call. + +### Docs + +- **`docs/telemetry.md`**: + - Added "Forwarding to an external backend" section with corrected + `span_processors` / `metric_readers` code examples (replaces the broken + `set_tracer_provider()` pattern). + - Added "`workflow_scope()` context manager" section with usage examples and + auto-ID generation. + - Added "OTel span hierarchy" section showing the four-level + `session -> workflow -> block -> task` tree and explaining how + `asyncflow.workflow_id` propagates to every span attribute and JSONL event. + - Updated `start_telemetry()` signature to include `span_processors`, + `metric_readers`, and `resource` parameters. + ## [0.3.1] - 2026-03-09 ### Fixed From 1edd3feb1faff0cbeb104ec1740fcc100ba245b6 Mon Sep 17 00:00:00 2001 From: AymenFJA Date: Mon, 11 May 2026 22:00:26 +0000 Subject: [PATCH 04/10] fix pre-commit --- src/radical/asyncflow/workflow_manager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/radical/asyncflow/workflow_manager.py b/src/radical/asyncflow/workflow_manager.py index 6fb9836..cb551fa 100644 --- a/src/radical/asyncflow/workflow_manager.py +++ b/src/radical/asyncflow/workflow_manager.py @@ -15,7 +15,7 @@ from functools import wraps from itertools import count from pathlib import Path -from typing import Any, Callable, Optional, Union +from typing import Any, AsyncIterator, Callable, Optional, Union import typeguard @@ -275,7 +275,9 @@ def _emit( ) @asynccontextmanager - async def workflow_scope(self, workflow_id: str | None = None): + async def workflow_scope( + self, workflow_id: str | None = None + ) -> AsyncIterator[str]: """Tag all tasks registered inside this scope with a shared workflow_id. Also creates an OTel workflow span (when telemetry is active) so that task spans From ab52b26f290a83638bdc3ee391c5938c126ee53a Mon Sep 17 00:00:00 2001 From: AymenFJA Date: Tue, 12 May 2026 16:44:37 +0000 Subject: [PATCH 05/10] update docs --- docs/assets/workflow-telemetry-dashboard.png | Bin 0 -> 280368 bytes docs/telemetry.md | 25 ++++++++ examples/telemetry/README.md | 63 ++++++++++++++++--- 3 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 docs/assets/workflow-telemetry-dashboard.png diff --git a/docs/assets/workflow-telemetry-dashboard.png b/docs/assets/workflow-telemetry-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..fd38025f01ff2400f958a8f4cc82f336f6e0d546 GIT binary patch literal 280368 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