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