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

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 

15 

16import logging 

17import threading 

18from collections import OrderedDict 

19from collections.abc import MutableMapping 

20from typing import Optional, Sequence, Union 

21 

22from opentelemetry.util import types 

23 

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) 

27 

28 

29_logger = logging.getLogger(__name__) 

30 

31 

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. 

36 

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

38 

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. 

44 

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 """ 

49 

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

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

52 return None 

53 

54 if isinstance(value, _VALID_ATTR_VALUE_TYPES): 

55 return _clean_attribute_value(value, max_len) 

56 

57 if isinstance(value, Sequence): 

58 sequence_first_valid_type = None 

59 cleaned_seq = [] 

60 

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 

66 

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 

80 

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 

93 

94 cleaned_seq.append(element) 

95 

96 # Freeze mutable sequences defensively 

97 return tuple(cleaned_seq) 

98 

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 

106 

107 

108def _clean_attribute_value( 

109 value: types.AttributeValue, limit: Optional[int] 

110) -> Union[types.AttributeValue, None]: 

111 if value is None: 

112 return None 

113 

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 

120 

121 if limit is not None and isinstance(value, str): 

122 value = value[:limit] 

123 return value 

124 

125 

126class BoundedAttributes(MutableMapping): 

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

128 

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

130 added. 

131 """ 

132 

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 

154 

155 def __repr__(self): 

156 return ( 

157 f"{type(self).__name__}({dict(self._dict)}, maxlen={self.maxlen})" 

158 ) 

159 

160 def __getitem__(self, key): 

161 return self._dict[key] 

162 

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 

170 

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 

180 

181 self._dict[key] = value 

182 

183 def __delitem__(self, key): 

184 if getattr(self, "_immutable", False): 

185 raise TypeError 

186 with self._lock: 

187 del self._dict[key] 

188 

189 def __iter__(self): 

190 with self._lock: 

191 return iter(self._dict.copy()) 

192 

193 def __len__(self): 

194 return len(self._dict) 

195 

196 def copy(self): 

197 return self._dict.copy()