Skip to content

Commit 2f21502

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 f816287 commit 2f21502

File tree

5 files changed

+180
-34
lines changed

5 files changed

+180
-34
lines changed

Diff for: 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
([#1823](https://github.com/open-telemetry/opentelemetry-python/pull/1823))
1616
- Added support for OTEL_SERVICE_NAME.
1717
([#1829](https://github.com/open-telemetry/opentelemetry-python/pull/1829))
18+
- Lazily read/configure limits and allow limits to be unset.
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.

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

+97-18
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,81 @@ def _format_links(links):
564562
return f_links
565563

566564

565+
class _Limits:
566+
"""The limits that should be enforce on recorded data such as events, links, attributes etc.
567+
568+
This class does not enforce any limits itself. It only provides an a way read limits from env,
569+
default values and in future from user provided arguments.
570+
571+
All limit must be a either non-negative integers or None.
572+
Setting a limit to ``None`` 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+
max_attributes: int
581+
max_events: int
582+
max_links: int
583+
584+
def __init__(
585+
self,
586+
max_events: Optional[int] = None,
587+
max_links: Optional[int] = None,
588+
max_attributes: Optional[int] = None,
589+
):
590+
self.max_attributes = max_attributes
591+
self.max_events = max_events
592+
self.max_links = max_links
593+
594+
def __repr__(self):
595+
return "max_attributes={}, max_events={}, max_links={}".format(
596+
self.max_attributes, self.max_events, self.max_links
597+
)
598+
599+
@classmethod
600+
def _create(cls):
601+
return cls(
602+
max_attributes=cls._from_env(
603+
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_SPAN_ATTRIBUTES_LIMIT
604+
),
605+
max_events=cls._from_env(
606+
OTEL_SPAN_EVENT_COUNT_LIMIT, _DEFAULT_SPAN_EVENTS_LIMIT
607+
),
608+
max_links=cls._from_env(
609+
OTEL_SPAN_LINK_COUNT_LIMIT, _DEFAULT_SPAN_LINKS_LIMIT
610+
),
611+
)
612+
613+
@classmethod
614+
def _from_env(cls, env_var: str, default: int) -> Optional[int]:
615+
value = environ.get(env_var, "").strip().lower()
616+
if not value:
617+
return default
618+
if value == "unset":
619+
return None
620+
621+
err_msg = "{0} must be a non-negative number but got {1}".format(
622+
env_var, value
623+
)
624+
try:
625+
value = int(value)
626+
except ValueError:
627+
raise ValueError(err_msg)
628+
if value < 0:
629+
raise ValueError(err_msg)
630+
return value
631+
632+
633+
_UnsetLimits = _Limits()
634+
635+
SPAN_ATTRIBUTE_COUNT_LIMIT = _Limits._from_env(
636+
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_SPAN_ATTRIBUTES_LIMIT
637+
)
638+
639+
567640
class Span(trace_api.Span, ReadableSpan):
568641
"""See `opentelemetry.trace.Span`.
569642
@@ -604,6 +677,7 @@ def __init__(
604677
links: Sequence[trace_api.Link] = (),
605678
kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL,
606679
span_processor: SpanProcessor = SpanProcessor(),
680+
limits: _Limits = _UnsetLimits,
607681
instrumentation_info: InstrumentationInfo = None,
608682
record_exception: bool = True,
609683
set_status_on_exception: bool = True,
@@ -621,14 +695,15 @@ def __init__(
621695
self._record_exception = record_exception
622696
self._set_status_on_exception = set_status_on_exception
623697
self._span_processor = span_processor
698+
self._limits = limits
624699
self._lock = threading.Lock()
625700

626701
_filter_attribute_values(attributes)
627702
if not attributes:
628703
self._attributes = self._new_attributes()
629704
else:
630705
self._attributes = BoundedDict.from_map(
631-
SPAN_ATTRIBUTE_COUNT_LIMIT, attributes
706+
self._limits.max_attributes, attributes
632707
)
633708

634709
self._events = self._new_events()
@@ -644,24 +719,21 @@ def __init__(
644719
if links is None:
645720
self._links = self._new_links()
646721
else:
647-
self._links = BoundedList.from_seq(_SPAN_LINK_COUNT_LIMIT, links)
722+
self._links = BoundedList.from_seq(self._limits.max_links, links)
648723

649724
def __repr__(self):
650725
return '{}(name="{}", context={})'.format(
651726
type(self).__name__, self._name, self._context
652727
)
653728

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

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

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

666738
def get_span_context(self):
667739
return self._context
@@ -853,6 +925,11 @@ def __init__(
853925
self.span_processor = span_processor
854926
self.id_generator = id_generator
855927
self.instrumentation_info = instrumentation_info
928+
self._limits = None
929+
930+
def _with_limits(self, limits: _Limits) -> "Tracer":
931+
self._limits = limits
932+
return self
856933

857934
@contextmanager
858935
def start_as_current_span(
@@ -954,6 +1031,7 @@ def start_span( # pylint: disable=too-many-locals
9541031
instrumentation_info=self.instrumentation_info,
9551032
record_exception=record_exception,
9561033
set_status_on_exception=set_status_on_exception,
1034+
limits=self._limits,
9571035
)
9581036
span.start(start_time=start_time, parent_context=context)
9591037
else:
@@ -983,6 +1061,7 @@ def __init__(
9831061
self.id_generator = id_generator
9841062
self._resource = resource
9851063
self.sampler = sampler
1064+
self._limits = _Limits._create()
9861065
self._atexit_handler = None
9871066
if shutdown_on_exit:
9881067
self._atexit_handler = atexit.register(self.shutdown)
@@ -1007,7 +1086,7 @@ def get_tracer(
10071086
InstrumentationInfo(
10081087
instrumenting_module_name, instrumenting_library_version
10091088
),
1010-
)
1089+
)._with_limits(self._limits)
10111090

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

Diff for: 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

Diff for: 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)

Diff for: opentelemetry-sdk/tests/trace/test_trace.py

+45-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import unittest
1919
from importlib import reload
2020
from logging import ERROR, WARNING
21+
from random import randint
2122
from typing import Optional
2223
from unittest import mock
2324

@@ -1281,8 +1282,7 @@ class TestSpanLimits(unittest.TestCase):
12811282
OTEL_SPAN_LINK_COUNT_LIMIT: "30",
12821283
},
12831284
)
1284-
def test_span_environment_limits(self):
1285-
reload(trace)
1285+
def test_span_limits_env(self):
12861286
tracer = new_tracer()
12871287
id_generator = RandomIdGenerator()
12881288
some_links = [
@@ -1306,3 +1306,46 @@ def test_span_environment_limits(self):
13061306

13071307
self.assertEqual(len(root.attributes), 10)
13081308
self.assertEqual(len(root.events), 20)
1309+
1310+
@mock.patch.dict(
1311+
"os.environ",
1312+
{
1313+
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "unset",
1314+
OTEL_SPAN_EVENT_COUNT_LIMIT: "unset",
1315+
OTEL_SPAN_LINK_COUNT_LIMIT: "unset",
1316+
},
1317+
)
1318+
def test_span_no_limits_env(self):
1319+
# pylint: disable=protected-access
1320+
num_links = int(trace._DEFAULT_SPAN_LINKS_LIMIT) + randint(1, 100)
1321+
1322+
tracer = new_tracer()
1323+
id_generator = RandomIdGenerator()
1324+
some_links = [
1325+
trace_api.Link(
1326+
trace_api.SpanContext(
1327+
trace_id=id_generator.generate_trace_id(),
1328+
span_id=id_generator.generate_span_id(),
1329+
is_remote=False,
1330+
)
1331+
)
1332+
for _ in range(num_links)
1333+
]
1334+
with tracer.start_as_current_span("root", links=some_links) as root:
1335+
self.assertEqual(len(root.links), num_links)
1336+
1337+
num_events = int(trace._DEFAULT_SPAN_EVENTS_LIMIT) + randint(1, 100)
1338+
with tracer.start_as_current_span("root") as root:
1339+
for idx in range(num_events):
1340+
root.add_event("my_event_{}".format(idx))
1341+
1342+
self.assertEqual(len(root.events), num_events)
1343+
1344+
num_attributes = int(trace._DEFAULT_SPAN_ATTRIBUTES_LIMIT) + randint(
1345+
1, 100
1346+
)
1347+
with tracer.start_as_current_span("root") as root:
1348+
for idx in range(num_attributes):
1349+
root.set_attribute("my_attribute_{}".format(idx), 0)
1350+
1351+
self.assertEqual(len(root.attributes), num_attributes)

0 commit comments

Comments
 (0)