codekingpro/portable-devtools
114k
1# Copyright The OpenTelemetry Authors2#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 at6#7# http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# 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 and13# limitations under the License.14 15import copy16import logging17import threading18from collections import OrderedDict19from collections.abc import MutableMapping20from typing import Mapping, Optional, Sequence, Tuple, Union21 22from opentelemetry.util import types23 24# bytes are accepted as a user supplied value for attributes but25# decoded to strings internally.26_VALID_ATTR_VALUE_TYPES = (bool, str, bytes, int, float)27# AnyValue possible values28_VALID_ANY_VALUE_TYPES = (29 type(None),30 bool,31 bytes,32 int,33 float,34 str,35 Sequence,36 Mapping,37)38 39 40# TODO: Remove this workaround and revert to the simpler implementation41# once Python 3.9 support is dropped (planned around May 2026).42# This exists only to avoid issues caused by deprecated behavior in 3.9.43def _type_name(t):44 return getattr(t, "__name__", getattr(t, "_name", repr(t)))45 46 47_logger = logging.getLogger(__name__)48 49 50def _clean_attribute(51 key: str, value: types.AttributeValue, max_len: Optional[int]52) -> Optional[Union[types.AttributeValue, Tuple[Union[str, int, float], ...]]]:53 """Checks if attribute value is valid and cleans it if required.54 55 The function returns the cleaned value or None if the value is not valid.56 57 An attribute value is valid if it is either:58 - A primitive type: string, boolean, double precision floating59 point (IEEE 754-1985) or integer.60 - An array of primitive type values. The array MUST be homogeneous,61 i.e. it MUST NOT contain values of different types.62 63 An attribute needs cleansing if:64 - Its length is greater than the maximum allowed length.65 - It needs to be encoded/decoded e.g, bytes to strings.66 """67 68 if not (key and isinstance(key, str)):69 _logger.warning("invalid key `%s`. must be non-empty string.", key)70 return None71 72 if isinstance(value, _VALID_ATTR_VALUE_TYPES):73 return _clean_attribute_value(value, max_len)74 75 if isinstance(value, Sequence):76 sequence_first_valid_type = None77 cleaned_seq = []78 79 for element in value:80 element = _clean_attribute_value(element, max_len) # type: ignore81 if element is None:82 cleaned_seq.append(element)83 continue84 85 element_type = type(element)86 # Reject attribute value if sequence contains a value with an incompatible type.87 if element_type not in _VALID_ATTR_VALUE_TYPES:88 _logger.warning(89 "Invalid type %s in attribute '%s' value sequence. Expected one of "90 "%s or None",91 element_type.__name__,92 key,93 [94 valid_type.__name__95 for valid_type in _VALID_ATTR_VALUE_TYPES96 ],97 )98 return None99 100 # The type of the sequence must be homogeneous. The first non-None101 # element determines the type of the sequence102 if sequence_first_valid_type is None:103 sequence_first_valid_type = element_type104 # use equality instead of isinstance as isinstance(True, int) evaluates to True105 elif element_type != sequence_first_valid_type:106 _logger.warning(107 "Attribute %r mixes types %s and %s in attribute value sequence",108 key,109 sequence_first_valid_type.__name__,110 type(element).__name__,111 )112 return None113 114 cleaned_seq.append(element)115 116 # Freeze mutable sequences defensively117 return tuple(cleaned_seq)118 119 _logger.warning(120 "Invalid type %s for attribute '%s' value. Expected one of %s or a "121 "sequence of those types",122 type(value).__name__,123 key,124 [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES],125 )126 return None127 128 129def _clean_extended_attribute_value( # pylint: disable=too-many-branches130 value: types.AnyValue, max_len: Optional[int]131) -> types.AnyValue:132 # for primitive types just return the value and eventually shorten the string length133 if value is None or isinstance(value, _VALID_ATTR_VALUE_TYPES):134 if max_len is not None and isinstance(value, str):135 value = value[:max_len]136 return value137 138 if isinstance(value, Mapping):139 cleaned_dict: dict[str, types.AnyValue] = {}140 for key, element in value.items():141 # skip invalid keys142 if not (key and isinstance(key, str)):143 _logger.warning(144 "invalid key `%s`. must be non-empty string.", key145 )146 continue147 148 cleaned_dict[key] = _clean_extended_attribute(149 key=key, value=element, max_len=max_len150 )151 152 return cleaned_dict153 154 if isinstance(value, Sequence):155 sequence_first_valid_type = None156 cleaned_seq: list[types.AnyValue] = []157 158 for element in value:159 if element is None:160 cleaned_seq.append(element)161 continue162 163 if max_len is not None and isinstance(element, str):164 element = element[:max_len]165 166 element_type = type(element)167 if element_type not in _VALID_ATTR_VALUE_TYPES:168 element = _clean_extended_attribute_value(169 element, max_len=max_len170 )171 element_type = type(element) # type: ignore172 173 # The type of the sequence must be homogeneous. The first non-None174 # element determines the type of the sequence175 if sequence_first_valid_type is None:176 sequence_first_valid_type = element_type177 # use equality instead of isinstance as isinstance(True, int) evaluates to True178 elif element_type != sequence_first_valid_type:179 _logger.warning(180 "Mixed types %s and %s in attribute value sequence",181 sequence_first_valid_type.__name__,182 type(element).__name__,183 )184 return None185 186 cleaned_seq.append(element)187 188 # Freeze mutable sequences defensively189 return tuple(cleaned_seq)190 191 # Some applications such as Django add values to log records whose types fall outside the192 # primitive types and `_VALID_ANY_VALUE_TYPES`, i.e., they are not of type `AnyValue`.193 # Rather than attempt to whitelist every possible instrumentation, we stringify those values here194 # so they can still be represented as attributes, falling back to the original TypeError only if195 # converting to string raises.196 try:197 return str(value)198 except Exception:199 raise TypeError(200 f"Invalid type {type(value).__name__} for attribute value. "201 f"Expected one of {[_type_name(valid_type) for valid_type in _VALID_ANY_VALUE_TYPES]} or a "202 "sequence of those types",203 )204 205 206def _clean_extended_attribute(207 key: str, value: types.AnyValue, max_len: Optional[int]208) -> types.AnyValue:209 """Checks if attribute value is valid and cleans it if required.210 211 The function returns the cleaned value or None if the value is not valid.212 213 An attribute value is valid if it is an AnyValue.214 An attribute needs cleansing if:215 - Its length is greater than the maximum allowed length.216 """217 218 if not (key and isinstance(key, str)):219 _logger.warning("invalid key `%s`. must be non-empty string.", key)220 return None221 222 try:223 return _clean_extended_attribute_value(value, max_len=max_len)224 except TypeError as exception:225 _logger.warning("Attribute %s: %s", key, exception)226 return None227 228 229def _clean_attribute_value(230 value: types.AttributeValue, limit: Optional[int]231) -> Optional[types.AttributeValue]:232 if value is None:233 return None234 235 if isinstance(value, bytes):236 try:237 value = value.decode()238 except UnicodeDecodeError:239 _logger.warning("Byte attribute could not be decoded.")240 return None241 242 if limit is not None and isinstance(value, str):243 value = value[:limit]244 return value245 246 247class BoundedAttributes(MutableMapping): # type: ignore248 """An ordered dict with a fixed max capacity.249 250 Oldest elements are dropped when the dict is full and a new element is251 added.252 """253 254 def __init__(255 self,256 maxlen: Optional[int] = None,257 attributes: Optional[types._ExtendedAttributes] = None,258 immutable: bool = True,259 max_value_len: Optional[int] = None,260 extended_attributes: bool = False,261 ):262 if maxlen is not None:263 if not isinstance(maxlen, int) or maxlen < 0:264 raise ValueError(265 "maxlen must be valid int greater or equal to 0"266 )267 self.maxlen = maxlen268 self.dropped = 0269 self.max_value_len = max_value_len270 self._extended_attributes = extended_attributes271 # OrderedDict is not used until the maxlen is reached for efficiency.272 273 self._dict: Union[274 MutableMapping[str, types.AnyValue],275 OrderedDict[str, types.AnyValue],276 ] = {}277 self._lock = threading.RLock()278 if attributes:279 for key, value in attributes.items():280 self[key] = value281 self._immutable = immutable282 283 def __repr__(self) -> str:284 return f"{dict(self._dict)}"285 286 def __getitem__(self, key: str) -> types.AnyValue:287 return self._dict[key]288 289 def __setitem__(self, key: str, value: types.AnyValue) -> None:290 if getattr(self, "_immutable", False): # type: ignore291 raise TypeError292 with self._lock:293 if self.maxlen is not None and self.maxlen == 0:294 self.dropped += 1295 return296 297 if self._extended_attributes:298 value = _clean_extended_attribute(299 key, value, self.max_value_len300 )301 else:302 value = _clean_attribute(key, value, self.max_value_len) # type: ignore303 if value is None:304 return305 306 if key in self._dict:307 del self._dict[key]308 elif self.maxlen is not None and len(self._dict) == self.maxlen:309 if not isinstance(self._dict, OrderedDict):310 self._dict = OrderedDict(self._dict)311 self._dict.popitem(last=False) # type: ignore312 self.dropped += 1313 314 self._dict[key] = value # type: ignore315 316 def __delitem__(self, key: str) -> None:317 if getattr(self, "_immutable", False): # type: ignore318 raise TypeError319 with self._lock:320 del self._dict[key]321 322 def __iter__(self): # type: ignore323 with self._lock:324 return iter(self._dict.copy()) # type: ignore325 326 def __len__(self) -> int:327 return len(self._dict)328 329 def __deepcopy__(self, memo: dict) -> "BoundedAttributes":330 copy_ = BoundedAttributes(331 maxlen=self.maxlen,332 immutable=self._immutable,333 max_value_len=self.max_value_len,334 extended_attributes=self._extended_attributes,335 )336 memo[id(self)] = copy_337 with self._lock:338 # Assign _dict directly to avoid re-cleaning already clean values339 # and to bypass the immutability guard in __setitem__340 copy_._dict = copy.deepcopy(self._dict, memo)341 copy_.dropped = self.dropped342 return copy_343 344 def copy(self): # type: ignore345 return self._dict.copy() # type: ignore346 