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

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

182 statements  

1# Copyright The OpenTelemetry Authors 

2# SPDX-License-Identifier: Apache-2.0 

3 

4import copy 

5import logging 

6import threading 

7from collections import OrderedDict 

8from collections.abc import Mapping, MutableMapping, Sequence 

9 

10from opentelemetry.util import types 

11 

12# bytes are accepted as a user supplied value for attributes but 

13# decoded to strings internally. 

14_VALID_ATTR_VALUE_TYPES = (bool, str, bytes, int, float) 

15# AnyValue possible values 

16_VALID_ANY_VALUE_TYPES = ( 

17 type(None), 

18 bool, 

19 bytes, 

20 int, 

21 float, 

22 str, 

23 Sequence, 

24 Mapping, 

25) 

26 

27 

28# TODO: Remove this workaround and revert to the simpler implementation 

29# once Python 3.9 support is dropped (planned around May 2026). 

30# This exists only to avoid issues caused by deprecated behavior in 3.9. 

31def _type_name(t): 

32 return getattr(t, "__name__", getattr(t, "_name", repr(t))) 

33 

34 

35_logger = logging.getLogger(__name__) 

36 

37 

38# pylint: disable=too-many-return-statements 

39# pylint: disable=too-many-branches 

40def _clean_attribute( 

41 key: str, value: types.AttributeValue, max_len: int | None 

42) -> types.AttributeValue | tuple[str | int | float, ...] | None: 

43 """Checks if attribute value is valid and cleans it if required. 

44 

45 The function returns the cleaned value or None if the value is not valid. 

46 

47 An attribute value is valid if it is either: 

48 - A primitive type: string, boolean, double precision floating 

49 point (IEEE 754-1985) or integer. 

50 - An array of primitive type values. The array MUST be homogeneous, 

51 i.e. it MUST NOT contain values of different types. 

52 

53 An attribute needs cleansing if: 

54 - Its length is greater than the maximum allowed length. 

55 - It needs to be encoded/decoded e.g, bytes to strings. 

56 """ 

57 

58 if not (key and isinstance(key, str)): 

59 _logger.warning("invalid key `%s`. must be non-empty string.", key) 

60 return None 

61 

62 if isinstance(value, _VALID_ATTR_VALUE_TYPES): 

63 if isinstance(value, bytes): 

64 try: 

65 value = value.decode() 

66 except UnicodeDecodeError: 

67 _logger.warning("Byte attribute could not be decoded.") 

68 return None 

69 if max_len is not None and isinstance(value, str): 

70 value = value[:max_len] 

71 return value 

72 

73 if isinstance(value, Sequence): 

74 sequence_first_valid_type = None 

75 cleaned_seq = [] 

76 

77 for element in value: 

78 if isinstance(element, bytes): 

79 try: 

80 element = element.decode() 

81 except UnicodeDecodeError: 

82 _logger.warning("Byte attribute could not be decoded.") 

83 cleaned_seq.append(None) 

84 continue 

85 if max_len is not None and isinstance(element, str): 

86 element = element[:max_len] 

87 elif element is None: 

88 cleaned_seq.append(None) 

89 continue 

90 

91 element_type = type(element) 

92 # Reject attribute value if sequence contains a value with an incompatible type. 

93 if element_type not in _VALID_ATTR_VALUE_TYPES: 

94 _logger.warning( 

95 "Invalid type %s in attribute '%s' value sequence. Expected one of " 

96 "%s or None", 

97 element_type.__name__, 

98 key, 

99 [ 

100 valid_type.__name__ 

101 for valid_type in _VALID_ATTR_VALUE_TYPES 

102 ], 

103 ) 

104 return None 

105 

106 # The type of the sequence must be homogeneous. The first non-None 

107 # element determines the type of the sequence 

108 if sequence_first_valid_type is None: 

109 sequence_first_valid_type = element_type 

110 # use equality instead of isinstance as isinstance(True, int) evaluates to True 

111 elif element_type != sequence_first_valid_type: 

112 _logger.warning( 

113 "Attribute %r mixes types %s and %s in attribute value sequence", 

114 key, 

115 sequence_first_valid_type.__name__, 

116 type(element).__name__, 

117 ) 

118 return None 

119 

120 cleaned_seq.append(element) 

121 

122 # Freeze mutable sequences defensively 

123 return tuple(cleaned_seq) 

124 

125 _logger.warning( 

126 "Invalid type %s for attribute '%s' value. Expected one of %s or a " 

127 "sequence of those types", 

128 type(value).__name__, 

129 key, 

130 [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES], 

131 ) 

132 return None 

133 

134 

135def _clean_extended_attribute_value( # pylint: disable=too-many-branches 

136 value: types.AnyValue, max_len: int | None 

137) -> types.AnyValue: 

138 # for primitive types just return the value and eventually shorten the string length 

139 if value is None or isinstance(value, _VALID_ATTR_VALUE_TYPES): 

140 if max_len is not None and isinstance(value, str): 

141 value = value[:max_len] 

142 return value 

143 

144 if isinstance(value, Mapping): 

145 cleaned_dict: dict[str, types.AnyValue] = {} 

146 for key, element in value.items(): 

147 # skip invalid keys 

148 if not (key and isinstance(key, str)): 

149 _logger.warning( 

150 "invalid key `%s`. must be non-empty string.", key 

151 ) 

152 continue 

153 

154 cleaned_dict[key] = _clean_extended_attribute( 

155 key=key, value=element, max_len=max_len 

156 ) 

157 

158 return cleaned_dict 

159 

160 if isinstance(value, Sequence): 

161 sequence_first_valid_type = None 

162 cleaned_seq: list[types.AnyValue] = [] 

163 

164 for element in value: 

165 if element is None: 

166 cleaned_seq.append(element) 

167 continue 

168 

169 if max_len is not None and isinstance(element, str): 

170 element = element[:max_len] 

171 

172 element_type = type(element) 

173 if element_type not in _VALID_ATTR_VALUE_TYPES: 

174 element = _clean_extended_attribute_value( 

175 element, max_len=max_len 

176 ) 

177 element_type = type(element) # type: ignore 

178 

179 # The type of the sequence must be homogeneous. The first non-None 

180 # element determines the type of the sequence 

181 if sequence_first_valid_type is None: 

182 sequence_first_valid_type = element_type 

183 # use equality instead of isinstance as isinstance(True, int) evaluates to True 

184 elif element_type != sequence_first_valid_type: 

185 _logger.warning( 

186 "Mixed types %s and %s in attribute value sequence", 

187 sequence_first_valid_type.__name__, 

188 type(element).__name__, 

189 ) 

190 return None 

191 

192 cleaned_seq.append(element) 

193 

194 # Freeze mutable sequences defensively 

195 return tuple(cleaned_seq) 

196 

197 # Some applications such as Django add values to log records whose types fall outside the 

198 # primitive types and `_VALID_ANY_VALUE_TYPES`, i.e., they are not of type `AnyValue`. 

199 # Rather than attempt to whitelist every possible instrumentation, we stringify those values here 

200 # so they can still be represented as attributes, falling back to the original TypeError only if 

201 # converting to string raises. 

202 try: 

203 return str(value) 

204 except Exception: 

205 raise TypeError( 

206 f"Invalid type {type(value).__name__} for attribute value. " 

207 f"Expected one of {[_type_name(valid_type) for valid_type in _VALID_ANY_VALUE_TYPES]} or a " 

208 "sequence of those types", 

209 ) 

210 

211 

212def _clean_extended_attribute( 

213 key: str, value: types.AnyValue, max_len: int | None 

214) -> types.AnyValue: 

215 """Checks if attribute value is valid and cleans it if required. 

216 

217 The function returns the cleaned value or None if the value is not valid. 

218 

219 An attribute value is valid if it is an AnyValue. 

220 An attribute needs cleansing if: 

221 - Its length is greater than the maximum allowed length. 

222 """ 

223 

224 if not (key and isinstance(key, str)): 

225 _logger.warning("invalid key `%s`. must be non-empty string.", key) 

226 return None 

227 

228 try: 

229 return _clean_extended_attribute_value(value, max_len=max_len) 

230 except TypeError as exception: 

231 _logger.warning("Attribute %s: %s", key, exception) 

232 return None 

233 

234 

235class BoundedAttributes(MutableMapping): # type: ignore 

236 """An ordered dict with a fixed max capacity. 

237 

238 Oldest elements are dropped when the dict is full and a new element is 

239 added. 

240 """ 

241 

242 def __init__( 

243 self, 

244 maxlen: int | None = None, 

245 attributes: types._ExtendedAttributes | None = None, 

246 immutable: bool = True, 

247 max_value_len: int | None = None, 

248 extended_attributes: bool = False, 

249 ): 

250 if maxlen is not None: 

251 if not isinstance(maxlen, int) or maxlen < 0: 

252 raise ValueError( 

253 "maxlen must be valid int greater or equal to 0" 

254 ) 

255 self.maxlen = maxlen 

256 self.dropped = 0 

257 self.max_value_len = max_value_len 

258 self._extended_attributes = extended_attributes 

259 # OrderedDict is not used until the maxlen is reached for efficiency. 

260 

261 self._dict: ( 

262 MutableMapping[str, types.AnyValue] 

263 | OrderedDict[str, types.AnyValue] 

264 ) = {} 

265 self._lock = threading.Lock() 

266 if attributes: 

267 for key, value in attributes.items(): 

268 self[key] = value 

269 self._immutable = immutable 

270 

271 def __repr__(self) -> str: 

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

273 

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

275 return self._dict[key] 

276 

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

278 if getattr(self, "_immutable", False): # type: ignore 

279 raise TypeError 

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

281 with self._lock: 

282 self.dropped += 1 

283 return 

284 if self._extended_attributes: 

285 value = _clean_extended_attribute(key, value, self.max_value_len) 

286 else: 

287 value = _clean_attribute(key, value, self.max_value_len) # type: ignore 

288 if value is None: 

289 return 

290 with self._lock: 

291 self._setitem_locked(key, value) 

292 

293 def _set_items(self, attributes: "types._ExtendedAttributes") -> None: 

294 if getattr(self, "_immutable", False): # type: ignore 

295 raise TypeError 

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

297 with self._lock: 

298 self.dropped += len(attributes) 

299 return 

300 cleaned = [] 

301 for key, value in attributes.items(): 

302 if self._extended_attributes: 

303 cv = _clean_extended_attribute(key, value, self.max_value_len) 

304 else: 

305 cv = _clean_attribute(key, value, self.max_value_len) # type: ignore 

306 if cv is None: 

307 continue 

308 cleaned.append((key, cv)) 

309 with self._lock: 

310 for key, cv in cleaned: 

311 self._setitem_locked(key, cv) 

312 

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

314 if key in self._dict: 

315 del self._dict[key] 

316 elif self.maxlen is not None and len(self._dict) == self.maxlen: 

317 if not isinstance(self._dict, OrderedDict): 

318 self._dict = OrderedDict(self._dict) 

319 self._dict.popitem(last=False) # type: ignore 

320 self.dropped += 1 

321 

322 self._dict[key] = value # type: ignore 

323 

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

325 if getattr(self, "_immutable", False): # type: ignore 

326 raise TypeError 

327 with self._lock: 

328 del self._dict[key] 

329 

330 def __iter__(self): 

331 if self._immutable: 

332 return iter(self._dict) 

333 with self._lock: 

334 return iter(list(self._dict)) 

335 

336 def __len__(self) -> int: 

337 return len(self._dict) 

338 

339 def __deepcopy__(self, memo: dict) -> "BoundedAttributes": 

340 copy_ = BoundedAttributes( 

341 maxlen=self.maxlen, 

342 immutable=self._immutable, 

343 max_value_len=self.max_value_len, 

344 extended_attributes=self._extended_attributes, 

345 ) 

346 memo[id(self)] = copy_ 

347 with self._lock: 

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

349 # and to bypass the immutability guard in __setitem__ 

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

351 copy_.dropped = self.dropped 

352 return copy_ 

353 

354 def copy(self): # type: ignore 

355 return self._dict.copy() # type: ignore