Skip to content

Commit 619ba4a

Browse files
Alexey NikitinNewGlad
Alexey Nikitin
authored andcommitted
feat: Add AppendLabelLoggingAdapter to have an ability to adapt logger with predefined labels
1 parent 1037afc commit 619ba4a

File tree

2 files changed

+156
-2
lines changed

2 files changed

+156
-2
lines changed

google/cloud/logging_v2/handlers/structured_log.py

+63-2
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
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,
26+
_format_and_parse_message,
27+
)
2628

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

tests/unit/handlers/test_structured_log.py

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

0 commit comments

Comments
 (0)