__init__.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. # Copyright The OpenTelemetry Authors
  2. # SPDX-License-Identifier: Apache-2.0
  3. import copy
  4. import logging
  5. import threading
  6. from collections import OrderedDict
  7. from collections.abc import Mapping, MutableMapping, Sequence
  8. from opentelemetry.util import types
  9. # bytes are accepted as a user supplied value for attributes but
  10. # decoded to strings internally.
  11. _VALID_ATTR_VALUE_TYPES = (bool, str, bytes, int, float)
  12. # AnyValue possible values
  13. _VALID_ANY_VALUE_TYPES = (
  14. type(None),
  15. bool,
  16. bytes,
  17. int,
  18. float,
  19. str,
  20. Sequence,
  21. Mapping,
  22. )
  23. # TODO: Remove this workaround and revert to the simpler implementation
  24. # once Python 3.9 support is dropped (planned around May 2026).
  25. # This exists only to avoid issues caused by deprecated behavior in 3.9.
  26. def _type_name(t):
  27. return getattr(t, "__name__", getattr(t, "_name", repr(t)))
  28. _logger = logging.getLogger(__name__)
  29. # pylint: disable=too-many-return-statements
  30. # pylint: disable=too-many-branches
  31. def _clean_attribute(
  32. key: str, value: types.AttributeValue, max_len: int | None
  33. ) -> types.AttributeValue | tuple[str | int | float, ...] | None:
  34. """Checks if attribute value is valid and cleans it if required.
  35. The function returns the cleaned value or None if the value is not valid.
  36. An attribute value is valid if it is either:
  37. - A primitive type: string, boolean, double precision floating
  38. point (IEEE 754-1985) or integer.
  39. - An array of primitive type values. The array MUST be homogeneous,
  40. i.e. it MUST NOT contain values of different types.
  41. An attribute needs cleansing if:
  42. - Its length is greater than the maximum allowed length.
  43. - It needs to be encoded/decoded e.g, bytes to strings.
  44. """
  45. if not (key and isinstance(key, str)):
  46. _logger.warning("invalid key `%s`. must be non-empty string.", key)
  47. return None
  48. if isinstance(value, _VALID_ATTR_VALUE_TYPES):
  49. if isinstance(value, bytes):
  50. try:
  51. value = value.decode()
  52. except UnicodeDecodeError:
  53. _logger.warning("Byte attribute could not be decoded.")
  54. return None
  55. if max_len is not None and isinstance(value, str):
  56. value = value[:max_len]
  57. return value
  58. if isinstance(value, Sequence):
  59. sequence_first_valid_type = None
  60. cleaned_seq = []
  61. for element in value:
  62. if isinstance(element, bytes):
  63. try:
  64. element = element.decode()
  65. except UnicodeDecodeError:
  66. _logger.warning("Byte attribute could not be decoded.")
  67. cleaned_seq.append(None)
  68. continue
  69. if max_len is not None and isinstance(element, str):
  70. element = element[:max_len]
  71. elif element is None:
  72. cleaned_seq.append(None)
  73. continue
  74. element_type = type(element)
  75. # Reject attribute value if sequence contains a value with an incompatible type.
  76. if element_type not in _VALID_ATTR_VALUE_TYPES:
  77. _logger.warning(
  78. "Invalid type %s in attribute '%s' value sequence. Expected one of "
  79. "%s or None",
  80. element_type.__name__,
  81. key,
  82. [
  83. valid_type.__name__
  84. for valid_type in _VALID_ATTR_VALUE_TYPES
  85. ],
  86. )
  87. return None
  88. # The type of the sequence must be homogeneous. The first non-None
  89. # element determines the type of the sequence
  90. if sequence_first_valid_type is None:
  91. sequence_first_valid_type = element_type
  92. # use equality instead of isinstance as isinstance(True, int) evaluates to True
  93. elif element_type != sequence_first_valid_type:
  94. _logger.warning(
  95. "Attribute %r mixes types %s and %s in attribute value sequence",
  96. key,
  97. sequence_first_valid_type.__name__,
  98. type(element).__name__,
  99. )
  100. return None
  101. cleaned_seq.append(element)
  102. # Freeze mutable sequences defensively
  103. return tuple(cleaned_seq)
  104. _logger.warning(
  105. "Invalid type %s for attribute '%s' value. Expected one of %s or a "
  106. "sequence of those types",
  107. type(value).__name__,
  108. key,
  109. [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES],
  110. )
  111. return None
  112. def _clean_extended_attribute_value( # pylint: disable=too-many-branches
  113. value: types.AnyValue, max_len: int | None
  114. ) -> types.AnyValue:
  115. # for primitive types just return the value and eventually shorten the string length
  116. if value is None or isinstance(value, _VALID_ATTR_VALUE_TYPES):
  117. if max_len is not None and isinstance(value, str):
  118. value = value[:max_len]
  119. return value
  120. if isinstance(value, Mapping):
  121. cleaned_dict: dict[str, types.AnyValue] = {}
  122. for key, element in value.items():
  123. # skip invalid keys
  124. if not (key and isinstance(key, str)):
  125. _logger.warning(
  126. "invalid key `%s`. must be non-empty string.", key
  127. )
  128. continue
  129. cleaned_dict[key] = _clean_extended_attribute(
  130. key=key, value=element, max_len=max_len
  131. )
  132. return cleaned_dict
  133. if isinstance(value, Sequence):
  134. sequence_first_valid_type = None
  135. cleaned_seq: list[types.AnyValue] = []
  136. for element in value:
  137. if element is None:
  138. cleaned_seq.append(element)
  139. continue
  140. if max_len is not None and isinstance(element, str):
  141. element = element[:max_len]
  142. element_type = type(element)
  143. if element_type not in _VALID_ATTR_VALUE_TYPES:
  144. element = _clean_extended_attribute_value(
  145. element, max_len=max_len
  146. )
  147. element_type = type(element) # type: ignore
  148. # The type of the sequence must be homogeneous. The first non-None
  149. # element determines the type of the sequence
  150. if sequence_first_valid_type is None:
  151. sequence_first_valid_type = element_type
  152. # use equality instead of isinstance as isinstance(True, int) evaluates to True
  153. elif element_type != sequence_first_valid_type:
  154. _logger.warning(
  155. "Mixed types %s and %s in attribute value sequence",
  156. sequence_first_valid_type.__name__,
  157. type(element).__name__,
  158. )
  159. return None
  160. cleaned_seq.append(element)
  161. # Freeze mutable sequences defensively
  162. return tuple(cleaned_seq)
  163. # Some applications such as Django add values to log records whose types fall outside the
  164. # primitive types and `_VALID_ANY_VALUE_TYPES`, i.e., they are not of type `AnyValue`.
  165. # Rather than attempt to whitelist every possible instrumentation, we stringify those values here
  166. # so they can still be represented as attributes, falling back to the original TypeError only if
  167. # converting to string raises.
  168. try:
  169. return str(value)
  170. except Exception:
  171. raise TypeError(
  172. f"Invalid type {type(value).__name__} for attribute value. "
  173. f"Expected one of {[_type_name(valid_type) for valid_type in _VALID_ANY_VALUE_TYPES]} or a "
  174. "sequence of those types",
  175. )
  176. def _clean_extended_attribute(
  177. key: str, value: types.AnyValue, max_len: int | None
  178. ) -> types.AnyValue:
  179. """Checks if attribute value is valid and cleans it if required.
  180. The function returns the cleaned value or None if the value is not valid.
  181. An attribute value is valid if it is an AnyValue.
  182. An attribute needs cleansing if:
  183. - Its length is greater than the maximum allowed length.
  184. """
  185. if not (key and isinstance(key, str)):
  186. _logger.warning("invalid key `%s`. must be non-empty string.", key)
  187. return None
  188. try:
  189. return _clean_extended_attribute_value(value, max_len=max_len)
  190. except TypeError as exception:
  191. _logger.warning("Attribute %s: %s", key, exception)
  192. return None
  193. class BoundedAttributes(MutableMapping): # type: ignore
  194. """An ordered dict with a fixed max capacity.
  195. Oldest elements are dropped when the dict is full and a new element is
  196. added.
  197. """
  198. def __init__(
  199. self,
  200. maxlen: int | None = None,
  201. attributes: types._ExtendedAttributes | None = None,
  202. immutable: bool = True,
  203. max_value_len: int | None = None,
  204. extended_attributes: bool = False,
  205. ):
  206. if maxlen is not None:
  207. if not isinstance(maxlen, int) or maxlen < 0:
  208. raise ValueError(
  209. "maxlen must be valid int greater or equal to 0"
  210. )
  211. self.maxlen = maxlen
  212. self.dropped = 0
  213. self.max_value_len = max_value_len
  214. self._extended_attributes = extended_attributes
  215. # OrderedDict is not used until the maxlen is reached for efficiency.
  216. self._dict: (
  217. MutableMapping[str, types.AnyValue]
  218. | OrderedDict[str, types.AnyValue]
  219. ) = {}
  220. self._lock = threading.Lock()
  221. if attributes:
  222. for key, value in attributes.items():
  223. self[key] = value
  224. self._immutable = immutable
  225. def __repr__(self) -> str:
  226. return f"{dict(self._dict)}"
  227. def __getitem__(self, key: str) -> types.AnyValue:
  228. return self._dict[key]
  229. def __setitem__(self, key: str, value: types.AnyValue) -> None:
  230. if getattr(self, "_immutable", False): # type: ignore
  231. raise TypeError
  232. if self.maxlen is not None and self.maxlen == 0:
  233. with self._lock:
  234. self.dropped += 1
  235. return
  236. if self._extended_attributes:
  237. value = _clean_extended_attribute(key, value, self.max_value_len)
  238. else:
  239. value = _clean_attribute(key, value, self.max_value_len) # type: ignore
  240. if value is None:
  241. return
  242. with self._lock:
  243. self._setitem_locked(key, value)
  244. def _set_items(self, attributes: "types._ExtendedAttributes") -> None:
  245. if getattr(self, "_immutable", False): # type: ignore
  246. raise TypeError
  247. if self.maxlen is not None and self.maxlen == 0:
  248. with self._lock:
  249. self.dropped += len(attributes)
  250. return
  251. cleaned = []
  252. for key, value in attributes.items():
  253. if self._extended_attributes:
  254. cv = _clean_extended_attribute(key, value, self.max_value_len)
  255. else:
  256. cv = _clean_attribute(key, value, self.max_value_len) # type: ignore
  257. if cv is None:
  258. continue
  259. cleaned.append((key, cv))
  260. with self._lock:
  261. for key, cv in cleaned:
  262. self._setitem_locked(key, cv)
  263. def _setitem_locked(self, key: str, value: types.AnyValue) -> None:
  264. if key in self._dict:
  265. del self._dict[key]
  266. elif self.maxlen is not None and len(self._dict) == self.maxlen:
  267. if not isinstance(self._dict, OrderedDict):
  268. self._dict = OrderedDict(self._dict)
  269. self._dict.popitem(last=False) # type: ignore
  270. self.dropped += 1
  271. self._dict[key] = value # type: ignore
  272. def __delitem__(self, key: str) -> None:
  273. if getattr(self, "_immutable", False): # type: ignore
  274. raise TypeError
  275. with self._lock:
  276. del self._dict[key]
  277. def __iter__(self):
  278. if self._immutable:
  279. return iter(self._dict)
  280. with self._lock:
  281. return iter(list(self._dict))
  282. def __len__(self) -> int:
  283. return len(self._dict)
  284. def __deepcopy__(self, memo: dict) -> "BoundedAttributes":
  285. copy_ = BoundedAttributes(
  286. maxlen=self.maxlen,
  287. immutable=self._immutable,
  288. max_value_len=self.max_value_len,
  289. extended_attributes=self._extended_attributes,
  290. )
  291. memo[id(self)] = copy_
  292. with self._lock:
  293. # Assign _dict directly to avoid re-cleaning already clean values
  294. # and to bypass the immutability guard in __setitem__
  295. copy_._dict = copy.deepcopy(self._dict, memo)
  296. copy_.dropped = self.dropped
  297. return copy_
  298. def copy(self): # type: ignore
  299. return self._dict.copy() # type: ignore