From c15f4b2c87a464316e4c9310e7ab323ee73722f9 Mon Sep 17 00:00:00 2001 From: Angela Li Date: Fri, 2 Jun 2017 15:24:02 -0700 Subject: [PATCH] Send trace context with logs from web applications (#3448) --- .../google/cloud/logging/handlers/_helpers.py | 66 +++++++ .../cloud/logging/handlers/app_engine.py | 23 ++- .../google/cloud/logging/handlers/handlers.py | 13 +- .../logging/handlers/middleware/__init__.py | 17 ++ .../logging/handlers/middleware/request.py | 45 +++++ .../handlers/transports/background_thread.py | 13 +- .../cloud/logging/handlers/transports/base.py | 5 +- .../cloud/logging/handlers/transports/sync.py | 11 +- logging/nox.py | 4 +- .../unit/handlers/middleware/test_request.py | 86 +++++++++ logging/tests/unit/handlers/test__helpers.py | 171 ++++++++++++++++++ .../tests/unit/handlers/test_app_engine.py | 48 ++++- logging/tests/unit/handlers/test_handlers.py | 8 +- .../transports/test_background_thread.py | 9 +- .../unit/handlers/transports/test_sync.py | 7 +- 15 files changed, 499 insertions(+), 27 deletions(-) create mode 100644 logging/google/cloud/logging/handlers/middleware/__init__.py create mode 100644 logging/google/cloud/logging/handlers/middleware/request.py create mode 100644 logging/tests/unit/handlers/middleware/test_request.py create mode 100644 logging/tests/unit/handlers/test__helpers.py diff --git a/logging/google/cloud/logging/handlers/_helpers.py b/logging/google/cloud/logging/handlers/_helpers.py index 81adcf0eb5454..1ebb064ed228a 100644 --- a/logging/google/cloud/logging/handlers/_helpers.py +++ b/logging/google/cloud/logging/handlers/_helpers.py @@ -17,6 +17,17 @@ import math import json +try: + import flask +except ImportError: # pragma: NO COVER + flask = None + +from google.cloud.logging.handlers.middleware.request import ( + _get_django_request) + +_FLASK_TRACE_HEADER = 'X_CLOUD_TRACE_CONTEXT' +_DJANGO_TRACE_HEADER = 'HTTP_X_CLOUD_TRACE_CONTEXT' + def format_stackdriver_json(record, message): """Helper to format a LogRecord in in Stackdriver fluentd format. @@ -37,3 +48,58 @@ def format_stackdriver_json(record, message): } return json.dumps(payload) + + +def get_trace_id_from_flask(): + """Get trace_id from flask request headers. + + :rtype: str + :return: Trace_id in HTTP request headers. + """ + if flask is None or not flask.request: + return None + + header = flask.request.headers.get(_FLASK_TRACE_HEADER) + + if header is None: + return None + + trace_id = header.split('/', 1)[0] + + return trace_id + + +def get_trace_id_from_django(): + """Get trace_id from django request headers. + + :rtype: str + :return: Trace_id in HTTP request headers. + """ + request = _get_django_request() + + if request is None: + return None + + header = request.META.get(_DJANGO_TRACE_HEADER) + if header is None: + return None + + trace_id = header.split('/', 1)[0] + + return trace_id + + +def get_trace_id(): + """Helper to get trace_id from web application request header. + + :rtype: str + :returns: Trace_id in HTTP request headers. + """ + checkers = (get_trace_id_from_django, get_trace_id_from_flask) + + for checker in checkers: + trace_id = checker() + if trace_id is not None: + return trace_id + + return None diff --git a/logging/google/cloud/logging/handlers/app_engine.py b/logging/google/cloud/logging/handlers/app_engine.py index 7011819f8a2fe..509bf8002fb14 100644 --- a/logging/google/cloud/logging/handlers/app_engine.py +++ b/logging/google/cloud/logging/handlers/app_engine.py @@ -20,6 +20,7 @@ import os +from google.cloud.logging.handlers._helpers import get_trace_id from google.cloud.logging.handlers.handlers import CloudLoggingHandler from google.cloud.logging.handlers.transports import BackgroundThreadTransport from google.cloud.logging.resource import Resource @@ -30,6 +31,8 @@ _GAE_SERVICE_ENV = 'GAE_SERVICE' _GAE_VERSION_ENV = 'GAE_VERSION' +_TRACE_ID_LABEL = 'appengine.googleapis.com/trace_id' + class AppEngineHandler(CloudLoggingHandler): """A logging handler that sends App Engine-formatted logs to Stackdriver. @@ -50,7 +53,8 @@ def __init__(self, client, client, name=_DEFAULT_GAE_LOGGER_NAME, transport=transport, - resource=self.get_gae_resource()) + resource=self.get_gae_resource(), + labels=self.get_gae_labels()) def get_gae_resource(self): """Return the GAE resource using the environment variables. @@ -67,3 +71,20 @@ def get_gae_resource(self): }, ) return gae_resource + + def get_gae_labels(self): + """Return the labels for GAE app. + + If the trace ID can be detected, it will be included as a label. + Currently, no other labels are included. + + :rtype: dict + :returns: Labels for GAE app. + """ + gae_labels = {} + + trace_id = get_trace_id() + if trace_id is not None: + gae_labels[_TRACE_ID_LABEL] = trace_id + + return gae_labels diff --git a/logging/google/cloud/logging/handlers/handlers.py b/logging/google/cloud/logging/handlers/handlers.py index 97afde9f87fbe..fe9848848d38e 100644 --- a/logging/google/cloud/logging/handlers/handlers.py +++ b/logging/google/cloud/logging/handlers/handlers.py @@ -57,6 +57,9 @@ class CloudLoggingHandler(logging.StreamHandler): :param resource: (Optional) Monitored resource of the entry, defaults to the global resource type. + :type labels: dict + :param labels: (Optional) Mapping of labels for the entry. + Example: .. code-block:: python @@ -79,12 +82,14 @@ class CloudLoggingHandler(logging.StreamHandler): def __init__(self, client, name=DEFAULT_LOGGER_NAME, transport=BackgroundThreadTransport, - resource=_GLOBAL_RESOURCE): + resource=_GLOBAL_RESOURCE, + labels=None): super(CloudLoggingHandler, self).__init__() self.name = name self.client = client self.transport = transport(client, name) self.resource = resource + self.labels = labels def emit(self, record): """Actually log the specified logging record. @@ -97,7 +102,11 @@ def emit(self, record): :param record: The record to be logged. """ message = super(CloudLoggingHandler, self).format(record) - self.transport.send(record, message, resource=self.resource) + self.transport.send( + record, + message, + resource=self.resource, + labels=self.labels) def setup_logging(handler, excluded_loggers=EXCLUDED_LOGGER_DEFAULTS, diff --git a/logging/google/cloud/logging/handlers/middleware/__init__.py b/logging/google/cloud/logging/handlers/middleware/__init__.py new file mode 100644 index 0000000000000..c340235b8bdd3 --- /dev/null +++ b/logging/google/cloud/logging/handlers/middleware/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.cloud.logging.handlers.middleware.request import RequestMiddleware + +__all__ = ['RequestMiddleware'] diff --git a/logging/google/cloud/logging/handlers/middleware/request.py b/logging/google/cloud/logging/handlers/middleware/request.py new file mode 100644 index 0000000000000..4c0b22a8e96bf --- /dev/null +++ b/logging/google/cloud/logging/handlers/middleware/request.py @@ -0,0 +1,45 @@ +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Django middleware helper to capture a request. + +The request is stored on a thread-local so that it can be +inspected by other helpers. +""" + +import threading + + +_thread_locals = threading.local() + + +def _get_django_request(): + """Get Django request from thread local. + + :rtype: str + :returns: Django request. + """ + return getattr(_thread_locals, 'request', None) + + +class RequestMiddleware(object): + """Saves the request in thread local""" + + def process_request(self, request): + """Called on each request, before Django decides which view to execute. + + :type request: :class:`~django.http.request.HttpRequest` + :param request: Django http request. + """ + _thread_locals.request = request diff --git a/logging/google/cloud/logging/handlers/transports/background_thread.py b/logging/google/cloud/logging/handlers/transports/background_thread.py index 010c06b36bc91..d889bed62626f 100644 --- a/logging/google/cloud/logging/handlers/transports/background_thread.py +++ b/logging/google/cloud/logging/handlers/transports/background_thread.py @@ -203,7 +203,7 @@ def _main_thread_terminated(self): else: print('Failed to send %d pending logs.' % (self._queue.qsize(),)) - def enqueue(self, record, message, resource=None): + def enqueue(self, record, message, resource=None, labels=None): """Queues a log entry to be written by the background thread. :type record: :class:`logging.LogRecord` @@ -215,6 +215,9 @@ def enqueue(self, record, message, resource=None): :type resource: :class:`~google.cloud.logging.resource.Resource` :param resource: (Optional) Monitored resource of the entry + + :type labels: dict + :param labels: (Optional) Mapping of labels for the entry. """ self._queue.put_nowait({ 'info': { @@ -223,6 +226,7 @@ def enqueue(self, record, message, resource=None): }, 'severity': record.levelname, 'resource': resource, + 'labels': labels, }) def flush(self): @@ -257,7 +261,7 @@ def __init__(self, client, name, grace_period=_DEFAULT_GRACE_PERIOD, self.worker = _Worker(logger) self.worker.start() - def send(self, record, message, resource=None): + def send(self, record, message, resource=None, labels=None): """Overrides Transport.send(). :type record: :class:`logging.LogRecord` @@ -269,8 +273,11 @@ def send(self, record, message, resource=None): :type resource: :class:`~google.cloud.logging.resource.Resource` :param resource: (Optional) Monitored resource of the entry. + + :type labels: dict + :param labels: (Optional) Mapping of labels for the entry. """ - self.worker.enqueue(record, message, resource=resource) + self.worker.enqueue(record, message, resource=resource, labels=labels) def flush(self): """Submit any pending log records.""" diff --git a/logging/google/cloud/logging/handlers/transports/base.py b/logging/google/cloud/logging/handlers/transports/base.py index 21957021793fc..7829201b1c98f 100644 --- a/logging/google/cloud/logging/handlers/transports/base.py +++ b/logging/google/cloud/logging/handlers/transports/base.py @@ -22,7 +22,7 @@ class Transport(object): client and name object, and must override :meth:`send`. """ - def send(self, record, message, resource=None): + def send(self, record, message, resource=None, labels=None): """Transport send to be implemented by subclasses. :type record: :class:`logging.LogRecord` @@ -34,6 +34,9 @@ def send(self, record, message, resource=None): :type resource: :class:`~google.cloud.logging.resource.Resource` :param resource: (Optional) Monitored resource of the entry. + + :type labels: dict + :param labels: (Optional) Mapping of labels for the entry. """ raise NotImplementedError diff --git a/logging/google/cloud/logging/handlers/transports/sync.py b/logging/google/cloud/logging/handlers/transports/sync.py index 0dd6e0bd7e241..be70e60a14e15 100644 --- a/logging/google/cloud/logging/handlers/transports/sync.py +++ b/logging/google/cloud/logging/handlers/transports/sync.py @@ -29,7 +29,7 @@ class SyncTransport(Transport): def __init__(self, client, name): self.logger = client.logger(name) - def send(self, record, message, resource=None): + def send(self, record, message, resource=None, labels=None): """Overrides transport.send(). :type record: :class:`logging.LogRecord` @@ -38,8 +38,15 @@ def send(self, record, message, resource=None): :type message: str :param message: The message from the ``LogRecord`` after being formatted by the associated log formatters. + + :type resource: :class:`~google.cloud.logging.resource.Resource` + :param resource: (Optional) Monitored resource of the entry. + + :type labels: dict + :param labels: (Optional) Mapping of labels for the entry. """ info = {'message': message, 'python_logger': record.name} self.logger.log_struct(info, severity=record.levelname, - resource=resource) + resource=resource, + labels=labels) diff --git a/logging/nox.py b/logging/nox.py index 5d4751a955a57..fbbbec1958c19 100644 --- a/logging/nox.py +++ b/logging/nox.py @@ -31,7 +31,9 @@ def unit_tests(session, python_version): session.interpreter = 'python{}'.format(python_version) # Install all test dependencies, then install this package in-place. - session.install('mock', 'pytest', 'pytest-cov', *LOCAL_DEPS) + session.install( + 'mock', 'pytest', 'pytest-cov', + 'flask', 'django', *LOCAL_DEPS) session.install('-e', '.') # Run py.test against the unit tests. diff --git a/logging/tests/unit/handlers/middleware/test_request.py b/logging/tests/unit/handlers/middleware/test_request.py new file mode 100644 index 0000000000000..983d67129647c --- /dev/null +++ b/logging/tests/unit/handlers/middleware/test_request.py @@ -0,0 +1,86 @@ +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import mock + + +class DjangoBase(unittest.TestCase): + + @classmethod + def setUpClass(cls): + from django.conf import settings + from django.test.utils import setup_test_environment + + if not settings.configured: + settings.configure() + setup_test_environment() + + @classmethod + def tearDownClass(cls): + from django.test.utils import teardown_test_environment + + teardown_test_environment() + + +class TestRequestMiddleware(DjangoBase): + + def _get_target_class(self): + from google.cloud.logging.handlers.middleware import request + + return request.RequestMiddleware + + def _make_one(self, *args, **kw): + return self._get_target_class()(*args, **kw) + + def test_process_request(self): + from django.test import RequestFactory + from google.cloud.logging.handlers.middleware import request + + middleware = self._make_one() + mock_request = RequestFactory().get('/') + middleware.process_request(mock_request) + + django_request = request._get_django_request() + self.assertEqual(django_request, mock_request) + + +class Test__get_django_request(DjangoBase): + + @staticmethod + def _call_fut(): + from google.cloud.logging.handlers.middleware import request + + return request._get_django_request() + + @staticmethod + def _make_patch(new_locals): + return mock.patch( + 'google.cloud.logging.handlers.middleware.request._thread_locals', + new=new_locals) + + def test_with_request(self): + thread_locals = mock.Mock(spec=['request']) + with self._make_patch(thread_locals): + django_request = self._call_fut() + + self.assertIs(django_request, thread_locals.request) + + def test_without_request(self): + thread_locals = mock.Mock(spec=[]) + with self._make_patch(thread_locals): + django_request = self._call_fut() + + self.assertIsNone(django_request) diff --git a/logging/tests/unit/handlers/test__helpers.py b/logging/tests/unit/handlers/test__helpers.py new file mode 100644 index 0000000000000..0731c825d32cc --- /dev/null +++ b/logging/tests/unit/handlers/test__helpers.py @@ -0,0 +1,171 @@ +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import mock + + +class Test_get_trace_id_from_flask(unittest.TestCase): + + @staticmethod + def _call_fut(): + from google.cloud.logging.handlers import _helpers + + return _helpers.get_trace_id_from_flask() + + @staticmethod + def create_app(): + import flask + + app = flask.Flask(__name__) + + @app.route('/') + def index(): + return 'test flask trace' # pragma: NO COVER + + return app + + def setUp(self): + self.app = self.create_app() + + def test_no_context_header(self): + with self.app.test_request_context( + path='/', + headers={}): + trace_id = self._call_fut() + + self.assertIsNone(trace_id) + + def test_valid_context_header(self): + flask_trace_header = 'X_CLOUD_TRACE_CONTEXT' + expected_trace_id = 'testtraceidflask' + flask_trace_id = expected_trace_id + '/testspanid' + + context = self.app.test_request_context( + path='/', + headers={flask_trace_header: flask_trace_id}) + + with context: + trace_id = self._call_fut() + + self.assertEqual(trace_id, expected_trace_id) + + +class Test_get_trace_id_from_django(unittest.TestCase): + + @staticmethod + def _call_fut(): + from google.cloud.logging.handlers import _helpers + + return _helpers.get_trace_id_from_django() + + def setUp(self): + from django.conf import settings + from django.test.utils import setup_test_environment + + if not settings.configured: + settings.configure() + setup_test_environment() + + def tearDown(self): + from django.test.utils import teardown_test_environment + from google.cloud.logging.handlers.middleware import request + + teardown_test_environment() + request._thread_locals.__dict__.clear() + + def test_no_context_header(self): + from django.test import RequestFactory + from google.cloud.logging.handlers.middleware import request + + django_request = RequestFactory().get('/') + + middleware = request.RequestMiddleware() + middleware.process_request(django_request) + trace_id = self._call_fut() + self.assertIsNone(trace_id) + + def test_valid_context_header(self): + from django.test import RequestFactory + from google.cloud.logging.handlers.middleware import request + + django_trace_header = 'HTTP_X_CLOUD_TRACE_CONTEXT' + expected_trace_id = 'testtraceiddjango' + django_trace_id = expected_trace_id + '/testspanid' + + django_request = RequestFactory().get( + '/', + **{django_trace_header: django_trace_id}) + + middleware = request.RequestMiddleware() + middleware.process_request(django_request) + trace_id = self._call_fut() + + self.assertEqual(trace_id, expected_trace_id) + + +class Test_get_trace_id(unittest.TestCase): + + @staticmethod + def _call_fut(): + from google.cloud.logging.handlers import _helpers + + return _helpers.get_trace_id() + + def _helper(self, django_return, flask_return): + django_patch = mock.patch( + 'google.cloud.logging.handlers._helpers.get_trace_id_from_django', + return_value=django_return) + flask_patch = mock.patch( + 'google.cloud.logging.handlers._helpers.get_trace_id_from_flask', + return_value=flask_return) + + with django_patch as django_mock: + with flask_patch as flask_mock: + trace_id = self._call_fut() + + return django_mock, flask_mock, trace_id + + def test_from_django(self): + django_mock, flask_mock, trace_id = self._helper( + 'test-django-trace-id', None) + self.assertEqual(trace_id, django_mock.return_value) + + django_mock.assert_called_once_with() + flask_mock.assert_not_called() + + def test_from_flask(self): + django_mock, flask_mock, trace_id = self._helper( + None, 'test-flask-trace-id') + self.assertEqual(trace_id, flask_mock.return_value) + + django_mock.assert_called_once_with() + flask_mock.assert_called_once_with() + + def test_from_django_and_flask(self): + django_mock, flask_mock, trace_id = self._helper( + 'test-django-trace-id', 'test-flask-trace-id') + # Django wins. + self.assertEqual(trace_id, django_mock.return_value) + + django_mock.assert_called_once_with() + flask_mock.assert_not_called() + + def test_missing(self): + django_mock, flask_mock, trace_id = self._helper(None, None) + self.assertIsNone(trace_id) + + django_mock.assert_called_once_with() + flask_mock.assert_called_once_with() diff --git a/logging/tests/unit/handlers/test_app_engine.py b/logging/tests/unit/handlers/test_app_engine.py index c39328593f7a5..6438c4abb8a0d 100644 --- a/logging/tests/unit/handlers/test_app_engine.py +++ b/logging/tests/unit/handlers/test_app_engine.py @@ -15,8 +15,10 @@ import logging import unittest +import mock -class TestAppEngineHandlerHandler(unittest.TestCase): + +class TestAppEngineHandler(unittest.TestCase): PROJECT = 'PROJECT' def _get_target_class(self): @@ -28,12 +30,13 @@ def _make_one(self, *args, **kw): return self._get_target_class()(*args, **kw) def test_constructor(self): - import mock from google.cloud.logging.handlers.app_engine import _GAE_PROJECT_ENV from google.cloud.logging.handlers.app_engine import _GAE_SERVICE_ENV from google.cloud.logging.handlers.app_engine import _GAE_VERSION_ENV + from google.cloud.logging.handlers.app_engine import _TRACE_ID_LABEL client = mock.Mock(project=self.PROJECT, spec=['project']) + with mock.patch('os.environ', new={_GAE_PROJECT_ENV: 'test_project', _GAE_SERVICE_ENV: 'test_service', _GAE_VERSION_ENV: 'test_version'}): @@ -43,13 +46,13 @@ def test_constructor(self): self.assertEqual(handler.resource.labels['project_id'], 'test_project') self.assertEqual(handler.resource.labels['module_id'], 'test_service') self.assertEqual(handler.resource.labels['version_id'], 'test_version') + self.assertEqual(handler.labels, {}) def test_emit(self): - import mock - client = mock.Mock(project=self.PROJECT, spec=['project']) handler = self._make_one(client, transport=_Transport) gae_resource = handler.get_gae_resource() + gae_labels = handler.get_gae_labels() logname = 'app' message = 'hello world' record = logging.LogRecord(logname, logging, None, None, message, @@ -58,7 +61,38 @@ def test_emit(self): self.assertIs(handler.transport.client, client) self.assertEqual(handler.transport.name, logname) - self.assertEqual(handler.transport.send_called_with, (record, message, gae_resource)) + self.assertEqual( + handler.transport.send_called_with, + (record, message, gae_resource, gae_labels)) + + def _get_gae_labels_helper(self, trace_id): + get_trace_patch = mock.patch( + 'google.cloud.logging.handlers.app_engine.get_trace_id', + return_value=trace_id) + + client = mock.Mock(project=self.PROJECT, spec=['project']) + # The handler actually calls ``get_gae_labels()``. + with get_trace_patch as mock_get_trace: + handler = self._make_one(client, transport=_Transport) + mock_get_trace.assert_called_once_with() + + gae_labels = handler.get_gae_labels() + self.assertEqual(mock_get_trace.mock_calls, + [mock.call(), mock.call()]) + + return gae_labels + + def test_get_gae_labels_with_label(self): + from google.cloud.logging.handlers import app_engine + + trace_id = 'test-gae-trace-id' + gae_labels = self._get_gae_labels_helper(trace_id) + expected_labels = {app_engine._TRACE_ID_LABEL: trace_id} + self.assertEqual(gae_labels, expected_labels) + + def test_get_gae_labels_without_label(self): + gae_labels = self._get_gae_labels_helper(None) + self.assertEqual(gae_labels, {}) class _Transport(object): @@ -67,5 +101,5 @@ def __init__(self, client, name): self.client = client self.name = name - def send(self, record, message, resource): - self.send_called_with = (record, message, resource) + def send(self, record, message, resource, labels): + self.send_called_with = (record, message, resource, labels) diff --git a/logging/tests/unit/handlers/test_handlers.py b/logging/tests/unit/handlers/test_handlers.py index 05dc876314783..96823b2e906dc 100644 --- a/logging/tests/unit/handlers/test_handlers.py +++ b/logging/tests/unit/handlers/test_handlers.py @@ -45,7 +45,9 @@ def test_emit(self): None, None) handler.emit(record) - self.assertEqual(handler.transport.send_called_with, (record, message, _GLOBAL_RESOURCE)) + self.assertEqual( + handler.transport.send_called_with, + (record, message, _GLOBAL_RESOURCE, None)) class TestSetupLogging(unittest.TestCase): @@ -110,5 +112,5 @@ class _Transport(object): def __init__(self, client, name): pass - def send(self, record, message, resource): - self.send_called_with = (record, message, resource) + def send(self, record, message, resource, labels=None): + self.send_called_with = (record, message, resource, labels) diff --git a/logging/tests/unit/handlers/transports/test_background_thread.py b/logging/tests/unit/handlers/transports/test_background_thread.py index 3e3378dcd3616..f6671273b53d1 100644 --- a/logging/tests/unit/handlers/transports/test_background_thread.py +++ b/logging/tests/unit/handlers/transports/test_background_thread.py @@ -61,9 +61,10 @@ def test_send(self): python_logger_name, logging.INFO, None, None, message, None, None) - transport.send(record, message, _GLOBAL_RESOURCE) + transport.send(record, message, _GLOBAL_RESOURCE, None) - transport.worker.enqueue.assert_called_once_with(record, message, _GLOBAL_RESOURCE) + transport.worker.enqueue.assert_called_once_with( + record, message, _GLOBAL_RESOURCE, None) def test_flush(self): client = _Client(self.PROJECT) @@ -287,13 +288,13 @@ def __init__(self): self.commit_called = False self.commit_count = None - def log_struct(self, info, severity=logging.INFO, resource=None): + def log_struct(self, info, severity=logging.INFO, resource=None, labels=None): from google.cloud.logging.logger import _GLOBAL_RESOURCE assert resource is None resource = _GLOBAL_RESOURCE - self.log_struct_called_with = (info, severity, resource) + self.log_struct_called_with = (info, severity, resource, labels) self.entries.append(info) def commit(self): diff --git a/logging/tests/unit/handlers/transports/test_sync.py b/logging/tests/unit/handlers/transports/test_sync.py index 475ecc9c6a71b..01c15240f3b7d 100644 --- a/logging/tests/unit/handlers/transports/test_sync.py +++ b/logging/tests/unit/handlers/transports/test_sync.py @@ -52,7 +52,7 @@ def test_send(self): 'message': message, 'python_logger': python_logger_name, } - EXPECTED_SENT = (EXPECTED_STRUCT, 'INFO', _GLOBAL_RESOURCE) + EXPECTED_SENT = (EXPECTED_STRUCT, 'INFO', _GLOBAL_RESOURCE, None) self.assertEqual( transport.logger.log_struct_called_with, EXPECTED_SENT) @@ -63,8 +63,9 @@ class _Logger(object): def __init__(self, name): self.name = name - def log_struct(self, message, severity=None, resource=_GLOBAL_RESOURCE): - self.log_struct_called_with = (message, severity, resource) + def log_struct(self, message, severity=None, + resource=_GLOBAL_RESOURCE, labels=None): + self.log_struct_called_with = (message, severity, resource, labels) class _Client(object):