Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 08d35bb

Browse filesBrowse files
feat: trace improvements (#450)
1 parent 898b5fe commit 08d35bb
Copy full SHA for 08d35bb

File tree

Expand file treeCollapse file tree

10 files changed

+380
-102
lines changed
Filter options
Expand file treeCollapse file tree

10 files changed

+380
-102
lines changed

‎google/cloud/logging_v2/handlers/_helpers.py

Copy file name to clipboardExpand all lines: google/cloud/logging_v2/handlers/_helpers.py
+81-36Lines changed: 81 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
from google.cloud.logging_v2.handlers.middleware.request import _get_django_request
2828

2929
_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"
3132
_DJANGO_USERAGENT_HEADER = "HTTP_USER_AGENT"
3233
_DJANGO_REMOTE_ADDR_HEADER = "REMOTE_ADDR"
3334
_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"
3537
_PROTOCOL_HEADER = "SERVER_PROTOCOL"
3638

3739

@@ -62,13 +64,12 @@ def get_request_data_from_flask():
6264
"""Get http_request and trace data from flask request headers.
6365
6466
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.
6970
"""
7071
if flask is None or not flask.request:
71-
return None, None, None
72+
return None, None, None, False
7273

7374
# build http_request
7475
http_request = {
@@ -79,25 +80,29 @@ def get_request_data_from_flask():
7980
}
8081

8182
# 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)
8490

85-
return http_request, trace_id, span_id
91+
return http_request, trace_id, span_id, trace_sampled
8692

8793

8894
def get_request_data_from_django():
8995
"""Get http_request and trace data from django request headers.
9096
9197
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.
96101
"""
97102
request = _get_django_request()
98103

99104
if request is None:
100-
return None, None, None
105+
return None, None, None, False
101106

102107
# build http_request
103108
http_request = {
@@ -108,54 +113,94 @@ def get_request_data_from_django():
108113
}
109114

110115
# 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)
113123

114-
return http_request, trace_id, span_id
124+
return http_request, trace_id, span_id, trace_sampled
115125

116126

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):
118163
"""Given an X_CLOUD_TRACE header, extract the trace and span ids.
119164
120165
Args:
121166
header (str): the string extracted from the X_CLOUD_TRACE header
122167
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
125170
Each field will be None if not found.
126171
"""
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
129175
if header:
130176
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"
136182
except IndexError:
137183
pass
138-
return trace_id, span_id
184+
return trace_id, span_id, trace_sampled
139185

140186

141187
def get_request_data():
142188
"""Helper to get http_request and trace data from supported web
143189
frameworks (currently supported: Flask and Django).
144190
145191
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.
150195
"""
151196
checkers = (
152197
get_request_data_from_django,
153198
get_request_data_from_flask,
154199
)
155200

156201
for checker in checkers:
157-
http_request, trace_id, span_id = checker()
202+
http_request, trace_id, span_id, trace_sampled = checker()
158203
if http_request is not None:
159-
return http_request, trace_id, span_id
204+
return http_request, trace_id, span_id, trace_sampled
160205

161-
return None, None, None
206+
return None, None, None, False

‎google/cloud/logging_v2/handlers/app_engine.py

Copy file name to clipboardExpand all lines: google/cloud/logging_v2/handlers/app_engine.py
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def get_gae_labels(self):
9898
"""
9999
gae_labels = {}
100100

101-
_, trace_id, _ = get_request_data()
101+
_, trace_id, _, _ = get_request_data()
102102
if trace_id is not None:
103103
gae_labels[_TRACE_ID_LABEL] = trace_id
104104

@@ -115,7 +115,7 @@ def emit(self, record):
115115
record (logging.LogRecord): The record to be logged.
116116
"""
117117
message = super(AppEngineHandler, self).format(record)
118-
inferred_http, inferred_trace, _ = get_request_data()
118+
inferred_http, inferred_trace, _, _ = get_request_data()
119119
if inferred_trace is not None:
120120
inferred_trace = f"projects/{self.project_id}/traces/{inferred_trace}"
121121
# allow user overrides

‎google/cloud/logging_v2/handlers/handlers.py

Copy file name to clipboardExpand all lines: google/cloud/logging_v2/handlers/handlers.py
+9-1Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,20 @@ def filter(self, record):
8282
"""
8383
user_labels = getattr(record, "labels", {})
8484
# infer request data from the environment
85-
inferred_http, inferred_trace, inferred_span = get_request_data()
85+
(
86+
inferred_http,
87+
inferred_trace,
88+
inferred_span,
89+
inferred_sampled,
90+
) = get_request_data()
8691
if inferred_trace is not None and self.project is not None:
8792
# add full path for detected trace
8893
inferred_trace = f"projects/{self.project}/traces/{inferred_trace}"
8994
# set new record values
9095
record._resource = getattr(record, "resource", None)
9196
record._trace = getattr(record, "trace", inferred_trace) or None
9297
record._span_id = getattr(record, "span_id", inferred_span) or None
98+
record._trace_sampled = bool(getattr(record, "trace_sampled", inferred_sampled))
9399
record._http_request = getattr(record, "http_request", inferred_http)
94100
record._source_location = CloudLoggingFilter._infer_source_location(record)
95101
# add logger name as a label if possible
@@ -98,6 +104,7 @@ def filter(self, record):
98104
# create string representations for structured logging
99105
record._trace_str = record._trace or ""
100106
record._span_id_str = record._span_id or ""
107+
record._trace_sampled_str = "true" if record._trace_sampled else "false"
101108
record._http_request_str = json.dumps(
102109
record._http_request or {}, ensure_ascii=False
103110
)
@@ -205,6 +212,7 @@ def emit(self, record):
205212
labels=labels,
206213
trace=record._trace,
207214
span_id=record._span_id,
215+
trace_sampled=record._trace_sampled,
208216
http_request=record._http_request,
209217
source_location=record._source_location,
210218
)

‎google/cloud/logging_v2/handlers/structured_log.py

Copy file name to clipboardExpand all lines: google/cloud/logging_v2/handlers/structured_log.py
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
'"logging.googleapis.com/labels": %(_labels_str)s, '
2828
'"logging.googleapis.com/trace": "%(_trace_str)s", '
2929
'"logging.googleapis.com/spanId": "%(_span_id_str)s", '
30+
'"logging.googleapis.com/trace_sampled": %(_trace_sampled_str)s, '
3031
'"logging.googleapis.com/sourceLocation": %(_source_location_str)s, '
3132
'"httpRequest": %(_http_request_str)s '
3233
"}"

‎tests/system/test_system.py

Copy file name to clipboardExpand all lines: tests/system/test_system.py
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ def test_log_empty(self):
454454

455455
self.assertEqual(len(entries), 1)
456456
self.assertIsNone(entries[0].payload)
457+
self.assertFalse(entries[0].trace_sampled)
457458

458459
def test_log_struct_logentry_data(self):
459460
logger = Config.CLIENT.logger(self._logger_name("log_w_struct"))
@@ -473,6 +474,7 @@ def test_log_struct_logentry_data(self):
473474
self.assertEqual(entries[0].severity, "WARNING")
474475
self.assertEqual(entries[0].trace, JSON_PAYLOAD["trace"])
475476
self.assertEqual(entries[0].span_id, JSON_PAYLOAD["span_id"])
477+
self.assertFalse(entries[0].trace_sampled)
476478

477479
def test_log_handler_async(self):
478480
LOG_MESSAGE = "It was the worst of times"
@@ -534,6 +536,7 @@ def test_handlers_w_extras(self):
534536
extra = {
535537
"trace": "123",
536538
"span_id": "456",
539+
"trace_sampled": True,
537540
"http_request": expected_request,
538541
"source_location": expected_source,
539542
"resource": Resource(type="cloudiot_device", labels={}),
@@ -545,6 +548,7 @@ def test_handlers_w_extras(self):
545548
self.assertEqual(len(entries), 1)
546549
self.assertEqual(entries[0].trace, extra["trace"])
547550
self.assertEqual(entries[0].span_id, extra["span_id"])
551+
self.assertTrue(entries[0].trace_sampled)
548552
self.assertEqual(entries[0].http_request, expected_request)
549553
self.assertEqual(
550554
entries[0].labels, {**extra["labels"], "python_logger": LOGGER_NAME}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.