diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d77447ad31..659032381e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1285](https://github.com/open-telemetry/opentelemetry-python/pull/1285)) - Added `__repr__` for `DefaultSpan`, added `trace_flags` to `__repr__` of `SpanContext` ([#1485](https://github.com/open-telemetry/opentelemetry-python/pull/1485)]) +- Add `Span.set_attributes` method to set multiple values with one call ### Changed - `opentelemetry-exporter-zipkin` Updated zipkin exporter status code and error tag ([#1486](https://github.com/open-telemetry/opentelemetry-python/pull/1486)) diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 507b0513684..a29f0d219a8 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -33,6 +33,17 @@ def get_span_context(self) -> "SpanContext": A :class:`opentelemetry.trace.SpanContext` with a copy of this span's immutable state. """ + @abc.abstractmethod + def set_attributes( + self, attributes: typing.Dict[str, types.AttributeValue] + ) -> None: + """Sets Attributes. + + Sets Attributes with the key and value passed as arguments dict. + + Note: The behavior of `None` value attributes is undefined, and hence strongly discouraged. + """ + @abc.abstractmethod def set_attribute(self, key: str, value: types.AttributeValue) -> None: """Sets an Attribute. @@ -266,6 +277,11 @@ def is_recording(self) -> bool: def end(self, end_time: typing.Optional[int] = None) -> None: pass + def set_attributes( + self, attributes: typing.Dict[str, types.AttributeValue] + ) -> None: + pass + def set_attribute(self, key: str, value: types.AttributeValue) -> None: pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index c8dec7bc62e..c0d8db134fc 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -26,6 +26,7 @@ from typing import ( Any, Callable, + Dict, Iterator, MutableSequence, Optional, @@ -576,29 +577,35 @@ def to_json(self, indent=4): def get_span_context(self): return self.context - def set_attribute(self, key: str, value: types.AttributeValue) -> None: - if not _is_valid_attribute_value(value): - return - - if not key: - logger.warning("invalid key (empty or null)") - return - + def set_attributes( + self, attributes: Dict[str, types.AttributeValue] + ) -> None: with self._lock: if self.end_time is not None: logger.warning("Setting attribute on ended span.") return - # Freeze mutable sequences defensively - if isinstance(value, MutableSequence): - value = tuple(value) - if isinstance(value, bytes): - try: - value = value.decode() - except ValueError: - logger.warning("Byte attribute could not be decoded.") - return - self.attributes[key] = value + for key, value in attributes.items(): + if not _is_valid_attribute_value(value): + continue + + if not key: + logger.warning("invalid key `%s` (empty or null)", key) + continue + + # Freeze mutable sequences defensively + if isinstance(value, MutableSequence): + value = tuple(value) + if isinstance(value, bytes): + try: + value = value.decode() + except ValueError: + logger.warning("Byte attribute could not be decoded.") + return + self.attributes[key] = value + + def set_attribute(self, key: str, value: types.AttributeValue) -> None: + return self.set_attributes({key: value}) @_check_span_ended def _add_event(self, event: EventBase) -> None: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index bfefdfcb42a..1006034ab91 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -483,11 +483,14 @@ def test_basic_span(self): def test_attributes(self): with self.tracer.start_as_current_span("root") as root: - root.set_attribute("component", "http") - root.set_attribute("http.method", "GET") - root.set_attribute( - "http.url", "https://example.com:779/path/12/?q=d#123" + root.set_attributes( + { + "component": "http", + "http.method": "GET", + "http.url": "https://example.com:779/path/12/?q=d#123", + } ) + root.set_attribute("http.status_code", 200) root.set_attribute("http.status_text", "OK") root.set_attribute("misc.pi", 3.14) @@ -545,6 +548,10 @@ def test_attributes(self): def test_invalid_attribute_values(self): with self.tracer.start_as_current_span("root") as root: + root.set_attributes( + {"correct-value": "foo", "non-primitive-data-type": dict(),} + ) + root.set_attribute("non-primitive-data-type", dict()) root.set_attribute( "list-of-mixed-data-types-numeric-first", @@ -561,7 +568,8 @@ def test_invalid_attribute_values(self): root.set_attribute("", 123) root.set_attribute(None, 123) - self.assertEqual(len(root.attributes), 0) + self.assertEqual(len(root.attributes), 1) + self.assertEqual(root.attributes["correct-value"], "foo") def test_byte_type_attribute_value(self): with self.tracer.start_as_current_span("root") as root: