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.
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.
38
48
"""
39
49
50
+ if key is None or key == "" :
51
+ _logger .warning ("invalid key `%s` (empty or null)" , key )
52
+ return None
53
+
40
54
if isinstance (value , _VALID_ATTR_VALUE_TYPES ):
41
- return True
55
+ return _clean_attribute_value ( value , max_len )
42
56
43
57
if isinstance (value , Sequence ):
44
-
45
58
sequence_first_valid_type = None
59
+ cleaned_seq = []
60
+
46
61
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 )
47
67
if element is None :
48
68
continue
69
+
49
70
element_type = type (element )
71
+ # Reject attribute value if sequence contains a value with an incompatible type.
50
72
if element_type not in _VALID_ATTR_VALUE_TYPES :
51
73
_logger .warning (
52
74
"Invalid type %s in attribute value sequence. Expected one of "
@@ -57,56 +79,53 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool:
57
79
for valid_type in _VALID_ATTR_VALUE_TYPES
58
80
],
59
81
)
60
- return False
82
+ return None
83
+
61
84
# The type of the sequence must be homogeneous. The first non-None
62
85
# element determines the type of the sequence
63
86
if sequence_first_valid_type is None :
64
87
sequence_first_valid_type = element_type
65
- elif not isinstance (element , sequence_first_valid_type ):
88
+ # use equality instead of isinstance as isinstance(True, int) evaluates to True
89
+ elif element_type != sequence_first_valid_type :
66
90
_logger .warning (
67
91
"Mixed types %s and %s in attribute value sequence" ,
68
92
sequence_first_valid_type .__name__ ,
69
93
type (element ).__name__ ,
70
94
)
71
- return False
72
- return True
95
+ return None
96
+
97
+ # reject invalid elements
98
+ if element is not None :
99
+ cleaned_seq .append (element )
100
+
101
+ # Freeze mutable sequences defensively
102
+ return tuple (cleaned_seq )
73
103
74
104
_logger .warning (
75
105
"Invalid type %s for attribute value. Expected one of %s or a "
76
106
"sequence of those types" ,
77
107
type (value ).__name__ ,
78
108
[valid_type .__name__ for valid_type in _VALID_ATTR_VALUE_TYPES ],
79
109
)
80
- return False
81
-
110
+ return None
82
111
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
112
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
113
+ def _clean_attribute_value (
114
+ value : types .AttributeValue , limit : Optional [int ]
115
+ ) -> Union [types .AttributeValue , None ]:
116
+ if value is None :
117
+ return None
95
118
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 )
119
+ if isinstance (value , bytes ):
120
+ try :
121
+ value = value .decode ()
122
+ except ValueError :
123
+ _logger .warning ("Byte attribute could not be decoded." )
124
+ return None
107
125
108
-
109
- _DEFAULT_LIMIT = 128
126
+ if limit is not None and isinstance (value , str ):
127
+ value = value [:limit ]
128
+ return value
110
129
111
130
112
131
class BoundedAttributes (MutableMapping ):
@@ -118,9 +137,10 @@ class BoundedAttributes(MutableMapping):
118
137
119
138
def __init__ (
120
139
self ,
121
- maxlen : Optional [int ] = _DEFAULT_LIMIT ,
140
+ maxlen : Optional [int ] = None ,
122
141
attributes : types .Attributes = None ,
123
142
immutable : bool = True ,
143
+ max_value_len : Optional [int ] = None ,
124
144
):
125
145
if maxlen is not None :
126
146
if not isinstance (maxlen , int ) or maxlen < 0 :
@@ -129,10 +149,10 @@ def __init__(
129
149
)
130
150
self .maxlen = maxlen
131
151
self .dropped = 0
152
+ self .max_value_len = max_value_len
132
153
self ._dict = OrderedDict () # type: OrderedDict
133
154
self ._lock = threading .Lock () # type: threading.Lock
134
155
if attributes :
135
- _filter_attributes (attributes )
136
156
for key , value in attributes .items ():
137
157
self [key ] = value
138
158
self ._immutable = immutable
@@ -158,7 +178,10 @@ def __setitem__(self, key, value):
158
178
elif self .maxlen is not None and len (self ._dict ) == self .maxlen :
159
179
del self ._dict [next (iter (self ._dict .keys ()))]
160
180
self .dropped += 1
161
- self ._dict [key ] = value
181
+
182
+ value = _clean_attribute (key , value , self .max_value_len )
183
+ if value is not None :
184
+ self ._dict [key ] = value
162
185
163
186
def __delitem__ (self , key ):
164
187
if getattr (self , "_immutable" , False ):
0 commit comments