Skip to content

Commit 9d77737

Browse files
committed
Added Span limits support to the tracing SDK
- Added SpanLimits class - TracerProvider now optionally accepts an instance of SpanLimits which is passed all the way down to Spans. - Spans use the limits to created bounded lists or dicts for different fields.
1 parent cc18b73 commit 9d77737

File tree

5 files changed

+209
-44
lines changed

5 files changed

+209
-44
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
([#1803](https://github.com/open-telemetry/opentelemetry-python/pull/1803))
1616
- Added support for OTEL_SERVICE_NAME.
1717
([#1829](https://github.com/open-telemetry/opentelemetry-python/pull/1829))
18+
- Lazily configure Span limits and allow limits limits to be configured via code.
19+
([#1839](https://github.com/open-telemetry/opentelemetry-python/pull/1839))
1820

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

opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py

+93-17
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,11 @@
5858

5959
logger = logging.getLogger(__name__)
6060

61-
SPAN_ATTRIBUTE_COUNT_LIMIT = int(
62-
environ.get(OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, 128)
63-
)
64-
65-
_SPAN_EVENT_COUNT_LIMIT = int(environ.get(OTEL_SPAN_EVENT_COUNT_LIMIT, 128))
66-
_SPAN_LINK_COUNT_LIMIT = int(environ.get(OTEL_SPAN_LINK_COUNT_LIMIT, 128))
61+
_DEFAULT_SPAN_EVENTS_LIMIT = 128
62+
_DEFAULT_SPAN_LINKS_LIMIT = 128
63+
_DEFAULT_SPAN_ATTRIBUTES_LIMIT = 128
6764
_VALID_ATTR_VALUE_TYPES = (bool, str, int, float)
65+
6866
# pylint: disable=protected-access
6967
_TRACE_SAMPLER = sampling._get_from_env_or_default()
7068

@@ -564,6 +562,79 @@ def _format_links(links):
564562
return f_links
565563

566564

565+
class SpanLimits:
566+
"""The limits Spans should enforce on recorded data such as events, links, attributes etc.
567+
568+
This class does not enforce any limits itself. It only provides an API to set the limits
569+
when creating a tracer provider.
570+
571+
All arguments must either be a non-negative integer or a reference to :attr:`~UNSET`.
572+
Setting a limit to `SpanLimits.UNSET` will not set an limits for that data type.
573+
574+
Args:
575+
max_events: Maximum number of events that can be added to a Span.
576+
max_links: Maximum number of links that can be added to a Span.
577+
max_attributes: Maximum number of attributes that can be added to a Span.
578+
"""
579+
580+
UNSET = -1
581+
582+
max_events: int
583+
max_links: int
584+
max_attributes: int
585+
586+
def __init__(
587+
self,
588+
max_events: Optional[int] = None,
589+
max_links: Optional[int] = None,
590+
max_attributes: Optional[int] = None,
591+
):
592+
self.max_events = SpanLimits._value_or_default(
593+
max_events, OTEL_SPAN_EVENT_COUNT_LIMIT, _DEFAULT_SPAN_EVENTS_LIMIT
594+
)
595+
self.max_links = SpanLimits._value_or_default(
596+
max_links, OTEL_SPAN_LINK_COUNT_LIMIT, _DEFAULT_SPAN_LINKS_LIMIT
597+
)
598+
self.max_attributes = SpanLimits._value_or_default(
599+
max_attributes,
600+
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
601+
_DEFAULT_SPAN_ATTRIBUTES_LIMIT,
602+
)
603+
604+
@classmethod
605+
def _value_or_default(
606+
cls, value: Optional[int], name: str, default: int
607+
) -> Optional[int]:
608+
if value is not None:
609+
if value is SpanLimits.UNSET:
610+
return None
611+
if not isinstance(value, int):
612+
raise ValueError("SpanLimit value must be an integer")
613+
if value < 0:
614+
raise ValueError(
615+
"SpanLimit value must be a non-negative number"
616+
)
617+
return value
618+
619+
value = environ.get(name, "").strip().lower()
620+
if value == "unset":
621+
return None
622+
if value:
623+
return int(value)
624+
return default
625+
626+
627+
_UnsetSpanLimits = SpanLimits(
628+
max_events=SpanLimits.UNSET,
629+
max_links=SpanLimits.UNSET,
630+
max_attributes=SpanLimits.UNSET,
631+
)
632+
633+
SPAN_ATTRIBUTE_COUNT_LIMIT = SpanLimits._value_or_default(
634+
None, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_SPAN_ATTRIBUTES_LIMIT
635+
)
636+
637+
567638
class Span(trace_api.Span, ReadableSpan):
568639
"""See `opentelemetry.trace.Span`.
569640
@@ -604,6 +675,7 @@ def __init__(
604675
links: Sequence[trace_api.Link] = (),
605676
kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL,
606677
span_processor: SpanProcessor = SpanProcessor(),
678+
limits: SpanLimits = _UnsetSpanLimits,
607679
instrumentation_info: InstrumentationInfo = None,
608680
record_exception: bool = True,
609681
set_status_on_exception: bool = True,
@@ -621,14 +693,15 @@ def __init__(
621693
self._record_exception = record_exception
622694
self._set_status_on_exception = set_status_on_exception
623695
self._span_processor = span_processor
696+
self._limits = limits
624697
self._lock = threading.Lock()
625698

626699
_filter_attribute_values(attributes)
627700
if not attributes:
628701
self._attributes = self._new_attributes()
629702
else:
630703
self._attributes = BoundedDict.from_map(
631-
SPAN_ATTRIBUTE_COUNT_LIMIT, attributes
704+
self._limits.max_attributes, attributes
632705
)
633706

634707
self._events = self._new_events()
@@ -644,24 +717,21 @@ def __init__(
644717
if links is None:
645718
self._links = self._new_links()
646719
else:
647-
self._links = BoundedList.from_seq(_SPAN_LINK_COUNT_LIMIT, links)
720+
self._links = BoundedList.from_seq(self._limits.max_links, links)
648721

649722
def __repr__(self):
650723
return '{}(name="{}", context={})'.format(
651724
type(self).__name__, self._name, self._context
652725
)
653726

654-
@staticmethod
655-
def _new_attributes():
656-
return BoundedDict(SPAN_ATTRIBUTE_COUNT_LIMIT)
727+
def _new_attributes(self):
728+
return BoundedDict(self._limits.max_attributes)
657729

658-
@staticmethod
659-
def _new_events():
660-
return BoundedList(_SPAN_EVENT_COUNT_LIMIT)
730+
def _new_events(self):
731+
return BoundedList(self._limits.max_events)
661732

662-
@staticmethod
663-
def _new_links():
664-
return BoundedList(_SPAN_LINK_COUNT_LIMIT)
733+
def _new_links(self):
734+
return BoundedList(self._limits.max_links)
665735

666736
def get_span_context(self):
667737
return self._context
@@ -847,11 +917,13 @@ def __init__(
847917
],
848918
id_generator: IdGenerator,
849919
instrumentation_info: InstrumentationInfo,
920+
span_limits: SpanLimits,
850921
) -> None:
851922
self.sampler = sampler
852923
self.resource = resource
853924
self.span_processor = span_processor
854925
self.id_generator = id_generator
926+
self._span_limits = span_limits
855927
self.instrumentation_info = instrumentation_info
856928

857929
@contextmanager
@@ -954,6 +1026,7 @@ def start_span( # pylint: disable=too-many-locals
9541026
instrumentation_info=self.instrumentation_info,
9551027
record_exception=record_exception,
9561028
set_status_on_exception=set_status_on_exception,
1029+
limits=self._span_limits,
9571030
)
9581031
span.start(start_time=start_time, parent_context=context)
9591032
else:
@@ -973,6 +1046,7 @@ def __init__(
9731046
SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor
9741047
] = None,
9751048
id_generator: IdGenerator = None,
1049+
span_limits=None,
9761050
):
9771051
self._active_span_processor = (
9781052
active_span_processor or SynchronousMultiSpanProcessor()
@@ -983,6 +1057,7 @@ def __init__(
9831057
self.id_generator = id_generator
9841058
self._resource = resource
9851059
self.sampler = sampler
1060+
self._span_limits = span_limits or SpanLimits()
9861061
self._atexit_handler = None
9871062
if shutdown_on_exit:
9881063
self._atexit_handler = atexit.register(self.shutdown)
@@ -1007,6 +1082,7 @@ def get_tracer(
10071082
InstrumentationInfo(
10081083
instrumenting_module_name, instrumenting_library_version
10091084
),
1085+
self._span_limits,
10101086
)
10111087

10121088
def add_span_processor(self, span_processor: SpanProcessor) -> None:

opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py

+20-14
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,21 +68,25 @@ 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
8287
def from_seq(cls, maxlen, seq):
8388
seq = tuple(seq)
84-
if len(seq) > maxlen:
89+
if maxlen is not None and len(seq) > maxlen:
8590
raise ValueError
8691
bounded_list = cls(maxlen)
8792
# pylint: disable=protected-access
@@ -96,11 +101,12 @@ class BoundedDict(MutableMapping):
96101
added.
97102
"""
98103

99-
def __init__(self, maxlen):
100-
if not isinstance(maxlen, int):
101-
raise ValueError
102-
if maxlen < 0:
103-
raise ValueError
104+
def __init__(self, maxlen: Optional[int]):
105+
if maxlen is not None:
106+
if not isinstance(maxlen, int):
107+
raise ValueError
108+
if maxlen < 0:
109+
raise ValueError
104110
self.maxlen = maxlen
105111
self.dropped = 0
106112
self._dict = OrderedDict() # type: OrderedDict
@@ -116,13 +122,13 @@ def __getitem__(self, key):
116122

117123
def __setitem__(self, key, value):
118124
with self._lock:
119-
if self.maxlen == 0:
125+
if self.maxlen is not None and self.maxlen == 0:
120126
self.dropped += 1
121127
return
122128

123129
if key in self._dict:
124130
del self._dict[key]
125-
elif len(self._dict) == self.maxlen:
131+
elif self.maxlen is not None and len(self._dict) == self.maxlen:
126132
del self._dict[next(iter(self._dict.keys()))]
127133
self.dropped += 1
128134
self._dict[key] = value
@@ -140,7 +146,7 @@ def __len__(self):
140146
@classmethod
141147
def from_map(cls, maxlen, mapping):
142148
mapping = OrderedDict(mapping)
143-
if len(mapping) > maxlen:
149+
if maxlen is not None and len(mapping) > maxlen:
144150
raise ValueError
145151
bounded_dict = cls(maxlen)
146152
# pylint: disable=protected-access

opentelemetry-sdk/tests/test_util.py

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

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

138146
class TestBoundedDict(unittest.TestCase):
139147
base = collections.OrderedDict(
@@ -211,3 +219,11 @@ def test_bounded_dict(self):
211219

212220
with self.assertRaises(KeyError):
213221
_ = bdict["new-name"]
222+
223+
def test_no_limit_code(self):
224+
bdict = BoundedDict(maxlen=None)
225+
for num in range(100):
226+
bdict[num] = num
227+
228+
for num in range(100):
229+
self.assertEqual(bdict[num], num)

0 commit comments

Comments
 (0)