Skip to content

Commit 04ee650

Browse files
feat: record source locations (#254)
1 parent 6a6b052 commit 04ee650

File tree

8 files changed

+129
-14
lines changed

8 files changed

+129
-14
lines changed

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ def __init__(
126126
self.project_id = client.project
127127
self.resource = resource
128128
self.labels = labels
129+
# add extra keys to log record
130+
self.addFilter(CloudLoggingFilter(self.project_id))
129131

130132
def emit(self, record):
131133
"""Actually log the specified logging record.
@@ -138,25 +140,31 @@ def emit(self, record):
138140
record (logging.LogRecord): The record to be logged.
139141
"""
140142
message = super(CloudLoggingHandler, self).format(record)
141-
trace_id = getattr(record, "trace", None)
142-
span_id = getattr(record, "span_id", None)
143-
http_request = getattr(record, "http_request", None)
144-
resource = getattr(record, "resource", self.resource)
145143
user_labels = getattr(record, "labels", {})
146144
# merge labels
147145
total_labels = self.labels if self.labels is not None else {}
148146
total_labels.update(user_labels)
149147
if len(total_labels) == 0:
150148
total_labels = None
149+
# create source location object
150+
if record.lineno and record.funcName and record.pathname:
151+
source_location = {
152+
"file": record.pathname,
153+
"line": str(record.lineno),
154+
"function": record.funcName,
155+
}
156+
else:
157+
source_location = None
151158
# send off request
152159
self.transport.send(
153160
record,
154161
message,
155-
resource=resource,
162+
resource=getattr(record, "resource", self.resource),
156163
labels=(total_labels if total_labels else None),
157-
trace=trace_id,
158-
span_id=span_id,
159-
http_request=http_request,
164+
trace=(record.trace if record.trace else None),
165+
span_id=getattr(record, "span_id", None),
166+
http_request=(record.http_request if record.http_request else None),
167+
source_location=source_location,
160168
)
161169

162170

packages/google-cloud-logging/tests/environment/deployable/python/snippets.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ def pylogging(log_text="pylogging", severity="WARNING", **kwargs):
5353
else:
5454
logging.critical(log_text)
5555

56+
def pylogging_flask(log_text="pylogging_flask", path="/", base_url="http://google", agent="Chrome", trace="123", **kwargs):
57+
import flask
58+
app = flask.Flask(__name__)
59+
with app.test_request_context(
60+
path, base_url, headers={'User-Agent': agent, "X_CLOUD_TRACE_CONTEXT": trace}):
61+
logging.info(log_text)
5662

5763
def print_handlers(**kwargs):
5864
root_logger = logging.getLogger()

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,14 @@ def _trigger(self, function, **kwargs):
7070
args_str = ",".join([f'{k}="{v}"' for k, v in kwargs.items()])
7171
self._script.run_command(Command.Trigger, [function, args_str])
7272

73-
@RetryErrors(exception=(LogsNotFound, RpcError), delay=2)
73+
@RetryErrors(exception=(LogsNotFound, RpcError), delay=2, max_tries=2)
7474
def trigger_and_retrieve(
7575
self, log_text, function="simplelog", append_uuid=True, max_tries=6, **kwargs
7676
):
7777
if append_uuid:
7878
log_text = f"{log_text} {uuid.uuid1()}"
7979
self._trigger(function, log_text=log_text, **kwargs)
80+
sleep(2)
8081
filter_str = self._add_time_condition_to_filter(log_text)
8182
# give the command time to be received
8283
tries = 0
@@ -86,7 +87,7 @@ def trigger_and_retrieve(
8687
log_list = self._get_logs(filter_str)
8788
return log_list
8889
except (LogsNotFound, RpcError) as e:
89-
sleep(10)
90+
sleep(5)
9091
tries += 1
9192
# log not found
9293
raise LogsNotFound

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,50 @@ def test_severity_pylogging(self):
6161
found_severity = log_list[-1].severity
6262

6363
self.assertEqual(found_severity.lower(), severity.lower())
64+
65+
def test_source_location_pylogging(self):
66+
if self.environment == "kubernetes" or "appengine" in self.environment:
67+
# disable these tests on environments with custom handlers
68+
# todo: enable in v3.0.0
69+
return
70+
log_text = f"{inspect.currentframe().f_code.co_name}"
71+
log_list = self.trigger_and_retrieve(log_text, function="pylogging")
72+
found_source = log_list[-1].source_location
73+
74+
self.assertIsNotNone(found_source)
75+
self.assertIsNotNone(found_source['file'])
76+
self.assertIsNotNone(found_source['function'])
77+
self.assertIsNotNone(found_source['line'])
78+
self.assertIn("snippets.py", found_source['file'])
79+
self.assertEqual(found_source['function'], "pylogging")
80+
self.assertTrue(int(found_source['line']) > 0)
81+
82+
def test_flask_http_request_pylogging(self):
83+
if self.environment == "kubernetes" or "appengine" in self.environment:
84+
# disable these tests on environments with custom handlers
85+
# todo: enable in v3.0.0
86+
return
87+
log_text = f"{inspect.currentframe().f_code.co_name}"
88+
89+
expected_agent = "test-agent"
90+
expected_base_url = "http://test"
91+
expected_path = "/pylogging"
92+
expected_trace = "123"
93+
94+
log_list = self.trigger_and_retrieve(log_text, function="pylogging_flask",
95+
path=expected_path, trace=expected_trace, base_url=expected_base_url, agent=expected_agent)
96+
found_request = log_list[-1].http_request
97+
98+
self.assertIsNotNone(found_request)
99+
self.assertIsNotNone(found_request['requestMethod'])
100+
self.assertIsNotNone(found_request['requestUrl'])
101+
self.assertIsNotNone(found_request['userAgent'])
102+
self.assertIsNotNone(found_request['protocol'])
103+
self.assertEqual(found_request['requestMethod'], 'GET')
104+
self.assertEqual(found_request['requestUrl'], expected_base_url + expected_path)
105+
self.assertEqual(found_request['userAgent'], expected_agent)
106+
self.assertEqual(found_request['protocol'], 'HTTP/1.1')
107+
108+
found_trace = log_list[-1].trace
109+
self.assertIsNotNone(found_trace)
110+
self.assertIn("projects/", found_trace)

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,24 @@ class TestCloudRun(Common, CommonPython, unittest.TestCase):
3737
"location",
3838
"configuration_name",
3939
]
40+
41+
def test_default_http_request_pylogging(self):
42+
"""
43+
Cloud Run should automatically attach http request information
44+
"""
45+
log_text = f"{inspect.currentframe().f_code.co_name}"
46+
47+
log_list = self.trigger_and_retrieve(log_text, function="pylogging")
48+
found_request = log_list[-1].http_request
49+
found_trace = log_list[-1].trace
50+
51+
self.assertIsNotNone(found_request)
52+
self.assertIsNotNone(found_request['requestMethod'])
53+
self.assertIsNotNone(found_request['requestUrl'])
54+
self.assertIsNotNone(found_request['userAgent'])
55+
self.assertIsNotNone(found_request['protocol'])
56+
self.assertEqual(found_request['requestMethod'], 'POST')
57+
self.assertEqual(found_request['protocol'], 'HTTP/1.1')
58+
59+
self.assertIsNotNone(found_trace)
60+
self.assertIn("projects/", found_trace)

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,31 @@
2121
from ..common.common import Common
2222
from ..common.python import CommonPython
2323

24-
2524
class TestCloudFunctions(Common, CommonPython, unittest.TestCase):
2625

2726
environment = "functions"
2827
language = "python"
2928

3029
monitored_resource_name = "cloud_function"
3130
monitored_resource_labels = ["region", "function_name"]
31+
32+
def test_default_http_request_pylogging(self):
33+
"""
34+
Cloud Functions should automatically attach http request information
35+
"""
36+
log_text = f"{inspect.currentframe().f_code.co_name}"
37+
38+
log_list = self.trigger_and_retrieve(log_text, function="pylogging")
39+
found_request = log_list[-1].http_request
40+
found_trace = log_list[-1].trace
41+
42+
self.assertIsNotNone(found_request)
43+
self.assertIsNotNone(found_request['requestMethod'])
44+
self.assertIsNotNone(found_request['requestUrl'])
45+
self.assertIsNotNone(found_request['userAgent'])
46+
self.assertIsNotNone(found_request['protocol'])
47+
self.assertEqual(found_request['requestMethod'], 'POST')
48+
self.assertEqual(found_request['protocol'], 'HTTP/1.1')
49+
50+
self.assertIsNotNone(found_trace)
51+
self.assertIn("projects/", found_trace)

packages/google-cloud-logging/tests/unit/handlers/test_handlers.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,11 +257,11 @@ def test_emit(self):
257257
logname = "loggername"
258258
message = "hello world"
259259
record = logging.LogRecord(logname, logging, None, None, message, None, None)
260+
handler.filter(record)
260261
handler.emit(record)
261-
262262
self.assertEqual(
263263
handler.transport.send_called_with,
264-
(record, message, _GLOBAL_RESOURCE, None, None, None, None),
264+
(record, message, _GLOBAL_RESOURCE, None, None, None, None, None),
265265
)
266266

267267
def test_emit_manual_field_override(self):
@@ -286,6 +286,15 @@ def test_emit_manual_field_override(self):
286286
setattr(record, "resource", expected_resource)
287287
expected_labels = {"test-label": "manual"}
288288
setattr(record, "labels", expected_labels)
289+
expected_source = {
290+
"file": "test-file",
291+
"line": str(1),
292+
"function": "test-func",
293+
}
294+
setattr(record, "lineno", int(expected_source["line"]))
295+
setattr(record, "funcName", expected_source["function"])
296+
setattr(record, "pathname", expected_source["file"])
297+
handler.filter(record)
289298
handler.emit(record)
290299

291300
self.assertEqual(
@@ -298,6 +307,7 @@ def test_emit_manual_field_override(self):
298307
expected_trace,
299308
expected_span,
300309
expected_http,
310+
expected_source,
301311
),
302312
)
303313

@@ -413,6 +423,7 @@ def send(
413423
trace=None,
414424
span_id=None,
415425
http_request=None,
426+
source_location=None,
416427
):
417428
self.send_called_with = (
418429
record,
@@ -422,4 +433,5 @@ def send(
422433
trace,
423434
span_id,
424435
http_request,
436+
source_location,
425437
)

tests/environment

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Subproject commit f0e2726579ef96f8e6b3ceaed8145d2bfbfa32bc
1+
Subproject commit 273db6c60b8f39fa5092b01730ff4d2dffcca17e

0 commit comments

Comments
 (0)