diff --git a/CHANGELOG.md b/CHANGELOG.md index c0171dab168..6f38f17f133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3823] (https://github.com/open-telemetry/opentelemetry-python/pull/3823)) - Add span flags to OTLP spans and links ([#3881](https://github.com/open-telemetry/opentelemetry-python/pull/3881)) +- Record links with invalid SpanContext if either attributes or TraceState are not empty + ([#3917](https://github.com/open-telemetry/opentelemetry-python/pull/3917/)) - Add OpenTelemetry trove classifiers to PyPI packages ([#3913] (https://github.com/open-telemetry/opentelemetry-python/pull/3913)) diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 5d46ffcb4a9..4afc4d520a2 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -127,7 +127,8 @@ def add_link( # pylint: disable=no-self-use Adds a single `Link` with the `SpanContext` of the span to link to and, optionally, attributes passed as arguments. Implementations may ignore - calls with an invalid span context. + calls with an invalid span context if both attributes and TraceState + are empty. Note: It is preferred to add links at span creation, instead of calling this method later since samplers can only consider information already diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 55a375315b4..99c0825f9e5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -347,6 +347,12 @@ def wrapper(self, *args, **kwargs): return wrapper +def _is_valid_link(context: SpanContext, attributes: types.Attributes) -> bool: + return bool( + context and (context.is_valid or (attributes or context.trace_state)) + ) + + class ReadableSpan: """Provides read-only access to span attributes. @@ -867,7 +873,8 @@ def add_link( context: SpanContext, attributes: types.Attributes = None, ) -> None: - if context is None or not context.is_valid: + + if not _is_valid_link(context, attributes): return attributes = BoundedAttributes( diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index f0334194cea..30f4f0e2731 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -941,9 +941,30 @@ def test_add_link_with_invalid_span_context(self): with self.tracer.start_as_current_span("root") as root: root.add_link(other_context) - + root.add_link(None) self.assertEqual(len(root.links), 0) + def test_add_link_with_invalid_span_context_with_attributes(self): + invalid_context = trace_api.INVALID_SPAN_CONTEXT + + with self.tracer.start_as_current_span("root") as root: + root.add_link(invalid_context, {"name": "neighbor"}) + self.assertEqual(len(root.links), 1) + self.assertEqual(root.links[0].attributes, {"name": "neighbor"}) + + def test_add_link_with_invalid_span_context_with_tracestate(self): + invalid_context = trace.SpanContext( + trace_api.INVALID_TRACE_ID, + trace_api.INVALID_SPAN_ID, + is_remote=False, + trace_state="foo=bar", + ) + + with self.tracer.start_as_current_span("root") as root: + root.add_link(invalid_context) + self.assertEqual(len(root.links), 1) + self.assertEqual(root.links[0].context.trace_state, "foo=bar") + def test_update_name(self): with self.tracer.start_as_current_span("root") as root: # name