1616import threading
1717from collections import OrderedDict
1818from collections .abc import MutableMapping
19- from typing import Optional , Sequence , Tuple , Union
19+ from typing import Mapping , Optional , Sequence , Tuple , Union
2020
2121from opentelemetry .util import types
2222
2323# bytes are accepted as a user supplied value for attributes but
2424# decoded to strings internally.
2525_VALID_ATTR_VALUE_TYPES = (bool , str , bytes , int , float )
26+ # AnyValue possible values
27+ _VALID_ANY_VALUE_TYPES = (
28+ type (None ),
29+ bool ,
30+ bytes ,
31+ int ,
32+ float ,
33+ str ,
34+ Sequence ,
35+ Mapping ,
36+ )
2637
2738
2839_logger = logging .getLogger (__name__ )
@@ -107,6 +118,96 @@ def _clean_attribute(
107118 return None
108119
109120
121+ def _clean_extended_attribute_value (
122+ value : types .AttributeValue , max_len : Optional [int ]
123+ ) -> Optional [Union [types .AttributeValue , Tuple [Union [str , int , float ], ...]]]:
124+ # for primitive types just return the value and eventually shorten the string length
125+ if value is None or isinstance (value , _VALID_ATTR_VALUE_TYPES ):
126+ if max_len is not None and isinstance (value , str ):
127+ value = value [:max_len ]
128+ return value
129+
130+ if isinstance (value , Mapping ):
131+ cleaned_dict = {}
132+ for key , element in value .items ():
133+ # skip invalid keys
134+ if not (key and isinstance (key , str )):
135+ _logger .warning (
136+ "invalid key `%s`. must be non-empty string." , key
137+ )
138+ continue
139+
140+ cleaned_dict [key ] = _clean_extended_attribute (
141+ key = key , value = element , max_len = max_len
142+ )
143+
144+ return cleaned_dict
145+
146+ if isinstance (value , Sequence ):
147+ sequence_first_valid_type = None
148+ cleaned_seq = []
149+
150+ for element in value :
151+ if element is None :
152+ cleaned_seq .append (element )
153+ continue
154+
155+ if max_len is not None and isinstance (element , str ):
156+ element = element [:max_len ]
157+
158+ element_type = type (element )
159+ if element_type not in _VALID_ATTR_VALUE_TYPES :
160+ return _clean_extended_attribute (element , max_len = max_len )
161+
162+ # The type of the sequence must be homogeneous. The first non-None
163+ # element determines the type of the sequence
164+ if sequence_first_valid_type is None :
165+ sequence_first_valid_type = element_type
166+ # use equality instead of isinstance as isinstance(True, int) evaluates to True
167+ elif element_type != sequence_first_valid_type :
168+ _logger .warning (
169+ "Mixed types %s and %s in attribute value sequence" ,
170+ sequence_first_valid_type .__name__ ,
171+ type (element ).__name__ ,
172+ )
173+ return None
174+
175+ cleaned_seq .append (element )
176+
177+ # Freeze mutable sequences defensively
178+ return tuple (cleaned_seq )
179+
180+ raise TypeError (
181+ "Invalid type %s for attribute value. Expected one of %s or a "
182+ "sequence of those types" ,
183+ type (value ).__name__ ,
184+ [valid_type .__name__ for valid_type in _VALID_ANY_VALUE_TYPES ],
185+ )
186+
187+
188+ def _clean_extended_attribute (
189+ key : str , value : types .AttributeValue , max_len : Optional [int ]
190+ ) -> Optional [Union [types .AttributeValue , Tuple [Union [str , int , float ], ...]]]:
191+ """Checks if attribute value is valid and cleans it if required.
192+
193+ The function returns the cleaned value or None if the value is not valid.
194+
195+ An attribute value is valid if it is an AnyValue.
196+ An attribute needs cleansing if:
197+ - Its length is greater than the maximum allowed length.
198+ """
199+
200+ if not (key and isinstance (key , str )):
201+ _logger .warning ("invalid key `%s`. must be non-empty string." , key )
202+ return None
203+
204+ try :
205+ return _clean_extended_attribute_value (value , max_len = max_len )
206+ except TypeError as exception :
207+ _logger .warning (f"Attribute { key } : { exception } " )
208+ return None
209+
210+
110211def _clean_attribute_value (
111212 value : types .AttributeValue , limit : Optional [int ]
112213) -> Optional [types .AttributeValue ]:
@@ -138,6 +239,7 @@ def __init__(
138239 attributes : types .Attributes = None ,
139240 immutable : bool = True ,
140241 max_value_len : Optional [int ] = None ,
242+ extended_attributes : bool = False ,
141243 ):
142244 if maxlen is not None :
143245 if not isinstance (maxlen , int ) or maxlen < 0 :
@@ -147,6 +249,7 @@ def __init__(
147249 self .maxlen = maxlen
148250 self .dropped = 0
149251 self .max_value_len = max_value_len
252+ self ._extended_attributes = extended_attributes
150253 # OrderedDict is not used until the maxlen is reached for efficiency.
151254
152255 self ._dict : Union [
@@ -173,19 +276,24 @@ def __setitem__(self, key: str, value: types.AttributeValue) -> None:
173276 self .dropped += 1
174277 return
175278
176- value = _clean_attribute (key , value , self .max_value_len ) # type: ignore
177- if value is not None :
178- if key in self ._dict :
179- del self ._dict [key ]
180- elif (
181- self .maxlen is not None and len (self ._dict ) == self .maxlen
182- ):
183- if not isinstance (self ._dict , OrderedDict ):
184- self ._dict = OrderedDict (self ._dict )
185- self ._dict .popitem (last = False ) # type: ignore
186- self .dropped += 1
187-
188- self ._dict [key ] = value # type: ignore
279+ if self ._extended_attributes :
280+ value = _clean_extended_attribute (
281+ key , value , self .max_value_len
282+ ) # type: ignore
283+ else :
284+ value = _clean_attribute (key , value , self .max_value_len ) # type: ignore
285+ if value is None :
286+ return
287+
288+ if key in self ._dict :
289+ del self ._dict [key ]
290+ elif self .maxlen is not None and len (self ._dict ) == self .maxlen :
291+ if not isinstance (self ._dict , OrderedDict ):
292+ self ._dict = OrderedDict (self ._dict )
293+ self ._dict .popitem (last = False ) # type: ignore
294+ self .dropped += 1
295+
296+ self ._dict [key ] = value # type: ignore
189297
190298 def __delitem__ (self , key : str ) -> None :
191299 if getattr (self , "_immutable" , False ): # type: ignore
0 commit comments