From 14d3e463bacca53284418c480213799dee03d0cb Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 17 Aug 2021 15:06:28 +0530 Subject: [PATCH] Added support for `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` Fixes #2045 Fixes #2043 Fixes #2042 Fixes #2041 --- CHANGELOG.md | 4 + .../src/opentelemetry/attributes/__init__.py | 101 +++++++++------ .../tests/attributes/test_attributes.py | 95 ++++++-------- .../sdk/environment_variables/__init__.py | 25 ++++ .../src/opentelemetry/sdk/trace/__init__.py | 71 ++++++----- opentelemetry-sdk/tests/trace/test_trace.py | 119 ++++++++++++++---- .../src/opentelemetry/test/spantestutil.py | 37 +++--- 7 files changed, 280 insertions(+), 172 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6bd176ae94..026381e9793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add Trace ID validation to meet [TraceID spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#spancontext) ([#1992](https://github.com/open-telemetry/opentelemetry-python/pull/1992)) - Fixed Python 3.10 incompatibility in `opentelemetry-opentracing-shim` tests ([#2018](https://github.com/open-telemetry/opentelemetry-python/pull/2018)) +- `opentelemetry-sdk` added support for `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` + ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) +- `opentelemetry-sdk` Fixed bugs (#2041 & #2042) in Span Limits + ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) ## [0.23.1](https://github.com/open-telemetry/opentelemetry-python/pull/1987) - 2021-07-26 diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index e0b2a48ae2a..b6adf402585 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -17,36 +17,58 @@ import threading from collections import OrderedDict from collections.abc import MutableMapping -from typing import MutableSequence, Optional, Sequence +from typing import Optional, Sequence, Union from opentelemetry.util import types -_VALID_ATTR_VALUE_TYPES = (bool, str, int, float) +# bytes are accepted as a user supplied value for attributes but +# decoded to strings internally. +_VALID_ATTR_VALUE_TYPES = (bool, str, bytes, int, float) _logger = logging.getLogger(__name__) -def _is_valid_attribute_value(value: types.AttributeValue) -> bool: - """Checks if attribute value is valid. +def _clean_attribute( + key: str, value: types.AttributeValue, max_len: Optional[int] +) -> Optional[types.AttributeValue]: + """Checks if attribute value is valid and cleans it if required. + + The function returns the cleaned value or None if the value is not valid. An attribute value is valid if it is either: - A primitive type: string, boolean, double precision floating point (IEEE 754-1985) or integer. - An array of primitive type values. The array MUST be homogeneous, i.e. it MUST NOT contain values of different types. + + An attribute needs cleansing if: + - Its length is greater than the maximum allowed length. + - It needs to be encoded/decoded e.g, bytes to strings. """ + if key is None or key == "": + _logger.warning("invalid key `%s` (empty or null)", key) + return None + if isinstance(value, _VALID_ATTR_VALUE_TYPES): - return True + return _clean_attribute_value(value, max_len) if isinstance(value, Sequence): - sequence_first_valid_type = None + cleaned_seq = [] + for element in value: + # None is considered valid in any sequence + if element is None: + cleaned_seq.append(element) + + element = _clean_attribute_value(element, max_len) if element is None: continue + element_type = type(element) + # Reject attribute value if sequence contains a value with an incompatible type. if element_type not in _VALID_ATTR_VALUE_TYPES: _logger.warning( "Invalid type %s in attribute value sequence. Expected one of " @@ -57,19 +79,27 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool: for valid_type in _VALID_ATTR_VALUE_TYPES ], ) - return False + return None + # The type of the sequence must be homogeneous. The first non-None # element determines the type of the sequence if sequence_first_valid_type is None: sequence_first_valid_type = element_type - elif not isinstance(element, sequence_first_valid_type): + # use equality instead of isinstance as isinstance(True, int) evaluates to True + elif element_type != sequence_first_valid_type: _logger.warning( "Mixed types %s and %s in attribute value sequence", sequence_first_valid_type.__name__, type(element).__name__, ) - return False - return True + return None + + # reject invalid elements + if element is not None: + cleaned_seq.append(element) + + # Freeze mutable sequences defensively + return tuple(cleaned_seq) _logger.warning( "Invalid type %s for attribute value. Expected one of %s or a " @@ -77,36 +107,25 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool: type(value).__name__, [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES], ) - return False - + return None -def _filter_attributes(attributes: types.Attributes) -> None: - """Applies attribute validation rules and drops (key, value) pairs - that doesn't adhere to attributes specification. - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/common.md#attributes. - """ - if attributes: - for attr_key, attr_value in list(attributes.items()): - if not attr_key: - _logger.warning("invalid key `%s` (empty or null)", attr_key) - attributes.pop(attr_key) - continue +def _clean_attribute_value( + value: types.AttributeValue, limit: Optional[int] +) -> Union[types.AttributeValue, None]: + if value is None: + return None - if _is_valid_attribute_value(attr_value): - if isinstance(attr_value, MutableSequence): - attributes[attr_key] = tuple(attr_value) - if isinstance(attr_value, bytes): - try: - attributes[attr_key] = attr_value.decode() - except ValueError: - attributes.pop(attr_key) - _logger.warning("Byte attribute could not be decoded.") - else: - attributes.pop(attr_key) + if isinstance(value, bytes): + try: + value = value.decode() + except ValueError: + _logger.warning("Byte attribute could not be decoded.") + return None - -_DEFAULT_LIMIT = 128 + if limit is not None and isinstance(value, str): + value = value[:limit] + return value class BoundedAttributes(MutableMapping): @@ -118,9 +137,10 @@ class BoundedAttributes(MutableMapping): def __init__( self, - maxlen: Optional[int] = _DEFAULT_LIMIT, + maxlen: Optional[int] = None, attributes: types.Attributes = None, immutable: bool = True, + max_value_len: Optional[int] = None, ): if maxlen is not None: if not isinstance(maxlen, int) or maxlen < 0: @@ -129,10 +149,10 @@ def __init__( ) self.maxlen = maxlen self.dropped = 0 + self.max_value_len = max_value_len self._dict = OrderedDict() # type: OrderedDict self._lock = threading.Lock() # type: threading.Lock if attributes: - _filter_attributes(attributes) for key, value in attributes.items(): self[key] = value self._immutable = immutable @@ -158,7 +178,10 @@ def __setitem__(self, key, value): elif self.maxlen is not None and len(self._dict) == self.maxlen: del self._dict[next(iter(self._dict.keys()))] self.dropped += 1 - self._dict[key] = value + + value = _clean_attribute(key, value, self.max_value_len) + if value is not None: + self._dict[key] = value def __delitem__(self, key): if getattr(self, "_immutable", False): diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index c1151bf4d41..045c1f7125c 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -16,69 +16,48 @@ import collections import unittest +from typing import MutableSequence -from opentelemetry.attributes import ( - BoundedAttributes, - _filter_attributes, - _is_valid_attribute_value, -) +from opentelemetry.attributes import BoundedAttributes, _clean_attribute class TestAttributes(unittest.TestCase): - def test_is_valid_attribute_value(self): - self.assertFalse(_is_valid_attribute_value([1, 2, 3.4, "ss", 4])) - self.assertFalse(_is_valid_attribute_value([dict(), 1, 2, 3.4, 4])) - self.assertFalse(_is_valid_attribute_value(["sw", "lf", 3.4, "ss"])) - self.assertFalse(_is_valid_attribute_value([1, 2, 3.4, 5])) - self.assertFalse(_is_valid_attribute_value(dict())) - self.assertTrue(_is_valid_attribute_value(True)) - self.assertTrue(_is_valid_attribute_value("hi")) - self.assertTrue(_is_valid_attribute_value(3.4)) - self.assertTrue(_is_valid_attribute_value(15)) - self.assertTrue(_is_valid_attribute_value([1, 2, 3, 5])) - self.assertTrue(_is_valid_attribute_value([1.2, 2.3, 3.4, 4.5])) - self.assertTrue(_is_valid_attribute_value([True, False])) - self.assertTrue(_is_valid_attribute_value(["ss", "dw", "fw"])) - self.assertTrue(_is_valid_attribute_value([])) + def assertValid(self, value, key="k"): + expected = value + if isinstance(value, MutableSequence): + expected = tuple(value) + self.assertEqual(_clean_attribute(key, value, None), expected) + + def assertInvalid(self, value, key="k"): + self.assertIsNone(_clean_attribute(key, value, None)) + + def test_clean_attribute(self): + self.assertInvalid([1, 2, 3.4, "ss", 4]) + self.assertInvalid([dict(), 1, 2, 3.4, 4]) + self.assertInvalid(["sw", "lf", 3.4, "ss"]) + self.assertInvalid([1, 2, 3.4, 5]) + self.assertInvalid(dict()) + self.assertInvalid([1, True]) + self.assertValid(True) + self.assertValid("hi") + self.assertValid(3.4) + self.assertValid(15) + self.assertValid([1, 2, 3, 5]) + self.assertValid([1.2, 2.3, 3.4, 4.5]) + self.assertValid([True, False]) + self.assertValid(["ss", "dw", "fw"]) + self.assertValid([]) # None in sequences are valid - self.assertTrue(_is_valid_attribute_value(["A", None, None])) - self.assertTrue(_is_valid_attribute_value(["A", None, None, "B"])) - self.assertTrue(_is_valid_attribute_value([None, None])) - self.assertFalse(_is_valid_attribute_value(["A", None, 1])) - self.assertFalse(_is_valid_attribute_value([None, "A", None, 1])) - - def test_filter_attributes(self): - attrs_with_invalid_keys = { - "": "empty-key", - None: "None-value", - "attr-key": "attr-value", - } - _filter_attributes(attrs_with_invalid_keys) - self.assertTrue(len(attrs_with_invalid_keys), 1) - self.assertEqual(attrs_with_invalid_keys, {"attr-key": "attr-value"}) - - attrs_with_invalid_values = { - "nonhomogeneous": [1, 2, 3.4, "ss", 4], - "nonprimitive": dict(), - "mixed": [1, 2.4, "st", dict()], - "validkey1": "validvalue1", - "intkey": 5, - "floatkey": 3.14, - "boolkey": True, - "valid-byte-string": b"hello-otel", - } - _filter_attributes(attrs_with_invalid_values) - self.assertEqual(len(attrs_with_invalid_values), 5) - self.assertEqual( - attrs_with_invalid_values, - { - "validkey1": "validvalue1", - "intkey": 5, - "floatkey": 3.14, - "boolkey": True, - "valid-byte-string": "hello-otel", - }, - ) + self.assertValid(["A", None, None]) + self.assertValid(["A", None, None, "B"]) + self.assertValid([None, None]) + self.assertInvalid(["A", None, 1]) + self.assertInvalid([None, "A", None, 1]) + + # test keys + self.assertValid("value", "key") + self.assertInvalid("value", "") + self.assertInvalid("value", None) class TestBoundedAttributes(unittest.TestCase): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 1a98d1f831e..86ead4ae2d0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -98,6 +98,22 @@ Default: 512 """ +OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT = "OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT" +""" +.. envvar:: OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT + +The :envvar:`OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed event attribute count. +Default: 128 +""" + +OTEL_LINK_ATTRIBUTE_COUNT_LIMIT = "OTEL_LINK_ATTRIBUTE_COUNT_LIMIT" +""" +.. envvar:: OTEL_LINK_ATTRIBUTE_COUNT_LIMIT + +The :envvar:`OTEL_LINK_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed link attribute count. +Default: 128 +""" + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT" """ .. envvar:: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT @@ -106,6 +122,15 @@ Default: 128 """ +OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT = ( + "OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT" +) +""" +.. envvar:: OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT + +The :envvar:`OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` represents the maximum allowed length attribute values can have. +""" + OTEL_SPAN_EVENT_COUNT_LIMIT = "OTEL_SPAN_EVENT_COUNT_LIMIT" """ .. envvar:: OTEL_SPAN_EVENT_COUNT_LIMIT diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index ca22f39ee87..60319e1bfa8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -29,7 +29,6 @@ Callable, Dict, Iterator, - MutableSequence, Optional, Sequence, Tuple, @@ -39,13 +38,13 @@ from opentelemetry import context as context_api from opentelemetry import trace as trace_api -from opentelemetry.attributes import ( - BoundedAttributes, - _is_valid_attribute_value, -) +from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk import util from opentelemetry.sdk.environment_variables import ( + OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, + OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT, OTEL_SPAN_LINK_COUNT_LIMIT, ) @@ -550,6 +549,8 @@ class SpanLimits: Default: {_DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT} max_link_attributes: Maximum number of attributes that can be added to a Link. Default: {_DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT} + max_attribute_length: Maximum length an attribute value can have. Values longer than + the specified length will be truncated. """ UNSET = -1 @@ -561,6 +562,7 @@ def __init__( max_links: Optional[int] = None, max_event_attributes: Optional[int] = None, max_link_attributes: Optional[int] = None, + max_attribute_length: Optional[int] = None, ): self.max_attributes = self._from_env_if_absent( max_attributes, @@ -579,28 +581,33 @@ def __init__( ) self.max_event_attributes = self._from_env_if_absent( max_event_attributes, - OTEL_SPAN_LINK_COUNT_LIMIT, + OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, ) self.max_link_attributes = self._from_env_if_absent( max_link_attributes, - OTEL_SPAN_LINK_COUNT_LIMIT, + OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, ) + self.max_attribute_length = self._from_env_if_absent( + max_attribute_length, + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, + ) def __repr__(self): - return "{}(max_attributes={}, max_events={}, max_links={}, max_event_attributes={}, max_link_attributes={})".format( + return "{}(max_attributes={}, max_events={}, max_links={}, max_event_attributes={}, max_link_attributes={}, max_attribute_length={})".format( type(self).__name__, self.max_attributes, self.max_events, self.max_links, self.max_event_attributes, self.max_link_attributes, + self.max_attribute_length, ) @classmethod def _from_env_if_absent( - cls, value: Optional[int], env_var: str, default: Optional[int] + cls, value: Optional[int], env_var: str, default: Optional[int] = None ) -> Optional[int]: if value is cls.UNSET: return None @@ -630,8 +637,10 @@ def _from_env_if_absent( max_links=SpanLimits.UNSET, max_event_attributes=SpanLimits.UNSET, max_link_attributes=SpanLimits.UNSET, + max_attribute_length=SpanLimits.UNSET, ) +# not remove for backward compat. please use SpanLimits instead. SPAN_ATTRIBUTE_COUNT_LIMIT = SpanLimits._from_env_if_absent( None, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, @@ -701,19 +710,30 @@ def __init__( self._limits = limits self._lock = threading.Lock() self._attributes = BoundedAttributes( - self._limits.max_attributes, attributes, immutable=False + self._limits.max_attributes, + attributes, + immutable=False, + max_value_len=self._limits.max_attribute_length, ) self._events = self._new_events() if events: for event in events: event._attributes = BoundedAttributes( - self._limits.max_event_attributes, event.attributes + self._limits.max_event_attributes, + event.attributes, + max_value_len=self._limits.max_attribute_length, ) self._events.append(event) if links is None: self._links = self._new_links() else: + for link in links: + link._attributes = BoundedAttributes( + self._limits.max_link_attributes, + link.attributes, + max_value_len=self._limits.max_attribute_length, + ) self._links = BoundedList.from_seq(self._limits.max_links, links) def __repr__(self): @@ -739,25 +759,6 @@ def set_attributes( return 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 for key `%s`.", - key, - ) - return self._attributes[key] = value def set_attribute(self, key: str, value: types.AttributeValue) -> None: @@ -774,7 +775,9 @@ def add_event( timestamp: Optional[int] = None, ) -> None: attributes = BoundedAttributes( - self._limits.max_event_attributes, attributes + self._limits.max_event_attributes, + attributes, + max_value_len=self._limits.max_attribute_length, ) self._add_event( Event( @@ -1062,6 +1065,12 @@ def __init__( self.sampler = sampler self._span_limits = span_limits or SpanLimits() self._atexit_handler = None + + self._resource._attributes = BoundedAttributes( + self._span_limits.max_attributes, + self._resource._attributes, + max_value_len=self._span_limits.max_attribute_length, + ) if shutdown_on_exit: self._atexit_handler = atexit.register(self.shutdown) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index e331642e44d..29609184d25 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -27,6 +27,7 @@ from opentelemetry.sdk import resources, trace from opentelemetry.sdk.environment_variables import ( OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT, OTEL_SPAN_LINK_COUNT_LIMIT, OTEL_TRACES_SAMPLER, @@ -38,15 +39,12 @@ from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, + new_tracer, ) from opentelemetry.trace import StatusCode from opentelemetry.util._time import _time_ns -def new_tracer(span_limits=None) -> trace_api.Tracer: - return trace.TracerProvider(span_limits=span_limits).get_tracer(__name__) - - class TestTracer(unittest.TestCase): def test_extends_api(self): tracer = new_tracer() @@ -653,6 +651,7 @@ def test_invalid_attribute_values(self): root.set_attribute( "list-with-non-primitive-data-type", [dict(), 123] ) + root.set_attribute("list-with-numeric-and-bool", [1, True]) root.set_attribute("", 123) root.set_attribute(None, 123) @@ -1314,6 +1313,15 @@ def test_attributes_to_json(self): class TestSpanLimits(unittest.TestCase): # pylint: disable=protected-access + long_val = "v" * 1000 + + def _assert_attr_length(self, attr_val, max_len): + if isinstance(attr_val, str): + expected = self.long_val + if max_len is not None: + expected = expected[:max_len] + self.assertEqual(attr_val, expected) + def test_limits_defaults(self): limits = trace.SpanLimits() self.assertEqual( @@ -1326,9 +1334,11 @@ def test_limits_defaults(self): self.assertEqual( limits.max_links, trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT ) + self.assertIsNone(limits.max_attribute_length) def test_limits_values_code(self): - max_attributes, max_events, max_links = ( + max_attributes, max_events, max_links, max_attr_length = ( + randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), @@ -1337,13 +1347,16 @@ def test_limits_values_code(self): max_attributes=max_attributes, max_events=max_events, max_links=max_links, + max_attribute_length=max_attr_length, ) self.assertEqual(limits.max_attributes, max_attributes) self.assertEqual(limits.max_events, max_events) self.assertEqual(limits.max_links, max_links) + self.assertEqual(limits.max_attribute_length, max_attr_length) def test_limits_values_env(self): - max_attributes, max_events, max_links = ( + max_attributes, max_events, max_links, max_attr_length = ( + randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), @@ -1354,6 +1367,7 @@ def test_limits_values_env(self): OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: str(max_attributes), OTEL_SPAN_EVENT_COUNT_LIMIT: str(max_events), OTEL_SPAN_LINK_COUNT_LIMIT: str(max_links), + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: str(max_attr_length), }, ): limits = trace.SpanLimits() @@ -1361,7 +1375,9 @@ def test_limits_values_env(self): self.assertEqual(limits.max_events, max_events) self.assertEqual(limits.max_links, max_links) - def _test_span_limits(self, tracer): + def _test_span_limits( + self, tracer, max_attrs, max_events, max_links, max_attr_len + ): id_generator = RandomIdGenerator() some_links = [ trace_api.Link( @@ -1369,25 +1385,48 @@ def _test_span_limits(self, tracer): trace_id=id_generator.generate_trace_id(), span_id=id_generator.generate_span_id(), is_remote=False, - ) + ), + attributes={"k": self.long_val}, ) for _ in range(100) ] some_attrs = { - "init_attribute_{}".format(idx): idx for idx in range(100) + "init_attribute_{}".format(idx): self.long_val + for idx in range(100) } with tracer.start_as_current_span( "root", links=some_links, attributes=some_attrs ) as root: - self.assertEqual(len(root.links), 30) - self.assertEqual(len(root.attributes), 10) + self.assertEqual(len(root.links), max_links) + self.assertEqual(len(root.attributes), max_attrs) for idx in range(100): - root.set_attribute("my_attribute_{}".format(idx), 0) - root.add_event("my_event_{}".format(idx)) + root.set_attribute( + "my_str_attribute_{}".format(idx), self.long_val + ) + root.set_attribute( + "my_byte_attribute_{}".format(idx), self.long_val.encode() + ) + root.set_attribute( + "my_int_attribute_{}".format(idx), self.long_val.encode() + ) + root.add_event( + "my_event_{}".format(idx), attributes={"k": self.long_val} + ) - self.assertEqual(len(root.attributes), 10) - self.assertEqual(len(root.events), 20) + self.assertEqual(len(root.attributes), max_attrs) + self.assertEqual(len(root.events), max_events) + + for link in root.links: + for attr_val in link.attributes.values(): + self._assert_attr_length(attr_val, max_attr_len) + + for event in root.events: + for attr_val in event.attributes.values(): + self._assert_attr_length(attr_val, max_attr_len) + + for attr_val in root.attributes.values(): + self._assert_attr_length(attr_val, max_attr_len) def _test_span_no_limits(self, tracer): num_links = int(trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT) + randint( @@ -1413,7 +1452,9 @@ def _test_span_no_limits(self, tracer): ) with tracer.start_as_current_span("root") as root: for idx in range(num_events): - root.add_event("my_event_{}".format(idx)) + root.add_event( + "my_event_{}".format(idx), attributes={"k": self.long_val} + ) self.assertEqual(len(root.events), num_events) @@ -1422,20 +1463,31 @@ def _test_span_no_limits(self, tracer): ) + randint(1, 100) with tracer.start_as_current_span("root") as root: for idx in range(num_attributes): - root.set_attribute("my_attribute_{}".format(idx), 0) + root.set_attribute( + "my_attribute_{}".format(idx), self.long_val + ) self.assertEqual(len(root.attributes), num_attributes) + for attr_val in root.attributes.values(): + self.assertEqual(attr_val, self.long_val) @mock.patch.dict( "os.environ", { - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10", - OTEL_SPAN_EVENT_COUNT_LIMIT: "20", - OTEL_SPAN_LINK_COUNT_LIMIT: "30", + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "13", + OTEL_SPAN_EVENT_COUNT_LIMIT: "7", + OTEL_SPAN_LINK_COUNT_LIMIT: "4", + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "11", }, ) def test_span_limits_env(self): - self._test_span_limits(new_tracer()) + self._test_span_limits( + new_tracer(), + max_attrs=13, + max_events=7, + max_links=4, + max_attr_len=11, + ) @mock.patch.dict( "os.environ", @@ -1443,24 +1495,39 @@ def test_span_limits_env(self): OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10", OTEL_SPAN_EVENT_COUNT_LIMIT: "20", OTEL_SPAN_LINK_COUNT_LIMIT: "30", + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "40", }, ) def test_span_limits_default_to_env(self): self._test_span_limits( new_tracer( span_limits=trace.SpanLimits( - max_attributes=None, max_events=None, max_links=None + max_attributes=None, + max_events=None, + max_links=None, + max_attribute_length=None, ) - ) + ), + max_attrs=10, + max_events=20, + max_links=30, + max_attr_len=40, ) def test_span_limits_code(self): self._test_span_limits( new_tracer( span_limits=trace.SpanLimits( - max_attributes=10, max_events=20, max_links=30 + max_attributes=11, + max_events=15, + max_links=13, + max_attribute_length=9, ) - ) + ), + max_attrs=11, + max_events=15, + max_links=13, + max_attr_len=9, ) @mock.patch.dict( @@ -1469,6 +1536,7 @@ def test_span_limits_code(self): OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "unset", OTEL_SPAN_EVENT_COUNT_LIMIT: "unset", OTEL_SPAN_LINK_COUNT_LIMIT: "unset", + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "unset", }, ) def test_span_no_limits_env(self): @@ -1481,6 +1549,7 @@ def test_span_no_limits_code(self): max_attributes=trace.SpanLimits.UNSET, max_links=trace.SpanLimits.UNSET, max_events=trace.SpanLimits.UNSET, + max_attribute_length=trace.SpanLimits.UNSET, ) ) ) diff --git a/tests/util/src/opentelemetry/test/spantestutil.py b/tests/util/src/opentelemetry/test/spantestutil.py index faf135f2aee..408f4c4947f 100644 --- a/tests/util/src/opentelemetry/test/spantestutil.py +++ b/tests/util/src/opentelemetry/test/spantestutil.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest +from functools import partial from importlib import reload from opentelemetry import trace as trace_api @@ -25,6 +26,13 @@ _MEMORY_EXPORTER = None +def new_tracer(span_limits=None, resource=None) -> trace_api.Tracer: + provider_factory = trace_sdk.TracerProvider + if resource is not None: + provider_factory = partial(provider_factory, resource=resource) + return provider_factory(span_limits=span_limits).get_tracer(__name__) + + class SpanTestBase(unittest.TestCase): @classmethod def setUpClass(cls): @@ -60,23 +68,14 @@ def get_span_with_dropped_attributes_events_links(): attributes=attributes, ) ) - span = trace_sdk._Span( - limits=trace_sdk.SpanLimits(), - name="span", - resource=Resource( - attributes=attributes, - ), - context=trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x00000000DEADBEF0, - is_remote=False, - ), - links=links, - attributes=attributes, - ) - span.start() - for index in range(131): - span.add_event("event{}".format(index), attributes=attributes) - span.end() - return span + tracer = new_tracer( + span_limits=trace_sdk.SpanLimits(), + resource=Resource(attributes=attributes), + ) + with tracer.start_as_current_span( + "span", links=links, attributes=attributes + ) as span: + for index in range(131): + span.add_event("event{}".format(index), attributes=attributes) + return span