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 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 == "" :
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,52 @@ 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
+ # reject invalid elements
97
+ if element is not None :
98
+ cleaned_seq .append (element )
99
+
100
+ # Freeze mutable sequences defensively
101
+ return tuple (cleaned_seq )
73
102
74
103
_logger .warning (
75
104
"Invalid type %s for attribute value. Expected one of %s or a "
76
105
"sequence of those types" ,
77
106
type (value ).__name__ ,
78
107
[valid_type .__name__ for valid_type in _VALID_ATTR_VALUE_TYPES ],
79
108
)
80
- return False
81
-
82
109
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
110
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
111
+ def _clean_attribute_value (
112
+ value : types .AttributeValue , limit : Optional [int ]
113
+ ) -> Union [types .AttributeValue , None ]:
114
+ if value is None :
115
+ return
95
116
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 )
117
+ if isinstance (value , bytes ):
118
+ try :
119
+ value = value .decode ()
120
+ except ValueError :
121
+ _logger .warning ("Byte attribute could not be decoded." )
122
+ return None
107
123
108
-
109
- _DEFAULT_LIMIT = 128
124
+ if limit is not None and isinstance (value , str ):
125
+ value = value [:limit ]
126
+ return value
110
127
111
128
112
129
class BoundedAttributes (MutableMapping ):
@@ -118,9 +135,10 @@ class BoundedAttributes(MutableMapping):
118
135
119
136
def __init__ (
120
137
self ,
121
- maxlen : Optional [int ] = _DEFAULT_LIMIT ,
138
+ maxlen : Optional [int ] = None ,
122
139
attributes : types .Attributes = None ,
123
140
immutable : bool = True ,
141
+ max_value_len : Optional [int ] = None ,
124
142
):
125
143
if maxlen is not None :
126
144
if not isinstance (maxlen , int ) or maxlen < 0 :
@@ -129,10 +147,10 @@ def __init__(
129
147
)
130
148
self .maxlen = maxlen
131
149
self .dropped = 0
150
+ self .max_value_len = max_value_len
132
151
self ._dict = OrderedDict () # type: OrderedDict
133
152
self ._lock = threading .Lock () # type: threading.Lock
134
153
if attributes :
135
- _filter_attributes (attributes )
136
154
for key , value in attributes .items ():
137
155
self [key ] = value
138
156
self ._immutable = immutable
@@ -158,7 +176,10 @@ def __setitem__(self, key, value):
158
176
elif self .maxlen is not None and len (self ._dict ) == self .maxlen :
159
177
del self ._dict [next (iter (self ._dict .keys ()))]
160
178
self .dropped += 1
161
- self ._dict [key ] = value
179
+
180
+ value = _clean_attribute (key , value , self .max_value_len )
181
+ if value is not None :
182
+ self ._dict [key ] = value
162
183
163
184
def __delitem__ (self , key ):
164
185
if getattr (self , "_immutable" , False ):
0 commit comments