1717import threading
1818from collections import OrderedDict
1919from collections .abc import MutableMapping
20- from typing import MutableSequence , Optional , Sequence
20+ from typing import Optional , Sequence , Union
2121
2222from opentelemetry .util import types
2323
24- _VALID_ATTR_VALUE_TYPES = (bool , str , int , float )
24+ # bytes are accepted as a user supplied value for attributes but
25+ # decoded to strings internally.
26+ _VALID_ATTR_VALUE_TYPES = (bool , str , bytes , int , float )
2527
2628
2729_logger = logging .getLogger (__name__ )
2830
2931
30- def _is_valid_attribute_value (value : types .AttributeValue ) -> bool :
31- """Checks if attribute value is valid.
32+ def _clean_attribute (
33+ key : str , value : types .AttributeValue , max_len : Optional [int ]
34+ ) -> Optional [types .AttributeValue ]:
35+ """Checks if attribute value is valid and cleans it if required.
36+
37+ The function returns the cleaned value or None if the value is not valid.
3238
3339 An attribute value is valid if it is either:
3440 - A primitive type: string, boolean, double precision floating
3541 point (IEEE 754-1985) or integer.
3642 - An array of primitive type values. The array MUST be homogeneous,
3743 i.e. it MUST NOT contain values of different types.
44+
45+ An attribute needs cleansing if:
46+ - Its length is greater than the maximum allowed length.
47+ - It needs to be encoded/decoded e.g, bytes to strings.
3848 """
3949
50+ if key is None or key == "" :
51+ _logger .warning ("invalid key `%s` (empty or null)" , key )
52+ return None
53+
4054 if isinstance (value , _VALID_ATTR_VALUE_TYPES ):
41- return True
55+ return _clean_attribute_value ( value , max_len )
4256
4357 if isinstance (value , Sequence ):
44-
4558 sequence_first_valid_type = None
59+ cleaned_seq = []
60+
4661 for element in value :
62+ # None is considered valid in any sequence
63+ if element is None :
64+ cleaned_seq .append (element )
65+
66+ element = _clean_attribute_value (element , max_len )
67+ # reject invalid elements
4768 if element is None :
4869 continue
70+
4971 element_type = type (element )
72+ # Reject attribute value if sequence contains a value with an incompatible type.
5073 if element_type not in _VALID_ATTR_VALUE_TYPES :
5174 _logger .warning (
5275 "Invalid type %s in attribute value sequence. Expected one of "
@@ -57,56 +80,51 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool:
5780 for valid_type in _VALID_ATTR_VALUE_TYPES
5881 ],
5982 )
60- return False
83+ return None
84+
6185 # The type of the sequence must be homogeneous. The first non-None
6286 # element determines the type of the sequence
6387 if sequence_first_valid_type is None :
6488 sequence_first_valid_type = element_type
65- elif not isinstance (element , sequence_first_valid_type ):
89+ # use equality instead of isinstance as isinstance(True, int) evaluates to True
90+ elif element_type != sequence_first_valid_type :
6691 _logger .warning (
6792 "Mixed types %s and %s in attribute value sequence" ,
6893 sequence_first_valid_type .__name__ ,
6994 type (element ).__name__ ,
7095 )
71- return False
72- return True
96+ return None
97+
98+ cleaned_seq .append (element )
99+
100+ # Freeze mutable sequences defensively
101+ return tuple (cleaned_seq )
73102
74103 _logger .warning (
75104 "Invalid type %s for attribute value. Expected one of %s or a "
76105 "sequence of those types" ,
77106 type (value ).__name__ ,
78107 [valid_type .__name__ for valid_type in _VALID_ATTR_VALUE_TYPES ],
79108 )
80- return False
81-
109+ return None
82110
83- def _filter_attributes (attributes : types .Attributes ) -> None :
84- """Applies attribute validation rules and drops (key, value) pairs
85- that doesn't adhere to attributes specification.
86111
87- https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/common.md#attributes.
88- """
89- if attributes :
90- for attr_key , attr_value in list (attributes .items ()):
91- if not attr_key :
92- _logger .warning ("invalid key `%s` (empty or null)" , attr_key )
93- attributes .pop (attr_key )
94- continue
112+ def _clean_attribute_value (
113+ value : types .AttributeValue , limit : Optional [int ]
114+ ) -> Union [types .AttributeValue , None ]:
115+ if value is None :
116+ return None
95117
96- if _is_valid_attribute_value (attr_value ):
97- if isinstance (attr_value , MutableSequence ):
98- attributes [attr_key ] = tuple (attr_value )
99- if isinstance (attr_value , bytes ):
100- try :
101- attributes [attr_key ] = attr_value .decode ()
102- except ValueError :
103- attributes .pop (attr_key )
104- _logger .warning ("Byte attribute could not be decoded." )
105- else :
106- attributes .pop (attr_key )
118+ if isinstance (value , bytes ):
119+ try :
120+ value = value .decode ()
121+ except ValueError :
122+ _logger .warning ("Byte attribute could not be decoded." )
123+ return None
107124
108-
109- _DEFAULT_LIMIT = 128
125+ if limit is not None and isinstance (value , str ):
126+ value = value [:limit ]
127+ return value
110128
111129
112130class BoundedAttributes (MutableMapping ):
@@ -118,9 +136,10 @@ class BoundedAttributes(MutableMapping):
118136
119137 def __init__ (
120138 self ,
121- maxlen : Optional [int ] = _DEFAULT_LIMIT ,
139+ maxlen : Optional [int ] = None ,
122140 attributes : types .Attributes = None ,
123141 immutable : bool = True ,
142+ max_value_len : Optional [int ] = None ,
124143 ):
125144 if maxlen is not None :
126145 if not isinstance (maxlen , int ) or maxlen < 0 :
@@ -129,10 +148,10 @@ def __init__(
129148 )
130149 self .maxlen = maxlen
131150 self .dropped = 0
151+ self .max_value_len = max_value_len
132152 self ._dict = OrderedDict () # type: OrderedDict
133153 self ._lock = threading .Lock () # type: threading.Lock
134154 if attributes :
135- _filter_attributes (attributes )
136155 for key , value in attributes .items ():
137156 self [key ] = value
138157 self ._immutable = immutable
@@ -158,7 +177,10 @@ def __setitem__(self, key, value):
158177 elif self .maxlen is not None and len (self ._dict ) == self .maxlen :
159178 del self ._dict [next (iter (self ._dict .keys ()))]
160179 self .dropped += 1
161- self ._dict [key ] = value
180+
181+ value = _clean_attribute (key , value , self .max_value_len )
182+ if value is not None :
183+ self ._dict [key ] = value
162184
163185 def __delitem__ (self , key ):
164186 if getattr (self , "_immutable" , False ):
0 commit comments