dbos 1.8.0a1__tar.gz → 1.8.0a3__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.

Potentially problematic release.


This version of dbos might be problematic. Click here for more details.

Files changed (109) hide show
  1. {dbos-1.8.0a1 → dbos-1.8.0a3}/PKG-INFO +1 -1
  2. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_context.py +37 -12
  3. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_error.py +1 -1
  4. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_sys_db.py +8 -1
  5. {dbos-1.8.0a1 → dbos-1.8.0a3}/pyproject.toml +1 -1
  6. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_queue.py +7 -0
  7. dbos-1.8.0a3/tests/test_spans.py +272 -0
  8. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_workflow_management.py +1 -1
  9. dbos-1.8.0a1/tests/test_spans.py +0 -147
  10. {dbos-1.8.0a1 → dbos-1.8.0a3}/LICENSE +0 -0
  11. {dbos-1.8.0a1 → dbos-1.8.0a3}/README.md +0 -0
  12. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/__init__.py +0 -0
  13. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/__main__.py +0 -0
  14. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_admin_server.py +0 -0
  15. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_app_db.py +0 -0
  16. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_classproperty.py +0 -0
  17. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_client.py +0 -0
  18. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_conductor/conductor.py +0 -0
  19. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_conductor/protocol.py +0 -0
  20. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_core.py +0 -0
  21. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_croniter.py +0 -0
  22. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_dbos.py +0 -0
  23. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_dbos_config.py +0 -0
  24. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_debug.py +0 -0
  25. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_docker_pg_helper.py +0 -0
  26. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_event_loop.py +0 -0
  27. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_fastapi.py +0 -0
  28. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_flask.py +0 -0
  29. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_kafka.py +0 -0
  30. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_kafka_message.py +0 -0
  31. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_logger.py +0 -0
  32. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/env.py +0 -0
  33. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/script.py.mako +0 -0
  34. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  35. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
  36. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  37. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  38. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/versions/66478e1b95e5_consolidate_queues.py +0 -0
  39. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
  40. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
  41. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  42. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  43. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  44. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/versions/d994145b47b6_consolidate_inputs.py +0 -0
  45. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  46. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  47. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_outcome.py +0 -0
  48. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_queue.py +0 -0
  49. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_recovery.py +0 -0
  50. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_registrations.py +0 -0
  51. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_roles.py +0 -0
  52. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_scheduler.py +0 -0
  53. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_schemas/__init__.py +0 -0
  54. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_schemas/application_database.py +0 -0
  55. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_schemas/system_database.py +0 -0
  56. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_serialization.py +0 -0
  57. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_templates/dbos-db-starter/README.md +0 -0
  58. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  59. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
  60. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  61. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  62. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  63. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  64. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  65. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  66. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  67. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_tracer.py +0 -0
  68. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_utils.py +0 -0
  69. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/_workflow_commands.py +0 -0
  70. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/cli/_github_init.py +0 -0
  71. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/cli/_template_init.py +0 -0
  72. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/cli/cli.py +0 -0
  73. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/dbos-config.schema.json +0 -0
  74. {dbos-1.8.0a1 → dbos-1.8.0a3}/dbos/py.typed +0 -0
  75. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/__init__.py +0 -0
  76. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/atexit_no_ctor.py +0 -0
  77. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/atexit_no_launch.py +0 -0
  78. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/classdefs.py +0 -0
  79. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/client_collateral.py +0 -0
  80. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/client_worker.py +0 -0
  81. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/conftest.py +0 -0
  82. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/dupname_classdefs1.py +0 -0
  83. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/dupname_classdefsa.py +0 -0
  84. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/more_classdefs.py +0 -0
  85. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/queuedworkflow.py +0 -0
  86. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_admin_server.py +0 -0
  87. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_async.py +0 -0
  88. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_classdecorators.py +0 -0
  89. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_cli.py +0 -0
  90. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_client.py +0 -0
  91. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_concurrency.py +0 -0
  92. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_config.py +0 -0
  93. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_croniter.py +0 -0
  94. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_dbos.py +0 -0
  95. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_debug.py +0 -0
  96. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_docker_secrets.py +0 -0
  97. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_failures.py +0 -0
  98. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_fastapi.py +0 -0
  99. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_fastapi_roles.py +0 -0
  100. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_flask.py +0 -0
  101. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_kafka.py +0 -0
  102. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_outcome.py +0 -0
  103. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_package.py +0 -0
  104. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_scheduler.py +0 -0
  105. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_schema_migration.py +0 -0
  106. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_singleton.py +0 -0
  107. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_sqlalchemy.py +0 -0
  108. {dbos-1.8.0a1 → dbos-1.8.0a3}/tests/test_workflow_introspection.py +0 -0
  109. {dbos-1.8.0a1 → dbos-1.8.0a3}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 1.8.0a1
3
+ Version: 1.8.0a3
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -10,7 +10,7 @@ from enum import Enum
10
10
  from types import TracebackType
11
11
  from typing import List, Literal, Optional, Type, TypedDict
12
12
 
13
- from opentelemetry.trace import Span, Status, StatusCode
13
+ from opentelemetry.trace import Span, Status, StatusCode, use_span
14
14
  from sqlalchemy.orm import Session
15
15
 
16
16
  from dbos._utils import GlobalParams
@@ -68,6 +68,20 @@ class StepStatus:
68
68
  max_attempts: Optional[int]
69
69
 
70
70
 
71
+ @dataclass
72
+ class ContextSpan:
73
+ """
74
+ A span that is used to track the context of a workflow or step execution.
75
+
76
+ Attributes:
77
+ span: The OpenTelemetry span object.
78
+ context_manager: The context manager that is used to manage the span's lifecycle.
79
+ """
80
+
81
+ span: Span
82
+ context_manager: AbstractContextManager[Span]
83
+
84
+
71
85
  class DBOSContext:
72
86
  def __init__(self) -> None:
73
87
  self.executor_id = GlobalParams.executor_id
@@ -86,7 +100,7 @@ class DBOSContext:
86
100
  self.curr_step_function_id: int = -1
87
101
  self.curr_tx_function_id: int = -1
88
102
  self.sql_session: Optional[Session] = None
89
- self.spans: list[Span] = []
103
+ self.context_spans: list[ContextSpan] = []
90
104
 
91
105
  self.authenticated_user: Optional[str] = None
92
106
  self.authenticated_roles: Optional[List[str]] = None
@@ -202,8 +216,8 @@ class DBOSContext:
202
216
  self._end_span(exc_value)
203
217
 
204
218
  def get_current_span(self) -> Optional[Span]:
205
- if len(self.spans):
206
- return self.spans[-1]
219
+ if len(self.context_spans) > 0:
220
+ return self.context_spans[-1].span
207
221
  return None
208
222
 
209
223
  def _start_span(self, attributes: TracedAttributes) -> None:
@@ -218,27 +232,38 @@ class DBOSContext:
218
232
  )
219
233
  attributes["authenticatedUserAssumedRole"] = self.assumed_role
220
234
  span = dbos_tracer.start_span(
221
- attributes, parent=self.spans[-1] if len(self.spans) > 0 else None
235
+ attributes,
236
+ parent=self.context_spans[-1].span if len(self.context_spans) > 0 else None,
237
+ )
238
+ # Activate the current span
239
+ cm = use_span(
240
+ span,
241
+ end_on_exit=False,
242
+ record_exception=False,
243
+ set_status_on_exception=False,
222
244
  )
223
- self.spans.append(span)
245
+ self.context_spans.append(ContextSpan(span, cm))
246
+ cm.__enter__()
224
247
 
225
248
  def _end_span(self, exc_value: Optional[BaseException]) -> None:
249
+ context_span = self.context_spans.pop()
226
250
  if exc_value is None:
227
- self.spans[-1].set_status(Status(StatusCode.OK))
251
+ context_span.span.set_status(Status(StatusCode.OK))
228
252
  else:
229
- self.spans[-1].set_status(
253
+ context_span.span.set_status(
230
254
  Status(StatusCode.ERROR, description=str(exc_value))
231
255
  )
232
- dbos_tracer.end_span(self.spans.pop())
256
+ dbos_tracer.end_span(context_span.span)
257
+ context_span.context_manager.__exit__(None, None, None)
233
258
 
234
259
  def set_authentication(
235
260
  self, user: Optional[str], roles: Optional[List[str]]
236
261
  ) -> None:
237
262
  self.authenticated_user = user
238
263
  self.authenticated_roles = roles
239
- if user is not None and len(self.spans) > 0:
240
- self.spans[-1].set_attribute("authenticatedUser", user)
241
- self.spans[-1].set_attribute(
264
+ if user is not None and len(self.context_spans) > 0:
265
+ self.context_spans[-1].span.set_attribute("authenticatedUser", user)
266
+ self.context_spans[-1].span.set_attribute(
242
267
  "authenticatedUserRoles", json.dumps(roles) if roles is not None else ""
243
268
  )
244
269
 
@@ -126,7 +126,7 @@ class DBOSDeadLetterQueueError(DBOSException):
126
126
 
127
127
  def __init__(self, wf_id: str, max_retries: int):
128
128
  super().__init__(
129
- f"Workflow {wf_id} has been moved to the dead-letter queue after exceeding the maximum of ${max_retries} retries",
129
+ f"Workflow {wf_id} has been moved to the dead-letter queue after exceeding the maximum of {max_retries} retries",
130
130
  dbos_error_code=DBOSErrorCode.DeadLetterQueueError.value,
131
131
  )
132
132
 
@@ -437,7 +437,14 @@ class SystemDatabase:
437
437
 
438
438
  # Values to update when a row already exists for this workflow
439
439
  update_values: dict[str, Any] = {
440
- "recovery_attempts": SystemSchema.workflow_status.c.recovery_attempts + 1,
440
+ "recovery_attempts": sa.case(
441
+ (
442
+ SystemSchema.workflow_status.c.status
443
+ != WorkflowStatusString.ENQUEUED.value,
444
+ SystemSchema.workflow_status.c.recovery_attempts + 1,
445
+ ),
446
+ else_=SystemSchema.workflow_status.c.recovery_attempts,
447
+ ),
441
448
  "updated_at": func.extract("epoch", func.now()) * 1000,
442
449
  }
443
450
  # Don't update an existing executor ID when enqueueing a workflow.
@@ -27,7 +27,7 @@ dependencies = [
27
27
  ]
28
28
  requires-python = ">=3.9"
29
29
  readme = "README.md"
30
- version = "1.8.0a1"
30
+ version = "1.8.0a3"
31
31
 
32
32
  [project.license]
33
33
  text = "MIT"
@@ -1016,6 +1016,13 @@ def test_dlq_enqueued_workflows(dbos: DBOS) -> None:
1016
1016
  blocked_handle = queue.enqueue(blocked_workflow)
1017
1017
  regular_handle = queue.enqueue(regular_workflow)
1018
1018
 
1019
+ # Enqueue the blocked workflow repeatedly, verify recovery attempts is not increased
1020
+ for _ in range(max_recovery_attempts):
1021
+ with SetWorkflowID(blocked_handle.workflow_id):
1022
+ queue.enqueue(blocked_workflow)
1023
+ recovery_attempts = blocked_handle.get_status().recovery_attempts
1024
+ assert recovery_attempts is not None and recovery_attempts <= 1
1025
+
1019
1026
  # Verify that the blocked workflow starts and is PENDING while the regular workflow remains ENQUEUED.
1020
1027
  start_event.wait()
1021
1028
  assert blocked_handle.get_status().status == WorkflowStatusString.PENDING.value
@@ -0,0 +1,272 @@
1
+ from typing import Tuple
2
+
3
+ import pytest
4
+ from fastapi import FastAPI
5
+ from fastapi.testclient import TestClient
6
+ from opentelemetry._logs import set_logger_provider
7
+ from opentelemetry.sdk import trace as tracesdk
8
+ from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
9
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, InMemoryLogExporter
10
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
11
+ from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
12
+ from opentelemetry.trace.span import format_trace_id
13
+
14
+ from dbos import DBOS, DBOSConfig
15
+ from dbos._logger import dbos_logger
16
+ from dbos._tracer import dbos_tracer
17
+ from dbos._utils import GlobalParams
18
+
19
+
20
+ def test_spans(config: DBOSConfig) -> None:
21
+ DBOS.destroy(destroy_registry=True)
22
+ config["otlp_attributes"] = {"foo": "bar"}
23
+ DBOS(config=config)
24
+ DBOS.launch()
25
+
26
+ @DBOS.workflow()
27
+ def test_workflow() -> None:
28
+ test_step()
29
+ current_span = DBOS.span
30
+ subspan = DBOS.tracer.start_span({"name": "a new span"}, parent=current_span)
31
+ # Note: DBOS.tracer.start_span() does not set the new span as the current span. So this log is still attached to the workflow span.
32
+ DBOS.logger.info("This is a test_workflow")
33
+ subspan.add_event("greeting_event", {"name": "a new event"})
34
+ DBOS.tracer.end_span(subspan)
35
+
36
+ @DBOS.step()
37
+ def test_step() -> None:
38
+ DBOS.logger.info("This is a test_step")
39
+ return
40
+
41
+ exporter = InMemorySpanExporter()
42
+ span_processor = SimpleSpanProcessor(exporter)
43
+ provider = tracesdk.TracerProvider()
44
+ provider.add_span_processor(span_processor)
45
+ dbos_tracer.set_provider(provider)
46
+
47
+ # Set up in-memory log exporter
48
+ log_exporter = InMemoryLogExporter() # type: ignore
49
+ log_processor = BatchLogRecordProcessor(log_exporter)
50
+ log_provider = LoggerProvider()
51
+ log_provider.add_log_record_processor(log_processor)
52
+ set_logger_provider(log_provider)
53
+ dbos_logger.addHandler(LoggingHandler(logger_provider=log_provider))
54
+
55
+ test_workflow()
56
+ test_step()
57
+
58
+ log_processor.force_flush(timeout_millis=5000)
59
+ logs = log_exporter.get_finished_logs()
60
+ assert len(logs) == 3
61
+ for log in logs:
62
+ assert log.log_record.attributes is not None
63
+ assert (
64
+ log.log_record.attributes["applicationVersion"] == GlobalParams.app_version
65
+ )
66
+ assert log.log_record.attributes["executorID"] == GlobalParams.executor_id
67
+ assert log.log_record.attributes["foo"] == "bar"
68
+ # Make sure the log record has a span_id and trace_id
69
+ assert log.log_record.span_id is not None and log.log_record.span_id > 0
70
+ assert log.log_record.trace_id is not None and log.log_record.trace_id > 0
71
+ assert (
72
+ log.log_record.body == "This is a test_step"
73
+ or log.log_record.body == "This is a test_workflow"
74
+ )
75
+ assert log.log_record.attributes["traceId"] == format_trace_id(
76
+ log.log_record.trace_id
77
+ )
78
+
79
+ spans = exporter.get_finished_spans()
80
+
81
+ assert len(spans) == 5
82
+
83
+ for span in spans:
84
+ assert span.attributes is not None
85
+ assert span.attributes["applicationVersion"] == GlobalParams.app_version
86
+ assert span.attributes["executorID"] == GlobalParams.executor_id
87
+ assert span.context is not None
88
+ assert span.attributes["foo"] == "bar"
89
+ assert span.context.span_id > 0
90
+ assert span.context.trace_id > 0
91
+
92
+ assert spans[0].name == test_step.__qualname__
93
+ assert spans[1].name == "a new span"
94
+ assert spans[2].name == test_workflow.__qualname__
95
+ assert spans[3].name == test_step.__qualname__
96
+ assert spans[4].name == f"<temp>.{test_step.__qualname__}"
97
+
98
+ assert spans[0].parent.span_id == spans[2].context.span_id # type: ignore
99
+ assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
100
+ assert spans[2].parent == None
101
+ assert spans[3].parent.span_id == spans[4].context.span_id # type: ignore
102
+ assert spans[4].parent == None
103
+
104
+ # Span ID and trace ID should match the log record
105
+ # For pyright
106
+ assert spans[0].context is not None
107
+ assert spans[2].context is not None
108
+ assert spans[3].context is not None
109
+ assert logs[0].log_record.span_id == spans[0].context.span_id
110
+ assert logs[0].log_record.trace_id == spans[0].context.trace_id
111
+ assert logs[1].log_record.span_id == spans[2].context.span_id
112
+ assert logs[1].log_record.trace_id == spans[2].context.trace_id
113
+ assert logs[2].log_record.span_id == spans[3].context.span_id
114
+ assert logs[2].log_record.trace_id == spans[3].context.trace_id
115
+
116
+
117
+ @pytest.mark.asyncio
118
+ async def test_spans_async(dbos: DBOS) -> None:
119
+
120
+ @DBOS.workflow()
121
+ async def test_workflow() -> None:
122
+ await test_step()
123
+ current_span = DBOS.span
124
+ subspan = DBOS.tracer.start_span({"name": "a new span"}, parent=current_span)
125
+ # Note: DBOS.tracer.start_span() does not set the new span as the current span. So this log is still attached to the workflow span.
126
+ DBOS.logger.info("This is a test_workflow")
127
+ subspan.add_event("greeting_event", {"name": "a new event"})
128
+ DBOS.tracer.end_span(subspan)
129
+
130
+ @DBOS.step()
131
+ async def test_step() -> None:
132
+ DBOS.logger.info("This is a test_step")
133
+ return
134
+
135
+ exporter = InMemorySpanExporter()
136
+ span_processor = SimpleSpanProcessor(exporter)
137
+ provider = tracesdk.TracerProvider()
138
+ provider.add_span_processor(span_processor)
139
+ dbos_tracer.set_provider(provider)
140
+
141
+ # Set up in-memory log exporter
142
+ log_exporter = InMemoryLogExporter() # type: ignore
143
+ log_processor = BatchLogRecordProcessor(log_exporter)
144
+ log_provider = LoggerProvider()
145
+ log_provider.add_log_record_processor(log_processor)
146
+ set_logger_provider(log_provider)
147
+ dbos_logger.addHandler(LoggingHandler(logger_provider=log_provider))
148
+
149
+ await test_workflow()
150
+ await test_step()
151
+
152
+ log_processor.force_flush(timeout_millis=5000)
153
+ logs = log_exporter.get_finished_logs()
154
+ assert len(logs) == 3
155
+ for log in logs:
156
+ assert log.log_record.attributes is not None
157
+ assert (
158
+ log.log_record.attributes["applicationVersion"] == GlobalParams.app_version
159
+ )
160
+ assert log.log_record.attributes["executorID"] == GlobalParams.executor_id
161
+ # Make sure the log record has a span_id and trace_id
162
+ assert log.log_record.span_id is not None and log.log_record.span_id > 0
163
+ assert log.log_record.trace_id is not None and log.log_record.trace_id > 0
164
+ assert (
165
+ log.log_record.body == "This is a test_step"
166
+ or log.log_record.body == "This is a test_workflow"
167
+ )
168
+ assert log.log_record.attributes["traceId"] == format_trace_id(
169
+ log.log_record.trace_id
170
+ )
171
+
172
+ spans = exporter.get_finished_spans()
173
+
174
+ assert len(spans) == 5
175
+
176
+ for span in spans:
177
+ assert span.attributes is not None
178
+ assert span.attributes["applicationVersion"] == GlobalParams.app_version
179
+ assert span.attributes["executorID"] == GlobalParams.executor_id
180
+ assert span.context is not None
181
+ assert span.context.span_id > 0
182
+ assert span.context.trace_id > 0
183
+
184
+ assert spans[0].name == test_step.__qualname__
185
+ assert spans[1].name == "a new span"
186
+ assert spans[2].name == test_workflow.__qualname__
187
+ assert spans[3].name == test_step.__qualname__
188
+ assert spans[4].name == f"<temp>.{test_step.__qualname__}"
189
+
190
+ assert spans[0].parent.span_id == spans[2].context.span_id # type: ignore
191
+ assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
192
+ assert spans[2].parent == None
193
+ assert spans[3].parent.span_id == spans[4].context.span_id # type: ignore
194
+ assert spans[4].parent == None
195
+
196
+ # Span ID and trace ID should match the log record
197
+ assert spans[0].context is not None
198
+ assert spans[2].context is not None
199
+ assert spans[3].context is not None
200
+ assert logs[0].log_record.span_id == spans[0].context.span_id
201
+ assert logs[0].log_record.trace_id == spans[0].context.trace_id
202
+ assert logs[1].log_record.span_id == spans[2].context.span_id
203
+ assert logs[1].log_record.trace_id == spans[2].context.trace_id
204
+ assert logs[2].log_record.span_id == spans[3].context.span_id
205
+ assert logs[2].log_record.trace_id == spans[3].context.trace_id
206
+
207
+
208
+ def test_temp_wf_fastapi(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
209
+ dbos, app = dbos_fastapi
210
+
211
+ @app.get("/step")
212
+ @DBOS.step()
213
+ def test_step_endpoint() -> str:
214
+ dbos.logger.info("This is a test_step_endpoint")
215
+ return "test"
216
+
217
+ exporter = InMemorySpanExporter()
218
+ span_processor = SimpleSpanProcessor(exporter)
219
+ provider = tracesdk.TracerProvider()
220
+ provider.add_span_processor(span_processor)
221
+ dbos_tracer.set_provider(provider)
222
+
223
+ # Set up in-memory log exporter
224
+ log_exporter = InMemoryLogExporter() # type: ignore
225
+ log_processor = BatchLogRecordProcessor(log_exporter)
226
+ log_provider = LoggerProvider()
227
+ log_provider.add_log_record_processor(log_processor)
228
+ set_logger_provider(log_provider)
229
+ dbos_logger.addHandler(LoggingHandler(logger_provider=log_provider))
230
+
231
+ client = TestClient(app)
232
+ response = client.get("/step")
233
+ assert response.status_code == 200
234
+ assert response.text == '"test"'
235
+
236
+ log_processor.force_flush(timeout_millis=5000)
237
+ logs = log_exporter.get_finished_logs()
238
+ assert len(logs) == 1
239
+ assert logs[0].log_record.attributes is not None
240
+ assert (
241
+ logs[0].log_record.attributes["applicationVersion"] == GlobalParams.app_version
242
+ )
243
+ assert logs[0].log_record.span_id is not None and logs[0].log_record.span_id > 0
244
+ assert logs[0].log_record.trace_id is not None and logs[0].log_record.trace_id > 0
245
+ assert logs[0].log_record.body == "This is a test_step_endpoint"
246
+ assert logs[0].log_record.attributes["traceId"] == format_trace_id(
247
+ logs[0].log_record.trace_id
248
+ )
249
+
250
+ spans = exporter.get_finished_spans()
251
+
252
+ assert len(spans) == 3
253
+
254
+ for span in spans:
255
+ assert span.attributes is not None
256
+ assert span.attributes["applicationVersion"] == GlobalParams.app_version
257
+ assert span.context is not None
258
+ assert span.context.span_id > 0
259
+ assert span.context.trace_id > 0
260
+
261
+ assert spans[0].name == test_step_endpoint.__qualname__
262
+ assert spans[1].name == f"<temp>.{test_step_endpoint.__qualname__}"
263
+ assert spans[2].name == "/step"
264
+
265
+ assert spans[0].parent.span_id == spans[1].context.span_id # type: ignore
266
+ assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
267
+ assert spans[2].parent == None
268
+
269
+ # Span ID and trace ID should match the log record
270
+ assert spans[0].context is not None
271
+ assert logs[0].log_record.span_id == spans[0].context.span_id
272
+ assert logs[0].log_record.trace_id == spans[0].context.trace_id
@@ -745,4 +745,4 @@ def test_global_timeout(dbos: DBOS) -> None:
745
745
  with pytest.raises(DBOSWorkflowCancelledError):
746
746
  handle.get_result()
747
747
  event.set()
748
- final_handle.get_result() is not None
748
+ assert final_handle.get_result() is not None
@@ -1,147 +0,0 @@
1
- from typing import Tuple
2
-
3
- import pytest
4
- from fastapi import FastAPI
5
- from fastapi.testclient import TestClient
6
- from opentelemetry.sdk import trace as tracesdk
7
- from opentelemetry.sdk.trace.export import SimpleSpanProcessor
8
- from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
9
-
10
- from dbos import DBOS, DBOSConfig
11
- from dbos._tracer import dbos_tracer
12
- from dbos._utils import GlobalParams
13
-
14
-
15
- def test_spans(config: DBOSConfig) -> None:
16
- DBOS.destroy(destroy_registry=True)
17
- config["otlp_attributes"] = {"foo": "bar"}
18
- DBOS(config=config)
19
- DBOS.launch()
20
-
21
- @DBOS.workflow()
22
- def test_workflow() -> None:
23
- test_step()
24
- current_span = DBOS.span
25
- subspan = DBOS.tracer.start_span({"name": "a new span"}, parent=current_span)
26
- subspan.add_event("greeting_event", {"name": "a new event"})
27
- DBOS.tracer.end_span(subspan)
28
-
29
- @DBOS.step()
30
- def test_step() -> None:
31
- return
32
-
33
- exporter = InMemorySpanExporter()
34
- span_processor = SimpleSpanProcessor(exporter)
35
- provider = tracesdk.TracerProvider()
36
- provider.add_span_processor(span_processor)
37
- dbos_tracer.set_provider(provider)
38
-
39
- test_workflow()
40
- test_step()
41
-
42
- spans = exporter.get_finished_spans()
43
-
44
- assert len(spans) == 5
45
-
46
- for span in spans:
47
- assert span.attributes is not None
48
- assert span.attributes["applicationVersion"] == GlobalParams.app_version
49
- assert span.attributes["executorID"] == GlobalParams.executor_id
50
- assert span.context is not None
51
- assert span.attributes["foo"] == "bar"
52
-
53
- assert spans[0].name == test_step.__qualname__
54
- assert spans[1].name == "a new span"
55
- assert spans[2].name == test_workflow.__qualname__
56
- assert spans[3].name == test_step.__qualname__
57
- assert spans[4].name == f"<temp>.{test_step.__qualname__}"
58
-
59
- assert spans[0].parent.span_id == spans[2].context.span_id # type: ignore
60
- assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
61
- assert spans[2].parent == None
62
- assert spans[3].parent.span_id == spans[4].context.span_id # type: ignore
63
- assert spans[4].parent == None
64
-
65
-
66
- @pytest.mark.asyncio
67
- async def test_spans_async(dbos: DBOS) -> None:
68
-
69
- @DBOS.workflow()
70
- async def test_workflow() -> None:
71
- await test_step()
72
- current_span = DBOS.span
73
- subspan = DBOS.tracer.start_span({"name": "a new span"}, parent=current_span)
74
- subspan.add_event("greeting_event", {"name": "a new event"})
75
- DBOS.tracer.end_span(subspan)
76
-
77
- @DBOS.step()
78
- async def test_step() -> None:
79
- return
80
-
81
- exporter = InMemorySpanExporter()
82
- span_processor = SimpleSpanProcessor(exporter)
83
- provider = tracesdk.TracerProvider()
84
- provider.add_span_processor(span_processor)
85
- dbos_tracer.set_provider(provider)
86
-
87
- await test_workflow()
88
- await test_step()
89
-
90
- spans = exporter.get_finished_spans()
91
-
92
- assert len(spans) == 5
93
-
94
- for span in spans:
95
- assert span.attributes is not None
96
- assert span.attributes["applicationVersion"] == GlobalParams.app_version
97
- assert span.attributes["executorID"] == GlobalParams.executor_id
98
- assert span.context is not None
99
-
100
- assert spans[0].name == test_step.__qualname__
101
- assert spans[1].name == "a new span"
102
- assert spans[2].name == test_workflow.__qualname__
103
- assert spans[3].name == test_step.__qualname__
104
- assert spans[4].name == f"<temp>.{test_step.__qualname__}"
105
-
106
- assert spans[0].parent.span_id == spans[2].context.span_id # type: ignore
107
- assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
108
- assert spans[2].parent == None
109
- assert spans[3].parent.span_id == spans[4].context.span_id # type: ignore
110
- assert spans[4].parent == None
111
-
112
-
113
- def test_temp_wf_fastapi(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
114
- dbos, app = dbos_fastapi
115
-
116
- @app.get("/step")
117
- @DBOS.step()
118
- def test_step_endpoint() -> str:
119
- return "test"
120
-
121
- exporter = InMemorySpanExporter()
122
- span_processor = SimpleSpanProcessor(exporter)
123
- provider = tracesdk.TracerProvider()
124
- provider.add_span_processor(span_processor)
125
- dbos_tracer.set_provider(provider)
126
-
127
- client = TestClient(app)
128
- response = client.get("/step")
129
- assert response.status_code == 200
130
- assert response.text == '"test"'
131
-
132
- spans = exporter.get_finished_spans()
133
-
134
- assert len(spans) == 3
135
-
136
- for span in spans:
137
- assert span.attributes is not None
138
- assert span.attributes["applicationVersion"] == GlobalParams.app_version
139
- assert span.context is not None
140
-
141
- assert spans[0].name == test_step_endpoint.__qualname__
142
- assert spans[1].name == f"<temp>.{test_step_endpoint.__qualname__}"
143
- assert spans[2].name == "/step"
144
-
145
- assert spans[0].parent.span_id == spans[1].context.span_id # type: ignore
146
- assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
147
- assert spans[2].parent == None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes