17
17
import threading
18
18
from collections import OrderedDict
19
19
from collections .abc import MutableMapping
20
- from typing import MutableSequence , Optional , Sequence
20
+ from typing import MutableSequence , Optional , Sequence , Union
21
21
22
22
from opentelemetry .util import types
23
23
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 )
25
27
26
28
27
29
_logger = logging .getLogger (__name__ )
28
30
29
31
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.
32
38
33
39
An attribute value is valid if it is either:
34
40
- A primitive type: string, boolean, double precision floating
35
41
point (IEEE 754-1985) or integer.
36
42
- An array of primitive type values. The array MUST be homogeneous,
37
43
i.e. it MUST NOT contain values of different types.
38
- """
39
44
40
- if isinstance (value , _VALID_ATTR_VALUE_TYPES ):
41
- return True
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.
48
+ """
42
49
43
- if isinstance (value , Sequence ):
50
+ if key is None or key is "" :
51
+ _logger .warning ("invalid key `%s` (empty or null)" , key )
52
+ return None
44
53
54
+ if isinstance (value , _VALID_ATTR_VALUE_TYPES ):
55
+ return _clean_attribute_value (value , max_len )
56
+ elif isinstance (value , Sequence ):
45
57
sequence_first_valid_type = None
58
+ cleaned_seq = []
59
+
46
60
for element in value :
61
+ # None is considered valid in any sequence
62
+ if element is None :
63
+ cleaned_seq .append (element )
64
+
65
+ element = _clean_attribute_value (element , max_len )
47
66
if element is None :
48
67
continue
68
+
49
69
element_type = type (element )
70
+ # Reject attribute value if sequence contains a value with an incompatible type.
50
71
if element_type not in _VALID_ATTR_VALUE_TYPES :
51
72
_logger .warning (
52
73
"Invalid type %s in attribute value sequence. Expected one of "
@@ -57,56 +78,51 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool:
57
78
for valid_type in _VALID_ATTR_VALUE_TYPES
58
79
],
59
80
)
60
- return False
81
+ return None
82
+
61
83
# The type of the sequence must be homogeneous. The first non-None
62
84
# element determines the type of the sequence
63
85
if sequence_first_valid_type is None :
64
86
sequence_first_valid_type = element_type
65
- elif not isinstance (element , sequence_first_valid_type ):
87
+ # use equality instead of isinstance as isinstance(True, int) evaluates to True
88
+ elif element_type != sequence_first_valid_type :
66
89
_logger .warning (
67
90
"Mixed types %s and %s in attribute value sequence" ,
68
91
sequence_first_valid_type .__name__ ,
69
92
type (element ).__name__ ,
70
93
)
71
- return False
72
- return True
94
+ return None
95
+
96
+ if element is not None :
97
+ cleaned_seq .append (element )
98
+
99
+ # Freeze mutable sequences defensively
100
+ return tuple (cleaned_seq )
73
101
74
102
_logger .warning (
75
103
"Invalid type %s for attribute value. Expected one of %s or a "
76
104
"sequence of those types" ,
77
105
type (value ).__name__ ,
78
106
[valid_type .__name__ for valid_type in _VALID_ATTR_VALUE_TYPES ],
79
107
)
80
- return False
81
-
82
108
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.
86
109
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
110
+ def _clean_attribute_value (
111
+ value : types .AttributeValue , limit : Optional [int ]
112
+ ) -> Union [types .AttributeValue , None ]:
113
+ if value is None :
114
+ return
95
115
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 )
116
+ if isinstance (value , bytes ):
117
+ try :
118
+ value = value .decode ()
119
+ except ValueError :
120
+ _logger .warning ("Byte attribute could not be decoded." )
121
+ return None
107
122
108
-
109
- _DEFAULT_LIMIT = 128
123
+ if limit is not None and isinstance (value , str ):
124
+ value = value [:limit ]
125
+ return value
110
126
111
127
112
128
class BoundedAttributes (MutableMapping ):
@@ -118,9 +134,10 @@ class BoundedAttributes(MutableMapping):
118
134
119
135
def __init__ (
120
136
self ,
121
- maxlen : Optional [int ] = _DEFAULT_LIMIT ,
137
+ maxlen : Optional [int ] = None ,
122
138
attributes : types .Attributes = None ,
123
139
immutable : bool = True ,
140
+ max_value_len : Optional [int ] = None ,
124
141
):
125
142
if maxlen is not None :
126
143
if not isinstance (maxlen , int ) or maxlen < 0 :
@@ -129,10 +146,10 @@ def __init__(
129
146
)
130
147
self .maxlen = maxlen
131
148
self .dropped = 0
149
+ self .max_value_len = max_value_len
132
150
self ._dict = OrderedDict () # type: OrderedDict
133
151
self ._lock = threading .Lock () # type: threading.Lock
134
152
if attributes :
135
- _filter_attributes (attributes )
136
153
for key , value in attributes .items ():
137
154
self [key ] = value
138
155
self ._immutable = immutable
@@ -158,7 +175,10 @@ def __setitem__(self, key, value):
158
175
elif self .maxlen is not None and len (self ._dict ) == self .maxlen :
159
176
del self ._dict [next (iter (self ._dict .keys ()))]
160
177
self .dropped += 1
161
- self ._dict [key ] = value
178
+
179
+ value = _clean_attribute (key , value , self .max_value_len )
180
+ if value is not None :
181
+ self ._dict [key ] = value
162
182
163
183
def __delitem__ (self , key ):
164
184
if getattr (self , "_immutable" , False ):
0 commit comments