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.
Files changed (157) hide show
  1. {edda_framework-0.3.1 → edda_framework-0.5.0}/PKG-INFO +53 -1
  2. {edda_framework-0.3.1 → edda_framework-0.5.0}/README.md +48 -0
  3. {edda_framework-0.3.1 → edda_framework-0.5.0}/demo_app.py +4 -4
  4. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/durable-execution/replay.md +8 -8
  5. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/hooks.md +43 -8
  6. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/workflows-activities.md +4 -4
  7. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/getting-started/first-workflow.md +32 -28
  8. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/index.md +30 -7
  9. edda_framework-0.5.0/docs/integrations/opentelemetry.md +179 -0
  10. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/app.py +16 -1
  11. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/hooks.py +11 -11
  12. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/integrations/mcp/decorators.py +3 -4
  13. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/integrations/mcp/server.py +157 -5
  14. edda_framework-0.5.0/edda/integrations/opentelemetry/__init__.py +39 -0
  15. edda_framework-0.5.0/edda/integrations/opentelemetry/hooks.py +579 -0
  16. edda_framework-0.5.0/examples/mcp/prompts_example.py +281 -0
  17. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/observability_with_logfire.py +1 -1
  18. edda_framework-0.5.0/examples/observability_with_opentelemetry.py +211 -0
  19. {edda_framework-0.3.1 → edda_framework-0.5.0}/pyproject.toml +6 -1
  20. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/integrations/mcp/test_integration.py +6 -6
  21. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/integrations/mcp/test_jsonrpc.py +3 -3
  22. edda_framework-0.5.0/tests/integrations/mcp/test_prompts.py +203 -0
  23. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/integrations/mcp/test_server.py +2 -2
  24. edda_framework-0.5.0/tests/integrations/opentelemetry/__init__.py +1 -0
  25. edda_framework-0.5.0/tests/integrations/opentelemetry/test_hooks.py +696 -0
  26. {edda_framework-0.3.1 → edda_framework-0.5.0}/uv.lock +226 -2
  27. {edda_framework-0.3.1 → edda_framework-0.5.0}/.github/workflows/ci.yml +0 -0
  28. {edda_framework-0.3.1 → edda_framework-0.5.0}/.github/workflows/docs.yml +0 -0
  29. {edda_framework-0.3.1 → edda_framework-0.5.0}/.github/workflows/release.yml +0 -0
  30. {edda_framework-0.3.1 → edda_framework-0.5.0}/.gitignore +0 -0
  31. {edda_framework-0.3.1 → edda_framework-0.5.0}/.python-version +0 -0
  32. {edda_framework-0.3.1 → edda_framework-0.5.0}/Justfile +0 -0
  33. {edda_framework-0.3.1 → edda_framework-0.5.0}/LICENSE +0 -0
  34. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
  35. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/events/wait-event.md +0 -0
  36. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/retry.md +0 -0
  37. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/saga-compensation.md +0 -0
  38. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/core-features/transactional-outbox.md +0 -0
  39. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/examples/ecommerce.md +0 -0
  40. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/examples/events.md +0 -0
  41. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/examples/fastapi-integration.md +0 -0
  42. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/examples/saga.md +0 -0
  43. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/examples/simple.md +0 -0
  44. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/getting-started/concepts.md +0 -0
  45. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/getting-started/installation.md +0 -0
  46. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/getting-started/quick-start.md +0 -0
  47. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/integrations/mcp.md +0 -0
  48. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/markdown.md +0 -0
  49. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
  50. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
  51. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/conditional-branching-diagram.png +0 -0
  52. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/detail-overview-panel.png +0 -0
  53. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/execution-history-panel.png +0 -0
  54. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/form-generation-example.png +0 -0
  55. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/hybrid-diagram-example.png +0 -0
  56. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
  57. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/start-workflow-dialog.png +0 -0
  58. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/status-badges-example.png +0 -0
  59. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
  60. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
  61. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/setup.md +0 -0
  62. {edda_framework-0.3.1 → edda_framework-0.5.0}/docs/viewer-ui/visualization.md +0 -0
  63. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/__init__.py +0 -0
  64. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/activity.py +0 -0
  65. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/compensation.py +0 -0
  66. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/context.py +0 -0
  67. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/events.py +0 -0
  68. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/exceptions.py +0 -0
  69. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/integrations/__init__.py +0 -0
  70. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/integrations/mcp/__init__.py +0 -0
  71. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/locking.py +0 -0
  72. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/outbox/__init__.py +0 -0
  73. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/outbox/relayer.py +0 -0
  74. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/outbox/transactional.py +0 -0
  75. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/pydantic_utils.py +0 -0
  76. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/replay.py +0 -0
  77. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/retry.py +0 -0
  78. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/serialization/__init__.py +0 -0
  79. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/serialization/base.py +0 -0
  80. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/serialization/json.py +0 -0
  81. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/storage/__init__.py +0 -0
  82. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/storage/models.py +0 -0
  83. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/storage/protocol.py +0 -0
  84. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/storage/sqlalchemy_storage.py +0 -0
  85. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/viewer_ui/__init__.py +0 -0
  86. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/viewer_ui/app.py +0 -0
  87. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/viewer_ui/components.py +0 -0
  88. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/viewer_ui/data_service.py +0 -0
  89. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/visualizer/__init__.py +0 -0
  90. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/visualizer/ast_analyzer.py +0 -0
  91. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/visualizer/mermaid_generator.py +0 -0
  92. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/workflow.py +0 -0
  93. {edda_framework-0.3.1 → edda_framework-0.5.0}/edda/wsgi.py +0 -0
  94. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/__init__.py +0 -0
  95. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/cancellable_workflow.py +0 -0
  96. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/compensation_workflow.py +0 -0
  97. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/event_waiting_app.py +0 -0
  98. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/event_waiting_workflow.py +0 -0
  99. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/event_waiting_workflow_complete.py +0 -0
  100. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/mcp/README.md +0 -0
  101. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/mcp/order_processing_mcp.py +0 -0
  102. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/mcp/remote_server_example.py +0 -0
  103. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/mcp/simple_mcp_server.py +0 -0
  104. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/pydantic_saga.py +0 -0
  105. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/retry_example.py +0 -0
  106. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/retry_with_compensation.py +0 -0
  107. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/simple_workflow.py +0 -0
  108. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/typeddict_example.py +0 -0
  109. {edda_framework-0.3.1 → edda_framework-0.5.0}/examples/with_outbox.py +0 -0
  110. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/__init__.py +0 -0
  111. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/conftest.py +0 -0
  112. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/integrations/__init__.py +0 -0
  113. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/integrations/mcp/__init__.py +0 -0
  114. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_activity.py +0 -0
  115. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_activity_retry.py +0 -0
  116. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_activity_sync.py +0 -0
  117. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_app.py +0 -0
  118. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_ast_analyzer.py +0 -0
  119. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_atomic_wait_event.py +0 -0
  120. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_binary_data.py +0 -0
  121. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_cloudevents_http_binding.py +0 -0
  122. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_compensation.py +0 -0
  123. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
  124. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_concurrent_outbox.py +0 -0
  125. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_context.py +0 -0
  126. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_ctx_session.py +0 -0
  127. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_distributed_event_delivery.py +0 -0
  128. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_events.py +0 -0
  129. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_lock_race_condition.py +0 -0
  130. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_lock_timeout_customization.py +0 -0
  131. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_locking.py +0 -0
  132. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_multidb_storage.py +0 -0
  133. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_outbox.py +0 -0
  134. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_pydantic_activity.py +0 -0
  135. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_pydantic_enum.py +0 -0
  136. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_pydantic_events.py +0 -0
  137. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_pydantic_saga.py +0 -0
  138. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_pydantic_utils.py +0 -0
  139. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_received_event.py +0 -0
  140. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_replay.py +0 -0
  141. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_retry_policy.py +0 -0
  142. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_saga_parameter_extraction.py +0 -0
  143. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_serialization.py +0 -0
  144. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_skip_locked.py +0 -0
  145. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_stale_workflow_recovery.py +0 -0
  146. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_storage.py +0 -0
  147. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_storage_mysql.py +0 -0
  148. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_storage_postgresql.py +0 -0
  149. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_transactions.py +0 -0
  150. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_viewer_pydantic_form.py +0 -0
  151. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_viewer_start_saga.py +0 -0
  152. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_wait_timer.py +0 -0
  153. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_workflow.py +0 -0
  154. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_workflow_auto_register.py +0 -0
  155. {edda_framework-0.3.1 → edda_framework-0.5.0}/tests/test_workflow_cancellation.py +0 -0
  156. {edda_framework-0.3.1 → edda_framework-0.5.0}/viewer_app.py +0 -0
  157. {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.1
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": "123 Main St",
496
- "city": "San Francisco",
497
- "state": "CA",
498
- "zip_code": "94102"
495
+ "street": "221B Baker Street",
496
+ "city": "London",
497
+ "state": "Greater London",
498
+ "zip_code": "NW1 6XE"
499
499
  }
500
500
  }
501
501
 
@@ -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, activity_id="reserve_inventory:1")
104
- payment = await process_payment(ctx, order_id, activity_id="process_payment:1")
105
- shipping = await arrange_shipping(ctx, order_id, activity_id="arrange_shipping:1")
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, activity_id="reserve_inventory:1")
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, activity_id="process_payment:1")
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, activity_id="arrange_shipping:1")
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, step, activity_name, result, cache_hit):
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`, `step`, `activity_name`, `is_replaying` | Called before an activity executes |
73
- | `on_activity_complete` | `instance_id`, `step`, `activity_name`, `result`, `cache_hit` | Called after an activity completes successfully |
74
- | `on_activity_failed` | `instance_id`, `step`, `activity_name`, `error` | Called when an activity fails with an exception |
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, step, activity_name, result, cache_hit):
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, step, activity_name, result, cache_hit):
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, step, activity_name, result, cache_hit):
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, step, activity_name, error):
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", activity_id="create:1")
151
- data = await async_activity(ctx, user_id, activity_id="fetch:1")
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, activity_id="user:1") # Sync
827
- payment = await process_payment(ctx, 99.99, activity_id="pay:1") # Async
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
- # Create order input
276
- order = OrderInput(
277
- order_id="ORD-12345",
278
- customer_email="customer@example.com",
279
- items=[
280
- OrderItem(product_id="PROD-1", quantity=2, unit_price=29.99),
281
- OrderItem(product_id="PROD-2", quantity=1, unit_price=49.99),
282
- ],
283
- shipping_address=ShippingAddress(
284
- street="123 Main St",
285
- city="San Francisco",
286
- postal_code="94105",
287
- country="USA"
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
- # Start workflow
292
- print("Starting order processing workflow...")
293
- instance_id = await order_processing_workflow.start(input=order)
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
- print(f"\n✅ Workflow started: {instance_id}")
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
- # Get result
298
- instance = await app.storage.get_instance(instance_id)
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 San Francisco, USA
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 San Francisco, USA
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
- # Simplified example - production code needs:
107
- # 1. await app.initialize() before starting workflows
108
- # 2. try-finally with await app.shutdown() for cleanup
109
- app = EddaApp(service_name="demo-service", db_url="sqlite:///workflow.db")
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/)