Skip to content

Commit 3dbbd1b

Browse files
authored
Allow users to "unset" SDK limits and evaluate default limits lazily instead of on import (#1839)
1 parent eda57f7 commit 3dbbd1b

File tree

5 files changed

+229
-34
lines changed

5 files changed

+229
-34
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
([#1823](https://github.com/open-telemetry/opentelemetry-python/pull/1823))
2222
- Added support for OTEL_SERVICE_NAME.
2323
([#1829](https://github.com/open-telemetry/opentelemetry-python/pull/1829))
24+
- Lazily read/configure limits and allow limits to be unset.
25+
([#1839](https://github.com/open-telemetry/opentelemetry-python/pull/1839))
2426

2527
### Changed
2628
- Fixed OTLP gRPC exporter silently failing if scheme is not specified in endpoint.

Diff for: opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py

+102-18
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,16 @@
5656
from opentelemetry.sdk.util import BoundedDict, BoundedList
5757
from opentelemetry.sdk.util.instrumentation import InstrumentationInfo
5858
from opentelemetry.trace import SpanContext
59-
from opentelemetry.trace.propagation import SPAN_KEY
6059
from opentelemetry.trace.status import Status, StatusCode
6160
from opentelemetry.util import types
6261
from opentelemetry.util._time import _time_ns
6362

6463
logger = logging.getLogger(__name__)
6564

66-
SPAN_ATTRIBUTE_COUNT_LIMIT = int(
67-
environ.get(OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, 128)
68-
)
65+
_DEFAULT_SPAN_EVENTS_LIMIT = 128
66+
_DEFAULT_SPAN_LINKS_LIMIT = 128
67+
_DEFAULT_SPAN_ATTRIBUTES_LIMIT = 128
6968

70-
_SPAN_EVENT_COUNT_LIMIT = int(environ.get(OTEL_SPAN_EVENT_COUNT_LIMIT, 128))
71-
_SPAN_LINK_COUNT_LIMIT = int(environ.get(OTEL_SPAN_LINK_COUNT_LIMIT, 128))
7269
# pylint: disable=protected-access
7370
_TRACE_SAMPLER = sampling._get_from_env_or_default()
7471

@@ -502,6 +499,87 @@ def _format_links(links):
502499
return f_links
503500

504501

502+
class _Limits:
503+
"""The limits that should be enforce on recorded data such as events, links, attributes etc.
504+
505+
This class does not enforce any limits itself. It only provides an a way read limits from env,
506+
default values and in future from user provided arguments.
507+
508+
All limit must be either a non-negative integer or ``None``.
509+
Setting a limit to ``None`` will not set any limits for that field/type.
510+
511+
Args:
512+
max_events: Maximum number of events that can be added to a Span.
513+
max_links: Maximum number of links that can be added to a Span.
514+
max_attributes: Maximum number of attributes that can be added to a Span.
515+
"""
516+
517+
UNSET = -1
518+
519+
max_attributes: int
520+
max_events: int
521+
max_links: int
522+
523+
def __init__(
524+
self,
525+
max_attributes: Optional[int] = None,
526+
max_events: Optional[int] = None,
527+
max_links: Optional[int] = None,
528+
):
529+
self.max_attributes = self._from_env_if_absent(
530+
max_attributes,
531+
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
532+
_DEFAULT_SPAN_ATTRIBUTES_LIMIT,
533+
)
534+
self.max_events = self._from_env_if_absent(
535+
max_events, OTEL_SPAN_EVENT_COUNT_LIMIT, _DEFAULT_SPAN_EVENTS_LIMIT
536+
)
537+
self.max_links = self._from_env_if_absent(
538+
max_links, OTEL_SPAN_LINK_COUNT_LIMIT, _DEFAULT_SPAN_LINKS_LIMIT
539+
)
540+
541+
def __repr__(self):
542+
return "max_attributes={}, max_events={}, max_links={}".format(
543+
self.max_attributes, self.max_events, self.max_links
544+
)
545+
546+
@classmethod
547+
def _from_env_if_absent(
548+
cls, value: Optional[int], env_var: str, default: Optional[int]
549+
) -> Optional[int]:
550+
if value is cls.UNSET:
551+
return None
552+
553+
err_msg = "{0} must be a non-negative integer but got {}"
554+
555+
if value is None:
556+
str_value = environ.get(env_var, "").strip().lower()
557+
if not str_value:
558+
return default
559+
if str_value == "unset":
560+
return None
561+
562+
try:
563+
value = int(str_value)
564+
except ValueError:
565+
raise ValueError(err_msg.format(env_var, str_value))
566+
567+
if value < 0:
568+
raise ValueError(err_msg.format(env_var, value))
569+
return value
570+
571+
572+
_UnsetLimits = _Limits(
573+
max_attributes=_Limits.UNSET,
574+
max_events=_Limits.UNSET,
575+
max_links=_Limits.UNSET,
576+
)
577+
578+
SPAN_ATTRIBUTE_COUNT_LIMIT = _Limits._from_env_if_absent(
579+
None, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_SPAN_ATTRIBUTES_LIMIT
580+
)
581+
582+
505583
class Span(trace_api.Span, ReadableSpan):
506584
"""See `opentelemetry.trace.Span`.
507585
@@ -566,7 +644,7 @@ def __init__(
566644
self._attributes = self._new_attributes()
567645
else:
568646
self._attributes = BoundedDict.from_map(
569-
SPAN_ATTRIBUTE_COUNT_LIMIT, attributes
647+
self._limits.max_attributes, attributes
570648
)
571649

572650
self._events = self._new_events()
@@ -582,24 +660,21 @@ def __init__(
582660
if links is None:
583661
self._links = self._new_links()
584662
else:
585-
self._links = BoundedList.from_seq(_SPAN_LINK_COUNT_LIMIT, links)
663+
self._links = BoundedList.from_seq(self._limits.max_links, links)
586664

587665
def __repr__(self):
588666
return '{}(name="{}", context={})'.format(
589667
type(self).__name__, self._name, self._context
590668
)
591669

592-
@staticmethod
593-
def _new_attributes():
594-
return BoundedDict(SPAN_ATTRIBUTE_COUNT_LIMIT)
670+
def _new_attributes(self):
671+
return BoundedDict(self._limits.max_attributes)
595672

596-
@staticmethod
597-
def _new_events():
598-
return BoundedList(_SPAN_EVENT_COUNT_LIMIT)
673+
def _new_events(self):
674+
return BoundedList(self._limits.max_events)
599675

600-
@staticmethod
601-
def _new_links():
602-
return BoundedList(_SPAN_LINK_COUNT_LIMIT)
676+
def _new_links(self):
677+
return BoundedList(self._limits.max_links)
603678

604679
def get_span_context(self):
605680
return self._context
@@ -772,6 +847,10 @@ class _Span(Span):
772847
by other mechanisms than through the `Tracer`.
773848
"""
774849

850+
def __init__(self, *args, limits=_UnsetLimits, **kwargs):
851+
self._limits = limits
852+
super().__init__(*args, **kwargs)
853+
775854

776855
class Tracer(trace_api.Tracer):
777856
"""See `opentelemetry.trace.Tracer`."""
@@ -791,6 +870,7 @@ def __init__(
791870
self.span_processor = span_processor
792871
self.id_generator = id_generator
793872
self.instrumentation_info = instrumentation_info
873+
self._limits = None
794874

795875
@contextmanager
796876
def start_as_current_span(
@@ -892,6 +972,7 @@ def start_span( # pylint: disable=too-many-locals
892972
instrumentation_info=self.instrumentation_info,
893973
record_exception=record_exception,
894974
set_status_on_exception=set_status_on_exception,
975+
limits=self._limits,
895976
)
896977
span.start(start_time=start_time, parent_context=context)
897978
else:
@@ -921,6 +1002,7 @@ def __init__(
9211002
self.id_generator = id_generator
9221003
self._resource = resource
9231004
self.sampler = sampler
1005+
self._limits = _Limits()
9241006
self._atexit_handler = None
9251007
if shutdown_on_exit:
9261008
self._atexit_handler = atexit.register(self.shutdown)
@@ -937,7 +1019,7 @@ def get_tracer(
9371019
if not instrumenting_module_name: # Reject empty strings too.
9381020
instrumenting_module_name = ""
9391021
logger.error("get_tracer called with missing module name.")
940-
return Tracer(
1022+
tracer = Tracer(
9411023
self.sampler,
9421024
self.resource,
9431025
self._active_span_processor,
@@ -946,6 +1028,8 @@ def get_tracer(
9461028
instrumenting_module_name, instrumenting_library_version
9471029
),
9481030
)
1031+
tracer._limits = self._limits
1032+
return tracer
9491033

9501034
def add_span_processor(self, span_processor: SpanProcessor) -> None:
9511035
"""Registers a new :class:`SpanProcessor` for this `TracerProvider`.

Diff for: opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py

+18-12
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import threading
1717
from collections import OrderedDict, deque
1818
from collections.abc import MutableMapping, Sequence
19+
from typing import Optional
1920

2021

2122
def ns_to_iso_str(nanoseconds):
@@ -45,7 +46,7 @@ class BoundedList(Sequence):
4546
not enough room.
4647
"""
4748

48-
def __init__(self, maxlen):
49+
def __init__(self, maxlen: Optional[int]):
4950
self.dropped = 0
5051
self._dq = deque(maxlen=maxlen) # type: deque
5152
self._lock = threading.Lock()
@@ -67,15 +68,19 @@ def __iter__(self):
6768

6869
def append(self, item):
6970
with self._lock:
70-
if len(self._dq) == self._dq.maxlen:
71+
if (
72+
self._dq.maxlen is not None
73+
and len(self._dq) == self._dq.maxlen
74+
):
7175
self.dropped += 1
7276
self._dq.append(item)
7377

7478
def extend(self, seq):
7579
with self._lock:
76-
to_drop = len(seq) + len(self._dq) - self._dq.maxlen
77-
if to_drop > 0:
78-
self.dropped += to_drop
80+
if self._dq.maxlen is not None:
81+
to_drop = len(seq) + len(self._dq) - self._dq.maxlen
82+
if to_drop > 0:
83+
self.dropped += to_drop
7984
self._dq.extend(seq)
8085

8186
@classmethod
@@ -93,11 +98,12 @@ class BoundedDict(MutableMapping):
9398
added.
9499
"""
95100

96-
def __init__(self, maxlen):
97-
if not isinstance(maxlen, int):
98-
raise ValueError
99-
if maxlen < 0:
100-
raise ValueError
101+
def __init__(self, maxlen: Optional[int]):
102+
if maxlen is not None:
103+
if not isinstance(maxlen, int):
104+
raise ValueError
105+
if maxlen < 0:
106+
raise ValueError
101107
self.maxlen = maxlen
102108
self.dropped = 0
103109
self._dict = OrderedDict() # type: OrderedDict
@@ -113,13 +119,13 @@ def __getitem__(self, key):
113119

114120
def __setitem__(self, key, value):
115121
with self._lock:
116-
if self.maxlen == 0:
122+
if self.maxlen is not None and self.maxlen == 0:
117123
self.dropped += 1
118124
return
119125

120126
if key in self._dict:
121127
del self._dict[key]
122-
elif len(self._dict) == self.maxlen:
128+
elif self.maxlen is not None and len(self._dict) == self.maxlen:
123129
del self._dict[next(iter(self._dict.keys()))]
124130
self.dropped += 1
125131
self._dict[key] = value

Diff for: opentelemetry-sdk/tests/test_util.py

+16
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ def test_extend_drop(self):
135135
self.assertEqual(len(blist), list_len)
136136
self.assertEqual(blist.dropped, len(other_list))
137137

138+
def test_no_limit(self):
139+
blist = BoundedList(maxlen=None)
140+
for num in range(100):
141+
blist.append(num)
142+
143+
for num in range(100):
144+
self.assertEqual(blist[num], num)
145+
138146

139147
class TestBoundedDict(unittest.TestCase):
140148
base = collections.OrderedDict(
@@ -214,3 +222,11 @@ def test_bounded_dict(self):
214222

215223
with self.assertRaises(KeyError):
216224
_ = bdict["new-name"]
225+
226+
def test_no_limit_code(self):
227+
bdict = BoundedDict(maxlen=None)
228+
for num in range(100):
229+
bdict[num] = num
230+
231+
for num in range(100):
232+
self.assertEqual(bdict[num], num)

0 commit comments

Comments
 (0)