Skip to content

Commit 1f9517d

Browse files
committed
fix: improve API compatibility for next release (#292)
1 parent bdf8273 commit 1f9517d

File tree

7 files changed

+69
-38
lines changed

7 files changed

+69
-38
lines changed

.gitignore

-3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,3 @@ system_tests/local_test_setup
6161
# Make sure a generated file isn't accidentally committed.
6262
pylintrc
6363
pylintrc.test
64-
65-
# ignore owlbot
66-
owl-bot-staging

google/cloud/logging_v2/handlers/_helpers.py

+13
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ def get_request_data_from_flask():
6868
http_request = {
6969
"requestMethod": flask.request.method,
7070
"requestUrl": flask.request.url,
71+
"requestSize": flask.request.content_length,
7172
"userAgent": flask.request.user_agent.string,
73+
"remoteIp": flask.request.remote_addr,
74+
"referer": flask.request.referrer,
7275
"protocol": flask.request.environ.get(_PROTOCOL_HEADER),
7376
}
7477

@@ -93,11 +96,21 @@ def get_request_data_from_django():
9396
if request is None:
9497
return None, None, None
9598

99+
# convert content_length to int if it exists
100+
content_length = None
101+
try:
102+
content_length = int(request.META.get(_DJANGO_CONTENT_LENGTH))
103+
except (ValueError, TypeError):
104+
content_length = None
105+
96106
# build http_request
97107
http_request = {
98108
"requestMethod": request.method,
99109
"requestUrl": request.build_absolute_uri(),
110+
"requestSize": content_length,
100111
"userAgent": request.META.get(_DJANGO_USERAGENT_HEADER),
112+
"remoteIp": request.META.get(_DJANGO_REMOTE_ADDR_HEADER),
113+
"referer": request.META.get(_DJANGO_REFERER_HEADER),
101114
"protocol": request.META.get(_PROTOCOL_HEADER),
102115
}
103116

google/cloud/logging_v2/handlers/handlers.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ class CloudLoggingFilter(logging.Filter):
4545
overwritten using the `extras` argument when writing logs.
4646
"""
4747

48+
# The subset of http_request fields have been tested to work consistently across GCP environments
49+
# https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#httprequest
50+
_supported_http_fields = ("requestMethod", "requestUrl", "userAgent", "protocol")
51+
4852
def __init__(self, project=None, default_labels=None):
4953
self.project = project
5054
self.default_labels = default_labels if default_labels else {}
@@ -74,8 +78,17 @@ def filter(self, record):
7478
Add new Cloud Logging data to each LogRecord as it comes in
7579
"""
7680
user_labels = getattr(record, "labels", {})
81+
# infer request data from the environment
7782
inferred_http, inferred_trace, inferred_span = get_request_data()
83+
if inferred_http is not None:
84+
# filter inferred_http to include only well-supported fields
85+
inferred_http = {
86+
k: v
87+
for (k, v) in inferred_http.items()
88+
if k in self._supported_http_fields and v is not None
89+
}
7890
if inferred_trace is not None and self.project is not None:
91+
# add full path for detected trace
7992
inferred_trace = f"projects/{self.project}/traces/{inferred_trace}"
8093
# set new record values
8194
record._resource = getattr(record, "resource", None)
@@ -84,13 +97,14 @@ def filter(self, record):
8497
record._http_request = getattr(record, "http_request", inferred_http)
8598
record._source_location = CloudLoggingFilter._infer_source_location(record)
8699
record._labels = {**self.default_labels, **user_labels} or None
87-
# create guaranteed string representations for structured logging
88-
record._msg_str = record.msg or ""
100+
# create string representations for structured logging
89101
record._trace_str = record._trace or ""
90102
record._span_id_str = record._span_id or ""
91103
record._http_request_str = json.dumps(record._http_request or {})
92104
record._source_location_str = json.dumps(record._source_location or {})
93105
record._labels_str = json.dumps(record._labels or {})
106+
# break quotes for parsing through structured logging
107+
record._msg_str = str(record.msg).replace('"', '\\"') if record.msg else ""
94108
return True
95109

96110

tests/unit/handlers/test__helpers.py

+6-12
Original file line numberDiff line numberDiff line change
@@ -75,34 +75,28 @@ def test_http_request_populated(self):
7575
expected_agent = "Mozilla/5.0"
7676
expected_referrer = "self"
7777
expected_ip = "10.1.2.3"
78-
body_content = "test"
7978
headers = {
8079
"User-Agent": expected_agent,
8180
"Referer": expected_referrer,
8281
}
8382

8483
app = self.create_app()
85-
with app.test_client() as c:
86-
c.put(
87-
path=expected_path,
88-
data=body_content,
89-
environ_base={"REMOTE_ADDR": expected_ip},
90-
headers=headers,
91-
)
84+
with app.test_request_context(
85+
expected_path, headers=headers, environ_base={"REMOTE_ADDR": expected_ip}
86+
):
9287
http_request, *_ = self._call_fut()
9388

94-
self.assertEqual(http_request["requestMethod"], "PUT")
89+
self.assertEqual(http_request["requestMethod"], "GET")
9590
self.assertEqual(http_request["requestUrl"], expected_path)
9691
self.assertEqual(http_request["userAgent"], expected_agent)
9792
self.assertEqual(http_request["protocol"], "HTTP/1.1")
9893

9994
def test_http_request_sparse(self):
10095
expected_path = "http://testserver/123"
10196
app = self.create_app()
102-
with app.test_client() as c:
103-
c.put(path=expected_path)
97+
with app.test_request_context(expected_path):
10498
http_request, *_ = self._call_fut()
105-
self.assertEqual(http_request["requestMethod"], "PUT")
99+
self.assertEqual(http_request["requestMethod"], "GET")
106100
self.assertEqual(http_request["requestUrl"], expected_path)
107101
self.assertEqual(http_request["protocol"], "HTTP/1.1")
108102

tests/unit/handlers/test_handlers.py

+8-10
Original file line numberDiff line numberDiff line change
@@ -134,22 +134,20 @@ def test_record_with_request(self):
134134
expected_span = "456"
135135
combined_trace = f"{expected_trace}/{expected_span}"
136136
expected_request = {
137-
"requestMethod": "PUT",
137+
"requestMethod": "GET",
138138
"requestUrl": expected_path,
139139
"userAgent": expected_agent,
140140
"protocol": "HTTP/1.1",
141141
}
142142

143143
app = self.create_app()
144-
with app.test_client() as c:
145-
c.put(
146-
path=expected_path,
147-
data="body",
148-
headers={
149-
"User-Agent": expected_agent,
150-
"X_CLOUD_TRACE_CONTEXT": combined_trace,
151-
},
152-
)
144+
with app.test_request_context(
145+
expected_path,
146+
headers={
147+
"User-Agent": expected_agent,
148+
"X_CLOUD_TRACE_CONTEXT": combined_trace,
149+
},
150+
):
153151
success = filter_obj.filter(record)
154152
self.assertTrue(success)
155153

tests/unit/handlers/test_structured_log.py

+25-10
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,23 @@ def test_format_minimal(self):
104104
value, result[key], f"expected_payload[{key}] != result[{key}]"
105105
)
106106

107+
def test_format_with_quotes(self):
108+
"""
109+
When logging a message containing quotes, escape chars should be added
110+
"""
111+
import logging
112+
import json
113+
114+
handler = self._make_one()
115+
message = '"test"'
116+
expected_result = '\\"test\\"'
117+
record = logging.LogRecord(None, logging.INFO, None, None, message, None, None,)
118+
record.created = None
119+
handler.filter(record)
120+
result = json.loads(handler.format(record))
121+
result["message"] = expected_result
122+
self.assertEqual(result["message"], expected_result)
123+
107124
def test_format_with_request(self):
108125
import logging
109126
import json
@@ -121,23 +138,21 @@ def test_format_with_request(self):
121138
"logging.googleapis.com/trace": expected_trace,
122139
"logging.googleapis.com/spanId": expected_span,
123140
"httpRequest": {
124-
"requestMethod": "PUT",
141+
"requestMethod": "GET",
125142
"requestUrl": expected_path,
126143
"userAgent": expected_agent,
127144
"protocol": "HTTP/1.1",
128145
},
129146
}
130147

131148
app = self.create_app()
132-
with app.test_client() as c:
133-
c.put(
134-
path=expected_path,
135-
data="body",
136-
headers={
137-
"User-Agent": expected_agent,
138-
"X_CLOUD_TRACE_CONTEXT": trace_header,
139-
},
140-
)
149+
with app.test_request_context(
150+
expected_path,
151+
headers={
152+
"User-Agent": expected_agent,
153+
"X_CLOUD_TRACE_CONTEXT": trace_header,
154+
},
155+
):
141156
handler.filter(record)
142157
result = json.loads(handler.format(record))
143158
for (key, value) in expected_payload.items():

0 commit comments

Comments
 (0)