Skip to content

Commit 660d289

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 660d289

File tree

5 files changed

+214
-43
lines changed

5 files changed

+214
-43
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

+94-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,80 @@ 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 SpanLimits.UNSET:
609+
return None
610+
611+
if value is not None:
612+
if not isinstance(value, int):
613+
raise ValueError("SpanLimit value must be an integer")
614+
if value < 0:
615+
raise ValueError(
616+
"SpanLimit value must be a non-negative number"
617+
)
618+
return value
619+
620+
value = environ.get(name, "").strip().lower()
621+
if value == "unset":
622+
return None
623+
if value:
624+
return int(value)
625+
return default
626+
627+
628+
_UnsetSpanLimits = SpanLimits(
629+
max_events=SpanLimits.UNSET,
630+
max_links=SpanLimits.UNSET,
631+
max_attributes=SpanLimits.UNSET,
632+
)
633+
634+
SPAN_ATTRIBUTE_COUNT_LIMIT = SpanLimits._value_or_default(
635+
None, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_SPAN_ATTRIBUTES_LIMIT
636+
)
637+
638+
567639
class Span(trace_api.Span, ReadableSpan):
568640
"""See `opentelemetry.trace.Span`.
569641
@@ -604,6 +676,7 @@ def __init__(
604676
links: Sequence[trace_api.Link] = (),
605677
kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL,
606678
span_processor: SpanProcessor = SpanProcessor(),
679+
limits: SpanLimits = _UnsetSpanLimits,
607680
instrumentation_info: InstrumentationInfo = None,
608681
record_exception: bool = True,
609682
set_status_on_exception: bool = True,
@@ -621,14 +694,15 @@ def __init__(
621694
self._record_exception = record_exception
622695
self._set_status_on_exception = set_status_on_exception
623696
self._span_processor = span_processor
697+
self._limits = limits
624698
self._lock = threading.Lock()
625699

626700
_filter_attribute_values(attributes)
627701
if not attributes:
628702
self._attributes = self._new_attributes()
629703
else:
630704
self._attributes = BoundedDict.from_map(
631-
SPAN_ATTRIBUTE_COUNT_LIMIT, attributes
705+
self._limits.max_attributes, attributes
632706
)
633707

634708
self._events = self._new_events()
@@ -644,24 +718,21 @@ def __init__(
644718
if links is None:
645719
self._links = self._new_links()
646720
else:
647-
self._links = BoundedList.from_seq(_SPAN_LINK_COUNT_LIMIT, links)
721+
self._links = BoundedList.from_seq(self._limits.max_links, links)
648722

649723
def __repr__(self):
650724
return '{}(name="{}", context={})'.format(
651725
type(self).__name__, self._name, self._context
652726
)
653727

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

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

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

666737
def get_span_context(self):
667738
return self._context
@@ -847,11 +918,13 @@ def __init__(
847918
],
848919
id_generator: IdGenerator,
849920
instrumentation_info: InstrumentationInfo,
921+
span_limits: SpanLimits,
850922
) -> None:
851923
self.sampler = sampler
852924
self.resource = resource
853925
self.span_processor = span_processor
854926
self.id_generator = id_generator
927+
self._span_limits = span_limits
855928
self.instrumentation_info = instrumentation_info
856929

857930
@contextmanager
@@ -954,6 +1027,7 @@ def start_span( # pylint: disable=too-many-locals
9541027
instrumentation_info=self.instrumentation_info,
9551028
record_exception=record_exception,
9561029
set_status_on_exception=set_status_on_exception,
1030+
limits=self._span_limits,
9571031
)
9581032
span.start(start_time=start_time, parent_context=context)
9591033
else:
@@ -973,6 +1047,7 @@ def __init__(
9731047
SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor
9741048
] = None,
9751049
id_generator: IdGenerator = None,
1050+
span_limits=None,
9761051
):
9771052
self._active_span_processor = (
9781053
active_span_processor or SynchronousMultiSpanProcessor()
@@ -983,6 +1058,7 @@ def __init__(
9831058
self.id_generator = id_generator
9841059
self._resource = resource
9851060
self.sampler = sampler
1061+
self._span_limits = span_limits or SpanLimits()
9861062
self._atexit_handler = None
9871063
if shutdown_on_exit:
9881064
self._atexit_handler = atexit.register(self.shutdown)
@@ -1007,6 +1083,7 @@ def get_tracer(
10071083
InstrumentationInfo(
10081084
instrumenting_module_name, instrumenting_library_version
10091085
),
1086+
self._span_limits,
10101087
)
10111088

10121089
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)