Repository: open-telemetry/opentelemetry-python

Issue:
Support Span Collection limit env variables
As per the spec, we need to add support for Span Collection limits via environment variables:

https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/sdk-environment-variables.md#span-collection-limits


Discussion:
Hey! I'd like to take on this one, looks pretty doable :)

Resolving patch:
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
@@ -37,6 +37,7 @@
 
 from opentelemetry import context as context_api
 from opentelemetry import trace as trace_api
+from opentelemetry.configuration import Configuration
 from opentelemetry.sdk import util
 from opentelemetry.sdk.resources import Resource
 from opentelemetry.sdk.trace import sampling
@@ -49,9 +50,11 @@
 
 logger = logging.getLogger(__name__)
 
-MAX_NUM_ATTRIBUTES = 1000
-MAX_NUM_EVENTS = 1000
-MAX_NUM_LINKS = 1000
+SPAN_ATTRIBUTE_COUNT_LIMIT = Configuration().get(
+    "SPAN_ATTRIBUTE_COUNT_LIMIT", 1000
+)
+SPAN_EVENT_COUNT_LIMIT = Configuration().get("SPAN_EVENT_COUNT_LIMIT", 1000)
+SPAN_LINK_COUNT_LIMIT = Configuration().get("SPAN_LINK_COUNT_LIMIT", 1000)
 VALID_ATTR_VALUE_TYPES = (bool, str, int, float)
 
 
@@ -446,7 +449,7 @@ def __init__(
             self.attributes = self._new_attributes()
         else:
             self.attributes = BoundedDict.from_map(
-                MAX_NUM_ATTRIBUTES, attributes
+                SPAN_ATTRIBUTE_COUNT_LIMIT, attributes
             )
 
         self.events = self._new_events()
@@ -462,7 +465,7 @@ def __init__(
         if links is None:
             self.links = self._new_links()
         else:
-            self.links = BoundedList.from_seq(MAX_NUM_LINKS, links)
+            self.links = BoundedList.from_seq(SPAN_LINK_COUNT_LIMIT, links)
 
         self._end_time = None  # type: Optional[int]
         self._start_time = None  # type: Optional[int]
@@ -483,15 +486,15 @@ def __repr__(self):
 
     @staticmethod
     def _new_attributes():
-        return BoundedDict(MAX_NUM_ATTRIBUTES)
+        return BoundedDict(SPAN_ATTRIBUTE_COUNT_LIMIT)
 
     @staticmethod
     def _new_events():
-        return BoundedList(MAX_NUM_EVENTS)
+        return BoundedList(SPAN_EVENT_COUNT_LIMIT)
 
     @staticmethod
     def _new_links():
-        return BoundedList(MAX_NUM_LINKS)
+        return BoundedList(SPAN_LINK_COUNT_LIMIT)
 
     @staticmethod
     def _format_context(context):


Test patch:
diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py
--- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py
+++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter.py
@@ -41,6 +41,12 @@ def setUp(self):
         self._test_span = trace._Span("test_span", context=context)
         self._test_span.start()
         self._test_span.end()
+        # pylint: disable=protected-access
+        Configuration._reset()
+
+    def tearDown(self):
+        # pylint: disable=protected-access
+        Configuration._reset()
 
     def test_constructor_default(self):
         """Test the default values assigned by constructor."""
@@ -142,8 +148,6 @@ def test_constructor_by_environment_variables(self):
 
         environ_patcher.stop()
 
-        Configuration._reset()
-
     def test_nsec_to_usec_round(self):
         # pylint: disable=protected-access
         nsec_to_usec_round = jaeger_exporter._nsec_to_usec_round
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
@@ -16,11 +16,15 @@
 import shutil
 import subprocess
 import unittest
+from importlib import reload
 from logging import ERROR, WARNING
 from typing import Optional
 from unittest import mock
 
+import pytest
+
 from opentelemetry import trace as trace_api
+from opentelemetry.configuration import Configuration
 from opentelemetry.context import Context
 from opentelemetry.sdk import resources, trace
 from opentelemetry.sdk.trace import Resource, sampling
@@ -1182,3 +1186,49 @@ def test_attributes_to_json(self):
             + date_str
             + '", "attributes": {"key2": "value2"}}], "links": [], "resource": {}}',
         )
+
+
+class TestSpanLimits(unittest.TestCase):
+    def setUp(self):
+        # reset global state of configuration object
+        # pylint: disable=protected-access
+        Configuration._reset()
+
+    def tearDown(self):
+        # reset global state of configuration object
+        # pylint: disable=protected-access
+        Configuration._reset()
+
+    @mock.patch.dict(
+        "os.environ",
+        {
+            "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT": "10",
+            "OTEL_SPAN_EVENT_COUNT_LIMIT": "20",
+            "OTEL_SPAN_LINK_COUNT_LIMIT": "30",
+        },
+    )
+    def test_span_environment_limits(self):
+        reload(trace)
+        tracer = new_tracer()
+        ids_generator = trace_api.RandomIdsGenerator()
+        some_links = [
+            trace_api.Link(
+                trace_api.SpanContext(
+                    trace_id=ids_generator.generate_trace_id(),
+                    span_id=ids_generator.generate_span_id(),
+                    is_remote=False,
+                )
+            )
+            for _ in range(100)
+        ]
+        with pytest.raises(ValueError):
+            with tracer.start_as_current_span("root", links=some_links):
+                pass
+
+        with tracer.start_as_current_span("root") as root:
+            for idx in range(100):
+                root.set_attribute("my_attribute_{}".format(idx), 0)
+                root.add_event("my_event_{}".format(idx))
+
+            self.assertEqual(len(root.attributes), 10)
+            self.assertEqual(len(root.events), 20)