Skip to content

Commit 5908b70

Browse files
author
Alexey Nikitin
committed
Add AppendLabelLoggingAdapter to have an ability to adapt logger with predefined labels
1 parent c1c8ce1 commit 5908b70

File tree

2 files changed

+152
-2
lines changed

2 files changed

+152
-2
lines changed

google/cloud/logging_v2/handlers/structured_log.py

+62-2
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@
1919
import logging
2020
import logging.handlers
2121

22-
from google.cloud.logging_v2.handlers.handlers import CloudLoggingFilter
23-
from google.cloud.logging_v2.handlers.handlers import _format_and_parse_message
2422
import google.cloud.logging_v2
2523
from google.cloud.logging_v2._instrumentation import _create_diagnostic_entry
24+
from google.cloud.logging_v2.handlers.handlers import (
25+
CloudLoggingFilter, _format_and_parse_message
26+
)
2627

2728
GCP_FORMAT = (
2829
"{%(_payload_str)s"
@@ -136,3 +137,62 @@ def emit_instrumentation_info(self):
136137
struct_logger.setLevel(logging.INFO)
137138
struct_logger.info(diagnostic_object.payload)
138139
struct_logger.handlers.clear()
140+
141+
142+
class AppendLabelLoggingAdapter(logging.LoggerAdapter):
143+
"""
144+
Logging adapter that allows to add required
145+
constant key/value to the labels part of log record.
146+
Example:
147+
148+
.. code-block:: python
149+
150+
import logging
151+
from google.cloud.logging_v2.handlers.structured_log import AppendLabelLoggingAdapter
152+
from google.cloud.logging_v2.handlers.structured_log import StructuredLogHandler
153+
logging.root.setLevel(logging.INFO)
154+
logging.root.handlers = [StructuredLogHandler()]
155+
first_adapter = AppendLabelLoggingAdapter(logging.root, {'a': 5, 'b': 6})
156+
first_adapter.info('first info')
157+
{
158+
"message": "first info",
159+
"severity": "INFO",
160+
"logging.googleapis.com/labels": {"python_logger": "root", "a": 5, "b": 6}
161+
[...]
162+
}
163+
# Could be stacked
164+
second_adapter=AppendLabelLoggingAdapter(first_adapter, {'hello': 'world'})
165+
second_adapter.info('second info')
166+
{
167+
"message": "second info",
168+
"severity": "INFO",
169+
"logging.googleapis.com/labels": {"python_logger": "root", "hello": "world", "a": 5, "b": 6}
170+
[...]
171+
}
172+
"""
173+
174+
def __init__(self, logger, append_labels):
175+
"""
176+
Args:
177+
logger (~logging.Logger):
178+
The Logger for this adapter to use.
179+
append_labels (~typing.Dict[str, str]): the required data to be added to logger "labels" field.
180+
"""
181+
self.append_labels = append_labels
182+
super().__init__(logger)
183+
184+
def process(self, msg, kwargs):
185+
"""
186+
Args:
187+
msg (str):
188+
Log message
189+
kwargs (dict):
190+
logging kwargs
191+
"""
192+
extra = kwargs.get("extra", {})
193+
labels = extra.get("labels", {})
194+
for label_key, label_value in self.append_labels.items():
195+
labels.setdefault(label_key, label_value)
196+
extra["labels"] = labels
197+
kwargs["extra"] = extra
198+
return msg, kwargs

tests/unit/handlers/test_structured_log.py

+90
Original file line numberDiff line numberDiff line change
@@ -667,3 +667,93 @@ def test_valid_instrumentation_info(self):
667667
inst_source_dict,
668668
"instrumentation payload not logged properly",
669669
)
670+
671+
def test_append_labels_adapter(self):
672+
import logging
673+
674+
import mock
675+
676+
from google.cloud.logging_v2.handlers.structured_log import \
677+
AppendLabelLoggingAdapter
678+
679+
logger = logging.getLogger("google.cloud.logging_v2.handlers.structured_log")
680+
handler = self._make_one()
681+
with mock.patch.object(handler, "emit_instrumentation_info"):
682+
with mock.patch.object(logger, "_log") as mock_log:
683+
logger.addHandler(handler)
684+
logger.setLevel(logging.INFO)
685+
adapted_logger = AppendLabelLoggingAdapter(
686+
logger, append_labels={"service_id": 1, "another_value": "foo"}
687+
)
688+
adapted_logger.info("test message")
689+
mock_log.assert_called_once()
690+
self.assertEqual(
691+
mock_log.call_args_list[0].kwargs,
692+
{"extra": {"labels": {"service_id": 1, "another_value": "foo"}}},
693+
)
694+
695+
def test_append_labels_adapter_override_defaults(self):
696+
import logging
697+
698+
import mock
699+
700+
from google.cloud.logging_v2.handlers.structured_log import \
701+
AppendLabelLoggingAdapter
702+
703+
logger = logging.getLogger("google.cloud.logging_v2.handlers.structured_log")
704+
handler = self._make_one()
705+
with mock.patch.object(handler, "emit_instrumentation_info"):
706+
with mock.patch.object(logger, "_log") as mock_log:
707+
logger.addHandler(handler)
708+
logger.setLevel(logging.INFO)
709+
adapted_logger = AppendLabelLoggingAdapter(
710+
logger, append_labels={"service_id": 1, "another_value": "foo"}
711+
)
712+
adapted_logger.info(
713+
"test message", extra={"labels": {"another_value": "baz"}}
714+
)
715+
mock_log.assert_called_once()
716+
# the default value was overridden
717+
self.assertEqual(
718+
mock_log.call_args_list[0].kwargs,
719+
{"extra": {"labels": {"service_id": 1, "another_value": "baz"}}},
720+
)
721+
722+
def test_append_labels_adapter_stacked(self):
723+
import logging
724+
725+
import mock
726+
727+
from google.cloud.logging_v2.handlers.structured_log import \
728+
AppendLabelLoggingAdapter
729+
730+
logger = logging.getLogger("google.cloud.logging_v2.handlers.structured_log")
731+
handler = self._make_one()
732+
with mock.patch.object(handler, "emit_instrumentation_info"):
733+
with mock.patch.object(logger, "_log") as mock_log:
734+
logger.addHandler(handler)
735+
logger.setLevel(logging.INFO)
736+
adapted_logger = AppendLabelLoggingAdapter(
737+
logger, append_labels={"service_id": 1, "another_value": "foo"}
738+
)
739+
twice_adapted_logger = AppendLabelLoggingAdapter(
740+
adapted_logger,
741+
# one fields is new, another was adapted already
742+
append_labels={"new_field": "new_value", "another_value": "baz"},
743+
)
744+
twice_adapted_logger.info(
745+
"test message", extra={"labels": {"another_value": "baz"}}
746+
)
747+
mock_log.assert_called_once()
748+
self.assertEqual(
749+
mock_log.call_args_list[0].kwargs,
750+
{
751+
"extra": {
752+
"labels": {
753+
"another_value": "baz", # value is changed by the second adapter
754+
"new_field": "new_value", # introduced by the second adapter
755+
"service_id": 1, # left as is from the first adapter configuration
756+
}
757+
}
758+
},
759+
)

0 commit comments

Comments
 (0)