edda-framework 0.4.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {edda_framework-0.4.0 → edda_framework-0.6.0}/.github/workflows/ci.yml +2 -2
- {edda_framework-0.4.0 → edda_framework-0.6.0}/PKG-INFO +32 -5
- {edda_framework-0.4.0 → edda_framework-0.6.0}/README.md +27 -4
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/core-features/hooks.md +43 -8
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/index.md +22 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/integrations/mcp.md +30 -4
- edda_framework-0.6.0/docs/integrations/opentelemetry.md +179 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/app.py +16 -1
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/hooks.py +11 -11
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/integrations/mcp/decorators.py +101 -5
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/integrations/mcp/server.py +36 -15
- edda_framework-0.6.0/edda/integrations/opentelemetry/__init__.py +39 -0
- edda_framework-0.6.0/edda/integrations/opentelemetry/hooks.py +579 -0
- edda_framework-0.6.0/examples/observability_with_opentelemetry.py +211 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/pyproject.toml +6 -1
- edda_framework-0.6.0/tests/integrations/mcp/test_cancel.py +166 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/integrations/mcp/test_integration.py +2 -2
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/integrations/mcp/test_jsonrpc.py +1 -1
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/integrations/mcp/test_prompts.py +3 -3
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/integrations/mcp/test_server.py +80 -1
- edda_framework-0.6.0/tests/integrations/opentelemetry/__init__.py +1 -0
- edda_framework-0.6.0/tests/integrations/opentelemetry/test_hooks.py +696 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/uv.lock +226 -2
- {edda_framework-0.4.0 → edda_framework-0.6.0}/.github/workflows/docs.yml +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/.github/workflows/release.yml +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/.gitignore +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/.python-version +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/Justfile +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/LICENSE +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/demo_app.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/core-features/durable-execution/replay.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/core-features/events/wait-event.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/core-features/retry.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/core-features/saga-compensation.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/core-features/transactional-outbox.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/core-features/workflows-activities.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/examples/ecommerce.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/examples/events.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/examples/fastapi-integration.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/examples/saga.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/examples/simple.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/getting-started/concepts.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/getting-started/first-workflow.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/getting-started/installation.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/getting-started/quick-start.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/markdown.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/images/conditional-branching-diagram.png +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/images/detail-overview-panel.png +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/images/execution-history-panel.png +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/images/form-generation-example.png +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/images/hybrid-diagram-example.png +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/images/start-workflow-dialog.png +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/images/status-badges-example.png +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/setup.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/docs/viewer-ui/visualization.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/__init__.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/activity.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/compensation.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/context.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/events.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/exceptions.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/integrations/__init__.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/locking.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/outbox/__init__.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/outbox/relayer.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/outbox/transactional.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/pydantic_utils.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/replay.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/retry.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/serialization/__init__.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/serialization/base.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/serialization/json.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/storage/__init__.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/storage/models.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/storage/protocol.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/storage/sqlalchemy_storage.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/viewer_ui/__init__.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/viewer_ui/app.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/viewer_ui/components.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/viewer_ui/data_service.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/visualizer/__init__.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/visualizer/ast_analyzer.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/visualizer/mermaid_generator.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/workflow.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/edda/wsgi.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/__init__.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/cancellable_workflow.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/compensation_workflow.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/event_waiting_app.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/event_waiting_workflow.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/event_waiting_workflow_complete.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/mcp/README.md +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/mcp/order_processing_mcp.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/mcp/prompts_example.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/mcp/remote_server_example.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/mcp/simple_mcp_server.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/observability_with_logfire.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/pydantic_saga.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/retry_example.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/retry_with_compensation.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/simple_workflow.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/typeddict_example.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/examples/with_outbox.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/__init__.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/conftest.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/integrations/__init__.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_activity.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_activity_retry.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_activity_sync.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_app.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_ast_analyzer.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_atomic_wait_event.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_binary_data.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_cloudevents_http_binding.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_compensation.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_concurrent_outbox.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_context.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_ctx_session.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_distributed_event_delivery.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_events.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_lock_race_condition.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_lock_timeout_customization.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_locking.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_multidb_storage.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_outbox.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_pydantic_activity.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_pydantic_enum.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_pydantic_events.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_pydantic_saga.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_pydantic_utils.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_received_event.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_replay.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_retry_policy.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_saga_parameter_extraction.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_serialization.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_skip_locked.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_stale_workflow_recovery.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_storage.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_storage_mysql.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_storage_postgresql.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_transactions.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_viewer_pydantic_form.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_viewer_start_saga.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_wait_timer.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_workflow.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_workflow_auto_register.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/tests/test_workflow_cancellation.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/viewer_app.py +0 -0
- {edda_framework-0.4.0 → edda_framework-0.6.0}/zensical.toml +0 -0
|
@@ -53,8 +53,8 @@ jobs:
|
|
|
53
53
|
- name: Set up Python ${{ matrix.python-version }}
|
|
54
54
|
run: uv python install ${{ matrix.python-version }}
|
|
55
55
|
|
|
56
|
-
- name: Install dependencies (all database drivers)
|
|
57
|
-
run: uv sync --extra dev --extra postgresql --extra mysql
|
|
56
|
+
- name: Install dependencies (all database drivers and opentelemetry)
|
|
57
|
+
run: uv sync --extra dev --extra postgresql --extra mysql --extra opentelemetry
|
|
58
58
|
|
|
59
59
|
- name: Run tests (all databases)
|
|
60
60
|
env:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edda-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Lightweight Durable Execution Framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/i2y/edda
|
|
6
6
|
Project-URL: Documentation, https://github.com/i2y/edda#readme
|
|
@@ -42,6 +42,10 @@ Provides-Extra: mcp
|
|
|
42
42
|
Requires-Dist: mcp>=1.22.0; extra == 'mcp'
|
|
43
43
|
Provides-Extra: mysql
|
|
44
44
|
Requires-Dist: aiomysql>=0.2.0; extra == 'mysql'
|
|
45
|
+
Provides-Extra: opentelemetry
|
|
46
|
+
Requires-Dist: opentelemetry-api>=1.20.0; extra == 'opentelemetry'
|
|
47
|
+
Requires-Dist: opentelemetry-exporter-otlp>=1.20.0; extra == 'opentelemetry'
|
|
48
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'opentelemetry'
|
|
45
49
|
Provides-Extra: postgresql
|
|
46
50
|
Requires-Dist: asyncpg>=0.30.0; extra == 'postgresql'
|
|
47
51
|
Provides-Extra: server
|
|
@@ -91,6 +95,28 @@ Edda excels at orchestrating **long-running workflows** that must survive failur
|
|
|
91
95
|
- **🤖 AI Agent Workflows**: Orchestrate multi-step AI tasks (LLM calls, tool usage, long-running inference)
|
|
92
96
|
- **📡 Event-Driven Workflows**: React to external events with guaranteed delivery and automatic retry
|
|
93
97
|
|
|
98
|
+
### Business Process Automation
|
|
99
|
+
|
|
100
|
+
Edda's waiting functions make it ideal for time-based and event-driven business processes:
|
|
101
|
+
|
|
102
|
+
- **📧 User Onboarding**: Send reminders if users haven't completed setup after N days
|
|
103
|
+
- **🎁 Campaign Processing**: Evaluate conditions and notify winners after campaign ends
|
|
104
|
+
- **💳 Payment Reminders**: Send escalating reminders before payment deadlines
|
|
105
|
+
- **📦 Scheduled Notifications**: Shipping updates, subscription renewals, appointment reminders
|
|
106
|
+
|
|
107
|
+
**Waiting functions**:
|
|
108
|
+
- `wait_timer(duration_seconds)`: Wait for a relative duration
|
|
109
|
+
- `wait_until(until_time)`: Wait until an absolute datetime (e.g., campaign end date)
|
|
110
|
+
- `wait_event(event_type)`: Wait for external events (near real-time response)
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
@workflow
|
|
114
|
+
async def onboarding_reminder(ctx: WorkflowContext, user_id: str):
|
|
115
|
+
await wait_timer(ctx, duration_seconds=3*24*60*60) # Wait 3 days
|
|
116
|
+
if not await check_completed(ctx, user_id):
|
|
117
|
+
await send_reminder(ctx, user_id)
|
|
118
|
+
```
|
|
119
|
+
|
|
94
120
|
**Key benefit**: Workflows **never lose progress** - crashes and restarts are handled automatically through deterministic replay.
|
|
95
121
|
|
|
96
122
|
## Architecture
|
|
@@ -683,7 +709,7 @@ def process_payment(ctx: WorkflowContext, amount: float) -> dict:
|
|
|
683
709
|
@workflow
|
|
684
710
|
async def payment_workflow(ctx: WorkflowContext, order_id: str) -> dict:
|
|
685
711
|
# Workflows still use async (for deterministic replay)
|
|
686
|
-
result = await process_payment(ctx, 99.99
|
|
712
|
+
result = await process_payment(ctx, 99.99)
|
|
687
713
|
return result
|
|
688
714
|
```
|
|
689
715
|
|
|
@@ -722,13 +748,14 @@ if __name__ == "__main__":
|
|
|
722
748
|
|
|
723
749
|
### Auto-Generated Tools
|
|
724
750
|
|
|
725
|
-
Each `@durable_tool` automatically generates **
|
|
751
|
+
Each `@durable_tool` automatically generates **four MCP tools**:
|
|
726
752
|
|
|
727
753
|
1. **Main tool** (`process_order`): Starts the workflow, returns instance ID
|
|
728
|
-
2. **Status tool** (`process_order_status`): Checks workflow progress
|
|
754
|
+
2. **Status tool** (`process_order_status`): Checks workflow progress with completed activity count and suggested poll interval
|
|
729
755
|
3. **Result tool** (`process_order_result`): Gets final result when completed
|
|
756
|
+
4. **Cancel tool** (`process_order_cancel`): Cancels workflow if running or waiting, executes compensation handlers
|
|
730
757
|
|
|
731
|
-
This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
|
|
758
|
+
This enables AI assistants to work with workflows that take minutes, hours, or even days to complete, with full control over the workflow lifecycle.
|
|
732
759
|
|
|
733
760
|
### MCP Prompts
|
|
734
761
|
|
|
@@ -39,6 +39,28 @@ Edda excels at orchestrating **long-running workflows** that must survive failur
|
|
|
39
39
|
- **🤖 AI Agent Workflows**: Orchestrate multi-step AI tasks (LLM calls, tool usage, long-running inference)
|
|
40
40
|
- **📡 Event-Driven Workflows**: React to external events with guaranteed delivery and automatic retry
|
|
41
41
|
|
|
42
|
+
### Business Process Automation
|
|
43
|
+
|
|
44
|
+
Edda's waiting functions make it ideal for time-based and event-driven business processes:
|
|
45
|
+
|
|
46
|
+
- **📧 User Onboarding**: Send reminders if users haven't completed setup after N days
|
|
47
|
+
- **🎁 Campaign Processing**: Evaluate conditions and notify winners after campaign ends
|
|
48
|
+
- **💳 Payment Reminders**: Send escalating reminders before payment deadlines
|
|
49
|
+
- **📦 Scheduled Notifications**: Shipping updates, subscription renewals, appointment reminders
|
|
50
|
+
|
|
51
|
+
**Waiting functions**:
|
|
52
|
+
- `wait_timer(duration_seconds)`: Wait for a relative duration
|
|
53
|
+
- `wait_until(until_time)`: Wait until an absolute datetime (e.g., campaign end date)
|
|
54
|
+
- `wait_event(event_type)`: Wait for external events (near real-time response)
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
@workflow
|
|
58
|
+
async def onboarding_reminder(ctx: WorkflowContext, user_id: str):
|
|
59
|
+
await wait_timer(ctx, duration_seconds=3*24*60*60) # Wait 3 days
|
|
60
|
+
if not await check_completed(ctx, user_id):
|
|
61
|
+
await send_reminder(ctx, user_id)
|
|
62
|
+
```
|
|
63
|
+
|
|
42
64
|
**Key benefit**: Workflows **never lose progress** - crashes and restarts are handled automatically through deterministic replay.
|
|
43
65
|
|
|
44
66
|
## Architecture
|
|
@@ -631,7 +653,7 @@ def process_payment(ctx: WorkflowContext, amount: float) -> dict:
|
|
|
631
653
|
@workflow
|
|
632
654
|
async def payment_workflow(ctx: WorkflowContext, order_id: str) -> dict:
|
|
633
655
|
# Workflows still use async (for deterministic replay)
|
|
634
|
-
result = await process_payment(ctx, 99.99
|
|
656
|
+
result = await process_payment(ctx, 99.99)
|
|
635
657
|
return result
|
|
636
658
|
```
|
|
637
659
|
|
|
@@ -670,13 +692,14 @@ if __name__ == "__main__":
|
|
|
670
692
|
|
|
671
693
|
### Auto-Generated Tools
|
|
672
694
|
|
|
673
|
-
Each `@durable_tool` automatically generates **
|
|
695
|
+
Each `@durable_tool` automatically generates **four MCP tools**:
|
|
674
696
|
|
|
675
697
|
1. **Main tool** (`process_order`): Starts the workflow, returns instance ID
|
|
676
|
-
2. **Status tool** (`process_order_status`): Checks workflow progress
|
|
698
|
+
2. **Status tool** (`process_order_status`): Checks workflow progress with completed activity count and suggested poll interval
|
|
677
699
|
3. **Result tool** (`process_order_result`): Gets final result when completed
|
|
700
|
+
4. **Cancel tool** (`process_order_cancel`): Cancels workflow if running or waiting, executes compensation handlers
|
|
678
701
|
|
|
679
|
-
This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
|
|
702
|
+
This enables AI assistants to work with workflows that take minutes, hours, or even days to complete, with full control over the workflow lifecycle.
|
|
680
703
|
|
|
681
704
|
### MCP Prompts
|
|
682
705
|
|
|
@@ -30,9 +30,10 @@ class LogfireHooks(HooksBase):
|
|
|
30
30
|
instance_id=instance_id,
|
|
31
31
|
workflow_name=workflow_name)
|
|
32
32
|
|
|
33
|
-
async def on_activity_complete(self, instance_id,
|
|
33
|
+
async def on_activity_complete(self, instance_id, activity_id, activity_name, result, cache_hit):
|
|
34
34
|
logfire.info("activity.complete",
|
|
35
35
|
instance_id=instance_id,
|
|
36
|
+
activity_id=activity_id,
|
|
36
37
|
activity_name=activity_name,
|
|
37
38
|
cache_hit=cache_hit
|
|
38
39
|
)
|
|
@@ -69,9 +70,9 @@ The `WorkflowHooks` Protocol defines these methods (all optional):
|
|
|
69
70
|
| `on_workflow_complete` | `instance_id`, `workflow_name`, `result` | Called when a workflow completes successfully |
|
|
70
71
|
| `on_workflow_failed` | `instance_id`, `workflow_name`, `error` | Called when a workflow fails with an exception |
|
|
71
72
|
| `on_workflow_cancelled` | `instance_id`, `workflow_name` | Called when a workflow is cancelled |
|
|
72
|
-
| `on_activity_start` | `instance_id`, `
|
|
73
|
-
| `on_activity_complete` | `instance_id`, `
|
|
74
|
-
| `on_activity_failed` | `instance_id`, `
|
|
73
|
+
| `on_activity_start` | `instance_id`, `activity_id`, `activity_name`, `is_replaying` | Called before an activity executes |
|
|
74
|
+
| `on_activity_complete` | `instance_id`, `activity_id`, `activity_name`, `result`, `cache_hit` | Called after an activity completes successfully |
|
|
75
|
+
| `on_activity_failed` | `instance_id`, `activity_id`, `activity_name`, `error` | Called when an activity fails with an exception |
|
|
75
76
|
| `on_event_sent` | `event_type`, `event_source`, `event_data` | Called when an event is sent (transactional outbox) |
|
|
76
77
|
| `on_event_received` | `instance_id`, `event_type`, `event_data` | Called when a workflow receives an awaited event |
|
|
77
78
|
|
|
@@ -150,8 +151,9 @@ class LogfireHooks(HooksBase):
|
|
|
150
151
|
instance_id=instance_id,
|
|
151
152
|
workflow_name=workflow_name)
|
|
152
153
|
|
|
153
|
-
async def on_activity_complete(self, instance_id,
|
|
154
|
+
async def on_activity_complete(self, instance_id, activity_id, activity_name, result, cache_hit):
|
|
154
155
|
logfire.info("activity.complete",
|
|
156
|
+
activity_id=activity_id,
|
|
155
157
|
activity_name=activity_name,
|
|
156
158
|
cache_hit=cache_hit)
|
|
157
159
|
|
|
@@ -181,7 +183,7 @@ class DatadogHooks(HooksBase):
|
|
|
181
183
|
span.set_tag("workflow.name", workflow_name)
|
|
182
184
|
span.set_tag("instance.id", instance_id)
|
|
183
185
|
|
|
184
|
-
async def on_activity_complete(self, instance_id,
|
|
186
|
+
async def on_activity_complete(self, instance_id, activity_id, activity_name, result, cache_hit):
|
|
185
187
|
statsd.increment('edda.activity.completed',
|
|
186
188
|
tags=[f'activity:{activity_name}', f'cache_hit:{cache_hit}'])
|
|
187
189
|
```
|
|
@@ -199,7 +201,7 @@ class PrometheusHooks(HooksBase):
|
|
|
199
201
|
async def on_workflow_start(self, instance_id, workflow_name, input_data):
|
|
200
202
|
workflow_started.labels(workflow_name=workflow_name).inc()
|
|
201
203
|
|
|
202
|
-
async def on_activity_complete(self, instance_id,
|
|
204
|
+
async def on_activity_complete(self, instance_id, activity_id, activity_name, result, cache_hit):
|
|
203
205
|
activity_executed.labels(activity_name=activity_name, cache_hit=str(cache_hit)).inc()
|
|
204
206
|
```
|
|
205
207
|
|
|
@@ -218,17 +220,50 @@ class SentryHooks(HooksBase):
|
|
|
218
220
|
})
|
|
219
221
|
sentry_sdk.capture_exception(error)
|
|
220
222
|
|
|
221
|
-
async def on_activity_failed(self, instance_id,
|
|
223
|
+
async def on_activity_failed(self, instance_id, activity_id, activity_name, error):
|
|
222
224
|
with sentry_sdk.push_scope() as scope:
|
|
223
225
|
scope.set_context("activity", {
|
|
224
226
|
"instance_id": instance_id,
|
|
227
|
+
"activity_id": activity_id,
|
|
225
228
|
"activity_name": activity_name,
|
|
226
229
|
})
|
|
227
230
|
sentry_sdk.capture_exception(error)
|
|
228
231
|
```
|
|
229
232
|
|
|
233
|
+
### OpenTelemetry (Official Integration)
|
|
234
|
+
|
|
235
|
+
Edda provides an official OpenTelemetry integration with full tracing, optional metrics, and W3C Trace Context propagation.
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from edda import EddaApp
|
|
239
|
+
from edda.integrations.opentelemetry import OpenTelemetryHooks
|
|
240
|
+
|
|
241
|
+
hooks = OpenTelemetryHooks(
|
|
242
|
+
service_name="order-service",
|
|
243
|
+
otlp_endpoint="http://localhost:4317", # Optional
|
|
244
|
+
enable_metrics=True, # Optional
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
app = EddaApp(
|
|
248
|
+
service_name="order-service",
|
|
249
|
+
db_url="sqlite:///workflow.db",
|
|
250
|
+
hooks=hooks,
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Features:**
|
|
255
|
+
|
|
256
|
+
- ✅ Distributed tracing with parent-child span relationships
|
|
257
|
+
- ✅ Optional metrics (counters, histograms)
|
|
258
|
+
- ✅ W3C Trace Context propagation via CloudEvents
|
|
259
|
+
- ✅ Automatic context inheritance from ASGI/WSGI middleware
|
|
260
|
+
|
|
261
|
+
👉 **See [OpenTelemetry Integration](../integrations/opentelemetry.md) for full documentation.**
|
|
262
|
+
|
|
230
263
|
## See Also
|
|
231
264
|
|
|
265
|
+
- **[OpenTelemetry Integration](../integrations/opentelemetry.md)**: Official OpenTelemetry integration with full documentation
|
|
232
266
|
- **[Complete Logfire Example](https://github.com/i2y/edda/blob/main/examples/observability_with_logfire.py)**: Full implementation with multiple workflows
|
|
267
|
+
- **[Complete OpenTelemetry Example](https://github.com/i2y/edda/blob/main/examples/observability_with_opentelemetry.py)**: Full implementation with tracing, optional metrics, and CloudEvents context propagation
|
|
233
268
|
- **[Observability Guide](https://github.com/i2y/edda/blob/main/examples/README_observability.md)**: Detailed guide with more integration examples
|
|
234
269
|
- **[API Reference](https://github.com/i2y/edda/blob/main/edda/hooks.py)**: WorkflowHooks Protocol definition
|
|
@@ -36,6 +36,28 @@ Edda excels at orchestrating **long-running workflows** that must survive failur
|
|
|
36
36
|
- **🤖 AI Agent Workflows**: Orchestrate multi-step AI tasks (LLM calls, tool usage, long-running inference)
|
|
37
37
|
- **📡 Event-Driven Workflows**: React to external events with guaranteed delivery and automatic retry
|
|
38
38
|
|
|
39
|
+
### Business Process Automation
|
|
40
|
+
|
|
41
|
+
Edda's waiting functions make it ideal for time-based and event-driven business processes:
|
|
42
|
+
|
|
43
|
+
- **📧 User Onboarding**: Send reminders if users haven't completed setup after N days
|
|
44
|
+
- **🎁 Campaign Processing**: Evaluate conditions and notify winners after campaign ends
|
|
45
|
+
- **💳 Payment Reminders**: Send escalating reminders before payment deadlines
|
|
46
|
+
- **📦 Scheduled Notifications**: Shipping updates, subscription renewals, appointment reminders
|
|
47
|
+
|
|
48
|
+
**Waiting functions**:
|
|
49
|
+
- `wait_timer(duration_seconds)`: Wait for a relative duration
|
|
50
|
+
- `wait_until(until_time)`: Wait until an absolute datetime (e.g., campaign end date)
|
|
51
|
+
- `wait_event(event_type)`: Wait for external events (near real-time response)
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
@workflow
|
|
55
|
+
async def onboarding_reminder(ctx: WorkflowContext, user_id: str):
|
|
56
|
+
await wait_timer(ctx, duration_seconds=3*24*60*60) # Wait 3 days
|
|
57
|
+
if not await check_completed(ctx, user_id):
|
|
58
|
+
await send_reminder(ctx, user_id)
|
|
59
|
+
```
|
|
60
|
+
|
|
39
61
|
**Key benefit**: Workflows **never lose progress** - crashes and restarts are handled automatically through deterministic replay.
|
|
40
62
|
|
|
41
63
|
## Architecture
|
|
@@ -7,10 +7,11 @@ Edda provides seamless integration with the [Model Context Protocol (MCP)](https
|
|
|
7
7
|
MCP is a standardized protocol for AI tool integration. Edda's MCP integration automatically converts your durable workflows into MCP-compliant tools that:
|
|
8
8
|
|
|
9
9
|
- **Start workflows** and return instance IDs immediately
|
|
10
|
-
- **Check workflow status** to monitor progress
|
|
10
|
+
- **Check workflow status** to monitor progress with completed activity count and suggested poll interval
|
|
11
11
|
- **Retrieve results** when workflows complete
|
|
12
|
+
- **Cancel workflows** if running or waiting, with automatic compensation execution
|
|
12
13
|
|
|
13
|
-
This enables AI assistants to work with long-running processes that may take minutes, hours, or even days to complete.
|
|
14
|
+
This enables AI assistants to work with long-running processes that may take minutes, hours, or even days to complete, with full control over the workflow lifecycle.
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
@@ -97,7 +98,7 @@ Add to your MCP client configuration (e.g., Claude Desktop: `~/Library/Applicati
|
|
|
97
98
|
|
|
98
99
|
## Auto-Generated Tools
|
|
99
100
|
|
|
100
|
-
Each `@durable_tool` automatically generates **
|
|
101
|
+
Each `@durable_tool` automatically generates **four MCP tools**:
|
|
101
102
|
|
|
102
103
|
### 1. Main Tool: Start Workflow
|
|
103
104
|
|
|
@@ -125,12 +126,16 @@ Input: {"instance_id": "abc123..."}
|
|
|
125
126
|
Output: {
|
|
126
127
|
"content": [{
|
|
127
128
|
"type": "text",
|
|
128
|
-
"text": "Workflow Status: running\nCurrent Activity: payment:1\nInstance ID: abc123..."
|
|
129
|
+
"text": "Workflow Status: running\nCurrent Activity: payment:1\nCompleted Activities: 1\nSuggested Poll Interval: 5000ms\nInstance ID: abc123..."
|
|
129
130
|
}],
|
|
130
131
|
"isError": false
|
|
131
132
|
}
|
|
132
133
|
```
|
|
133
134
|
|
|
135
|
+
The status tool provides progress metadata for efficient polling:
|
|
136
|
+
- **Completed Activities**: Number of activities that have finished
|
|
137
|
+
- **Suggested Poll Interval**: Recommended wait time before checking again (5000ms for running, 10000ms for waiting)
|
|
138
|
+
|
|
134
139
|
### 3. Result Tool: Get Final Result
|
|
135
140
|
|
|
136
141
|
```
|
|
@@ -147,6 +152,27 @@ Output: {
|
|
|
147
152
|
}
|
|
148
153
|
```
|
|
149
154
|
|
|
155
|
+
### 4. Cancel Tool: Stop Workflow
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
Tool Name: process_order_cancel
|
|
159
|
+
Description: Cancel process_order workflow (if running or waiting)
|
|
160
|
+
|
|
161
|
+
Input: {"instance_id": "abc123..."}
|
|
162
|
+
Output: {
|
|
163
|
+
"content": [{
|
|
164
|
+
"type": "text",
|
|
165
|
+
"text": "Workflow 'process_order' cancelled successfully.\nInstance ID: abc123...\nCompensations executed.\n\nThe workflow has been stopped and any side effects have been rolled back."
|
|
166
|
+
}],
|
|
167
|
+
"isError": false
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The cancel tool:
|
|
172
|
+
- Only works on workflows with status `running`, `waiting_for_event`, or `waiting_for_timer`
|
|
173
|
+
- Automatically executes SAGA compensation transactions to roll back side effects
|
|
174
|
+
- Returns an error for already completed, failed, or cancelled workflows
|
|
175
|
+
|
|
150
176
|
## Advanced Configuration
|
|
151
177
|
|
|
152
178
|
### Authentication
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# OpenTelemetry Integration
|
|
2
|
+
|
|
3
|
+
Edda provides official integration with [OpenTelemetry](https://opentelemetry.io/), enabling distributed tracing and optional metrics for your durable workflows.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
OpenTelemetry is an industry-standard observability framework. Edda's OpenTelemetry integration provides:
|
|
8
|
+
|
|
9
|
+
- **Distributed Tracing**: Workflow and activity spans with parent-child relationships
|
|
10
|
+
- **Optional Metrics**: Counters for workflow/activity execution, histograms for duration
|
|
11
|
+
- **W3C Trace Context**: Propagate traces across service boundaries via CloudEvents
|
|
12
|
+
- **Automatic Context Inheritance**: Inherit from ASGI/WSGI middleware or CloudEvents headers
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Install Edda with OpenTelemetry support:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install edda-framework[opentelemetry]
|
|
20
|
+
|
|
21
|
+
# Or using uv
|
|
22
|
+
uv add edda-framework --extra opentelemetry
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from edda import EddaApp, workflow, activity, WorkflowContext
|
|
29
|
+
from edda.integrations.opentelemetry import OpenTelemetryHooks
|
|
30
|
+
|
|
31
|
+
# Create hooks (console exporter for development)
|
|
32
|
+
hooks = OpenTelemetryHooks(
|
|
33
|
+
service_name="order-service",
|
|
34
|
+
otlp_endpoint=None, # Use console exporter
|
|
35
|
+
enable_metrics=False,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Or with OTLP exporter for production (Jaeger, Tempo, etc.)
|
|
39
|
+
hooks = OpenTelemetryHooks(
|
|
40
|
+
service_name="order-service",
|
|
41
|
+
otlp_endpoint="http://localhost:4317",
|
|
42
|
+
enable_metrics=True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
app = EddaApp(
|
|
46
|
+
service_name="order-service",
|
|
47
|
+
db_url="sqlite:///workflow.db",
|
|
48
|
+
hooks=hooks,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@activity
|
|
52
|
+
async def reserve_inventory(ctx: WorkflowContext, order_id: str):
|
|
53
|
+
return {"reserved": True}
|
|
54
|
+
|
|
55
|
+
@workflow
|
|
56
|
+
async def order_workflow(ctx: WorkflowContext, order_id: str):
|
|
57
|
+
await reserve_inventory(ctx, order_id)
|
|
58
|
+
return {"status": "completed"}
|
|
59
|
+
|
|
60
|
+
async def main():
|
|
61
|
+
await app.initialize()
|
|
62
|
+
await order_workflow.start(order_id="ORD-123")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Span Hierarchy
|
|
66
|
+
|
|
67
|
+
Edda creates a hierarchical span structure:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
workflow:order_workflow (parent)
|
|
71
|
+
├── activity:reserve_inventory:1 (child)
|
|
72
|
+
├── activity:process_payment:1 (child)
|
|
73
|
+
└── activity:ship_order:1 (child)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Span Attributes
|
|
77
|
+
|
|
78
|
+
**Workflow Spans**:
|
|
79
|
+
- `edda.workflow.instance_id`
|
|
80
|
+
- `edda.workflow.name`
|
|
81
|
+
- `edda.workflow.cancelled` (when cancelled)
|
|
82
|
+
|
|
83
|
+
**Activity Spans**:
|
|
84
|
+
- `edda.activity.id` (e.g., "reserve_inventory:1")
|
|
85
|
+
- `edda.activity.name`
|
|
86
|
+
- `edda.activity.is_replaying`
|
|
87
|
+
- `edda.activity.cache_hit`
|
|
88
|
+
|
|
89
|
+
## Metrics (Optional)
|
|
90
|
+
|
|
91
|
+
When `enable_metrics=True`:
|
|
92
|
+
|
|
93
|
+
| Metric | Type | Description |
|
|
94
|
+
|--------|------|-------------|
|
|
95
|
+
| `edda.workflow.started` | Counter | Workflows started |
|
|
96
|
+
| `edda.workflow.completed` | Counter | Workflows completed |
|
|
97
|
+
| `edda.workflow.failed` | Counter | Workflows failed |
|
|
98
|
+
| `edda.workflow.duration` | Histogram | Workflow execution time |
|
|
99
|
+
| `edda.activity.executed` | Counter | Activities executed |
|
|
100
|
+
| `edda.activity.cache_hit` | Counter | Activity cache hits |
|
|
101
|
+
| `edda.activity.duration` | Histogram | Activity execution time |
|
|
102
|
+
|
|
103
|
+
## Trace Context Propagation
|
|
104
|
+
|
|
105
|
+
### Automatic Context Inheritance
|
|
106
|
+
|
|
107
|
+
OpenTelemetryHooks automatically inherits trace context from multiple sources, with the following priority:
|
|
108
|
+
|
|
109
|
+
1. **Explicit `_trace_context` in input_data** (highest priority)
|
|
110
|
+
- Extracted from CloudEvents extension attributes
|
|
111
|
+
- Useful for cross-service trace propagation
|
|
112
|
+
|
|
113
|
+
2. **Current active span** (e.g., from ASGI/WSGI middleware)
|
|
114
|
+
- Automatically detected using `trace.get_current_span()`
|
|
115
|
+
- Works with OpenTelemetry instrumentation middleware
|
|
116
|
+
|
|
117
|
+
3. **New root span** (if no parent context is found)
|
|
118
|
+
|
|
119
|
+
### CloudEvents Integration
|
|
120
|
+
|
|
121
|
+
Inject trace context when sending events:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from edda.integrations.opentelemetry import inject_trace_context
|
|
125
|
+
|
|
126
|
+
event_data = {"order_id": "ORD-123"}
|
|
127
|
+
event_data = inject_trace_context(hooks, ctx.instance_id, event_data)
|
|
128
|
+
await send_event_transactional(ctx, "order.shipped", "orders", event_data)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
When a CloudEvent contains W3C Trace Context extension attributes (`traceparent`, `tracestate`), they are automatically extracted and used as the parent context:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# CloudEvent with trace context
|
|
135
|
+
curl -X POST http://localhost:8001/ \
|
|
136
|
+
-H "Content-Type: application/json" \
|
|
137
|
+
-H "ce-specversion: 1.0" \
|
|
138
|
+
-H "ce-type: order.created" \
|
|
139
|
+
-H "ce-source: external-service" \
|
|
140
|
+
-H "ce-id: event-123" \
|
|
141
|
+
-H "ce-traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" \
|
|
142
|
+
-d '{"order_id": "ORD-123"}'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### ASGI/WSGI Middleware
|
|
146
|
+
|
|
147
|
+
OpenTelemetryHooks automatically inherits from the current active span:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
|
|
151
|
+
|
|
152
|
+
# Middleware creates parent span for each HTTP request
|
|
153
|
+
app = OpenTelemetryMiddleware(edda_app)
|
|
154
|
+
|
|
155
|
+
# Workflow spans automatically inherit from the request span
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Existing TracerProvider Reuse
|
|
159
|
+
|
|
160
|
+
If a TracerProvider is already configured (e.g., by ASGI middleware or your application), OpenTelemetryHooks will reuse it instead of creating a new one:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from opentelemetry import trace
|
|
164
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
165
|
+
|
|
166
|
+
# Configure your own provider
|
|
167
|
+
provider = TracerProvider(resource=my_resource)
|
|
168
|
+
trace.set_tracer_provider(provider)
|
|
169
|
+
|
|
170
|
+
# OpenTelemetryHooks will use the existing provider
|
|
171
|
+
hooks = OpenTelemetryHooks(service_name="my-service")
|
|
172
|
+
# No new provider is created!
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Related Documentation
|
|
176
|
+
|
|
177
|
+
- [Lifecycle Hooks](../core-features/hooks.md) - Detailed hooks documentation
|
|
178
|
+
- [Example](../../examples/observability_with_opentelemetry.py) - Complete working example
|
|
179
|
+
- [OpenTelemetry Documentation](https://opentelemetry.io/docs/)
|
|
@@ -238,7 +238,9 @@ class EddaApp:
|
|
|
238
238
|
Register a default CloudEvent handler for a workflow.
|
|
239
239
|
|
|
240
240
|
The default handler extracts the CloudEvent data and passes it
|
|
241
|
-
as kwargs to workflow.start().
|
|
241
|
+
as kwargs to workflow.start(). If the CloudEvent contains
|
|
242
|
+
traceparent/tracestate extension attributes (for distributed tracing),
|
|
243
|
+
they are automatically injected into _trace_context.
|
|
242
244
|
|
|
243
245
|
Args:
|
|
244
246
|
event_type: CloudEvent type (same as workflow name)
|
|
@@ -250,11 +252,24 @@ class EddaApp:
|
|
|
250
252
|
# Extract data from CloudEvent
|
|
251
253
|
data = event.get_data()
|
|
252
254
|
|
|
255
|
+
# Extract trace context from CloudEvent extension attributes
|
|
256
|
+
# (W3C Trace Context: traceparent, tracestate)
|
|
257
|
+
trace_context: dict[str, str] = {}
|
|
258
|
+
attrs = event.get_attributes()
|
|
259
|
+
if "traceparent" in attrs:
|
|
260
|
+
trace_context["traceparent"] = str(attrs["traceparent"])
|
|
261
|
+
if "tracestate" in attrs:
|
|
262
|
+
trace_context["tracestate"] = str(attrs["tracestate"])
|
|
263
|
+
|
|
253
264
|
# Start workflow with data as kwargs
|
|
254
265
|
if isinstance(data, dict):
|
|
266
|
+
# Inject trace context if present
|
|
267
|
+
if trace_context:
|
|
268
|
+
data = {**data, "_trace_context": trace_context}
|
|
255
269
|
await wf.start(**data)
|
|
256
270
|
else:
|
|
257
271
|
# If data is not a dict, start without arguments
|
|
272
|
+
# (trace context cannot be injected)
|
|
258
273
|
await wf.start()
|
|
259
274
|
|
|
260
275
|
# Register the handler
|
|
@@ -12,8 +12,8 @@ Example:
|
|
|
12
12
|
... async def on_workflow_start(self, instance_id, workflow_name, input_data):
|
|
13
13
|
... print(f"Workflow {workflow_name} started: {instance_id}")
|
|
14
14
|
...
|
|
15
|
-
... async def on_activity_complete(self, instance_id,
|
|
16
|
-
... print(f"Activity {activity_name} completed (cache_hit={cache_hit})")
|
|
15
|
+
... async def on_activity_complete(self, instance_id, activity_id, activity_name, result, cache_hit):
|
|
16
|
+
... print(f"Activity {activity_name} ({activity_id}) completed (cache_hit={cache_hit})")
|
|
17
17
|
>>>
|
|
18
18
|
>>> app = EddaApp(service_name="my-service", db_url="...", hooks=MyHooks())
|
|
19
19
|
"""
|
|
@@ -86,7 +86,7 @@ class WorkflowHooks(Protocol):
|
|
|
86
86
|
async def on_activity_start(
|
|
87
87
|
self,
|
|
88
88
|
instance_id: str,
|
|
89
|
-
|
|
89
|
+
activity_id: str,
|
|
90
90
|
activity_name: str,
|
|
91
91
|
is_replaying: bool,
|
|
92
92
|
) -> None:
|
|
@@ -95,7 +95,7 @@ class WorkflowHooks(Protocol):
|
|
|
95
95
|
|
|
96
96
|
Args:
|
|
97
97
|
instance_id: Unique workflow instance ID
|
|
98
|
-
|
|
98
|
+
activity_id: Activity ID (e.g., "reserve_inventory:1")
|
|
99
99
|
activity_name: Name of the activity function
|
|
100
100
|
is_replaying: True if this is a replay (cached result)
|
|
101
101
|
"""
|
|
@@ -104,7 +104,7 @@ class WorkflowHooks(Protocol):
|
|
|
104
104
|
async def on_activity_complete(
|
|
105
105
|
self,
|
|
106
106
|
instance_id: str,
|
|
107
|
-
|
|
107
|
+
activity_id: str,
|
|
108
108
|
activity_name: str,
|
|
109
109
|
result: Any,
|
|
110
110
|
cache_hit: bool,
|
|
@@ -114,7 +114,7 @@ class WorkflowHooks(Protocol):
|
|
|
114
114
|
|
|
115
115
|
Args:
|
|
116
116
|
instance_id: Unique workflow instance ID
|
|
117
|
-
|
|
117
|
+
activity_id: Activity ID (e.g., "reserve_inventory:1")
|
|
118
118
|
activity_name: Name of the activity function
|
|
119
119
|
result: Return value from the activity
|
|
120
120
|
cache_hit: True if result was retrieved from cache (replay)
|
|
@@ -124,7 +124,7 @@ class WorkflowHooks(Protocol):
|
|
|
124
124
|
async def on_activity_failed(
|
|
125
125
|
self,
|
|
126
126
|
instance_id: str,
|
|
127
|
-
|
|
127
|
+
activity_id: str,
|
|
128
128
|
activity_name: str,
|
|
129
129
|
error: Exception,
|
|
130
130
|
) -> None:
|
|
@@ -133,7 +133,7 @@ class WorkflowHooks(Protocol):
|
|
|
133
133
|
|
|
134
134
|
Args:
|
|
135
135
|
instance_id: Unique workflow instance ID
|
|
136
|
-
|
|
136
|
+
activity_id: Activity ID (e.g., "reserve_inventory:1")
|
|
137
137
|
activity_name: Name of the activity function
|
|
138
138
|
error: Exception that caused the failure
|
|
139
139
|
"""
|
|
@@ -231,7 +231,7 @@ class HooksBase(WorkflowHooks, ABC):
|
|
|
231
231
|
async def on_activity_start(
|
|
232
232
|
self,
|
|
233
233
|
instance_id: str,
|
|
234
|
-
|
|
234
|
+
activity_id: str,
|
|
235
235
|
activity_name: str,
|
|
236
236
|
is_replaying: bool,
|
|
237
237
|
) -> None:
|
|
@@ -240,7 +240,7 @@ class HooksBase(WorkflowHooks, ABC):
|
|
|
240
240
|
async def on_activity_complete(
|
|
241
241
|
self,
|
|
242
242
|
instance_id: str,
|
|
243
|
-
|
|
243
|
+
activity_id: str,
|
|
244
244
|
activity_name: str,
|
|
245
245
|
result: Any,
|
|
246
246
|
cache_hit: bool,
|
|
@@ -250,7 +250,7 @@ class HooksBase(WorkflowHooks, ABC):
|
|
|
250
250
|
async def on_activity_failed(
|
|
251
251
|
self,
|
|
252
252
|
instance_id: str,
|
|
253
|
-
|
|
253
|
+
activity_id: str,
|
|
254
254
|
activity_name: str,
|
|
255
255
|
error: Exception,
|
|
256
256
|
) -> None:
|