Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/opentelemetry/attributes/__init__.py: 27%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

124 statements  

1# Copyright The OpenTelemetry Authors 

2# SPDX-License-Identifier: Apache-2.0 

3 

4# pyright: reportUnnecessaryIsInstance=false 

5 

6import copy 

7import logging 

8import threading 

9from collections.abc import Mapping, MutableMapping, Sequence 

10from types import NoneType 

11from typing import TYPE_CHECKING, Any, overload 

12 

13from typing_extensions import assert_never, deprecated 

14 

15from opentelemetry.util import types 

16 

17_logger = logging.getLogger(__name__) 

18 

19 

20# Calling str(x) will use an object's `__str__` method if it exists, otherwise it will use it's `__repr__` method. 

21# If neither is defined it uses the base class's `object.__repr__` method, which returns a string that is hard to understand. 

22# So in that case we drop the key/value pair. 

23def _is_non_custom_str(key: Any) -> bool: 

24 return type(key).__str__ is not object.__str__ or type(key).__repr__ is not object.__repr__ 

25 

26 

27@overload 

28def _clean_attribute_value( 

29 value: Mapping[str, types.AnyValue], 

30 max_string_value_length: int | None, 

31) -> Mapping[str, types.AnyValue]: ... 

32 

33 

34@overload 

35def _clean_attribute_value( 

36 value: types.AnyValue, 

37 max_string_value_length: int | None, 

38) -> types.AnyValue: ... 

39 

40 

41def _clean_attribute_value( 

42 value: types.AnyValue, 

43 max_string_value_length: int | None, 

44) -> types.AnyValue: 

45 """Recursively checks if an attribute value is valid and cleans it if required. 

46 

47 String values are truncated to max_string_value_length if provided. 

48 Anything that isn't of `types.AnyValue`, we attempt to cast to `str`. 

49 If this fails, the value is replaced with None. Sequence's are converted to tuples and mappings 

50 are copied into new dicts. 

51 

52 Returns: 

53 The recursively cleaned AnyValue. 

54 """ 

55 if isinstance(value, (NoneType, bool, int, float, bytes)): 

56 return value 

57 if isinstance(value, str): 

58 if max_string_value_length is not None and len(value) > max_string_value_length: 

59 _logger.warning( 

60 "String attribute value exceeds max length of %d, truncating.", 

61 max_string_value_length, 

62 ) 

63 value = value[:max_string_value_length] 

64 return value 

65 if isinstance(value, Sequence): 

66 return tuple(_clean_attribute_value(v, max_string_value_length) for v in value) 

67 if isinstance(value, Mapping): 

68 cleaned_mapping: dict[str, types.AnyValue] = {} 

69 for key, val in value.items(): 

70 if not key: 

71 _logger.warning( 

72 "invalid attribute key `%s`. must be non-empty string. Dropping key from attributes.", 

73 key, 

74 ) 

75 continue 

76 # Spec says to convert unknown types to strings if possible (here and below too). 

77 if not isinstance(key, str): 

78 _logger.warning( 

79 "Invalid type `%s` for attribute key `%s`, must be a str. Key's `__str__/__repr__` method will be called if it exists, otherwise the key/value pair will be dropped.", 

80 type(key), 

81 key, 

82 ) 

83 if _is_non_custom_str(key): 

84 key = str(key) 

85 else: 

86 continue 

87 cleaned_mapping[key] = _clean_attribute_value(val, max_string_value_length) 

88 return cleaned_mapping 

89 if TYPE_CHECKING: 

90 assert_never(value) 

91 _logger.warning( 

92 "Invalid type `%s` for attribute value. Expected one of bool, str, None, bytes, int, float or a " 

93 "Mapping or Sequence of those types. Value's __str__ method will be called if it exists, otherwise the value will be replaced with None.", 

94 type(value), 

95 ) 

96 if _is_non_custom_str(value): 

97 return str(value) 

98 return None 

99 

100 

101class BoundedAttributes(MutableMapping[str, types.AnyValue]): 

102 """A dict with a fixed max capacity which cleans and potentially drops values to ensure they are valid attribute values. 

103 

104 Args: 

105 maxlen: The maximum number of attributes to store, use None for no limit. 

106 attributes: The initial attributes to store. 

107 immutable: Defaults to true. Whether to allow adding/removing of attributes after the initialisation of the instance. 

108 max_value_len: The maximum length of string values, use None for no limit. 

109 extended_attributes: Deprecated. Kept for backwards compatibility. Extended attributes are now always used for attributes everywhere. 

110 

111 When the dict is full and a new element is added, the oldest element is dropped. Attributes are made to be immutable when set in this container. 

112 So passing a mutable list as an attribute value, and then mutating it after will not change it's value in this container. 

113 """ 

114 

115 @overload 

116 def __init__( 

117 self, 

118 maxlen: int | None = None, 

119 attributes: types.Attributes = None, 

120 immutable: bool = True, 

121 max_value_len: int | None = None, 

122 ) -> None: ... 

123 

124 @overload 

125 @deprecated( 

126 "Creating BoundedAttributes with `extended_attributes` set is deprecated. " 

127 "The `extended_attributes` param is no longer used and will be removed " 

128 "in a future release. Extended attributes are now always used for attributes everywhere." 

129 ) 

130 def __init__( 

131 self, 

132 maxlen: int | None = None, 

133 attributes: types.Attributes = None, 

134 immutable: bool = True, 

135 max_value_len: int | None = None, 

136 extended_attributes: bool = False, 

137 ) -> None: ... 

138 

139 def __init__( 

140 self, 

141 maxlen: int | None = None, 

142 attributes: types.Attributes = None, 

143 immutable: bool = True, 

144 max_value_len: int | None = None, 

145 extended_attributes: bool = False, 

146 ) -> None: 

147 if maxlen is not None and maxlen < 0: 

148 raise ValueError("maxlen must be valid int greater or equal to 0") 

149 if max_value_len is not None and max_value_len < 0: 

150 raise ValueError("max_value_len must be valid int greater or equal to 0") 

151 self._dict: dict[str, types.AnyValue] = {} 

152 self.maxlen = maxlen 

153 self.dropped = 0 

154 self.max_value_len = max_value_len 

155 self._lock = threading.Lock() 

156 # setting False before adding items 

157 self._immutable = False 

158 if attributes: 

159 self._set_items(attributes) 

160 self._immutable = immutable 

161 

162 def __repr__(self) -> str: 

163 return f"{dict(self._dict)}" 

164 

165 def __getitem__(self, key: str) -> types.AnyValue: 

166 return self._dict[key] 

167 

168 def _raise_if_immutable(self) -> None: 

169 if self._immutable: 

170 raise TypeError("Cannot mutate immutable BoundedAttributes") 

171 

172 def __setitem__(self, key: str, value: types.AnyValue) -> None: 

173 self._raise_if_immutable() 

174 if self.maxlen is not None and self.maxlen == 0: 

175 with self._lock: 

176 self.dropped += 1 

177 return 

178 if not key or not isinstance(key, str): 

179 _logger.warning( 

180 "invalid key `%s`. must be non-empty string. Dropping key from attributes.", 

181 key, 

182 ) 

183 with self._lock: 

184 self.dropped += 1 

185 return 

186 cleaned = _clean_attribute_value(value, self.max_value_len) 

187 with self._lock: 

188 self._setitem_locked(key, cleaned) 

189 

190 def _set_items(self, attributes: Mapping[str, types.AnyValue]) -> None: 

191 self._raise_if_immutable() 

192 if self.maxlen is not None and self.maxlen == 0: 

193 with self._lock: 

194 self.dropped += len(attributes) 

195 return 

196 cleaned_attributes: Mapping[str, types.AnyValue] = _clean_attribute_value(attributes, self.max_value_len) 

197 with self._lock: 

198 self.dropped += len(attributes) - len(cleaned_attributes) 

199 for key, value in cleaned_attributes.items(): 

200 self._setitem_locked(key, value) 

201 

202 def _setitem_locked(self, key: str, value: types.AnyValue) -> None: 

203 if key in self._dict: 

204 del self._dict[key] 

205 if self.maxlen is not None and len(self._dict) >= self.maxlen: 

206 _logger.warning( 

207 "Attributes dict is full. Dropping the oldest key-value pair from attributes to make space for the new key-value pair.", 

208 ) 

209 # Dictionaries are insertion ordered in Python, this is the recommended way to get the oldest value. 

210 del self._dict[next(iter(self._dict.keys()))] 

211 self.dropped += 1 

212 

213 self._dict[key] = value 

214 

215 def __delitem__(self, key: str) -> None: 

216 self._raise_if_immutable() 

217 del self._dict[key] 

218 

219 def __iter__(self): 

220 if self._immutable: 

221 return iter(self._dict) 

222 with self._lock: 

223 return iter(list(self._dict)) 

224 

225 def __len__(self) -> int: 

226 return len(self._dict) 

227 

228 def __deepcopy__(self, memo: dict[int, Any]) -> "BoundedAttributes": 

229 copy_ = BoundedAttributes( 

230 maxlen=self.maxlen, 

231 immutable=self._immutable, 

232 max_value_len=self.max_value_len, 

233 ) 

234 memo[id(self)] = copy_ 

235 with self._lock: 

236 # Assign _dict directly to avoid re-cleaning already clean values 

237 # and to bypass the immutability guard in __setitem__ 

238 copy_._dict = copy.deepcopy(self._dict, memo) 

239 copy_.dropped = self.dropped 

240 return copy_ 

241 

242 def copy(self) -> MutableMapping[str, types.AnyValue]: 

243 return self._dict.copy()