Repository: open-telemetry/opentelemetry-python

Issue:
Support global attribute limits
Support the following environment variables

- OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT


Spec: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#attribute-limits


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,13 @@
 Default: 512
 """
 
+OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT = "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT"
+"""
+.. envvar:: OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT
+
+The :envvar:`OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` represents the maximum allowed attribute length.
+"""
+
 OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT = "OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT"
 """
 .. envvar:: OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT
@@ -128,7 +135,8 @@
 """
 .. envvar:: OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT
 
-The :envvar:`OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` represents the maximum allowed length attribute values can have.
+The :envvar:`OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` represents the maximum allowed length
+span attribute values can have. This takes precedence over :envvar:`OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT`.
 """
 
 OTEL_SPAN_EVENT_COUNT_LIMIT = "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
@@ -41,6 +41,7 @@
 from opentelemetry.attributes import BoundedAttributes
 from opentelemetry.sdk import util
 from opentelemetry.sdk.environment_variables import (
+    OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
     OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT,
     OTEL_LINK_ATTRIBUTE_COUNT_LIMIT,
     OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
@@ -535,6 +536,12 @@ class SpanLimits:
       environment variable.
     - If the environment variable is not set, the default value, if any, will be used.
 
+    Limit precedence:
+
+    - If a model specific 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.
+
     Args:
         max_attributes: Maximum number of attributes that can be added to a Span.
             Environment variable: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT
@@ -551,6 +558,8 @@ class SpanLimits:
             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.
+        max_span_attribute_length: Maximum length a span attribute value can have. Values longer than
+            the specified length will be truncated.
     """
 
     UNSET = -1
@@ -563,6 +572,7 @@ def __init__(
         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,
@@ -589,19 +599,27 @@ def __init__(
             OTEL_LINK_ATTRIBUTE_COUNT_LIMIT,
             _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT,
         )
+
         self.max_attribute_length = self._from_env_if_absent(
             max_attribute_length,
+            OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
+        )
+        self.max_span_attribute_length = self._from_env_if_absent(
+            max_span_attribute_length,
             OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT,
+            # use global attribute length limit as default
+            self.max_attribute_length,
         )
 
     def __repr__(self):
-        return "{}(max_attributes={}, max_events={}, max_links={}, max_event_attributes={}, max_link_attributes={}, max_attribute_length={})".format(
+        return "{}(max_span_attributes={}, max_events_attributes={}, max_link_attributes={}, max_attributes={}, max_events={}, max_links={}, max_attribute_length={})".format(
             type(self).__name__,
+            self.max_span_attribute_length,
+            self.max_event_attributes,
+            self.max_link_attributes,
             self.max_attributes,
             self.max_events,
             self.max_links,
-            self.max_event_attributes,
-            self.max_link_attributes,
             self.max_attribute_length,
         )
 
@@ -641,6 +659,7 @@ def _from_env_if_absent(
     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.
@@ -716,7 +735,7 @@ def __init__(
             self._limits.max_attributes,
             attributes,
             immutable=False,
-            max_value_len=self._limits.max_attribute_length,
+            max_value_len=self._limits.max_span_attribute_length,
         )
         self._events = self._new_events()
         if events:


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,6 +26,7 @@
 from opentelemetry.context import Context
 from opentelemetry.sdk import resources, trace
 from opentelemetry.sdk.environment_variables import (
+    OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
     OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
     OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT,
     OTEL_SPAN_EVENT_COUNT_LIMIT,
@@ -1335,6 +1336,25 @@ def test_limits_defaults(self):
             limits.max_links, trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT
         )
         self.assertIsNone(limits.max_attribute_length)
+        self.assertIsNone(limits.max_span_attribute_length)
+
+    def test_limits_attribute_length_limits_code(self):
+        # global limit unset while span limit is set
+        limits = trace.SpanLimits(max_span_attribute_length=22)
+        self.assertIsNone(limits.max_attribute_length)
+        self.assertEqual(limits.max_span_attribute_length, 22)
+
+        # span limit falls back to global limit when no value is provided
+        limits = trace.SpanLimits(max_attribute_length=22)
+        self.assertEqual(limits.max_attribute_length, 22)
+        self.assertEqual(limits.max_span_attribute_length, 22)
+
+        # global and span limits set to different values
+        limits = trace.SpanLimits(
+            max_attribute_length=22, max_span_attribute_length=33
+        )
+        self.assertEqual(limits.max_attribute_length, 22)
+        self.assertEqual(limits.max_span_attribute_length, 33)
 
     def test_limits_values_code(self):
         max_attributes, max_events, max_links, max_attr_length = (
@@ -1375,8 +1395,113 @@ def test_limits_values_env(self):
             self.assertEqual(limits.max_events, max_events)
             self.assertEqual(limits.max_links, max_links)
 
+    @mock.patch.dict(
+        "os.environ",
+        {
+            OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "13",
+            OTEL_SPAN_EVENT_COUNT_LIMIT: "7",
+            OTEL_SPAN_LINK_COUNT_LIMIT: "4",
+            OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: "11",
+            OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "15",
+        },
+    )
+    def test_span_limits_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=15,
+        )
+
+    @mock.patch.dict(
+        "os.environ",
+        {
+            OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10",
+            OTEL_SPAN_EVENT_COUNT_LIMIT: "20",
+            OTEL_SPAN_LINK_COUNT_LIMIT: "30",
+            OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: "40",
+            OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "50",
+        },
+    )
+    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_attribute_length=None,
+                    max_span_attribute_length=None,
+                )
+            ),
+            max_attrs=10,
+            max_events=20,
+            max_links=30,
+            max_attr_len=40,
+            max_span_attr_len=50,
+        )
+
+    def test_span_limits_code(self):
+        self._test_span_limits(
+            new_tracer(
+                span_limits=trace.SpanLimits(
+                    max_attributes=11,
+                    max_events=15,
+                    max_links=13,
+                    max_attribute_length=9,
+                    max_span_attribute_length=25,
+                )
+            ),
+            max_attrs=11,
+            max_events=15,
+            max_links=13,
+            max_attr_len=9,
+            max_span_attr_len=25,
+        )
+
+    @mock.patch.dict(
+        "os.environ",
+        {
+            OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "",
+            OTEL_SPAN_EVENT_COUNT_LIMIT: "",
+            OTEL_SPAN_LINK_COUNT_LIMIT: "",
+            OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "",
+        },
+    )
+    def test_span_no_limits_env(self):
+        self._test_span_no_limits(new_tracer())
+
+    def test_span_no_limits_code(self):
+        self._test_span_no_limits(
+            new_tracer(
+                span_limits=trace.SpanLimits(
+                    max_attributes=trace.SpanLimits.UNSET,
+                    max_links=trace.SpanLimits.UNSET,
+                    max_events=trace.SpanLimits.UNSET,
+                    max_attribute_length=trace.SpanLimits.UNSET,
+                )
+            )
+        )
+
+    def test_dropped_attributes(self):
+        span = get_span_with_dropped_attributes_events_links()
+        self.assertEqual(1, span.dropped_links)
+        self.assertEqual(2, span.dropped_attributes)
+        self.assertEqual(3, span.dropped_events)
+        self.assertEqual(2, span.events[0].attributes.dropped)
+        self.assertEqual(2, span.links[0].attributes.dropped)
+        self.assertEqual(2, span.resource.attributes.dropped)
+
     def _test_span_limits(
-        self, tracer, max_attrs, max_events, max_links, max_attr_len
+        self,
+        tracer,
+        max_attrs,
+        max_events,
+        max_links,
+        max_attr_len,
+        max_span_attr_len,
     ):
         id_generator = RandomIdGenerator()
         some_links = [
@@ -1426,7 +1551,7 @@ def _test_span_limits(
                     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)
+                self._assert_attr_length(attr_val, max_span_attr_len)
 
     def _test_span_no_limits(self, tracer):
         num_links = int(trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT) + randint(
@@ -1470,95 +1595,3 @@ def _test_span_no_limits(self, tracer):
             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: "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(),
-            max_attrs=13,
-            max_events=7,
-            max_links=4,
-            max_attr_len=11,
-        )
-
-    @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_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_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=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(
-        "os.environ",
-        {
-            OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "",
-            OTEL_SPAN_EVENT_COUNT_LIMIT: "",
-            OTEL_SPAN_LINK_COUNT_LIMIT: "",
-            OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "",
-        },
-    )
-    def test_span_no_limits_env(self):
-        self._test_span_no_limits(new_tracer())
-
-    def test_span_no_limits_code(self):
-        self._test_span_no_limits(
-            new_tracer(
-                span_limits=trace.SpanLimits(
-                    max_attributes=trace.SpanLimits.UNSET,
-                    max_links=trace.SpanLimits.UNSET,
-                    max_events=trace.SpanLimits.UNSET,
-                    max_attribute_length=trace.SpanLimits.UNSET,
-                )
-            )
-        )
-
-    def test_dropped_attributes(self):
-        span = get_span_with_dropped_attributes_events_links()
-        self.assertEqual(1, span.dropped_links)
-        self.assertEqual(2, span.dropped_attributes)
-        self.assertEqual(3, span.dropped_events)
-        self.assertEqual(2, span.events[0].attributes.dropped)
-        self.assertEqual(2, span.links[0].attributes.dropped)
-        self.assertEqual(2, span.resource.attributes.dropped)