Skip to content

Commit 62606e9

Browse files
feat: support span inference (#267)
1 parent e65bdf6 commit 62606e9

File tree

10 files changed

+207
-63
lines changed

10 files changed

+207
-63
lines changed

packages/google-cloud-logging/google/cloud/logging_v2/handlers/_helpers.py

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import math
1818
import json
19+
import re
1920

2021
try:
2122
import flask
@@ -55,12 +56,13 @@ def get_request_data_from_flask():
5556
"""Get http_request and trace data from flask request headers.
5657
5758
Returns:
58-
Tuple[Optional[dict], Optional[str]]:
59-
Data related to the current http request and the trace_id for the
60-
request. Both fields will be None if a flask request isn't found.
59+
Tuple[Optional[dict], Optional[str], Optional[str]]:
60+
Data related to the current http request, trace_id, and span_id for
61+
the request. All fields will be None if a django request isn't
62+
found.
6163
"""
6264
if flask is None or not flask.request:
63-
return None, None
65+
return None, None, None
6466

6567
# build http_request
6668
http_request = {
@@ -73,27 +75,26 @@ def get_request_data_from_flask():
7375
"protocol": flask.request.environ.get(_PROTOCOL_HEADER),
7476
}
7577

76-
# find trace id
77-
trace_id = None
78+
# find trace id and span id
7879
header = flask.request.headers.get(_FLASK_TRACE_HEADER)
79-
if header:
80-
trace_id = header.split("/", 1)[0]
80+
trace_id, span_id = _parse_trace_span(header)
8181

82-
return http_request, trace_id
82+
return http_request, trace_id, span_id
8383

8484

8585
def get_request_data_from_django():
8686
"""Get http_request and trace data from django request headers.
8787
8888
Returns:
89-
Tuple[Optional[dict], Optional[str]]:
90-
Data related to the current http request and the trace_id for the
91-
request. Both fields will be None if a django request isn't found.
89+
Tuple[Optional[dict], Optional[str], Optional[str]]:
90+
Data related to the current http request, trace_id, and span_id for
91+
the request. All fields will be None if a django request isn't
92+
found.
9293
"""
9394
request = _get_django_request()
9495

9596
if request is None:
96-
return None, None
97+
return None, None, None
9798

9899
# convert content_length to int if it exists
99100
content_length = None
@@ -112,32 +113,55 @@ def get_request_data_from_django():
112113
"protocol": request.META.get(_PROTOCOL_HEADER),
113114
}
114115

115-
# find trace id
116-
trace_id = None
116+
# find trace id and span id
117117
header = request.META.get(_DJANGO_TRACE_HEADER)
118-
if header:
119-
trace_id = header.split("/", 1)[0]
118+
trace_id, span_id = _parse_trace_span(header)
120119

121-
return http_request, trace_id
120+
return http_request, trace_id, span_id
121+
122+
123+
def _parse_trace_span(header):
124+
"""Given an X_CLOUD_TRACE header, extract the trace and span ids.
125+
126+
Args:
127+
header (str): the string extracted from the X_CLOUD_TRACE header
128+
Returns:
129+
Tuple[Optional[dict], Optional[str]]:
130+
The trace_id and span_id extracted from the header
131+
Each field will be None if not found.
132+
"""
133+
trace_id = None
134+
span_id = None
135+
if header:
136+
try:
137+
split_header = header.split("/", 1)
138+
trace_id = split_header[0]
139+
header_suffix = split_header[1]
140+
# the span is the set of alphanumeric characters after the /
141+
span_id = re.findall(r"^\w+", header_suffix)[0]
142+
except IndexError:
143+
pass
144+
return trace_id, span_id
122145

123146

124147
def get_request_data():
125148
"""Helper to get http_request and trace data from supported web
126149
frameworks (currently supported: Flask and Django).
127150
128151
Returns:
129-
Tuple[Optional[dict], Optional[str]]:
130-
Data related to the current http request and the trace_id for the
131-
request. Both fields will be None if a supported web request isn't found.
152+
Tuple[Optional[dict], Optional[str], Optional[str]]:
153+
Data related to the current http request, trace_id, and span_id for
154+
the request. All fields will be None if a django request isn't
155+
found.
132156
"""
133157
checkers = (
134158
get_request_data_from_django,
135159
get_request_data_from_flask,
136160
)
137161

138162
for checker in checkers:
139-
http_request, trace_id = checker()
163+
http_request, trace_id, span_id = checker()
140164
if http_request is not None:
141-
return http_request, trace_id
165+
return http_request, trace_id, span_id
142166

143-
return None, None
167+
return None, None, None

packages/google-cloud-logging/google/cloud/logging_v2/handlers/app_engine.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def get_gae_labels(self):
9090
"""
9191
gae_labels = {}
9292

93-
_, trace_id = get_request_data()
93+
_, trace_id, _ = get_request_data()
9494
if trace_id is not None:
9595
gae_labels[_TRACE_ID_LABEL] = trace_id
9696

@@ -107,7 +107,7 @@ def emit(self, record):
107107
record (logging.LogRecord): The record to be logged.
108108
"""
109109
message = super(AppEngineHandler, self).format(record)
110-
inferred_http, inferred_trace = get_request_data()
110+
inferred_http, inferred_trace, _ = get_request_data()
111111
if inferred_trace is not None:
112112
inferred_trace = f"projects/{self.project_id}/traces/{inferred_trace}"
113113
# allow user overrides

packages/google-cloud-logging/google/cloud/logging_v2/handlers/handlers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def filter(self, record):
5959
}
6060
record.msg = "" if record.msg is None else record.msg
6161
# find http request data
62-
inferred_http, inferred_trace = get_request_data()
62+
inferred_http, inferred_trace, inferred_span = get_request_data()
6363
if inferred_trace is not None and self.project is not None:
6464
inferred_trace = f"projects/{self.project}/traces/{inferred_trace}"
6565
# set labels
@@ -70,6 +70,7 @@ def filter(self, record):
7070
)
7171

7272
record.trace = getattr(record, "trace", inferred_trace) or ""
73+
record.span_id = getattr(record, "span_id", inferred_span) or ""
7374
record.http_request = getattr(record, "http_request", inferred_http) or {}
7475
record.request_method = record.http_request.get("requestMethod", "")
7576
record.request_url = record.http_request.get("requestUrl", "")

packages/google-cloud-logging/google/cloud/logging_v2/handlers/structured_log.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
'"severity": "%(levelname)s", '
2525
'"logging.googleapis.com/labels": { %(total_labels_str)s }, '
2626
'"logging.googleapis.com/trace": "%(trace)s", '
27+
'"logging.googleapis.com/spanId": "%(span_id)s", '
2728
'"logging.googleapis.com/sourceLocation": { "file": "%(file)s", "line": "%(line)d", "function": "%(function)s"}, '
2829
'"httpRequest": {"requestMethod": "%(request_method)s", "requestUrl": "%(request_url)s", "userAgent": "%(user_agent)s", "protocol": "%(protocol)s"} }'
2930
)

packages/google-cloud-logging/tests/environment/tests/common/common.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,21 @@ def test_receive_log(self):
137137
found_log = log
138138
self.assertIsNotNone(found_log, "expected log text not found")
139139

140+
def test_receive_unicode_log(self):
141+
log_text = f"{inspect.currentframe().f_code.co_name} 嗨 世界 😀"
142+
log_list = self.trigger_and_retrieve(log_text, "simplelog")
143+
144+
found_log = None
145+
for log in log_list:
146+
message = (
147+
log.payload.get("message", None)
148+
if isinstance(log.payload, dict)
149+
else str(log.payload)
150+
)
151+
if message and log_text in message:
152+
found_log = log
153+
self.assertIsNotNone(found_log, "expected unicode log not found")
154+
140155
# add back after v3.0.0
141156
# def test_monitored_resource(self):
142157
# if self.language != "python":

packages/google-cloud-logging/tests/environment/tests/common/python.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323

2424
class CommonPython:
25-
def pylogging_test_receive_log(self):
25+
def test_pylogging_receive_log(self):
2626
log_text = f"{inspect.currentframe().f_code.co_name}"
2727
log_list = self.trigger_and_retrieve(log_text, "pylogging")
2828

@@ -37,6 +37,21 @@ def pylogging_test_receive_log(self):
3737
found_log = log
3838
self.assertIsNotNone(found_log, "expected log text not found")
3939

40+
def test_pylogging_receive_unicode_log(self):
41+
log_text = f"{inspect.currentframe().f_code.co_name} 嗨 世界 😀"
42+
log_list = self.trigger_and_retrieve(log_text, "pylogging")
43+
44+
found_log = None
45+
for log in log_list:
46+
message = (
47+
log.payload.get("message", None)
48+
if isinstance(log.payload, dict)
49+
else str(log.payload)
50+
)
51+
if message and log_text in message:
52+
found_log = log
53+
self.assertIsNotNone(found_log, "expected unicode log not found")
54+
4055
def test_monitored_resource_pylogging(self):
4156
log_text = f"{inspect.currentframe().f_code.co_name}"
4257
log_list = self.trigger_and_retrieve(log_text, "pylogging")
@@ -90,12 +105,14 @@ def test_flask_http_request_pylogging(self):
90105
expected_base_url = "http://test"
91106
expected_path = "/pylogging"
92107
expected_trace = "123"
108+
expected_span = "456"
109+
trace_header = f"{expected_trace}/{expected_span};o=1"
93110

94111
log_list = self.trigger_and_retrieve(
95112
log_text,
96113
"pylogging_flask",
97114
path=expected_path,
98-
trace=expected_trace,
115+
trace=trace_header,
99116
base_url=expected_base_url,
100117
agent=expected_agent,
101118
)
@@ -112,8 +129,13 @@ def test_flask_http_request_pylogging(self):
112129
self.assertEqual(found_request["protocol"], "HTTP/1.1")
113130

114131
found_trace = log_list[-1].trace
132+
found_span = log_list[-1].span_id
115133
self.assertIsNotNone(found_trace)
116134
self.assertIn("projects/", found_trace)
135+
if self.environment != "functions":
136+
# functions seems to override the user's trace value
137+
self.assertIn(expected_trace, found_trace)
138+
self.assertEqual(expected_span, found_span)
117139

118140
def test_pylogging_extras(self):
119141
if self.environment == "kubernetes" or "appengine" in self.environment:
@@ -123,6 +145,7 @@ def test_pylogging_extras(self):
123145
log_text = f"{inspect.currentframe().f_code.co_name}"
124146
kwargs = {
125147
"trace": "123",
148+
"span_id": "456",
126149
"requestMethod": "POST",
127150
"requestUrl": "http://test",
128151
"userAgent": "agent",
@@ -138,6 +161,7 @@ def test_pylogging_extras(self):
138161
if self.environment != "functions":
139162
# functions seems to override the user's trace value
140163
self.assertEqual(found_log.trace, kwargs["trace"])
164+
self.assertEqual(found_log.span_id, kwargs["span_id"])
141165

142166
# check that custom http request fields were set
143167
self.assertIsNotNone(found_log.http_request)

0 commit comments

Comments
 (0)