Repository: open-telemetry/opentelemetry-python

Issue:
Link and Resource attribute count limits don't work
**Describe your environment** 

Current implementation does not respect link and resource (span) attribute limits.


**Steps to reproduce**


```python
from opentelemetry import trace
from opentelemetry.sdk.trace import Resource, TracerProvider, SpanLimits
from opentelemetry.sdk.trace.export import (
    SimpleSpanProcessor,
    ConsoleSpanExporter,
)

attributes = {"k1": "v1", "k2": "v2"}

provider = TracerProvider(
    resource=Resource.create(attributes=attributes.copy()),
    span_limits=SpanLimits(max_attributes=1, max_link_attributes=1),
)
processor = SimpleSpanProcessor(ConsoleSpanExporter())
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)


tracer = trace.get_tracer(__name__)

link = trace.Link(
    context=trace.INVALID_SPAN.get_span_context(), attributes=attributes.copy()
)

with tracer.start_as_current_span("s1", links=[link], attributes=attributes.copy()):
    pass
```

Run it:
```
❯ python main.py
{
    "name": "s1",
    "context": {
        "trace_id": "0xc20060d23fb374cc0e9441de3cf643b4",
        "span_id": "0xddddbe3ecde87eff",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": null,
    "start_time": "2021-08-17T09:11:56.024238Z",
    "end_time": "2021-08-17T09:11:56.024257Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "k3": "v3"
    },
    "events": [],
    "links": [
        {
            "context": {
                "trace_id": "0x00000000000000000000000000000000",
                "span_id": "0x0000000000000000",
                "trace_state": "[]"
            },
            "attributes": {
                "k1": "v1",
                "k2": "v2",
                "k3": "v3"
            }
        }
    ],
    "resource": {
        "telemetry.sdk.language": "python",
        "telemetry.sdk.name": "opentelemetry",
        "telemetry.sdk.version": "1.4.1",
        "k1": "v1",
        "k2": "v2",
        "k3": "v3",
        "service.name": "unknown_service"
    }
}
```

**What is the expected behavior?**
Resource and Link should have a single attribute just like the span.

**What is the actual behavior?**
Attribute count limits are not respected for resource and links as both of them contain more attributes than the limit (1).



Discussion:

Resolving patch:
diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py
--- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py
+++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py
@@ -17,36 +17,59 @@
 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)
+            # reject invalid elements
             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 +80,25 @@ 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
+
+            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 +106,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 +136,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 +148,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 +177,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-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py
--- 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
--- 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)
 


Test patch:
diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py
--- 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/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py
--- 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
--- 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