Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/opentelemetry/attributes/__init__.py: 52%
92 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
1# Copyright The OpenTelemetry Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# type: ignore
16import logging
17import threading
18from collections import OrderedDict
19from collections.abc import MutableMapping
20from typing import Optional, Sequence, Union
22from opentelemetry.util import types
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)
29_logger = logging.getLogger(__name__)
32def _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.
37 The function returns the cleaned value or None if the value is not valid.
39 An attribute value is valid if it is either:
40 - A primitive type: string, boolean, double precision floating
41 point (IEEE 754-1985) or integer.
42 - An array of primitive type values. The array MUST be homogeneous,
43 i.e. it MUST NOT contain values of different types.
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 """
50 if not (key and isinstance(key, str)):
51 _logger.warning("invalid key `%s`. must be non-empty string.", key)
52 return None
54 if isinstance(value, _VALID_ATTR_VALUE_TYPES):
55 return _clean_attribute_value(value, max_len)
57 if isinstance(value, Sequence):
58 sequence_first_valid_type = None
59 cleaned_seq = []
61 for element in value:
62 element = _clean_attribute_value(element, max_len)
63 if element is None:
64 cleaned_seq.append(element)
65 continue
67 element_type = type(element)
68 # Reject attribute value if sequence contains a value with an incompatible type.
69 if element_type not in _VALID_ATTR_VALUE_TYPES:
70 _logger.warning(
71 "Invalid type %s in attribute value sequence. Expected one of "
72 "%s or None",
73 element_type.__name__,
74 [
75 valid_type.__name__
76 for valid_type in _VALID_ATTR_VALUE_TYPES
77 ],
78 )
79 return None
81 # The type of the sequence must be homogeneous. The first non-None
82 # element determines the type of the sequence
83 if sequence_first_valid_type is None:
84 sequence_first_valid_type = element_type
85 # use equality instead of isinstance as isinstance(True, int) evaluates to True
86 elif element_type != sequence_first_valid_type:
87 _logger.warning(
88 "Mixed types %s and %s in attribute value sequence",
89 sequence_first_valid_type.__name__,
90 type(element).__name__,
91 )
92 return None
94 cleaned_seq.append(element)
96 # Freeze mutable sequences defensively
97 return tuple(cleaned_seq)
99 _logger.warning(
100 "Invalid type %s for attribute value. Expected one of %s or a "
101 "sequence of those types",
102 type(value).__name__,
103 [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES],
104 )
105 return None
108def _clean_attribute_value(
109 value: types.AttributeValue, limit: Optional[int]
110) -> Union[types.AttributeValue, None]:
111 if value is None:
112 return None
114 if isinstance(value, bytes):
115 try:
116 value = value.decode()
117 except UnicodeDecodeError:
118 _logger.warning("Byte attribute could not be decoded.")
119 return None
121 if limit is not None and isinstance(value, str):
122 value = value[:limit]
123 return value
126class BoundedAttributes(MutableMapping):
127 """An ordered dict with a fixed max capacity.
129 Oldest elements are dropped when the dict is full and a new element is
130 added.
131 """
133 def __init__(
134 self,
135 maxlen: Optional[int] = None,
136 attributes: types.Attributes = None,
137 immutable: bool = True,
138 max_value_len: Optional[int] = None,
139 ):
140 if maxlen is not None:
141 if not isinstance(maxlen, int) or maxlen < 0:
142 raise ValueError(
143 "maxlen must be valid int greater or equal to 0"
144 )
145 self.maxlen = maxlen
146 self.dropped = 0
147 self.max_value_len = max_value_len
148 self._dict = OrderedDict() # type: OrderedDict
149 self._lock = threading.Lock() # type: threading.Lock
150 if attributes:
151 for key, value in attributes.items():
152 self[key] = value
153 self._immutable = immutable
155 def __repr__(self):
156 return (
157 f"{type(self).__name__}({dict(self._dict)}, maxlen={self.maxlen})"
158 )
160 def __getitem__(self, key):
161 return self._dict[key]
163 def __setitem__(self, key, value):
164 if getattr(self, "_immutable", False):
165 raise TypeError
166 with self._lock:
167 if self.maxlen is not None and self.maxlen == 0:
168 self.dropped += 1
169 return
171 value = _clean_attribute(key, value, self.max_value_len)
172 if value is not None:
173 if key in self._dict:
174 del self._dict[key]
175 elif (
176 self.maxlen is not None and len(self._dict) == self.maxlen
177 ):
178 self._dict.popitem(last=False)
179 self.dropped += 1
181 self._dict[key] = value
183 def __delitem__(self, key):
184 if getattr(self, "_immutable", False):
185 raise TypeError
186 with self._lock:
187 del self._dict[key]
189 def __iter__(self):
190 with self._lock:
191 return iter(self._dict.copy())
193 def __len__(self):
194 return len(self._dict)
196 def copy(self):
197 return self._dict.copy()