Repository: open-telemetry/opentelemetry-python

Issue:
Update limits implementation to comply with the spec
The spec was updated to prefer user defined global limits over model specific defaults in this PR: https://github.com/open-telemetry/opentelemetry-specification/pull/1893#event-5298851679

We should review our limits implementation and update it if necessary. 


Discussion:

Resolving patch:
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,15 @@
 Default: 512
 """
 
+OTEL_ATTRIBUTE_COUNT_LIMIT = "OTEL_ATTRIBUTE_COUNT_LIMIT"
+"""
+.. envvar:: OTEL_ATTRIBUTE_COUNT_LIMIT
+
+The :envvar:`OTEL_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed attribute count for spans, events and links.
+This limit is overriden by model specific limits such as OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT.
+Default: 128
+"""
+
 OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT = "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT"
 """
 .. envvar:: OTEL_ATTRIBUTE_VALUE_LENGTH_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
@@ -41,6 +41,7 @@
 from opentelemetry.attributes import BoundedAttributes
 from opentelemetry.sdk import util
 from opentelemetry.sdk.environment_variables import (
+    OTEL_ATTRIBUTE_COUNT_LIMIT,
     OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
     OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT,
     OTEL_LINK_ATTRIBUTE_COUNT_LIMIT,
@@ -61,11 +62,12 @@
 
 logger = logging.getLogger(__name__)
 
+_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128
 _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = 128
-_DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT = 128
-_DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT = 128
 _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT = 128
 _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT = 128
+_DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT = 128
+_DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT = 128
 
 
 _ENV_VALUE_UNSET = ""
@@ -533,19 +535,23 @@ class SpanLimits:
     Limit precedence:
 
     - If a model specific limit is set, it will be used.
+    - Else if the corresponding global limit is set, it will be used.
     - Else if the model specific limit has a default value, the default value will be used.
-    - Else if model specific limit has a corresponding global limit, the global limit will be used.
+    - Else if the global limit has a default value, the default value will be used.
 
     Args:
-        max_attributes: Maximum number of attributes that can be added to a Span.
-            Environment variable: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT
-            Default: {_DEFAULT_SPAN_ATTRIBUTE_COUNT_LIMIT}
+        max_attributes: Maximum number of attributes that can be added to a span, event, and link.
+            Environment variable: OTEL_ATTRIBUTE_COUNT_LIMIT
+            Default: {_DEFAULT_ATTRIBUTE_COUNT_LIMIT}
         max_events: Maximum number of events that can be added to a Span.
             Environment variable: OTEL_SPAN_EVENT_COUNT_LIMIT
             Default: {_DEFAULT_SPAN_EVENT_COUNT_LIMIT}
         max_links: Maximum number of links that can be added to a Span.
             Environment variable: OTEL_SPAN_LINK_COUNT_LIMIT
             Default: {_DEFAULT_SPAN_LINK_COUNT_LIMIT}
+        max_span_attributes: Maximum number of attributes that can be added to a Span.
+            Environment variable: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT
+            Default: {_DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT}
         max_event_attributes: Maximum number of attributes that can be added to an Event.
             Default: {_DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT}
         max_link_attributes: Maximum number of attributes that can be added to a Link.
@@ -563,16 +569,14 @@ def __init__(
         max_attributes: Optional[int] = None,
         max_events: Optional[int] = None,
         max_links: Optional[int] = None,
+        max_span_attributes: Optional[int] = None,
         max_event_attributes: Optional[int] = None,
         max_link_attributes: Optional[int] = None,
         max_attribute_length: Optional[int] = None,
         max_span_attribute_length: Optional[int] = None,
     ):
-        self.max_attributes = self._from_env_if_absent(
-            max_attributes,
-            OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
-            _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
-        )
+
+        # span events and links count
         self.max_events = self._from_env_if_absent(
             max_events,
             OTEL_SPAN_EVENT_COUNT_LIMIT,
@@ -583,17 +587,32 @@ def __init__(
             OTEL_SPAN_LINK_COUNT_LIMIT,
             _DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT,
         )
+
+        # attribute count
+        global_max_attributes = self._from_env_if_absent(
+            max_attributes, OTEL_ATTRIBUTE_COUNT_LIMIT
+        )
+        self.max_attributes = (
+            global_max_attributes or _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT
+        )
+
+        self.max_span_attributes = self._from_env_if_absent(
+            max_span_attributes,
+            OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
+            global_max_attributes or _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
+        )
         self.max_event_attributes = self._from_env_if_absent(
             max_event_attributes,
             OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT,
-            _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT,
+            global_max_attributes or _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT,
         )
         self.max_link_attributes = self._from_env_if_absent(
             max_link_attributes,
             OTEL_LINK_ATTRIBUTE_COUNT_LIMIT,
-            _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT,
+            global_max_attributes or _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT,
         )
 
+        # attribute length
         self.max_attribute_length = self._from_env_if_absent(
             max_attribute_length,
             OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
@@ -606,7 +625,7 @@ def __init__(
         )
 
     def __repr__(self):
-        return f"{type(self).__name__}(max_span_attributes={self.max_span_attribute_length}, max_events_attributes={self.max_event_attributes}, max_link_attributes={self.max_link_attributes}, max_attributes={self.max_attributes}, max_events={self.max_events}, max_links={self.max_links}, max_attribute_length={self.max_attribute_length})"
+        return f"{type(self).__name__}(max_span_attributes={self.max_span_attributes}, max_events_attributes={self.max_event_attributes}, max_link_attributes={self.max_link_attributes}, max_attributes={self.max_attributes}, max_events={self.max_events}, max_links={self.max_links}, max_attribute_length={self.max_attribute_length})"
 
     @classmethod
     def _from_env_if_absent(
@@ -641,13 +660,14 @@ def _from_env_if_absent(
     max_attributes=SpanLimits.UNSET,
     max_events=SpanLimits.UNSET,
     max_links=SpanLimits.UNSET,
+    max_span_attributes=SpanLimits.UNSET,
     max_event_attributes=SpanLimits.UNSET,
     max_link_attributes=SpanLimits.UNSET,
     max_attribute_length=SpanLimits.UNSET,
     max_span_attribute_length=SpanLimits.UNSET,
 )
 
-# not remove for backward compat. please use SpanLimits instead.
+# not removed for backward compat. please use SpanLimits instead.
 SPAN_ATTRIBUTE_COUNT_LIMIT = SpanLimits._from_env_if_absent(
     None,
     OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
@@ -717,7 +737,7 @@ def __init__(
         self._limits = limits
         self._lock = threading.Lock()
         self._attributes = BoundedAttributes(
-            self._limits.max_attributes,
+            self._limits.max_span_attributes,
             attributes,
             immutable=False,
             max_value_len=self._limits.max_span_attribute_length,


Test patch:
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
@@ -26,7 +26,10 @@
 from opentelemetry.context import Context
 from opentelemetry.sdk import resources, trace
 from opentelemetry.sdk.environment_variables import (
+    OTEL_ATTRIBUTE_COUNT_LIMIT,
     OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
+    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,
@@ -552,7 +555,7 @@ def test_surplus_span_links(self):
 
     def test_surplus_span_attributes(self):
         # pylint: disable=protected-access
-        max_attrs = trace.SpanLimits().max_attributes
+        max_attrs = trace.SpanLimits().max_span_attributes
         attributes = {str(idx): idx for idx in range(0, 16 + max_attrs)}
         tracer = new_tracer()
         with tracer.start_as_current_span(
@@ -1325,8 +1328,20 @@ def test_limits_defaults(self):
         limits = trace.SpanLimits()
         self.assertEqual(
             limits.max_attributes,
+            trace._DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT,
+        )
+        self.assertEqual(
+            limits.max_span_attributes,
             trace._DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
         )
+        self.assertEqual(
+            limits.max_event_attributes,
+            trace._DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT,
+        )
+        self.assertEqual(
+            limits.max_link_attributes,
+            trace._DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT,
+        )
         self.assertEqual(
             limits.max_events, trace._DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT
         )
@@ -1355,25 +1370,61 @@ def test_limits_attribute_length_limits_code(self):
         self.assertEqual(limits.max_span_attribute_length, 33)
 
     def test_limits_values_code(self):
-        max_attributes, max_events, max_links, max_attr_length = (
+        (
+            max_attributes,
+            max_span_attributes,
+            max_link_attributes,
+            max_event_attributes,
+            max_events,
+            max_links,
+            max_attr_length,
+            max_span_attr_length,
+        ) = (
+            randint(0, 10000),
+            randint(0, 10000),
+            randint(0, 10000),
+            randint(0, 10000),
             randint(0, 10000),
             randint(0, 10000),
             randint(0, 10000),
             randint(0, 10000),
         )
         limits = trace.SpanLimits(
-            max_attributes=max_attributes,
             max_events=max_events,
             max_links=max_links,
+            max_attributes=max_attributes,
+            max_span_attributes=max_span_attributes,
+            max_event_attributes=max_event_attributes,
+            max_link_attributes=max_link_attributes,
             max_attribute_length=max_attr_length,
+            max_span_attribute_length=max_span_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_attributes, max_attributes)
+        self.assertEqual(limits.max_span_attributes, max_span_attributes)
+        self.assertEqual(limits.max_event_attributes, max_event_attributes)
+        self.assertEqual(limits.max_link_attributes, max_link_attributes)
         self.assertEqual(limits.max_attribute_length, max_attr_length)
+        self.assertEqual(
+            limits.max_span_attribute_length, max_span_attr_length
+        )
 
     def test_limits_values_env(self):
-        max_attributes, max_events, max_links, max_attr_length = (
+        (
+            max_attributes,
+            max_span_attributes,
+            max_link_attributes,
+            max_event_attributes,
+            max_events,
+            max_links,
+            max_attr_length,
+            max_span_attr_length,
+        ) = (
+            randint(0, 10000),
+            randint(0, 10000),
+            randint(0, 10000),
+            randint(0, 10000),
             randint(0, 10000),
             randint(0, 10000),
             randint(0, 10000),
@@ -1382,16 +1433,29 @@ def test_limits_values_env(self):
         with mock.patch.dict(
             "os.environ",
             {
-                OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: str(max_attributes),
+                OTEL_ATTRIBUTE_COUNT_LIMIT: str(max_attributes),
+                OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: str(max_span_attributes),
+                OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT: str(max_event_attributes),
+                OTEL_LINK_ATTRIBUTE_COUNT_LIMIT: str(max_link_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),
+                OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: str(max_attr_length),
+                OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: str(
+                    max_span_attr_length
+                ),
             },
         ):
             limits = trace.SpanLimits()
-            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_attributes, max_attributes)
+            self.assertEqual(limits.max_span_attributes, max_span_attributes)
+            self.assertEqual(limits.max_event_attributes, max_event_attributes)
+            self.assertEqual(limits.max_link_attributes, max_link_attributes)
+            self.assertEqual(limits.max_attribute_length, max_attr_length)
+            self.assertEqual(
+                limits.max_span_attribute_length, max_span_attr_length
+            )
 
     @mock.patch.dict(
         "os.environ",
@@ -1413,6 +1477,25 @@ def test_span_limits_env(self):
             max_span_attr_len=15,
         )
 
+    @mock.patch.dict(
+        "os.environ",
+        {
+            OTEL_ATTRIBUTE_COUNT_LIMIT: "13",
+            OTEL_SPAN_EVENT_COUNT_LIMIT: "7",
+            OTEL_SPAN_LINK_COUNT_LIMIT: "4",
+            OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: "11",
+        },
+    )
+    def test_span_limits_global_env(self):
+        self._test_span_limits(
+            new_tracer(),
+            max_attrs=13,
+            max_events=7,
+            max_links=4,
+            max_attr_len=11,
+            max_span_attr_len=11,
+        )
+
     @mock.patch.dict(
         "os.environ",
         {
@@ -1475,7 +1558,7 @@ def test_span_no_limits_code(self):
         self._test_span_no_limits(
             new_tracer(
                 span_limits=trace.SpanLimits(
-                    max_attributes=trace.SpanLimits.UNSET,
+                    max_span_attributes=trace.SpanLimits.UNSET,
                     max_links=trace.SpanLimits.UNSET,
                     max_events=trace.SpanLimits.UNSET,
                     max_attribute_length=trace.SpanLimits.UNSET,