dbos 1.14.0a4__tar.gz → 1.14.0a5__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 (116) hide show
  1. {dbos-1.14.0a4 → dbos-1.14.0a5}/PKG-INFO +1 -1
  2. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_context.py +9 -2
  3. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_core.py +2 -2
  4. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_dbos.py +1 -1
  5. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_logger.py +1 -1
  6. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_tracer.py +7 -0
  7. {dbos-1.14.0a4 → dbos-1.14.0a5}/pyproject.toml +2 -1
  8. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_spans.py +128 -34
  9. {dbos-1.14.0a4 → dbos-1.14.0a5}/LICENSE +0 -0
  10. {dbos-1.14.0a4 → dbos-1.14.0a5}/README.md +0 -0
  11. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/__init__.py +0 -0
  12. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/__main__.py +0 -0
  13. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_admin_server.py +0 -0
  14. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/env.py +0 -0
  15. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/script.py.mako +0 -0
  16. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/01ce9f07bd10_streaming.py +0 -0
  17. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  18. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
  19. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/471b60d64126_dbos_migrations.py +0 -0
  20. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  21. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  22. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/66478e1b95e5_consolidate_queues.py +0 -0
  23. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
  24. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
  25. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  26. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  27. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  28. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/d994145b47b6_consolidate_inputs.py +0 -0
  29. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  30. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_alembic_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  31. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_app_db.py +0 -0
  32. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_classproperty.py +0 -0
  33. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_client.py +0 -0
  34. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_conductor/conductor.py +0 -0
  35. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_conductor/protocol.py +0 -0
  36. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_croniter.py +0 -0
  37. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_dbos_config.py +0 -0
  38. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_debug.py +0 -0
  39. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_docker_pg_helper.py +0 -0
  40. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_error.py +0 -0
  41. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_event_loop.py +0 -0
  42. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_fastapi.py +0 -0
  43. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_flask.py +0 -0
  44. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_kafka.py +0 -0
  45. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_kafka_message.py +0 -0
  46. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_migration.py +0 -0
  47. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_outcome.py +0 -0
  48. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_queue.py +0 -0
  49. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_recovery.py +0 -0
  50. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_registrations.py +0 -0
  51. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_roles.py +0 -0
  52. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_scheduler.py +0 -0
  53. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_schemas/__init__.py +0 -0
  54. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_schemas/application_database.py +0 -0
  55. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_schemas/system_database.py +0 -0
  56. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_serialization.py +0 -0
  57. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_sys_db.py +0 -0
  58. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_sys_db_postgres.py +0 -0
  59. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_sys_db_sqlite.py +0 -0
  60. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_templates/dbos-db-starter/README.md +0 -0
  61. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  62. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
  63. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  64. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  65. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  66. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  67. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  68. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  69. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  70. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_utils.py +0 -0
  71. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/_workflow_commands.py +0 -0
  72. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/cli/_github_init.py +0 -0
  73. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/cli/_template_init.py +0 -0
  74. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/cli/cli.py +0 -0
  75. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/cli/migration.py +0 -0
  76. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/dbos-config.schema.json +0 -0
  77. {dbos-1.14.0a4 → dbos-1.14.0a5}/dbos/py.typed +0 -0
  78. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/__init__.py +0 -0
  79. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/atexit_no_ctor.py +0 -0
  80. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/atexit_no_launch.py +0 -0
  81. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/classdefs.py +0 -0
  82. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/client_collateral.py +0 -0
  83. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/client_worker.py +0 -0
  84. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/conftest.py +0 -0
  85. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/dupname_classdefs1.py +0 -0
  86. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/dupname_classdefsa.py +0 -0
  87. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/more_classdefs.py +0 -0
  88. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/queuedworkflow.py +0 -0
  89. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_admin_server.py +0 -0
  90. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_async.py +0 -0
  91. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_async_workflow_management.py +0 -0
  92. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_classdecorators.py +0 -0
  93. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_cli.py +0 -0
  94. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_client.py +0 -0
  95. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_concurrency.py +0 -0
  96. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_config.py +0 -0
  97. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_croniter.py +0 -0
  98. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_dbos.py +0 -0
  99. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_debug.py +0 -0
  100. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_docker_secrets.py +0 -0
  101. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_failures.py +0 -0
  102. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_fastapi.py +0 -0
  103. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_fastapi_roles.py +0 -0
  104. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_flask.py +0 -0
  105. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_kafka.py +0 -0
  106. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_outcome.py +0 -0
  107. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_package.py +0 -0
  108. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_queue.py +0 -0
  109. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_scheduler.py +0 -0
  110. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_schema_migration.py +0 -0
  111. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_singleton.py +0 -0
  112. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_sqlalchemy.py +0 -0
  113. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_streaming.py +0 -0
  114. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_workflow_introspection.py +0 -0
  115. {dbos-1.14.0a4 → dbos-1.14.0a5}/tests/test_workflow_management.py +0 -0
  116. {dbos-1.14.0a4 → dbos-1.14.0a5}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 1.14.0a4
3
+ Version: 1.14.0a5
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -215,11 +215,18 @@ class DBOSContext:
215
215
  def end_handler(self, exc_value: Optional[BaseException]) -> None:
216
216
  self._end_span(exc_value)
217
217
 
218
- def get_current_span(self) -> Optional[Span]:
218
+ """ Return the current DBOS span if any. It must be a span created by DBOS."""
219
+
220
+ def get_current_dbos_span(self) -> Optional[Span]:
219
221
  if len(self.context_spans) > 0:
220
222
  return self.context_spans[-1].span
221
223
  return None
222
224
 
225
+ """ Return the current active span if any. It might not be a DBOS span."""
226
+
227
+ def get_current_active_span(self) -> Optional[Span]:
228
+ return dbos_tracer.get_current_span()
229
+
223
230
  def _start_span(self, attributes: TracedAttributes) -> None:
224
231
  if dbos_tracer.disable_otlp:
225
232
  return
@@ -235,7 +242,7 @@ class DBOSContext:
235
242
  attributes["authenticatedUserAssumedRole"] = self.assumed_role
236
243
  span = dbos_tracer.start_span(
237
244
  attributes,
238
- parent=self.context_spans[-1].span if len(self.context_spans) > 0 else None,
245
+ parent=None, # It'll use the current active span as the parent
239
246
  )
240
247
  # Activate the current span
241
248
  cm = use_span(
@@ -971,7 +971,7 @@ def decorate_transaction(
971
971
  dbapi_error
972
972
  ) or dbos._app_db._is_serialization_error(dbapi_error):
973
973
  # Retry on serialization failure
974
- span = ctx.get_current_span()
974
+ span = ctx.get_current_dbos_span()
975
975
  if span:
976
976
  span.add_event(
977
977
  "Transaction Failure",
@@ -1090,7 +1090,7 @@ def decorate_step(
1090
1090
  exc_info=error,
1091
1091
  )
1092
1092
  ctx = assert_current_dbos_context()
1093
- span = ctx.get_current_span()
1093
+ span = ctx.get_current_dbos_span()
1094
1094
  if span:
1095
1095
  span.add_event(
1096
1096
  f"Step attempt {attempt} failed",
@@ -1297,7 +1297,7 @@ class DBOS:
1297
1297
  def span(cls) -> Span:
1298
1298
  """Return the tracing `Span` associated with the current context."""
1299
1299
  ctx = assert_current_dbos_context()
1300
- span = ctx.get_current_span()
1300
+ span = ctx.get_current_active_span()
1301
1301
  assert span
1302
1302
  return span
1303
1303
 
@@ -39,7 +39,7 @@ class DBOSLogTransformer(logging.Filter):
39
39
  if ctx:
40
40
  if ctx.is_within_workflow():
41
41
  record.operationUUID = ctx.workflow_id
42
- span = ctx.get_current_span()
42
+ span = ctx.get_current_active_span()
43
43
  if span:
44
44
  trace_id = format_trace_id(span.get_span_context().trace_id)
45
45
  record.traceId = trace_id
@@ -77,5 +77,12 @@ class DBOSTracer:
77
77
  def end_span(self, span: Span) -> None:
78
78
  span.end()
79
79
 
80
+ def get_current_span(self) -> Optional[Span]:
81
+ # Return the current active span if any. It might not be a DBOS span.
82
+ span = trace.get_current_span()
83
+ if span.get_span_context().is_valid:
84
+ return span
85
+ return None
86
+
80
87
 
81
88
  dbos_tracer = DBOSTracer()
@@ -27,7 +27,7 @@ dependencies = [
27
27
  ]
28
28
  requires-python = ">=3.9"
29
29
  readme = "README.md"
30
- version = "1.14.0a4"
30
+ version = "1.14.0a5"
31
31
 
32
32
  [project.license]
33
33
  text = "MIT"
@@ -90,4 +90,5 @@ dev = [
90
90
  "pyright>=1.1.398",
91
91
  "types-docker>=7.1.0.20241229",
92
92
  "pytest-timeout>=2.3.1",
93
+ "inline-snapshot>=0.28.0",
93
94
  ]
@@ -1,8 +1,10 @@
1
- from typing import Tuple
1
+ from dataclasses import dataclass, field
2
+ from typing import Optional, Tuple
2
3
 
3
4
  import pytest
4
5
  from fastapi import FastAPI
5
6
  from fastapi.testclient import TestClient
7
+ from inline_snapshot import snapshot
6
8
  from opentelemetry._logs import set_logger_provider
7
9
  from opentelemetry.sdk import trace as tracesdk
8
10
  from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
@@ -17,33 +19,47 @@ from dbos._tracer import dbos_tracer
17
19
  from dbos._utils import GlobalParams
18
20
 
19
21
 
22
+ @dataclass
23
+ class BasicSpan:
24
+ content: str
25
+ children: list["BasicSpan"] = field(default_factory=list)
26
+ parent_id: Optional[int] = field(repr=False, compare=False, default=None)
27
+
28
+
20
29
  def test_spans(config: DBOSConfig) -> None:
30
+ exporter = InMemorySpanExporter()
31
+ span_processor = SimpleSpanProcessor(exporter)
32
+ provider = tracesdk.TracerProvider()
33
+ provider.add_span_processor(span_processor)
34
+ dbos_tracer.set_provider(provider)
35
+
21
36
  DBOS.destroy(destroy_registry=True)
22
37
  config["otlp_attributes"] = {"foo": "bar"}
23
38
  DBOS(config=config)
24
39
  DBOS.launch()
25
40
 
41
+ my_tracer = provider.get_tracer("dbos")
42
+
26
43
  @DBOS.workflow()
27
44
  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)
45
+ with my_tracer.start_as_current_span( # pyright: ignore[reportAttributeAccessIssue]
46
+ "manual_span"
47
+ ):
48
+ test_step()
49
+ current_span = DBOS.span
50
+ subspan = DBOS.tracer.start_span(
51
+ {"name": "a new span"}, parent=current_span
52
+ )
53
+ # 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.
54
+ DBOS.logger.info("This is a test_workflow")
55
+ subspan.add_event("greeting_event", {"name": "a new event"})
56
+ DBOS.tracer.end_span(subspan)
35
57
 
36
58
  @DBOS.step()
37
59
  def test_step() -> None:
38
60
  DBOS.logger.info("This is a test_step")
39
61
  return
40
62
 
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
63
  # Set up in-memory log exporter
48
64
  log_exporter = InMemoryLogExporter() # type: ignore
49
65
  log_processor = BatchLogRecordProcessor(log_exporter)
@@ -77,9 +93,10 @@ def test_spans(config: DBOSConfig) -> None:
77
93
 
78
94
  spans = exporter.get_finished_spans()
79
95
 
80
- assert len(spans) == 3
81
-
82
96
  for span in spans:
97
+ if span.name == "manual_span":
98
+ # Skip the manual span because it was not created by DBOS.tracer
99
+ continue
83
100
  assert span.attributes is not None
84
101
  assert span.attributes["applicationVersion"] == GlobalParams.app_version
85
102
  assert span.attributes["executorID"] == GlobalParams.executor_id
@@ -90,11 +107,12 @@ def test_spans(config: DBOSConfig) -> None:
90
107
 
91
108
  assert spans[0].name == test_step.__qualname__
92
109
  assert spans[1].name == "a new span"
93
- assert spans[2].name == test_workflow.__qualname__
110
+ assert spans[3].name == test_workflow.__qualname__
94
111
 
95
112
  assert spans[0].parent.span_id == spans[2].context.span_id # type: ignore
96
113
  assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
97
- assert spans[2].parent == None
114
+ assert spans[2].parent.span_id == spans[3].context.span_id # type: ignore
115
+ assert spans[3].parent == None
98
116
 
99
117
  # Span ID and trace ID should match the log record
100
118
  # For pyright
@@ -105,31 +123,70 @@ def test_spans(config: DBOSConfig) -> None:
105
123
  assert logs[1].log_record.span_id == spans[2].context.span_id
106
124
  assert logs[1].log_record.trace_id == spans[2].context.trace_id
107
125
 
126
+ # Test the span tree structure
127
+ basic_spans = {
128
+ span.context.span_id: BasicSpan( # pyright: ignore[reportOptionalMemberAccess]
129
+ content=span.name, parent_id=span.parent.span_id if span.parent else None
130
+ )
131
+ for span in spans
132
+ }
133
+ root_span = None
134
+ for basic_span in basic_spans.values():
135
+ if basic_span.parent_id is None:
136
+ root_span = basic_span
137
+ else:
138
+ parent_id = basic_span.parent_id
139
+ parent_span = basic_spans[parent_id]
140
+ parent_span.children.append(basic_span)
141
+
142
+ assert len(spans) == 4
143
+ # Make sure the span tree structure is correct
144
+ assert root_span == snapshot(
145
+ BasicSpan(
146
+ content="test_spans.<locals>.test_workflow",
147
+ children=[
148
+ BasicSpan(
149
+ content="manual_span",
150
+ children=[
151
+ BasicSpan(content="test_spans.<locals>.test_step"),
152
+ BasicSpan(content="a new span"),
153
+ ],
154
+ )
155
+ ],
156
+ )
157
+ )
158
+
108
159
 
109
160
  @pytest.mark.asyncio
110
161
  async def test_spans_async(dbos: DBOS) -> None:
162
+ exporter = InMemorySpanExporter()
163
+ span_processor = SimpleSpanProcessor(exporter)
164
+ provider = tracesdk.TracerProvider()
165
+ provider.add_span_processor(span_processor)
166
+ dbos_tracer.set_provider(provider)
167
+
168
+ my_tracer = provider.get_tracer("dbos")
111
169
 
112
170
  @DBOS.workflow()
113
171
  async def test_workflow() -> None:
114
- await test_step()
115
- current_span = DBOS.span
116
- subspan = DBOS.tracer.start_span({"name": "a new span"}, parent=current_span)
117
- # 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.
118
- DBOS.logger.info("This is a test_workflow")
119
- subspan.add_event("greeting_event", {"name": "a new event"})
120
- DBOS.tracer.end_span(subspan)
172
+ with my_tracer.start_as_current_span( # pyright: ignore[reportAttributeAccessIssue]
173
+ "manual_span"
174
+ ):
175
+ await test_step()
176
+ current_span = DBOS.span
177
+ subspan = DBOS.tracer.start_span(
178
+ {"name": "a new span"}, parent=current_span
179
+ )
180
+ # 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.
181
+ DBOS.logger.info("This is a test_workflow")
182
+ subspan.add_event("greeting_event", {"name": "a new event"})
183
+ DBOS.tracer.end_span(subspan)
121
184
 
122
185
  @DBOS.step()
123
186
  async def test_step() -> None:
124
187
  DBOS.logger.info("This is a test_step")
125
188
  return
126
189
 
127
- exporter = InMemorySpanExporter()
128
- span_processor = SimpleSpanProcessor(exporter)
129
- provider = tracesdk.TracerProvider()
130
- provider.add_span_processor(span_processor)
131
- dbos_tracer.set_provider(provider)
132
-
133
190
  # Set up in-memory log exporter
134
191
  log_exporter = InMemoryLogExporter() # type: ignore
135
192
  log_processor = BatchLogRecordProcessor(log_exporter)
@@ -162,9 +219,12 @@ async def test_spans_async(dbos: DBOS) -> None:
162
219
 
163
220
  spans = exporter.get_finished_spans()
164
221
 
165
- assert len(spans) == 3
222
+ assert len(spans) == 4
166
223
 
167
224
  for span in spans:
225
+ if span.name == "manual_span":
226
+ # Skip the manual span because it was not created by DBOS.tracer
227
+ continue
168
228
  assert span.attributes is not None
169
229
  assert span.attributes["applicationVersion"] == GlobalParams.app_version
170
230
  assert span.attributes["executorID"] == GlobalParams.executor_id
@@ -174,11 +234,12 @@ async def test_spans_async(dbos: DBOS) -> None:
174
234
 
175
235
  assert spans[0].name == test_step.__qualname__
176
236
  assert spans[1].name == "a new span"
177
- assert spans[2].name == test_workflow.__qualname__
237
+ assert spans[3].name == test_workflow.__qualname__
178
238
 
179
239
  assert spans[0].parent.span_id == spans[2].context.span_id # type: ignore
180
240
  assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
181
- assert spans[2].parent == None
241
+ assert spans[2].parent.span_id == spans[3].context.span_id # type: ignore
242
+ assert spans[3].parent == None
182
243
 
183
244
  # Span ID and trace ID should match the log record
184
245
  assert spans[0].context is not None
@@ -188,6 +249,39 @@ async def test_spans_async(dbos: DBOS) -> None:
188
249
  assert logs[1].log_record.span_id == spans[2].context.span_id
189
250
  assert logs[1].log_record.trace_id == spans[2].context.trace_id
190
251
 
252
+ # Test the span tree structure
253
+ basic_spans = {
254
+ span.context.span_id: BasicSpan( # pyright: ignore[reportOptionalMemberAccess]
255
+ content=span.name, parent_id=span.parent.span_id if span.parent else None
256
+ )
257
+ for span in spans
258
+ }
259
+ root_span = None
260
+ for basic_span in basic_spans.values():
261
+ if basic_span.parent_id is None:
262
+ root_span = basic_span
263
+ else:
264
+ parent_id = basic_span.parent_id
265
+ parent_span = basic_spans[parent_id]
266
+ parent_span.children.append(basic_span)
267
+
268
+ assert len(spans) == 4
269
+ # Make sure the span tree structure is correct
270
+ assert root_span == snapshot(
271
+ BasicSpan(
272
+ content="test_spans_async.<locals>.test_workflow",
273
+ children=[
274
+ BasicSpan(
275
+ content="manual_span",
276
+ children=[
277
+ BasicSpan(content="test_spans_async.<locals>.test_step"),
278
+ BasicSpan(content="a new span"),
279
+ ],
280
+ )
281
+ ],
282
+ )
283
+ )
284
+
191
285
 
192
286
  def test_wf_fastapi(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
193
287
  dbos, app = dbos_fastapi
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