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