Skip to content

Commit

Permalink
Added max attribute length span limit support
Browse files Browse the repository at this point in the history
  • Loading branch information
owais committed May 15, 2021
1 parent 9c8ac95 commit 00f90fd
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 57 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#1829](https://github.com/open-telemetry/opentelemetry-python/pull/1829))
- Lazily read/configure limits and allow limits to be unset.
([#1839](https://github.com/open-telemetry/opentelemetry-python/pull/1839))
- Added support for read/configure limits and allow limits to be unset.
([#1839](https://github.com/open-telemetry/opentelemetry-python/pull/1839))

### Changed
- Fixed OTLP gRPC exporter silently failing if scheme is not specified in endpoint.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@
.. envvar:: OTEL_BSP_MAX_EXPORT_BATCH_SIZE
"""

OTEL_SPAN_ATTRIBUTE_SIZE_LIMIT = "OTEL_SPAN_ATTRIBUTE_SIZE_LIMIT"
"""
.. envvar:: OTEL_SPAN_ATTRIBUTE_SIZE_LIMIT
"""

OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT"
"""
.. envvar:: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT
Expand Down
112 changes: 84 additions & 28 deletions opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from opentelemetry.sdk import util
from opentelemetry.sdk.environment_variables import (
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
OTEL_SPAN_ATTRIBUTE_SIZE_LIMIT,
OTEL_SPAN_EVENT_COUNT_LIMIT,
OTEL_SPAN_LINK_COUNT_LIMIT,
)
Expand All @@ -61,6 +62,7 @@
_DEFAULT_SPAN_EVENTS_LIMIT = 128
_DEFAULT_SPAN_LINKS_LIMIT = 128
_DEFAULT_SPAN_ATTRIBUTES_LIMIT = 128
_DEFAULT_SPAN_ATTRIBUTE_SIZE_LIMIT = None
_VALID_ATTR_VALUE_TYPES = (bool, str, int, float)

# pylint: disable=protected-access
Expand Down Expand Up @@ -313,21 +315,29 @@ def attributes(self) -> types.Attributes:
return self._attributes


def _is_valid_attribute_value(value: types.AttributeValue) -> bool:
"""Checks if attribute value is valid.
def _clean_attribute_value(
value: types.AttributeValue, max_length: Optional[int]
) -> Tuple[bool, Optional[types.AttributeValue]]:
"""Checks if attribute value is valid and cleans it up if required.
An attribute value is valid if it is one of the valid types.
If the value is a sequence, it is only valid if all items in the sequence:
- are of the same valid type or None
- are not a sequence
When the ``max_length`` argument is set, any strings values longer than the value
are truncated and returned back as the second value.
If the attribute value is not modified, the ``None`` is returned as the second return value.
"""

if isinstance(value, Sequence):
modified = False
if isinstance(value, Sequence) and not isinstance(value, str):
if len(value) == 0:
return True
return True, None

sequence_first_valid_type = None
for element in value:
for idx, element in enumerate(value):
if element is None:
continue
element_type = type(element)
Expand All @@ -341,7 +351,7 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool:
for valid_type in _VALID_ATTR_VALUE_TYPES
],
)
return False
return False, None
# The type of the sequence must be homogeneous. The first non-None
# element determines the type of the sequence
if sequence_first_valid_type is None:
Expand All @@ -352,7 +362,11 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool:
sequence_first_valid_type.__name__,
type(element).__name__,
)
return False
return False, None
if max_length is not None and isinstance(element, str):
element = element[:max_length]
value[idx] = element
modified = True

elif not isinstance(value, _VALID_ATTR_VALUE_TYPES):
logger.warning(
Expand All @@ -361,18 +375,31 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool:
type(value).__name__,
[valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES],
)
return False
return True


def _filter_attribute_values(attributes: types.Attributes):
if attributes:
for attr_key, attr_value in list(attributes.items()):
if _is_valid_attribute_value(attr_value):
if isinstance(attr_value, MutableSequence):
attributes[attr_key] = tuple(attr_value)
else:
attributes.pop(attr_key)
return False, None
if max_length is not None and isinstance(value, str):
value = value[:max_length]
modified = True

return True, value if modified else None


def _filter_attribute_values(
attributes: types.Attributes, max_length: Optional[int]
):
if not attributes:
return

for attr_key, attr_value in list(attributes.items()):
valid, cleaned_value = _clean_attribute_value(attr_value, max_length)
if valid:
if isinstance(attr_value, MutableSequence):
if cleaned_value is not None:
attr_value = cleaned_value
attributes[attr_key] = tuple(attr_value)
if cleaned_value:
attributes[attr_key] = cleaned_value
else:
attributes.pop(attr_key)


def _create_immutable_attributes(attributes):
Expand Down Expand Up @@ -575,25 +602,32 @@ class _Limits:
max_events: Maximum number of events that can be added to a Span.
max_links: Maximum number of links that can be added to a Span.
max_attributes: Maximum number of attributes that can be added to a Span.
max_attribute_value: Maximum length a string attribute an have.
"""

max_attributes: int
max_events: int
max_links: int
max_attribute_sizze: Optional[int]

def __init__(
self,
max_events: Optional[int] = None,
max_links: Optional[int] = None,
max_attributes: Optional[int] = None,
max_attribute_size: Optional[int] = None,
):
self.max_attributes = max_attributes
self.max_events = max_events
self.max_links = max_links
self.max_attribute_size = max_attribute_size

def __repr__(self):
return "max_attributes={}, max_events={}, max_links={}".format(
self.max_attributes, self.max_events, self.max_links
return "max_attributes={}, max_events={}, max_links={}, max_attribute_size={}".format(
self.max_attributes,
self.max_events,
self.max_links,
self.max_attribute_size,
)

@classmethod
Expand All @@ -608,6 +642,10 @@ def _create(cls):
max_links=cls._from_env(
OTEL_SPAN_LINK_COUNT_LIMIT, _DEFAULT_SPAN_LINKS_LIMIT
),
max_attribute_size=cls._from_env(
OTEL_SPAN_ATTRIBUTE_SIZE_LIMIT,
_DEFAULT_SPAN_ATTRIBUTE_SIZE_LIMIT,
),
)

@classmethod
Expand Down Expand Up @@ -698,7 +736,7 @@ def __init__(
self._limits = limits
self._lock = threading.Lock()

_filter_attribute_values(attributes)
_filter_attribute_values(attributes, self._limits.max_attribute_size)
if not attributes:
self._attributes = self._new_attributes()
else:
Expand All @@ -709,7 +747,9 @@ def __init__(
self._events = self._new_events()
if events:
for event in events:
_filter_attribute_values(event.attributes)
_filter_attribute_values(
event.attributes, self._limits.max_attribute_size
)
# pylint: disable=protected-access
event._attributes = _create_immutable_attributes(
event.attributes
Expand All @@ -721,6 +761,11 @@ def __init__(
else:
self._links = BoundedList.from_seq(self._limits.max_links, links)

for link in self._links:
_filter_attribute_values(
link.attributes, self._limits.max_attribute_size
)

def __repr__(self):
return '{}(name="{}", context={})'.format(
type(self).__name__, self._name, self._context
Expand All @@ -747,9 +792,15 @@ def set_attributes(
return

for key, value in attributes.items():
if not _is_valid_attribute_value(value):
valid, cleaned = _clean_attribute_value(
value, self._limits.max_attribute_size
)
if not valid:
continue

if cleaned is not None:
value = cleaned

if not key:
logger.warning("invalid key `%s` (empty or null)", key)
continue
Expand Down Expand Up @@ -781,7 +832,7 @@ def add_event(
attributes: types.Attributes = None,
timestamp: Optional[int] = None,
) -> None:
_filter_attribute_values(attributes)
_filter_attribute_values(attributes, self._limits.max_attribute_size)
attributes = _create_immutable_attributes(attributes)
self._add_event(
Event(
Expand Down Expand Up @@ -919,13 +970,12 @@ def __init__(
],
id_generator: IdGenerator,
instrumentation_info: InstrumentationInfo,
span_limits: SpanLimits,
) -> None:
self.sampler = sampler
self.resource = resource
self.span_processor = span_processor
self.id_generator = id_generator
self._span_limits = span_limits
self._limits = None
self.instrumentation_info = instrumentation_info
self._limits = None

Expand Down Expand Up @@ -1053,7 +1103,6 @@ def __init__(
SynchronousMultiSpanProcessor, ConcurrentMultiSpanProcessor
] = None,
id_generator: IdGenerator = None,
span_limits=None,
):
self._active_span_processor = (
active_span_processor or SynchronousMultiSpanProcessor()
Expand All @@ -1069,6 +1118,13 @@ def __init__(
if shutdown_on_exit:
self._atexit_handler = atexit.register(self.shutdown)

if self._resource and self._limits.max_attribute_size:
resource_attributes = self._resource.attributes
_filter_attribute_values(
resource_attributes, self._limits.max_attribute_size
)
self._resource = Resource(resource_attributes)

@property
def resource(self) -> Resource:
return self._resource
Expand Down
Loading

0 comments on commit 00f90fd

Please sign in to comment.