Skip to content

Commit 95a78b0

Browse files
committed
Added max attribute length span limit support
1 parent 1470a8c commit 95a78b0

7 files changed

Lines changed: 296 additions & 67 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
([#1829](https://github.com/open-telemetry/opentelemetry-python/pull/1829))
2424
- Lazily read/configure limits and allow limits to be unset.
2525
([#1839](https://github.com/open-telemetry/opentelemetry-python/pull/1839))
26+
- Added support for read/configure limits and allow limits to be unset.
27+
([#1839](https://github.com/open-telemetry/opentelemetry-python/pull/1839))
2628

2729
### Changed
2830
- Fixed OTLP gRPC exporter silently failing if scheme is not specified in endpoint.

opentelemetry-api/src/opentelemetry/attributes/__init__.py

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import logging
1717
from types import MappingProxyType
18-
from typing import MutableSequence, Sequence
18+
from typing import MutableSequence, Optional, Sequence, Tuple
1919

2020
from opentelemetry.util import types
2121

@@ -25,23 +25,33 @@
2525
_logger = logging.getLogger(__name__)
2626

2727

28-
def _is_valid_attribute_value(value: types.AttributeValue) -> bool:
28+
def _clean_attribute_value(
29+
value: types.AttributeValue, max_length: Optional[int]
30+
) -> Tuple[bool, Optional[types.AttributeValue]]:
2931
"""Checks if attribute value is valid.
3032
3133
An attribute value is valid if it is either:
3234
- A primitive type: string, boolean, double precision floating
3335
point (IEEE 754-1985) or integer.
3436
- An array of primitive type values. The array MUST be homogeneous,
3537
i.e. it MUST NOT contain values of different types.
38+
39+
When the ``max_length`` argument is set, any strings values longer than the value
40+
are truncated and returned back as the second value.
41+
If the attribute value is not modified, ``None`` is returned as the second return value.
3642
"""
3743

38-
if isinstance(value, Sequence):
44+
# pylint: disable=too-many-branches
45+
modified = False
46+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
3947
if len(value) == 0:
40-
return True
48+
return True, None
4149

4250
sequence_first_valid_type = None
51+
new_value = []
4352
for element in value:
4453
if element is None:
54+
new_value.append(element)
4555
continue
4656
element_type = type(element)
4757
if element_type not in _VALID_ATTR_VALUE_TYPES:
@@ -54,7 +64,7 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool:
5464
for valid_type in _VALID_ATTR_VALUE_TYPES
5565
],
5666
)
57-
return False
67+
return False, None
5868
# The type of the sequence must be homogeneous. The first non-None
5969
# element determines the type of the sequence
6070
if sequence_first_valid_type is None:
@@ -65,7 +75,22 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool:
6575
sequence_first_valid_type.__name__,
6676
type(element).__name__,
6777
)
68-
return False
78+
return False, None
79+
if max_length is not None and isinstance(element, str):
80+
element = element[:max_length]
81+
modified = True
82+
new_value.append(element)
83+
if isinstance(value, MutableSequence):
84+
modified = True
85+
value = tuple(new_value)
86+
87+
elif isinstance(value, bytes):
88+
try:
89+
value = value.decode()
90+
modified = True
91+
except ValueError:
92+
_logger.warning("Byte attribute could not be decoded.")
93+
return False, None
6994

7095
elif not isinstance(value, _VALID_ATTR_VALUE_TYPES):
7196
_logger.warning(
@@ -74,34 +99,38 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool:
7499
type(value).__name__,
75100
[valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES],
76101
)
77-
return False
78-
return True
102+
return False, None
103+
104+
if max_length is not None and isinstance(value, str):
105+
value = value[:max_length]
106+
modified = True
107+
return True, value if modified else None
79108

80109

81-
def _filter_attributes(attributes: types.Attributes) -> None:
82-
"""Applies attribute validation rules and drops (key, value) pairs
110+
def _clean_attributes(
111+
attributes: types.Attributes, max_length: Optional[int]
112+
) -> None:
113+
"""Applies attribute validation rules and truncates/drops (key, value) pairs
83114
that doesn't adhere to attributes specification.
84115
85116
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/common.md#attributes.
86117
"""
87-
if attributes:
88-
for attr_key, attr_value in list(attributes.items()):
89-
if not attr_key:
90-
_logger.warning("invalid key `%s` (empty or null)", attr_key)
91-
attributes.pop(attr_key)
92-
continue
93-
94-
if _is_valid_attribute_value(attr_value):
95-
if isinstance(attr_value, MutableSequence):
96-
attributes[attr_key] = tuple(attr_value)
97-
if isinstance(attr_value, bytes):
98-
try:
99-
attributes[attr_key] = attr_value.decode()
100-
except ValueError:
101-
attributes.pop(attr_key)
102-
_logger.warning("Byte attribute could not be decoded.")
103-
else:
104-
attributes.pop(attr_key)
118+
if not attributes:
119+
return
120+
121+
for attr_key, attr_value in list(attributes.items()):
122+
if not attr_key:
123+
_logger.warning("invalid key `%s` (empty or null)", attr_key)
124+
attributes.pop(attr_key)
125+
continue
126+
127+
valid, cleaned_value = _clean_attribute_value(attr_value, max_length)
128+
if not valid:
129+
attributes.pop(attr_key)
130+
continue
131+
132+
if cleaned_value is not None:
133+
attributes[attr_key] = cleaned_value
105134

106135

107136
def _create_immutable_attributes(

opentelemetry-api/tests/attributes/test_attributes.py

Lines changed: 135 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,42 +17,151 @@
1717
import unittest
1818

1919
from opentelemetry.attributes import (
20+
_clean_attribute_value,
21+
_clean_attributes,
2022
_create_immutable_attributes,
21-
_filter_attributes,
22-
_is_valid_attribute_value,
2323
)
2424

2525

2626
class TestAttributes(unittest.TestCase):
27-
def test_is_valid_attribute_value(self):
28-
self.assertFalse(_is_valid_attribute_value([1, 2, 3.4, "ss", 4]))
29-
self.assertFalse(_is_valid_attribute_value([dict(), 1, 2, 3.4, 4]))
30-
self.assertFalse(_is_valid_attribute_value(["sw", "lf", 3.4, "ss"]))
31-
self.assertFalse(_is_valid_attribute_value([1, 2, 3.4, 5]))
32-
self.assertFalse(_is_valid_attribute_value(dict()))
33-
self.assertTrue(_is_valid_attribute_value(True))
34-
self.assertTrue(_is_valid_attribute_value("hi"))
35-
self.assertTrue(_is_valid_attribute_value(3.4))
36-
self.assertTrue(_is_valid_attribute_value(15))
37-
self.assertTrue(_is_valid_attribute_value([1, 2, 3, 5]))
38-
self.assertTrue(_is_valid_attribute_value([1.2, 2.3, 3.4, 4.5]))
39-
self.assertTrue(_is_valid_attribute_value([True, False]))
40-
self.assertTrue(_is_valid_attribute_value(["ss", "dw", "fw"]))
41-
self.assertTrue(_is_valid_attribute_value([]))
42-
# None in sequences are valid
43-
self.assertTrue(_is_valid_attribute_value(["A", None, None]))
44-
self.assertTrue(_is_valid_attribute_value(["A", None, None, "B"]))
45-
self.assertTrue(_is_valid_attribute_value([None, None]))
46-
self.assertFalse(_is_valid_attribute_value(["A", None, 1]))
47-
self.assertFalse(_is_valid_attribute_value([None, "A", None, 1]))
27+
def assertCleanAttr(self, value, valid):
28+
# pylint: disable=protected-access
29+
is_valid, cleaned = _clean_attribute_value(value, None)
30+
self.assertEqual(is_valid, valid)
31+
self.assertEqual(cleaned, value if valid else None)
4832

49-
def test_filter_attributes(self):
33+
def test_validate_attribute_value(self):
34+
test_cases = [
35+
(
36+
[1, 2, 3.4, "ss", 4],
37+
False,
38+
),
39+
(
40+
[dict(), 1, 2, 3.4, 4],
41+
False,
42+
),
43+
(
44+
["sw", "lf", 3.4, "ss"],
45+
False,
46+
),
47+
(
48+
[1, 2, 3.4, 5],
49+
False,
50+
),
51+
(
52+
dict(),
53+
False,
54+
),
55+
(
56+
True,
57+
True,
58+
),
59+
(
60+
"hi",
61+
True,
62+
),
63+
(
64+
3.4,
65+
True,
66+
),
67+
(
68+
15,
69+
True,
70+
),
71+
(
72+
(1, 2, 3, 5),
73+
True,
74+
),
75+
(
76+
(1.2, 2.3, 3.4, 4.5),
77+
True,
78+
),
79+
(
80+
(True, False),
81+
True,
82+
),
83+
(
84+
("ss", "dw", "fw"),
85+
True,
86+
),
87+
(
88+
[],
89+
True,
90+
),
91+
# None in sequences are valid
92+
(
93+
("A", None, None),
94+
True,
95+
),
96+
(
97+
("A", None, None, "B"),
98+
True,
99+
),
100+
(
101+
(None, None),
102+
True,
103+
),
104+
(
105+
["A", None, 1],
106+
False,
107+
),
108+
(
109+
[None, "A", None, 1],
110+
False,
111+
),
112+
]
113+
114+
for value, want_valid in test_cases:
115+
# pylint: disable=protected-access
116+
got_valid, cleaned_value = _clean_attribute_value(value, None)
117+
self.assertEqual(got_valid, want_valid)
118+
self.assertIsNone(cleaned_value)
119+
120+
def test_clean_attribute_value_truncate(self):
121+
test_cases = [
122+
("a" * 50, None, None),
123+
("a" * 50, "a" * 10, 10),
124+
("abc", "a", 1),
125+
("abc" * 50, "abcabcabca", 10),
126+
("abc" * 50, "abc" * 50, 1000),
127+
("abc" * 50, None, None),
128+
([1, 2, 3, 5], (1, 2, 3, 5), 10),
129+
(
130+
[1.2, 2.3],
131+
(
132+
1.2,
133+
2.3,
134+
),
135+
20,
136+
),
137+
([True, False], (True, False), 10),
138+
([], None, 10),
139+
(True, None, 10),
140+
(
141+
3.4,
142+
None,
143+
True,
144+
),
145+
(
146+
15,
147+
None,
148+
True,
149+
),
150+
]
151+
152+
for value, expected, limit in test_cases:
153+
# pylint: disable=protected-access
154+
valid, cleaned = _clean_attribute_value(value, limit)
155+
self.assertTrue(valid)
156+
self.assertEqual(cleaned, expected)
157+
158+
def test_clean_attributes(self):
50159
attrs_with_invalid_keys = {
51160
"": "empty-key",
52161
None: "None-value",
53162
"attr-key": "attr-value",
54163
}
55-
_filter_attributes(attrs_with_invalid_keys)
164+
_clean_attributes(attrs_with_invalid_keys, None)
56165
self.assertTrue(len(attrs_with_invalid_keys), 1)
57166
self.assertEqual(attrs_with_invalid_keys, {"attr-key": "attr-value"})
58167

@@ -66,7 +175,7 @@ def test_filter_attributes(self):
66175
"boolkey": True,
67176
"valid-byte-string": b"hello-otel",
68177
}
69-
_filter_attributes(attrs_with_invalid_values)
178+
_clean_attributes(attrs_with_invalid_values, None)
70179
self.assertEqual(len(attrs_with_invalid_values), 5)
71180
self.assertEqual(
72181
attrs_with_invalid_values,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@
7373
.. envvar:: OTEL_BSP_MAX_EXPORT_BATCH_SIZE
7474
"""
7575

76+
OTEL_SPAN_ATTRIBUTE_SIZE_LIMIT = "OTEL_SPAN_ATTRIBUTE_SIZE_LIMIT"
77+
"""
78+
.. envvar:: OTEL_SPAN_ATTRIBUTE_SIZE_LIMIT
79+
"""
80+
7681
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT"
7782
"""
7883
.. envvar:: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464

6565
import pkg_resources
6666

67-
from opentelemetry.attributes import _filter_attributes
67+
from opentelemetry.attributes import _clean_attributes
6868
from opentelemetry.sdk.environment_variables import (
6969
OTEL_RESOURCE_ATTRIBUTES,
7070
OTEL_SERVICE_NAME,
@@ -142,7 +142,7 @@ class Resource:
142142
"""A Resource is an immutable representation of the entity producing telemetry as Attributes."""
143143

144144
def __init__(self, attributes: Attributes):
145-
_filter_attributes(attributes)
145+
_clean_attributes(attributes, None)
146146
self._attributes = attributes.copy()
147147

148148
@staticmethod

0 commit comments

Comments
 (0)