Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/cloud/logging_v2/handlers/_helpers.py: 27%

85 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:45 +0000

1# Copyright 2016 Google LLC All Rights Reserved. 

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 

15"""Helper functions for logging handlers.""" 

16 

17import math 

18import json 

19import re 

20import warnings 

21 

22try: 

23 import flask 

24except ImportError: # pragma: NO COVER 

25 flask = None 

26 

27from google.cloud.logging_v2.handlers.middleware.request import _get_django_request 

28 

29_DJANGO_CONTENT_LENGTH = "CONTENT_LENGTH" 

30_DJANGO_XCLOUD_TRACE_HEADER = "HTTP_X_CLOUD_TRACE_CONTEXT" 

31_DJANGO_TRACEPARENT = "HTTP_TRACEPARENT" 

32_DJANGO_USERAGENT_HEADER = "HTTP_USER_AGENT" 

33_DJANGO_REMOTE_ADDR_HEADER = "REMOTE_ADDR" 

34_DJANGO_REFERER_HEADER = "HTTP_REFERER" 

35_FLASK_XCLOUD_TRACE_HEADER = "X_CLOUD_TRACE_CONTEXT" 

36_FLASK_TRACEPARENT = "TRACEPARENT" 

37_PROTOCOL_HEADER = "SERVER_PROTOCOL" 

38 

39 

40def format_stackdriver_json(record, message): 

41 """Helper to format a LogRecord in in Stackdriver fluentd format. 

42 

43 Returns: 

44 str: JSON str to be written to the log file. 

45 

46 DEPRECATED: use StructuredLogHandler to write formatted logs to standard out instead. 

47 """ 

48 subsecond, second = math.modf(record.created) 

49 

50 payload = { 

51 "message": message, 

52 "timestamp": {"seconds": int(second), "nanos": int(subsecond * 1e9)}, 

53 "thread": record.thread, 

54 "severity": record.levelname, 

55 } 

56 warnings.warn( 

57 "format_stackdriver_json is deprecated. Use StructuredLogHandler instead.", 

58 DeprecationWarning, 

59 ) 

60 return json.dumps(payload, ensure_ascii=False) 

61 

62 

63def get_request_data_from_flask(): 

64 """Get http_request and trace data from flask request headers. 

65 

66 Returns: 

67 Tuple[Optional[dict], Optional[str], Optional[str], bool]: 

68 Data related to the current http request, trace_id, span_id and trace_sampled 

69 for the request. All fields will be None if a django request isn't found. 

70 """ 

71 if flask is None or not flask.request: 

72 return None, None, None, False 

73 

74 # build http_request 

75 http_request = { 

76 "requestMethod": flask.request.method, 

77 "requestUrl": flask.request.url, 

78 "userAgent": flask.request.user_agent.string, 

79 "protocol": flask.request.environ.get(_PROTOCOL_HEADER), 

80 } 

81 

82 # find trace id and span id 

83 # first check for w3c traceparent header 

84 header = flask.request.headers.get(_FLASK_TRACEPARENT) 

85 trace_id, span_id, trace_sampled = _parse_trace_parent(header) 

86 if trace_id is None: 

87 # traceparent not found. look for xcloud_trace_context header 

88 header = flask.request.headers.get(_FLASK_XCLOUD_TRACE_HEADER) 

89 trace_id, span_id, trace_sampled = _parse_xcloud_trace(header) 

90 

91 return http_request, trace_id, span_id, trace_sampled 

92 

93 

94def get_request_data_from_django(): 

95 """Get http_request and trace data from django request headers. 

96 

97 Returns: 

98 Tuple[Optional[dict], Optional[str], Optional[str], bool]: 

99 Data related to the current http request, trace_id, span_id, and trace_sampled 

100 for the request. All fields will be None if a django request isn't found. 

101 """ 

102 request = _get_django_request() 

103 

104 if request is None: 

105 return None, None, None, False 

106 

107 # Django can raise django.core.exceptions.DisallowedHost here for a 

108 # malformed HTTP_HOST header. But we don't want to import Django modules. 

109 try: 

110 request_url = request.build_absolute_uri() 

111 except Exception: 

112 request_url = None 

113 

114 # build http_request 

115 http_request = { 

116 "requestMethod": request.method, 

117 "requestUrl": request_url, 

118 "userAgent": request.META.get(_DJANGO_USERAGENT_HEADER), 

119 "protocol": request.META.get(_PROTOCOL_HEADER), 

120 } 

121 

122 # find trace id and span id 

123 # first check for w3c traceparent header 

124 header = request.META.get(_DJANGO_TRACEPARENT) 

125 trace_id, span_id, trace_sampled = _parse_trace_parent(header) 

126 if trace_id is None: 

127 # traceparent not found. look for xcloud_trace_context header 

128 header = request.META.get(_DJANGO_XCLOUD_TRACE_HEADER) 

129 trace_id, span_id, trace_sampled = _parse_xcloud_trace(header) 

130 

131 return http_request, trace_id, span_id, trace_sampled 

132 

133 

134def _parse_trace_parent(header): 

135 """Given a w3 traceparent header, extract the trace and span ids. 

136 For more information see https://www.w3.org/TR/trace-context/ 

137 

138 Args: 

139 header (str): the string extracted from the traceparent header 

140 example: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 

141 Returns: 

142 Tuple[Optional[dict], Optional[str], bool]: 

143 The trace_id, span_id and trace_sampled extracted from the header 

144 Each field will be None if header can't be parsed in expected format. 

145 """ 

146 trace_id = span_id = None 

147 trace_sampled = False 

148 # see https://www.w3.org/TR/trace-context/ for W3C traceparent format 

149 if header: 

150 try: 

151 VERSION_PART = r"(?!ff)[a-f\d]{2}" 

152 TRACE_ID_PART = r"(?![0]{32})[a-f\d]{32}" 

153 PARENT_ID_PART = r"(?![0]{16})[a-f\d]{16}" 

154 FLAGS_PART = r"[a-f\d]{2}" 

155 regex = f"^\\s?({VERSION_PART})-({TRACE_ID_PART})-({PARENT_ID_PART})-({FLAGS_PART})(-.*)?\\s?$" 

156 match = re.match(regex, header) 

157 trace_id = match.group(2) 

158 span_id = match.group(3) 

159 # trace-flag component is an 8-bit bit field. Read as an int 

160 int_flag = int(match.group(4), 16) 

161 # trace sampled is set if the right-most bit in flag component is set 

162 trace_sampled = bool(int_flag & 1) 

163 except (IndexError, AttributeError): 

164 # could not parse header as expected. Return None 

165 pass 

166 return trace_id, span_id, trace_sampled 

167 

168 

169def _parse_xcloud_trace(header): 

170 """Given an X_CLOUD_TRACE header, extract the trace and span ids. 

171 

172 Args: 

173 header (str): the string extracted from the X_CLOUD_TRACE header 

174 Returns: 

175 Tuple[Optional[dict], Optional[str], bool]: 

176 The trace_id, span_id and trace_sampled extracted from the header 

177 Each field will be None if not found. 

178 """ 

179 trace_id = span_id = None 

180 trace_sampled = False 

181 # see https://cloud.google.com/trace/docs/setup for X-Cloud-Trace_Context format 

182 if header: 

183 try: 

184 regex = r"([\w-]+)?(\/?([\w-]+))?(;?o=(\d))?" 

185 match = re.match(regex, header) 

186 trace_id = match.group(1) 

187 span_id = match.group(3) 

188 trace_sampled = match.group(5) == "1" 

189 except IndexError: 

190 pass 

191 return trace_id, span_id, trace_sampled 

192 

193 

194def get_request_data(): 

195 """Helper to get http_request and trace data from supported web 

196 frameworks (currently supported: Flask and Django). 

197 

198 Returns: 

199 Tuple[Optional[dict], Optional[str], Optional[str], bool]: 

200 Data related to the current http request, trace_id, span_id, and trace_sampled 

201 for the request. All fields will be None if a http request isn't found. 

202 """ 

203 checkers = ( 

204 get_request_data_from_django, 

205 get_request_data_from_flask, 

206 ) 

207 

208 for checker in checkers: 

209 http_request, trace_id, span_id, trace_sampled = checker() 

210 if http_request is not None: 

211 return http_request, trace_id, span_id, trace_sampled 

212 

213 return None, None, None, False