Skip to content

Commit fe4de39

Browse files
feat: allow custom labels with standard library logging (#264)
1 parent 6b10b74 commit fe4de39

File tree

6 files changed

+71
-38
lines changed

6 files changed

+71
-38
lines changed

google/cloud/logging_v2/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,9 +362,9 @@ def get_default_handler(self, **kw):
362362
):
363363
# Cloud Functions with runtimes > 3.8 supports structured logs on standard out
364364
# 3.7 should use the standard CloudLoggingHandler, which sends logs over the network.
365-
return StructuredLogHandler(**kw, project=self.project)
365+
return StructuredLogHandler(**kw, project_id=self.project)
366366
elif monitored_resource.type == _RUN_RESOURCE_TYPE:
367-
return StructuredLogHandler(**kw, project=self.project)
367+
return StructuredLogHandler(**kw, project_id=self.project)
368368
return CloudLoggingHandler(self, resource=monitored_resource, **kw)
369369

370370
def setup_logging(

google/cloud/logging_v2/handlers/handlers.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ class CloudLoggingFilter(logging.Filter):
3737
the `extras` argument when writing logs.
3838
"""
3939

40-
def __init__(self, project=None):
40+
def __init__(self, project=None, default_labels=None):
4141
self.project = project
42+
self.default_labels = default_labels if default_labels else {}
4243

4344
def filter(self, record):
4445
# ensure record has all required fields set
@@ -61,6 +62,12 @@ def filter(self, record):
6162
inferred_http, inferred_trace = get_request_data()
6263
if inferred_trace is not None and self.project is not None:
6364
inferred_trace = f"projects/{self.project}/traces/{inferred_trace}"
65+
# set labels
66+
user_labels = getattr(record, "labels", {})
67+
record.total_labels = {**self.default_labels, **user_labels}
68+
record.total_labels_str = ", ".join(
69+
[f'"{k}": "{v}"' for k, v in record.total_labels.items()]
70+
)
6471

6572
record.trace = getattr(record, "trace", inferred_trace) or ""
6673
record.http_request = getattr(record, "http_request", inferred_http) or {}
@@ -126,8 +133,7 @@ def __init__(
126133
option is :class:`.SyncTransport`.
127134
resource (~logging_v2.resource.Resource):
128135
Resource for this Handler. Defaults to ``global``.
129-
labels (Optional[dict]): Monitored resource of the entry, defaults
130-
to the global resource type.
136+
labels (Optional[dict]): Additional labels to attach to logs.
131137
stream (Optional[IO]): Stream to be used by the handler.
132138
"""
133139
super(CloudLoggingHandler, self).__init__(stream)
@@ -138,7 +144,8 @@ def __init__(
138144
self.resource = resource
139145
self.labels = labels
140146
# add extra keys to log record
141-
self.addFilter(CloudLoggingFilter(self.project_id))
147+
log_filter = CloudLoggingFilter(project=self.project_id, default_labels=labels)
148+
self.addFilter(log_filter)
142149

143150
def emit(self, record):
144151
"""Actually log the specified logging record.
@@ -151,22 +158,16 @@ def emit(self, record):
151158
record (logging.LogRecord): The record to be logged.
152159
"""
153160
message = super(CloudLoggingHandler, self).format(record)
154-
user_labels = getattr(record, "labels", {})
155-
# merge labels
156-
total_labels = self.labels if self.labels is not None else {}
157-
total_labels.update(user_labels)
158-
if len(total_labels) == 0:
159-
total_labels = None
160161
# send off request
161162
self.transport.send(
162163
record,
163164
message,
164165
resource=getattr(record, "resource", self.resource),
165-
labels=total_labels,
166-
trace=getattr(record, "trace", None),
167-
span_id=getattr(record, "span_id", None),
168-
http_request=getattr(record, "http_request", None),
169-
source_location=getattr(record, "source_location", None),
166+
labels=getattr(record, "total_labels", None) or None,
167+
trace=getattr(record, "trace", None) or None,
168+
span_id=getattr(record, "span_id", None) or None,
169+
http_request=getattr(record, "http_request", None) or None,
170+
source_location=getattr(record, "source_location", None) or None,
170171
)
171172

172173

google/cloud/logging_v2/handlers/structured_log.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,34 @@
1919

2020
from google.cloud.logging_v2.handlers.handlers import CloudLoggingFilter
2121

22-
GCP_FORMAT = '{"message": "%(message)s", "severity": "%(levelname)s", "logging.googleapis.com/trace": "%(trace)s", "logging.googleapis.com/sourceLocation": { "file": "%(file)s", "line": "%(line)d", "function": "%(function)s"}, "httpRequest": {"requestMethod": "%(request_method)s", "requestUrl": "%(request_url)s", "userAgent": "%(user_agent)s", "protocol": "%(protocol)s"} }'
22+
GCP_FORMAT = (
23+
'{"message": "%(message)s", '
24+
'"severity": "%(levelname)s", '
25+
'"logging.googleapis.com/labels": { %(total_labels_str)s }, '
26+
'"logging.googleapis.com/trace": "%(trace)s", '
27+
'"logging.googleapis.com/sourceLocation": { "file": "%(file)s", "line": "%(line)d", "function": "%(function)s"}, '
28+
'"httpRequest": {"requestMethod": "%(request_method)s", "requestUrl": "%(request_url)s", "userAgent": "%(user_agent)s", "protocol": "%(protocol)s"} }'
29+
)
2330

2431

2532
class StructuredLogHandler(logging.StreamHandler):
2633
"""Handler to format logs into the Cloud Logging structured log format,
2734
and write them to standard output
2835
"""
2936

30-
def __init__(self, *, name=None, stream=None, project=None):
37+
def __init__(self, *, labels=None, stream=None, project_id=None):
3138
"""
3239
Args:
33-
name (Optional[str]): The name of the custom log in Cloud Logging.
40+
labels (Optional[dict]): Additional labels to attach to logs.
3441
stream (Optional[IO]): Stream to be used by the handler.
42+
project (Optional[str]): Project Id associated with the logs.
3543
"""
3644
super(StructuredLogHandler, self).__init__(stream=stream)
37-
self.name = name
38-
self.project_id = project
45+
self.project_id = project_id
3946

4047
# add extra keys to log record
41-
self.addFilter(CloudLoggingFilter(project))
48+
log_filter = CloudLoggingFilter(project=project_id, default_labels=labels)
49+
self.addFilter(log_filter)
4250

4351
# make logs appear in GCP structured logging format
4452
self.formatter = logging.Formatter(GCP_FORMAT)

tests/unit/handlers/test_handlers.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -268,22 +268,27 @@ def test_emit(self):
268268
)
269269
logname = "loggername"
270270
message = "hello world"
271-
labels = {"test-key": "test-value"}
272271
record = logging.LogRecord(logname, logging, None, None, message, None, None)
273-
record.labels = labels
274-
handler.emit(record)
272+
handler.handle(record)
275273
self.assertEqual(
276274
handler.transport.send_called_with,
277-
(record, message, _GLOBAL_RESOURCE, labels, None, None, None, None),
275+
(record, message, _GLOBAL_RESOURCE, None, None, None, None, None),
278276
)
279277

280278
def test_emit_manual_field_override(self):
281279
from google.cloud.logging_v2.logger import _GLOBAL_RESOURCE
282280
from google.cloud.logging_v2.resource import Resource
283281

284282
client = _Client(self.PROJECT)
283+
default_labels = {
284+
"default_key": "default-value",
285+
"overwritten_key": "bad_value",
286+
}
285287
handler = self._make_one(
286-
client, transport=_Transport, resource=_GLOBAL_RESOURCE
288+
client,
289+
transport=_Transport,
290+
resource=_GLOBAL_RESOURCE,
291+
labels=default_labels,
287292
)
288293
logname = "loggername"
289294
message = "hello world"
@@ -299,9 +304,14 @@ def test_emit_manual_field_override(self):
299304
setattr(record, "source_location", expected_source)
300305
expected_resource = Resource(type="test", labels={})
301306
setattr(record, "resource", expected_resource)
302-
expected_labels = {"test-label": "manual"}
303-
setattr(record, "labels", expected_labels)
304-
handler.emit(record)
307+
added_labels = {"added_key": "added_value", "overwritten_key": "new_value"}
308+
expected_labels = {
309+
"default_key": "default-value",
310+
"overwritten_key": "new_value",
311+
"added_key": "added_value",
312+
}
313+
setattr(record, "labels", added_labels)
314+
handler.handle(record)
305315

306316
self.assertEqual(
307317
handler.transport.send_called_with,

tests/unit/handlers/test_structured_log.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,18 @@ def index():
4040

4141
def test_ctor_defaults(self):
4242
handler = self._make_one()
43-
self.assertIsNone(handler.name)
43+
self.assertIsNone(handler.project_id)
4444

45-
def test_ctor_w_name(self):
46-
handler = self._make_one(name="foo")
47-
self.assertEqual(handler.name, "foo")
45+
def test_ctor_w_project(self):
46+
handler = self._make_one(project_id="foo")
47+
self.assertEqual(handler.project_id, "foo")
4848

4949
def test_format(self):
5050
import logging
5151
import json
5252

53-
handler = self._make_one()
53+
labels = {"default_key": "default-value"}
54+
handler = self._make_one(labels=labels)
5455
logname = "loggername"
5556
message = "hello world,嗨 世界"
5657
pathname = "testpath"
@@ -74,6 +75,7 @@ def test_format(self):
7475
"userAgent": "",
7576
"protocol": "",
7677
},
78+
"logging.googleapis.com/labels": labels,
7779
}
7880
handler.filter(record)
7981
result = json.loads(handler.format(record))
@@ -106,6 +108,7 @@ def test_format_minimal(self):
106108
"userAgent": "",
107109
"protocol": "",
108110
},
111+
"logging.googleapis.com/labels": {},
109112
}
110113
handler.filter(record)
111114
result = json.loads(handler.format(record))
@@ -160,7 +163,11 @@ def test_format_overrides(self):
160163
import logging
161164
import json
162165

163-
handler = self._make_one()
166+
default_labels = {
167+
"default_key": "default-value",
168+
"overwritten_key": "bad_value",
169+
}
170+
handler = self._make_one(labels=default_labels)
164171
logname = "loggername"
165172
message = "hello world,嗨 世界"
166173
record = logging.LogRecord(logname, logging.INFO, "", 0, message, None, None)
@@ -172,6 +179,8 @@ def test_format_overrides(self):
172179
record.http_request = {"requestUrl": overwrite_path}
173180
record.source_location = {"file": overwrite_file}
174181
record.trace = overwrite_trace
182+
added_labels = {"added_key": "added_value", "overwritten_key": "new_value"}
183+
record.labels = added_labels
175184
expected_payload = {
176185
"logging.googleapis.com/trace": overwrite_trace,
177186
"logging.googleapis.com/sourceLocation": {
@@ -185,6 +194,11 @@ def test_format_overrides(self):
185194
"userAgent": "",
186195
"protocol": "",
187196
},
197+
"logging.googleapis.com/labels": {
198+
"default_key": "default-value",
199+
"overwritten_key": "new_value",
200+
"added_key": "added_value",
201+
},
188202
}
189203

190204
app = self.create_app()

0 commit comments

Comments
 (0)