diff --git a/CHANGELOG.md b/CHANGELOG.md index b2927d75f92..be58d61cea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `end_on_exit` argument to `start_as_current_span` ([#1519](https://github.com/open-telemetry/opentelemetry-python/pull/1519)]) +- Add `Span.set_attributes` method to set multiple values with one call + ([#1520](https://github.com/open-telemetry/opentelemetry-python/pull/1520)) ## [0.17b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.17b0) - 2021-01-20 diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index baeed762023..f46aca9c1e5 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -41,6 +41,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. @@ -450,6 +461,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 83c1eff93e1..09292cf4d95 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, @@ -582,29 +583,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 eb34714fbfc..71a1dcb44bd 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -531,11 +531,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) @@ -593,6 +596,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", @@ -609,7 +616,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: