27
27
from google .cloud .logging_v2 .handlers .middleware .request import _get_django_request
28
28
29
29
_DJANGO_CONTENT_LENGTH = "CONTENT_LENGTH"
30
- _DJANGO_TRACE_HEADER = "HTTP_X_CLOUD_TRACE_CONTEXT"
30
+ _DJANGO_XCLOUD_TRACE_HEADER = "HTTP_X_CLOUD_TRACE_CONTEXT"
31
+ _DJANGO_TRACEPARENT = "HTTP_TRACEPARENT"
31
32
_DJANGO_USERAGENT_HEADER = "HTTP_USER_AGENT"
32
33
_DJANGO_REMOTE_ADDR_HEADER = "REMOTE_ADDR"
33
34
_DJANGO_REFERER_HEADER = "HTTP_REFERER"
34
- _FLASK_TRACE_HEADER = "X_CLOUD_TRACE_CONTEXT"
35
+ _FLASK_XCLOUD_TRACE_HEADER = "X_CLOUD_TRACE_CONTEXT"
36
+ _FLASK_TRACEPARENT = "TRACEPARENT"
35
37
_PROTOCOL_HEADER = "SERVER_PROTOCOL"
36
38
37
39
@@ -62,13 +64,12 @@ def get_request_data_from_flask():
62
64
"""Get http_request and trace data from flask request headers.
63
65
64
66
Returns:
65
- Tuple[Optional[dict], Optional[str], Optional[str]]:
66
- Data related to the current http request, trace_id, and span_id for
67
- the request. All fields will be None if a django request isn't
68
- found.
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.
69
70
"""
70
71
if flask is None or not flask .request :
71
- return None , None , None
72
+ return None , None , None , False
72
73
73
74
# build http_request
74
75
http_request = {
@@ -79,25 +80,29 @@ def get_request_data_from_flask():
79
80
}
80
81
81
82
# find trace id and span id
82
- header = flask .request .headers .get (_FLASK_TRACE_HEADER )
83
- trace_id , span_id = _parse_trace_span (header )
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 )
84
90
85
- return http_request , trace_id , span_id
91
+ return http_request , trace_id , span_id , trace_sampled
86
92
87
93
88
94
def get_request_data_from_django ():
89
95
"""Get http_request and trace data from django request headers.
90
96
91
97
Returns:
92
- Tuple[Optional[dict], Optional[str], Optional[str]]:
93
- Data related to the current http request, trace_id, and span_id for
94
- the request. All fields will be None if a django request isn't
95
- found.
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.
96
101
"""
97
102
request = _get_django_request ()
98
103
99
104
if request is None :
100
- return None , None , None
105
+ return None , None , None , False
101
106
102
107
# build http_request
103
108
http_request = {
@@ -108,54 +113,94 @@ def get_request_data_from_django():
108
113
}
109
114
110
115
# find trace id and span id
111
- header = request .META .get (_DJANGO_TRACE_HEADER )
112
- trace_id , span_id = _parse_trace_span (header )
116
+ # first check for w3c traceparent header
117
+ header = request .META .get (_DJANGO_TRACEPARENT )
118
+ trace_id , span_id , trace_sampled = _parse_trace_parent (header )
119
+ if trace_id is None :
120
+ # traceparent not found. look for xcloud_trace_context header
121
+ header = request .META .get (_DJANGO_XCLOUD_TRACE_HEADER )
122
+ trace_id , span_id , trace_sampled = _parse_xcloud_trace (header )
113
123
114
- return http_request , trace_id , span_id
124
+ return http_request , trace_id , span_id , trace_sampled
115
125
116
126
117
- def _parse_trace_span (header ):
127
+ def _parse_trace_parent (header ):
128
+ """Given a w3 traceparent header, extract the trace and span ids.
129
+ For more information see https://www.w3.org/TR/trace-context/
130
+
131
+ Args:
132
+ header (str): the string extracted from the traceparent header
133
+ example: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
134
+ Returns:
135
+ Tuple[Optional[dict], Optional[str], bool]:
136
+ The trace_id, span_id and trace_sampled extracted from the header
137
+ Each field will be None if header can't be parsed in expected format.
138
+ """
139
+ trace_id = span_id = None
140
+ trace_sampled = False
141
+ # see https://www.w3.org/TR/trace-context/ for W3C traceparent format
142
+ if header :
143
+ try :
144
+ VERSION_PART = r"(?!ff)[a-f\d]{2}"
145
+ TRACE_ID_PART = r"(?![0]{32})[a-f\d]{32}"
146
+ PARENT_ID_PART = r"(?![0]{16})[a-f\d]{16}"
147
+ FLAGS_PART = r"[a-f\d]{2}"
148
+ regex = f"^\\ s?({ VERSION_PART } )-({ TRACE_ID_PART } )-({ PARENT_ID_PART } )-({ FLAGS_PART } )(-.*)?\\ s?$"
149
+ match = re .match (regex , header )
150
+ trace_id = match .group (2 )
151
+ span_id = match .group (3 )
152
+ # trace-flag component is an 8-bit bit field. Read as an int
153
+ int_flag = int (match .group (4 ), 16 )
154
+ # trace sampled is set if the right-most bit in flag component is set
155
+ trace_sampled = bool (int_flag & 1 )
156
+ except (IndexError , AttributeError ):
157
+ # could not parse header as expected. Return None
158
+ pass
159
+ return trace_id , span_id , trace_sampled
160
+
161
+
162
+ def _parse_xcloud_trace (header ):
118
163
"""Given an X_CLOUD_TRACE header, extract the trace and span ids.
119
164
120
165
Args:
121
166
header (str): the string extracted from the X_CLOUD_TRACE header
122
167
Returns:
123
- Tuple[Optional[dict], Optional[str]]:
124
- The trace_id and span_id extracted from the header
168
+ Tuple[Optional[dict], Optional[str], bool ]:
169
+ The trace_id, span_id and trace_sampled extracted from the header
125
170
Each field will be None if not found.
126
171
"""
127
- trace_id = None
128
- span_id = None
172
+ trace_id = span_id = None
173
+ trace_sampled = False
174
+ # see https://cloud.google.com/trace/docs/setup for X-Cloud-Trace_Context format
129
175
if header :
130
176
try :
131
- split_header = header . split ( "/" , 1 )
132
- trace_id = split_header [ 0 ]
133
- header_suffix = split_header [ 1 ]
134
- # the span is the set of alphanumeric characters after the /
135
- span_id = re . findall ( r"^\w+" , header_suffix )[ 0 ]
177
+ regex = r"([\w-]+)?(\/?([\w-]+))?(;?o=(\d))?"
178
+ match = re . match ( regex , header )
179
+ trace_id = match . group ( 1 )
180
+ span_id = match . group ( 3 )
181
+ trace_sampled = match . group ( 5 ) == "1"
136
182
except IndexError :
137
183
pass
138
- return trace_id , span_id
184
+ return trace_id , span_id , trace_sampled
139
185
140
186
141
187
def get_request_data ():
142
188
"""Helper to get http_request and trace data from supported web
143
189
frameworks (currently supported: Flask and Django).
144
190
145
191
Returns:
146
- Tuple[Optional[dict], Optional[str], Optional[str]]:
147
- Data related to the current http request, trace_id, and span_id for
148
- the request. All fields will be None if a django request isn't
149
- found.
192
+ Tuple[Optional[dict], Optional[str], Optional[str], bool]:
193
+ Data related to the current http request, trace_id, span_id, and trace_sampled
194
+ for the request. All fields will be None if a http request isn't found.
150
195
"""
151
196
checkers = (
152
197
get_request_data_from_django ,
153
198
get_request_data_from_flask ,
154
199
)
155
200
156
201
for checker in checkers :
157
- http_request , trace_id , span_id = checker ()
202
+ http_request , trace_id , span_id , trace_sampled = checker ()
158
203
if http_request is not None :
159
- return http_request , trace_id , span_id
204
+ return http_request , trace_id , span_id , trace_sampled
160
205
161
- return None , None , None
206
+ return None , None , None , False
0 commit comments