Skip to content

Commit 4877a52

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 4877a52

File tree

5 files changed

+182
-35
lines changed

5 files changed

+182
-35
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
([#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.

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

+99-19
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,17 @@
5151
from opentelemetry.sdk.util import BoundedDict, BoundedList
5252
from opentelemetry.sdk.util.instrumentation import InstrumentationInfo
5353
from opentelemetry.trace import SpanContext
54-
from opentelemetry.trace.propagation import SPAN_KEY
5554
from opentelemetry.trace.status import Status, StatusCode
5655
from opentelemetry.util import types
5756
from opentelemetry.util._time import _time_ns
5857

5958
logger = logging.getLogger(__name__)
6059

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))
60+
_DEFAULT_SPAN_EVENTS_LIMIT = 128
61+
_DEFAULT_SPAN_LINKS_LIMIT = 128
62+
_DEFAULT_SPAN_ATTRIBUTES_LIMIT = 128
6763
_VALID_ATTR_VALUE_TYPES = (bool, str, int, float)
64+
6865
# pylint: disable=protected-access
6966
_TRACE_SAMPLER = sampling._get_from_env_or_default()
7067

@@ -564,6 +561,81 @@ def _format_links(links):
564561
return f_links
565562

566563

564+
class _Limits:
565+
"""The limits that should be enforce on recorded data such as events, links, attributes etc.
566+
567+
This class does not enforce any limits itself. It only provides an a way read limits from env,
568+
default values and in future from user provided arguments.
569+
570+
All limit must be a either non-negative integers or None.
571+
Setting a limit to ``None`` will not set an limits for that data type.
572+
573+
Args:
574+
max_events: Maximum number of events that can be added to a Span.
575+
max_links: Maximum number of links that can be added to a Span.
576+
max_attributes: Maximum number of attributes that can be added to a Span.
577+
"""
578+
579+
max_attributes: int
580+
max_events: int
581+
max_links: int
582+
583+
def __init__(
584+
self,
585+
max_events: Optional[int] = None,
586+
max_links: Optional[int] = None,
587+
max_attributes: Optional[int] = None,
588+
):
589+
self.max_attributes = max_attributes
590+
self.max_events = max_events
591+
self.max_links = max_links
592+
593+
def __repr__(self):
594+
return "max_attributes={}, max_events={}, max_links={}".format(
595+
self.max_attributes, self.max_events, self.max_links
596+
)
597+
598+
@classmethod
599+
def _create_from_env(cls):
600+
return cls(
601+
max_attributes=cls._from_env(
602+
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_SPAN_ATTRIBUTES_LIMIT
603+
),
604+
max_events=cls._from_env(
605+
OTEL_SPAN_EVENT_COUNT_LIMIT, _DEFAULT_SPAN_EVENTS_LIMIT
606+
),
607+
max_links=cls._from_env(
608+
OTEL_SPAN_LINK_COUNT_LIMIT, _DEFAULT_SPAN_LINKS_LIMIT
609+
),
610+
)
611+
612+
@staticmethod
613+
def _from_env(env_var: str, default: int) -> Optional[int]:
614+
value = environ.get(env_var, "").strip().lower()
615+
if not value:
616+
return default
617+
if value == "unset":
618+
return None
619+
620+
err_msg = "{0} must be a non-negative number but got {1}".format(
621+
env_var, value
622+
)
623+
try:
624+
value = int(value)
625+
except ValueError:
626+
raise ValueError(err_msg)
627+
if value < 0:
628+
raise ValueError(err_msg)
629+
return value
630+
631+
632+
_UnsetLimits = _Limits()
633+
634+
SPAN_ATTRIBUTE_COUNT_LIMIT = _Limits._from_env(
635+
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
@@ -628,7 +700,7 @@ def __init__(
628700
self._attributes = self._new_attributes()
629701
else:
630702
self._attributes = BoundedDict.from_map(
631-
SPAN_ATTRIBUTE_COUNT_LIMIT, attributes
703+
self._limits.max_attributes, attributes
632704
)
633705

634706
self._events = self._new_events()
@@ -644,24 +716,21 @@ def __init__(
644716
if links is None:
645717
self._links = self._new_links()
646718
else:
647-
self._links = BoundedList.from_seq(_SPAN_LINK_COUNT_LIMIT, links)
719+
self._links = BoundedList.from_seq(self._limits.max_links, links)
648720

649721
def __repr__(self):
650722
return '{}(name="{}", context={})'.format(
651723
type(self).__name__, self._name, self._context
652724
)
653725

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

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

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

666735
def get_span_context(self):
667736
return self._context
@@ -834,6 +903,10 @@ class _Span(Span):
834903
by other mechanisms than through the `Tracer`.
835904
"""
836905

906+
def __init__(self, *args, limits=_UnsetLimits, **kwargs):
907+
self._limits = limits
908+
super().__init__(*args, **kwargs)
909+
837910

838911
class Tracer(trace_api.Tracer):
839912
"""See `opentelemetry.trace.Tracer`."""
@@ -853,6 +926,11 @@ def __init__(
853926
self.span_processor = span_processor
854927
self.id_generator = id_generator
855928
self.instrumentation_info = instrumentation_info
929+
self._limits = None
930+
931+
def _with_limits(self, limits: _Limits) -> "Tracer":
932+
self._limits = limits
933+
return self
856934

857935
@contextmanager
858936
def start_as_current_span(
@@ -954,6 +1032,7 @@ def start_span( # pylint: disable=too-many-locals
9541032
instrumentation_info=self.instrumentation_info,
9551033
record_exception=record_exception,
9561034
set_status_on_exception=set_status_on_exception,
1035+
limits=self._limits,
9571036
)
9581037
span.start(start_time=start_time, parent_context=context)
9591038
else:
@@ -983,6 +1062,7 @@ def __init__(
9831062
self.id_generator = id_generator
9841063
self._resource = resource
9851064
self.sampler = sampler
1065+
self._limits = _Limits._create_from_env()
9861066
self._atexit_handler = None
9871067
if shutdown_on_exit:
9881068
self._atexit_handler = atexit.register(self.shutdown)
@@ -1007,7 +1087,7 @@ def get_tracer(
10071087
InstrumentationInfo(
10081088
instrumenting_module_name, instrumenting_library_version
10091089
),
1010-
)
1090+
)._with_limits(self._limits)
10111091

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

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)

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)