diff --git a/CHANGELOG.md b/CHANGELOG.md index bcfcfffec0..fbda4fcd5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3875](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3875)) - `opentelemetry-instrumentation-aws-lambda`: Fix ImportError with slash-delimited handler paths ([#3894](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3894)) +- `opentelemetry-exporter-richconsole`: Prevent deadlock when parent span is not part of the batch + ([#3900](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3900)) ## Version 1.38.0/0.59b0 (2025-10-16) diff --git a/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole/__init__.py b/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole/__init__.py index 59415e2bd8..3e3aab7c74 100644 --- a/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole/__init__.py +++ b/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole/__init__.py @@ -173,14 +173,17 @@ def spans_to_tree(spans: typing.Sequence[ReadableSpan]) -> Dict[str, Tree]: trees = {} parents = {} spans = list(spans) + span_ids = {s.context.span_id for s in spans} while spans: for span in spans: - if not span.parent: + if not span.parent or span.parent.span_id not in span_ids: trace_id = opentelemetry.trace.format_trace_id( span.context.trace_id ) - trees[trace_id] = Tree(label=f"Trace {trace_id}") - child = trees[trace_id].add( + tree = trees.setdefault( + trace_id, Tree(label=f"Trace {trace_id}") + ) + child = tree.add( label=Text.from_markup( f"[blue][{_ns_to_time(span.start_time)}][/blue] [bold]{span.name}[/bold], span {opentelemetry.trace.format_span_id(span.context.span_id)}" ) diff --git a/exporter/opentelemetry-exporter-richconsole/test-requirements.txt b/exporter/opentelemetry-exporter-richconsole/test-requirements.txt index fb9ccfdb7e..40d37f1d66 100644 --- a/exporter/opentelemetry-exporter-richconsole/test-requirements.txt +++ b/exporter/opentelemetry-exporter-richconsole/test-requirements.txt @@ -9,6 +9,7 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 Pygments==2.17.2 pytest==7.4.4 +pytest-timeout==2.3.1 rich==13.7.1 tomli==2.0.1 typing_extensions==4.12.2 diff --git a/exporter/opentelemetry-exporter-richconsole/tests/test_rich_exporter.py b/exporter/opentelemetry-exporter-richconsole/tests/test_rich_exporter.py index f4dcd49fe9..44bebb4839 100644 --- a/exporter/opentelemetry-exporter-richconsole/tests/test_rich_exporter.py +++ b/exporter/opentelemetry-exporter-richconsole/tests/test_rich_exporter.py @@ -96,3 +96,15 @@ def test_multiple_traces(tracer_provider): parent_2.name in child.label for child in trees[traceid_1].children[0].children ) + + +@pytest.mark.timeout(30) +def test_no_deadlock(tracer_provider): + # non-regression test for https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3254 + + tracer = tracer_provider.get_tracer(__name__) + with tracer.start_as_current_span("parent"): + with tracer.start_as_current_span("child") as child: + pass + + RichConsoleSpanExporter.spans_to_tree((child,))