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 12, 2021
1 parent 9d77737 commit 1c1cbab
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@
.. envvar:: OTEL_BSP_MAX_EXPORT_BATCH_SIZE
"""

OTEL_ATTRIBUTE_LENGTH_LIMIT = "OTEL_SPAN_ATTRIBUTE_LENGTH_LIMIT"
"""
.. envvar:: OTEL_SPAN_ATTRIBUTE_LENGTH_LIMIT
"""

OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT"
"""
.. envvar:: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT
Expand Down
108 changes: 82 additions & 26 deletions opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from opentelemetry import trace as trace_api
from opentelemetry.sdk import util
from opentelemetry.sdk.environment_variables import (
OTEL_ATTRIBUTE_LENGTH_LIMIT,
OTEL_SPAN_ATTRIBUTE_COUNT_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_ATTRIBUTE_LENGTH_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,19 +602,22 @@ class SpanLimits:
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.
"""

UNSET = -1

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

def __init__(
self,
max_events: Optional[int] = None,
max_links: Optional[int] = None,
max_attributes: Optional[int] = None,
max_attribute_length: Optional[int] = None,
):
self.max_events = SpanLimits._value_or_default(
max_events, OTEL_SPAN_EVENT_COUNT_LIMIT, _DEFAULT_SPAN_EVENTS_LIMIT
Expand All @@ -600,6 +630,11 @@ def __init__(
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
_DEFAULT_SPAN_ATTRIBUTES_LIMIT,
)
self.max_attribute_length = SpanLimits._value_or_default(
max_attribute_length,
OTEL_ATTRIBUTE_LENGTH_LIMIT,
_DEFAULT_ATTRIBUTE_LENGTH_LIMIT,
)

@classmethod
def _value_or_default(
Expand Down Expand Up @@ -628,6 +663,7 @@ def _value_or_default(
max_events=SpanLimits.UNSET,
max_links=SpanLimits.UNSET,
max_attributes=SpanLimits.UNSET,
max_attribute_length=SpanLimits.UNSET,
)

SPAN_ATTRIBUTE_COUNT_LIMIT = SpanLimits._value_or_default(
Expand Down Expand Up @@ -696,7 +732,7 @@ def __init__(
self._limits = limits
self._lock = threading.Lock()

_filter_attribute_values(attributes)
_filter_attribute_values(attributes, self._limits.max_attribute_length)
if not attributes:
self._attributes = self._new_attributes()
else:
Expand All @@ -707,7 +743,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_length
)
# pylint: disable=protected-access
event._attributes = _create_immutable_attributes(
event.attributes
Expand All @@ -719,6 +757,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_length
)

def __repr__(self):
return '{}(name="{}", context={})'.format(
type(self).__name__, self._name, self._context
Expand All @@ -745,9 +788,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_length
)
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 @@ -779,7 +828,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_length)
attributes = _create_immutable_attributes(attributes)
self._add_event(
Event(
Expand Down Expand Up @@ -1062,6 +1111,13 @@ def __init__(
if shutdown_on_exit:
self._atexit_handler = atexit.register(self.shutdown)

if self._resource and self._span_limits.max_attribute_length:
resource_attributes = self._resource.attributes
_filter_attribute_values(
resource_attributes, self._span_limits.max_attribute_length
)
self._resource = Resource(resource_attributes)

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

0 comments on commit 1c1cbab

Please sign in to comment.