From 9481893f73ad456ae171b93ee3ac81de8b775f94 Mon Sep 17 00:00:00 2001 From: Chittal Patel <55499811+chittalpatel@users.noreply.github.com> Date: Thu, 29 Feb 2024 03:29:28 +0000 Subject: [PATCH] Add Span.add_link() method to add link after span start. (#3618) * Add Span.add_link() method to add link after span start. * Update CHANGELOG.md * Update CHANGELOG.md * Unmark Span.add_link() as abstract method and add warning. * Add Span.add_link() method to add link after span start. * Update CHANGELOG.md * Update CHANGELOG.md * Unmark Span.add_link() as abstract method and add warning. * Update warning message for Span.add_link(). --------- Co-authored-by: Diego Hurtado --- CHANGELOG.md | 2 ++ .../src/opentelemetry/trace/__init__.py | 5 +-- .../src/opentelemetry/trace/span.py | 28 +++++++++++++++++ .../src/opentelemetry/sdk/trace/__init__.py | 24 ++++++++++++++ opentelemetry-sdk/tests/trace/test_trace.py | 31 +++++++++++++++++++ 5 files changed, 86 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3edb32859a..276a5e9a037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3567](https://github.com/open-telemetry/opentelemetry-python/pull/3567)) - Fix flush error when no LoggerProvider configured for LoggingHandler ([#3608](https://github.com/open-telemetry/opentelemetry-python/pull/3608)) +- Add `Span.add_link()` method to add link after span start + ([#3618](https://github.com/open-telemetry/opentelemetry-python/pull/3618)) - Fix `OTLPMetricExporter` ignores `preferred_aggregation` property ([#3603](https://github.com/open-telemetry/opentelemetry-python/pull/3603)) - Logs: set `observed_timestamp` field diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index bf9e0b89a45..9f9abe0f719 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -85,7 +85,6 @@ from deprecated import deprecated from opentelemetry import context as context_api -from opentelemetry.attributes import BoundedAttributes # type: ignore from opentelemetry.context.context import Context from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER from opentelemetry.trace.propagation import ( @@ -144,9 +143,7 @@ def __init__( attributes: types.Attributes = None, ) -> None: super().__init__(context) - self._attributes = BoundedAttributes( - attributes=attributes - ) # type: types.Attributes + self._attributes = attributes @property def attributes(self) -> types.Attributes: diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 327d85fef48..8201fdb251e 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -3,6 +3,7 @@ import re import types as python_types import typing +import warnings from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types @@ -117,6 +118,26 @@ def add_event( timestamp if the `timestamp` argument is omitted. """ + def add_link( # pylint: disable=no-self-use + self, + context: "SpanContext", + attributes: types.Attributes = None, + ) -> None: + """Adds a `Link`. + + 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. + + Note: It is preferred to add links at span creation, instead of calling + this method later since samplers can only consider information already + present during span creation. + """ + warnings.warn( + "Span.add_link() not implemented and will be a no-op. " + "Use opentelemetry-sdk >= 1.23 to add links after span creation" + ) + @abc.abstractmethod def update_name(self, name: str) -> None: """Updates the `Span` name. @@ -523,6 +544,13 @@ def add_event( ) -> None: pass + def add_link( + self, + context: "SpanContext", + attributes: types.Attributes = None, + ) -> None: + pass + def update_name(self, name: str) -> None: pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 344838ba186..f937ece8958 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -850,6 +850,30 @@ def add_event( ) ) + @_check_span_ended + def _add_link(self, link: trace_api.Link) -> None: + self._links.append(link) + + def add_link( + self, + context: SpanContext, + attributes: types.Attributes = None, + ) -> None: + if context is None or not context.is_valid: + return + + attributes = BoundedAttributes( + self._limits.max_link_attributes, + attributes, + max_value_len=self._limits.max_attribute_length, + ) + self._add_link( + trace_api.Link( + context=context, + attributes=attributes, + ) + ) + def _readable_span(self) -> ReadableSpan: return ReadableSpan( name=self._name, diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 4150d60d104..40f4f4b3162 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -900,6 +900,37 @@ def test_links(self): with self.assertRaises(TypeError): root.links[1].attributes["name"] = "new_neighbour" + def test_add_link(self): + id_generator = RandomIdGenerator() + other_context = trace_api.SpanContext( + trace_id=id_generator.generate_trace_id(), + span_id=id_generator.generate_span_id(), + is_remote=False, + ) + + with self.tracer.start_as_current_span("root") as root: + root.add_link(other_context, {"name": "neighbor"}) + + self.assertEqual(len(root.links), 1) + self.assertEqual( + root.links[0].context.trace_id, other_context.trace_id + ) + self.assertEqual( + root.links[0].context.span_id, other_context.span_id + ) + self.assertEqual(root.links[0].attributes, {"name": "neighbor"}) + + with self.assertRaises(TypeError): + root.links[0].attributes["name"] = "new_neighbour" + + def test_add_link_with_invalid_span_context(self): + other_context = trace_api.INVALID_SPAN_CONTEXT + + with self.tracer.start_as_current_span("root") as root: + root.add_link(other_context) + + self.assertEqual(len(root.links), 0) + def test_update_name(self): with self.tracer.start_as_current_span("root") as root: # name