edda-framework 0.3.1__tar.gz → 0.5.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.3.1 → edda_framework-0.5.0}/PKG-INFO +53 -1
- {edda_framework-0.3.1 → edda_framework-0.5.0}/README.md +48 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/demo_app.py +4 -4
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/durable-execution/replay.md +8 -8
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/hooks.md +43 -8
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/workflows-activities.md +4 -4
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/getting-started/first-workflow.md +32 -28
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/index.md +30 -7
- edda_framework-0.5.0/docs/integrations/opentelemetry.md +179 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/app.py +16 -1
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/hooks.py +11 -11
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/integrations/mcp/decorators.py +3 -4
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/integrations/mcp/server.py +157 -5
- edda_framework-0.5.0/edda/integrations/opentelemetry/__init__.py +39 -0
- edda_framework-0.5.0/edda/integrations/opentelemetry/hooks.py +579 -0
- edda_framework-0.5.0/examples/mcp/prompts_example.py +281 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/observability_with_logfire.py +1 -1
- edda_framework-0.5.0/examples/observability_with_opentelemetry.py +211 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/pyproject.toml +6 -1
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/integrations/mcp/test_integration.py +6 -6
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/integrations/mcp/test_jsonrpc.py +3 -3
- edda_framework-0.5.0/tests/integrations/mcp/test_prompts.py +203 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/integrations/mcp/test_server.py +2 -2
- edda_framework-0.5.0/tests/integrations/opentelemetry/__init__.py +1 -0
- edda_framework-0.5.0/tests/integrations/opentelemetry/test_hooks.py +696 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/uv.lock +226 -2
- {edda_framework-0.3.1 → edda_framework-0.5.0}/.github/workflows/ci.yml +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/.github/workflows/docs.yml +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/.github/workflows/release.yml +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/.gitignore +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/.python-version +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/Justfile +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/LICENSE +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/events/wait-event.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/retry.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/saga-compensation.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/transactional-outbox.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/examples/ecommerce.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/examples/events.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/examples/fastapi-integration.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/examples/saga.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/examples/simple.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/getting-started/concepts.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/getting-started/installation.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/getting-started/quick-start.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/integrations/mcp.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/markdown.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/conditional-branching-diagram.png +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/detail-overview-panel.png +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/execution-history-panel.png +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/form-generation-example.png +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/hybrid-diagram-example.png +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/start-workflow-dialog.png +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/status-badges-example.png +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/setup.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/visualization.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/__init__.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/activity.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/compensation.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/context.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/events.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/exceptions.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/integrations/__init__.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/locking.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/outbox/__init__.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/outbox/relayer.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/outbox/transactional.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/pydantic_utils.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/replay.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/retry.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/serialization/__init__.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/serialization/base.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/serialization/json.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/storage/__init__.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/storage/models.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/storage/protocol.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/storage/sqlalchemy_storage.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/viewer_ui/__init__.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/viewer_ui/app.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/viewer_ui/components.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/viewer_ui/data_service.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/visualizer/__init__.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/visualizer/ast_analyzer.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/visualizer/mermaid_generator.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/workflow.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/wsgi.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/__init__.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/cancellable_workflow.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/compensation_workflow.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/event_waiting_app.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/event_waiting_workflow.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/event_waiting_workflow_complete.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/mcp/README.md +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/mcp/order_processing_mcp.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/mcp/remote_server_example.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/mcp/simple_mcp_server.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/pydantic_saga.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/retry_example.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/retry_with_compensation.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/simple_workflow.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/typeddict_example.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/with_outbox.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/__init__.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/conftest.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/integrations/__init__.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_activity.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_activity_retry.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_activity_sync.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_app.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_ast_analyzer.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_atomic_wait_event.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_binary_data.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_cloudevents_http_binding.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_compensation.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_concurrent_outbox.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_context.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_ctx_session.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_distributed_event_delivery.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_events.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_lock_race_condition.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_lock_timeout_customization.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_locking.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_multidb_storage.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_outbox.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_pydantic_activity.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_pydantic_enum.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_pydantic_events.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_pydantic_saga.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_pydantic_utils.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_received_event.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_replay.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_retry_policy.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_saga_parameter_extraction.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_serialization.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_skip_locked.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_stale_workflow_recovery.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_storage.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_storage_mysql.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_storage_postgresql.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_transactions.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_viewer_pydantic_form.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_viewer_start_saga.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_wait_timer.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_workflow.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_workflow_auto_register.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_workflow_cancellation.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/viewer_app.py +0 -0
- {edda_framework-0.3.1 → edda_framework-0.5.0}/zensical.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edda-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -730,6 +756,32 @@ Each `@durable_tool` automatically generates **three MCP tools**:
|
|
|
730
756
|
|
|
731
757
|
This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
|
|
732
758
|
|
|
759
|
+
### MCP Prompts
|
|
760
|
+
|
|
761
|
+
Define reusable prompt templates that can access workflow state:
|
|
762
|
+
|
|
763
|
+
```python
|
|
764
|
+
from mcp.server.fastmcp.prompts.base import UserMessage
|
|
765
|
+
from mcp.types import TextContent
|
|
766
|
+
|
|
767
|
+
@server.prompt(description="Analyze a workflow execution")
|
|
768
|
+
async def analyze_workflow(instance_id: str) -> UserMessage:
|
|
769
|
+
"""Generate analysis prompt for a specific workflow."""
|
|
770
|
+
instance = await server.storage.get_instance(instance_id)
|
|
771
|
+
history = await server.storage.get_history(instance_id)
|
|
772
|
+
|
|
773
|
+
text = f"""Analyze this workflow:
|
|
774
|
+
**Status**: {instance['status']}
|
|
775
|
+
**Activities**: {len(history)}
|
|
776
|
+
**Result**: {instance.get('output_data')}
|
|
777
|
+
|
|
778
|
+
Please provide insights and optimization suggestions."""
|
|
779
|
+
|
|
780
|
+
return UserMessage(content=TextContent(type="text", text=text))
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
AI clients can use these prompts to generate context-aware analysis of your workflows.
|
|
784
|
+
|
|
733
785
|
**For detailed documentation**, see [MCP Integration Guide](docs/integrations/mcp.md).
|
|
734
786
|
|
|
735
787
|
## Observability Hooks
|
|
@@ -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
|
|
@@ -678,6 +700,32 @@ Each `@durable_tool` automatically generates **three MCP tools**:
|
|
|
678
700
|
|
|
679
701
|
This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
|
|
680
702
|
|
|
703
|
+
### MCP Prompts
|
|
704
|
+
|
|
705
|
+
Define reusable prompt templates that can access workflow state:
|
|
706
|
+
|
|
707
|
+
```python
|
|
708
|
+
from mcp.server.fastmcp.prompts.base import UserMessage
|
|
709
|
+
from mcp.types import TextContent
|
|
710
|
+
|
|
711
|
+
@server.prompt(description="Analyze a workflow execution")
|
|
712
|
+
async def analyze_workflow(instance_id: str) -> UserMessage:
|
|
713
|
+
"""Generate analysis prompt for a specific workflow."""
|
|
714
|
+
instance = await server.storage.get_instance(instance_id)
|
|
715
|
+
history = await server.storage.get_history(instance_id)
|
|
716
|
+
|
|
717
|
+
text = f"""Analyze this workflow:
|
|
718
|
+
**Status**: {instance['status']}
|
|
719
|
+
**Activities**: {len(history)}
|
|
720
|
+
**Result**: {instance.get('output_data')}
|
|
721
|
+
|
|
722
|
+
Please provide insights and optimization suggestions."""
|
|
723
|
+
|
|
724
|
+
return UserMessage(content=TextContent(type="text", text=text))
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
AI clients can use these prompts to generate context-aware analysis of your workflows.
|
|
728
|
+
|
|
681
729
|
**For detailed documentation**, see [MCP Integration Guide](docs/integrations/mcp.md).
|
|
682
730
|
|
|
683
731
|
## Observability Hooks
|
|
@@ -492,10 +492,10 @@ async def order_processing_workflow(
|
|
|
492
492
|
{"item_id": "ITEM-2", "name": "Product B", "price": 49.99, "quantity": 1}
|
|
493
493
|
],
|
|
494
494
|
"shipping_address": {
|
|
495
|
-
"street": "
|
|
496
|
-
"city": "
|
|
497
|
-
"state": "
|
|
498
|
-
"zip_code": "
|
|
495
|
+
"street": "221B Baker Street",
|
|
496
|
+
"city": "London",
|
|
497
|
+
"state": "Greater London",
|
|
498
|
+
"zip_code": "NW1 6XE"
|
|
499
499
|
}
|
|
500
500
|
}
|
|
501
501
|
|
{edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/durable-execution/replay.md
RENAMED
|
@@ -100,9 +100,9 @@ async def arrange_shipping(ctx: WorkflowContext, order_id: str):
|
|
|
100
100
|
@workflow
|
|
101
101
|
async def order_workflow(ctx: WorkflowContext, order_id: str):
|
|
102
102
|
# Activity IDs are auto-generated for sequential calls
|
|
103
|
-
inventory = await reserve_inventory(ctx, order_id
|
|
104
|
-
payment = await process_payment(ctx, order_id
|
|
105
|
-
shipping = await arrange_shipping(ctx, order_id
|
|
103
|
+
inventory = await reserve_inventory(ctx, order_id)
|
|
104
|
+
payment = await process_payment(ctx, order_id)
|
|
105
|
+
shipping = await arrange_shipping(ctx, order_id)
|
|
106
106
|
|
|
107
107
|
return {"status": "completed"}
|
|
108
108
|
```
|
|
@@ -497,16 +497,16 @@ async with workflow_lock(storage, instance_id, worker_id):
|
|
|
497
497
|
```python
|
|
498
498
|
@workflow
|
|
499
499
|
async def order_workflow(ctx: WorkflowContext, order_id: str):
|
|
500
|
-
# Activity 1
|
|
501
|
-
inventory = await reserve_inventory(ctx, order_id
|
|
500
|
+
# Activity 1 (auto-generated ID: "reserve_inventory:1")
|
|
501
|
+
inventory = await reserve_inventory(ctx, order_id)
|
|
502
502
|
# → DB saved: activity_id="reserve_inventory:1", result={"reservation_id": "R123"}
|
|
503
503
|
|
|
504
|
-
# Activity 2
|
|
505
|
-
payment = await process_payment(ctx, order_id
|
|
504
|
+
# Activity 2 (auto-generated ID: "process_payment:1")
|
|
505
|
+
payment = await process_payment(ctx, order_id)
|
|
506
506
|
# → DB saved: activity_id="process_payment:1", result={"transaction_id": "T456"}
|
|
507
507
|
|
|
508
508
|
# Activity 3: Exception occurs (e.g., network error)
|
|
509
|
-
shipping = await arrange_shipping(ctx, order_id
|
|
509
|
+
shipping = await arrange_shipping(ctx, order_id)
|
|
510
510
|
# → Exception thrown, workflow interrupted
|
|
511
511
|
```
|
|
512
512
|
|
|
@@ -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
|
|
@@ -147,8 +147,8 @@ async def async_activity(ctx: WorkflowContext, data: str) -> dict:
|
|
|
147
147
|
async def mixed_workflow(ctx: WorkflowContext, user_id: str) -> dict:
|
|
148
148
|
# Workflows are always async (for deterministic replay)
|
|
149
149
|
# But can call both sync and async activities
|
|
150
|
-
user = await create_user_record(ctx, user_id, "user@example.com"
|
|
151
|
-
data = await async_activity(ctx, user_id
|
|
150
|
+
user = await create_user_record(ctx, user_id, "user@example.com")
|
|
151
|
+
data = await async_activity(ctx, user_id)
|
|
152
152
|
return {"user": user, "data": data}
|
|
153
153
|
```
|
|
154
154
|
|
|
@@ -823,8 +823,8 @@ def process_legacy_data(ctx: WorkflowContext, data: str) -> dict:
|
|
|
823
823
|
@workflow
|
|
824
824
|
async def order_workflow(ctx: WorkflowContext, order_id: str) -> dict:
|
|
825
825
|
# Both sync and async activities work fine
|
|
826
|
-
user = await create_user_record(ctx, order_id
|
|
827
|
-
payment = await process_payment(ctx, 99.99
|
|
826
|
+
user = await create_user_record(ctx, order_id) # Sync
|
|
827
|
+
payment = await process_payment(ctx, 99.99) # Async
|
|
828
828
|
return {"user": user, "payment": payment}
|
|
829
829
|
```
|
|
830
830
|
|
|
@@ -272,36 +272,40 @@ async def main():
|
|
|
272
272
|
# Initialize the app (required before starting workflows)
|
|
273
273
|
await app.initialize()
|
|
274
274
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
275
|
+
try:
|
|
276
|
+
# Create order input
|
|
277
|
+
order = OrderInput(
|
|
278
|
+
order_id="ORD-12345",
|
|
279
|
+
customer_email="customer@example.com",
|
|
280
|
+
items=[
|
|
281
|
+
OrderItem(product_id="PROD-1", quantity=2, unit_price=29.99),
|
|
282
|
+
OrderItem(product_id="PROD-2", quantity=1, unit_price=49.99),
|
|
283
|
+
],
|
|
284
|
+
shipping_address=ShippingAddress(
|
|
285
|
+
street="1-2-3 Dogenzaka",
|
|
286
|
+
city="Shibuya",
|
|
287
|
+
postal_code="150-0001",
|
|
288
|
+
country="Japan"
|
|
289
|
+
)
|
|
288
290
|
)
|
|
289
|
-
)
|
|
290
291
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
292
|
+
# Start workflow
|
|
293
|
+
print("Starting order processing workflow...")
|
|
294
|
+
instance_id = await order_processing_workflow.start(input=order)
|
|
295
|
+
|
|
296
|
+
print(f"\n✅ Workflow started: {instance_id}")
|
|
294
297
|
|
|
295
|
-
|
|
298
|
+
# Get result
|
|
299
|
+
instance = await app.storage.get_instance(instance_id)
|
|
300
|
+
if instance["status"] == "completed":
|
|
301
|
+
result = instance["output_data"]
|
|
302
|
+
print(f"📊 Order completed:")
|
|
303
|
+
print(f" - Order ID: {result['order_id']}")
|
|
304
|
+
print(f" - Total: ${result['total_amount']:.2f}")
|
|
305
|
+
print(f" - Tracking: {result['confirmation_number']}")
|
|
296
306
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if instance["status"] == "completed":
|
|
300
|
-
result = instance["output_data"]
|
|
301
|
-
print(f"📊 Order completed:")
|
|
302
|
-
print(f" - Order ID: {result['order_id']}")
|
|
303
|
-
print(f" - Total: ${result['total_amount']:.2f}")
|
|
304
|
-
print(f" - Tracking: {result['confirmation_number']}")
|
|
307
|
+
finally:
|
|
308
|
+
await app.shutdown()
|
|
305
309
|
|
|
306
310
|
if __name__ == "__main__":
|
|
307
311
|
asyncio.run(main())
|
|
@@ -322,7 +326,7 @@ Starting order processing workflow...
|
|
|
322
326
|
|
|
323
327
|
📦 Reserving inventory for ORD-12345: $109.97
|
|
324
328
|
💳 Processing payment for ORD-12345: $109.97
|
|
325
|
-
🚚 Shipping ORD-12345 to
|
|
329
|
+
🚚 Shipping ORD-12345 to Shibuya, Japan
|
|
326
330
|
|
|
327
331
|
✅ Workflow started: <instance_id>
|
|
328
332
|
📊 Order completed:
|
|
@@ -364,7 +368,7 @@ uv run python order_workflow.py
|
|
|
364
368
|
```
|
|
365
369
|
📦 Reserving inventory for ORD-12345: $109.97
|
|
366
370
|
💳 Processing payment for ORD-12345: $109.97
|
|
367
|
-
🚚 Shipping ORD-12345 to
|
|
371
|
+
🚚 Shipping ORD-12345 to Shibuya, Japan
|
|
368
372
|
💥 Exception: Shipping service unavailable!
|
|
369
373
|
|
|
370
374
|
❌ Refunding payment for ORD-12345: $109.97
|
|
@@ -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
|
|
@@ -93,21 +115,22 @@ from edda import EddaApp, workflow, activity, WorkflowContext
|
|
|
93
115
|
|
|
94
116
|
@activity
|
|
95
117
|
async def process_payment(ctx: WorkflowContext, amount: float):
|
|
96
|
-
# Durable execution - automatically recorded in history
|
|
97
118
|
print(f"Processing payment: ${amount}")
|
|
98
119
|
return {"status": "paid", "amount": amount}
|
|
99
120
|
|
|
100
121
|
@workflow
|
|
101
122
|
async def order_workflow(ctx: WorkflowContext, order_id: str, amount: float):
|
|
102
|
-
# Workflow orchestrates activities with automatic retry on crash
|
|
103
123
|
result = await process_payment(ctx, amount)
|
|
104
124
|
return {"order_id": order_id, **result}
|
|
105
125
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
instance_id = await order_workflow.start(order_id="ORD-123", amount=99.99)
|
|
126
|
+
async def main():
|
|
127
|
+
app = EddaApp(service_name="demo-service", db_url="sqlite:///workflow.db")
|
|
128
|
+
await app.initialize()
|
|
129
|
+
try:
|
|
130
|
+
instance_id = await order_workflow.start(order_id="ORD-123", amount=99.99)
|
|
131
|
+
print(f"Started workflow: {instance_id}")
|
|
132
|
+
finally:
|
|
133
|
+
await app.shutdown()
|
|
111
134
|
```
|
|
112
135
|
|
|
113
136
|
**What happens on crash?**
|
|
@@ -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/)
|