Repository: googleapis/google-cloud-python

Issue:
Implement monitored resource for logging client
Log entries can have a monitored resource associated with them as of v2 of the logging API.

* entries.write documentation: https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write
* MonitoredResource reference: https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource
* Existing tiny resource class in monitoring: https://github.com/GoogleCloudPlatform/google-cloud-python/blob/master/monitoring/google/cloud/monitoring/resource.py#L159

Essentially, the `_BaseEntry` class should be extend to accept a `resource`. Helper methods such as `log_text` and `log_struct` should likely be extended to accept the resource as well.

This'll need to work with http and gax.


Discussion:
We discovered this today in dev when moving from using the app log to request_log in google app engine. Is there any way to get the resource information per ProtobufEntry even without it being part of the _BaseEntry?
@jaitaiwan not without using the gapic layer directly. 

Resolving patch:
diff --git a/logging/google/cloud/logging/entries.py b/logging/google/cloud/logging/entries.py
--- a/logging/google/cloud/logging/entries.py
+++ b/logging/google/cloud/logging/entries.py
@@ -20,6 +20,7 @@
 from google.protobuf import any_pb2
 from google.protobuf.json_format import Parse
 
+from google.cloud.logging.resource import Resource
 from google.cloud._helpers import _name_from_project_path
 from google.cloud._helpers import _rfc3339_nanos_to_datetime
 
@@ -71,10 +72,13 @@ class _BaseEntry(object):
 
     :type http_request: dict
     :param http_request: (optional) info about HTTP request associated with
-                         the entry
+                         the entry.
+
+    :type resource: :class:`~google.cloud.logging.resource.Resource`
+    :param resource: (Optional) Monitored resource of the entry
     """
     def __init__(self, payload, logger, insert_id=None, timestamp=None,
-                 labels=None, severity=None, http_request=None):
+                 labels=None, severity=None, http_request=None, resource=None):
         self.payload = payload
         self.logger = logger
         self.insert_id = insert_id
@@ -82,6 +86,7 @@ def __init__(self, payload, logger, insert_id=None, timestamp=None,
         self.labels = labels
         self.severity = severity
         self.http_request = http_request
+        self.resource = resource
 
     @classmethod
     def from_api_repr(cls, resource, client, loggers=None):
@@ -118,8 +123,15 @@ def from_api_repr(cls, resource, client, loggers=None):
         labels = resource.get('labels')
         severity = resource.get('severity')
         http_request = resource.get('httpRequest')
+
+        monitored_resource_dict = resource.get('resource')
+        monitored_resource = None
+        if monitored_resource_dict is not None:
+            monitored_resource = Resource._from_dict(monitored_resource_dict)
+
         return cls(payload, logger, insert_id=insert_id, timestamp=timestamp,
-                   labels=labels, severity=severity, http_request=http_request)
+                   labels=labels, severity=severity, http_request=http_request,
+                   resource=monitored_resource)
 
 
 class TextEntry(_BaseEntry):
@@ -170,14 +182,18 @@ class ProtobufEntry(_BaseEntry):
     :type http_request: dict
     :param http_request: (optional) info about HTTP request associated with
                          the entry
+
+    :type resource: :class:`~google.cloud.logging.resource.Resource`
+    :param resource: (Optional) Monitored resource of the entry
     """
     _PAYLOAD_KEY = 'protoPayload'
 
     def __init__(self, payload, logger, insert_id=None, timestamp=None,
-                 labels=None, severity=None, http_request=None):
+                 labels=None, severity=None, http_request=None, resource=None):
         super(ProtobufEntry, self).__init__(
             payload, logger, insert_id=insert_id, timestamp=timestamp,
-            labels=labels, severity=severity, http_request=http_request)
+            labels=labels, severity=severity, http_request=http_request,
+            resource=resource)
         if isinstance(self.payload, any_pb2.Any):
             self.payload_pb = self.payload
             self.payload = None
diff --git a/logging/google/cloud/logging/logger.py b/logging/google/cloud/logging/logger.py
--- a/logging/google/cloud/logging/logger.py
+++ b/logging/google/cloud/logging/logger.py
@@ -16,6 +16,10 @@
 
 from google.protobuf.json_format import MessageToDict
 from google.cloud._helpers import _datetime_to_rfc3339
+from google.cloud.logging.resource import Resource
+
+
+_GLOBAL_RESOURCE = Resource(type='global', labels={})
 
 
 class Logger(object):
@@ -91,7 +95,8 @@ def batch(self, client=None):
 
     def _make_entry_resource(self, text=None, info=None, message=None,
                              labels=None, insert_id=None, severity=None,
-                             http_request=None, timestamp=None):
+                             http_request=None, timestamp=None,
+                             resource=_GLOBAL_RESOURCE):
         """Return a log entry resource of the appropriate type.
 
         Helper for :meth:`log_text`, :meth:`log_struct`, and :meth:`log_proto`.
@@ -123,19 +128,22 @@ def _make_entry_resource(self, text=None, info=None, message=None,
         :type timestamp: :class:`datetime.datetime`
         :param timestamp: (Optional) timestamp of event being logged.
 
+        :type resource: :class:`~google.cloud.logging.resource.Resource`
+        :param resource: (Optional) Monitored resource of the entry
+
         :rtype: dict
         :returns: The JSON resource created.
         """
-        resource = {
+        entry = {
             'logName': self.full_name,
-            'resource': {'type': 'global'},
+            'resource': resource._to_dict(),
         }
 
         if text is not None:
-            resource['textPayload'] = text
+            entry['textPayload'] = text
 
         if info is not None:
-            resource['jsonPayload'] = info
+            entry['jsonPayload'] = info
 
         if message is not None:
             # NOTE: If ``message`` contains an ``Any`` field with an
@@ -144,30 +152,31 @@ def _make_entry_resource(self, text=None, info=None, message=None,
             #       the assumption is that any types needed for the
             #       protobuf->JSON conversion will be known from already
             #       imported ``pb2`` modules.
-            resource['protoPayload'] = MessageToDict(message)
+            entry['protoPayload'] = MessageToDict(message)
 
         if labels is None:
             labels = self.labels
 
         if labels is not None:
-            resource['labels'] = labels
+            entry['labels'] = labels
 
         if insert_id is not None:
-            resource['insertId'] = insert_id
+            entry['insertId'] = insert_id
 
         if severity is not None:
-            resource['severity'] = severity
+            entry['severity'] = severity
 
         if http_request is not None:
-            resource['httpRequest'] = http_request
+            entry['httpRequest'] = http_request
 
         if timestamp is not None:
-            resource['timestamp'] = _datetime_to_rfc3339(timestamp)
+            entry['timestamp'] = _datetime_to_rfc3339(timestamp)
 
-        return resource
+        return entry
 
     def log_text(self, text, client=None, labels=None, insert_id=None,
-                 severity=None, http_request=None, timestamp=None):
+                 severity=None, http_request=None, timestamp=None,
+                 resource=_GLOBAL_RESOURCE):
         """API call:  log a text message via a POST request
 
         See:
@@ -194,17 +203,22 @@ def log_text(self, text, client=None, labels=None, insert_id=None,
         :param http_request: (optional) info about HTTP request associated with
                              the entry
 
+        :type resource: :class:`~google.cloud.logging.resource.Resource`
+        :param resource: Monitored resource of the entry, defaults
+                         to the global resource type.
+
         :type timestamp: :class:`datetime.datetime`
         :param timestamp: (optional) timestamp of event being logged.
         """
         client = self._require_client(client)
         entry_resource = self._make_entry_resource(
             text=text, labels=labels, insert_id=insert_id, severity=severity,
-            http_request=http_request, timestamp=timestamp)
+            http_request=http_request, timestamp=timestamp, resource=resource)
         client.logging_api.write_entries([entry_resource])
 
     def log_struct(self, info, client=None, labels=None, insert_id=None,
-                   severity=None, http_request=None, timestamp=None):
+                   severity=None, http_request=None, timestamp=None,
+                   resource=_GLOBAL_RESOURCE):
         """API call:  log a structured message via a POST request
 
         See:
@@ -231,17 +245,22 @@ def log_struct(self, info, client=None, labels=None, insert_id=None,
         :param http_request: (optional) info about HTTP request associated with
                              the entry.
 
+        :type resource: :class:`~google.cloud.logging.resource.Resource`
+        :param resource: Monitored resource of the entry, defaults
+                         to the global resource type.
+
         :type timestamp: :class:`datetime.datetime`
         :param timestamp: (optional) timestamp of event being logged.
         """
         client = self._require_client(client)
         entry_resource = self._make_entry_resource(
             info=info, labels=labels, insert_id=insert_id, severity=severity,
-            http_request=http_request, timestamp=timestamp)
+            http_request=http_request, timestamp=timestamp, resource=resource)
         client.logging_api.write_entries([entry_resource])
 
     def log_proto(self, message, client=None, labels=None, insert_id=None,
-                  severity=None, http_request=None, timestamp=None):
+                  severity=None, http_request=None, timestamp=None,
+                  resource=_GLOBAL_RESOURCE):
         """API call:  log a protobuf message via a POST request
 
         See:
@@ -268,13 +287,18 @@ def log_proto(self, message, client=None, labels=None, insert_id=None,
         :param http_request: (optional) info about HTTP request associated with
                              the entry.
 
+        :type resource: :class:`~google.cloud.logging.resource.Resource`
+        :param resource: Monitored resource of the entry, defaults
+                         to the global resource type.
+
         :type timestamp: :class:`datetime.datetime`
         :param timestamp: (optional) timestamp of event being logged.
         """
         client = self._require_client(client)
         entry_resource = self._make_entry_resource(
             message=message, labels=labels, insert_id=insert_id,
-            severity=severity, http_request=http_request, timestamp=timestamp)
+            severity=severity, http_request=http_request, timestamp=timestamp,
+            resource=resource)
         client.logging_api.write_entries([entry_resource])
 
     def delete(self, client=None):
@@ -344,11 +368,21 @@ class Batch(object):
 
     :type client: :class:`google.cloud.logging.client.Client`
     :param client: The client to use.
+
+    :type resource: :class:`~google.cloud.logging.resource.Resource`
+    :param resource: (Optional) Monitored resource of the batch, defaults
+                     to None, which requires that every entry should have a
+                     resource specified. Since the methods used to write
+                     entries default the entry's resource to the global
+                     resource type, this parameter is only required
+                     if explicitly set to None. If no entries' resource are
+                     set to None, this parameter will be ignored on the server.
     """
-    def __init__(self, logger, client):
+    def __init__(self, logger, client, resource=None):
         self.logger = logger
         self.entries = []
         self.client = client
+        self.resource = resource
 
     def __enter__(self):
         return self
@@ -358,7 +392,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
             self.commit()
 
     def log_text(self, text, labels=None, insert_id=None, severity=None,
-                 http_request=None, timestamp=None):
+                 http_request=None, timestamp=None, resource=_GLOBAL_RESOURCE):
         """Add a text entry to be logged during :meth:`commit`.
 
         :type text: str
@@ -379,13 +413,21 @@ def log_text(self, text, labels=None, insert_id=None, severity=None,
 
         :type timestamp: :class:`datetime.datetime`
         :param timestamp: (optional) timestamp of event being logged.
+
+        :type resource: :class:`~google.cloud.logging.resource.Resource`
+        :param resource: (Optional) Monitored resource of the entry. Defaults
+                         to the global resource type. If set to None, the
+                         resource of the batch is used for this entry. If
+                         both this resource and the Batch resource are None,
+                         the API will return an error.
         """
         self.entries.append(
             ('text', text, labels, insert_id, severity, http_request,
-             timestamp))
+             timestamp, resource))
 
     def log_struct(self, info, labels=None, insert_id=None, severity=None,
-                   http_request=None, timestamp=None):
+                   http_request=None, timestamp=None,
+                   resource=_GLOBAL_RESOURCE):
         """Add a struct entry to be logged during :meth:`commit`.
 
         :type info: dict
@@ -406,13 +448,21 @@ def log_struct(self, info, labels=None, insert_id=None, severity=None,
 
         :type timestamp: :class:`datetime.datetime`
         :param timestamp: (optional) timestamp of event being logged.
+
+        :type resource: :class:`~google.cloud.logging.resource.Resource`
+        :param resource: (Optional) Monitored resource of the entry. Defaults
+                         to the global resource type. If set to None, the
+                         resource of the batch is used for this entry. If
+                         both this resource and the Batch resource are None,
+                         the API will return an error.
         """
         self.entries.append(
             ('struct', info, labels, insert_id, severity, http_request,
-             timestamp))
+             timestamp, resource))
 
     def log_proto(self, message, labels=None, insert_id=None, severity=None,
-                  http_request=None, timestamp=None):
+                  http_request=None, timestamp=None,
+                  resource=_GLOBAL_RESOURCE):
         """Add a protobuf entry to be logged during :meth:`commit`.
 
         :type message: protobuf message
@@ -433,10 +483,17 @@ def log_proto(self, message, labels=None, insert_id=None, severity=None,
 
         :type timestamp: :class:`datetime.datetime`
         :param timestamp: (optional) timestamp of event being logged.
+
+        :type resource: :class:`~google.cloud.logging.resource.Resource`
+        :param resource: (Optional) Monitored resource of the entry. Defaults
+                         to the global resource type. If set to None, the
+                         resource of the batch is used for this entry. If
+                         both this resource and the Batch resource are None,
+                         the API will return an error.
         """
         self.entries.append(
             ('proto', message, labels, insert_id, severity, http_request,
-             timestamp))
+             timestamp, resource))
 
     def commit(self, client=None):
         """Send saved log entries as a single API call.
@@ -451,14 +508,16 @@ def commit(self, client=None):
 
         kwargs = {
             'logger_name': self.logger.full_name,
-            'resource': {'type': 'global'},
         }
+
+        if self.resource is not None:
+            kwargs['resource'] = self.resource._to_dict()
         if self.logger.labels is not None:
             kwargs['labels'] = self.logger.labels
 
         entries = []
         for (entry_type, entry, labels, iid, severity, http_req,
-             timestamp) in self.entries:
+             timestamp, resource) in self.entries:
             if entry_type == 'text':
                 info = {'textPayload': entry}
             elif entry_type == 'struct':
@@ -473,6 +532,8 @@ def commit(self, client=None):
                 info = {'protoPayload': MessageToDict(entry)}
             else:
                 raise ValueError('Unknown entry type: %s' % (entry_type,))
+            if resource is not None:
+                info['resource'] = resource._to_dict()
             if labels is not None:
                 info['labels'] = labels
             if iid is not None:
diff --git a/logging/google/cloud/logging/resource.py b/logging/google/cloud/logging/resource.py
new file mode 100644
--- /dev/null
+++ b/logging/google/cloud/logging/resource.py
@@ -0,0 +1,58 @@
+# Copyright 2017 Google Inc.
+#
+# 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.
+
+"""Monitored Resource for the Google Logging API V2."""
+
+import collections
+
+
+class Resource(collections.namedtuple('Resource', 'type labels')):
+    """A monitored resource identified by specifying values for all labels.
+
+    :type type: str
+    :param type: The resource type name.
+
+    :type labels: dict
+    :param labels: A mapping from label names to values for all labels
+                   enumerated in the associated :class:`ResourceDescriptor`.
+    """
+    __slots__ = ()
+
+    @classmethod
+    def _from_dict(cls, info):
+        """Construct a resource object from the parsed JSON representation.
+
+        :type info: dict
+        :param info:
+            A ``dict`` parsed from the JSON wire-format representation.
+
+        :rtype: :class:`Resource`
+        :returns: A resource object.
+        """
+        return cls(
+            type=info['type'],
+            labels=info.get('labels', {}),
+        )
+
+    def _to_dict(self):
+        """Build a dictionary ready to be serialized to the JSON format.
+
+        :rtype: dict
+        :returns: A dict representation of the object that can be written to
+                  the API.
+        """
+        return {
+            'type': self.type,
+            'labels': self.labels,
+        }


Test patch:
diff --git a/logging/tests/system.py b/logging/tests/system.py
--- a/logging/tests/system.py
+++ b/logging/tests/system.py
@@ -29,6 +29,7 @@
 from google.cloud.logging.handlers.handlers import CloudLoggingHandler
 from google.cloud.logging.handlers.transports import SyncTransport
 from google.cloud.logging import client
+from google.cloud.logging.resource import Resource
 
 from test_utils.retry import RetryErrors
 from test_utils.retry import RetryResult
@@ -171,6 +172,29 @@ def test_log_text_with_timestamp(self):
         self.assertEqual(entries[0].payload, text_payload)
         self.assertEqual(entries[0].timestamp, now.replace(tzinfo=UTC))
 
+    def test_log_text_with_resource(self):
+        text_payload = 'System test: test_log_text_with_timestamp'
+
+        logger = Config.CLIENT.logger(self._logger_name())
+        now = datetime.datetime.utcnow()
+        resource = Resource(
+            type='gae_app',
+            labels={
+                'module_id': 'default',
+                'version_id': 'test'
+            }
+        )
+
+        self.to_delete.append(logger)
+
+        logger.log_text(text_payload, timestamp=now, resource=resource)
+        entries = _list_entries(logger)
+        self.assertEqual(len(entries), 1)
+        self.assertEqual(entries[0].payload, text_payload)
+        # project_id is output only so we don't want it in assertion
+        del entries[0].resource.labels['project_id']
+        self.assertEqual(entries[0].resource, resource)
+
     def test_log_text_w_metadata(self):
         TEXT_PAYLOAD = 'System test: test_log_text'
         INSERT_ID = 'INSERTID'
diff --git a/logging/tests/unit/test_entries.py b/logging/tests/unit/test_entries.py
--- a/logging/tests/unit/test_entries.py
+++ b/logging/tests/unit/test_entries.py
@@ -67,9 +67,11 @@ def test_ctor_defaults(self):
         self.assertIsNone(entry.labels)
         self.assertIsNone(entry.severity)
         self.assertIsNone(entry.http_request)
+        self.assertIsNone(entry.resource)
 
     def test_ctor_explicit(self):
         import datetime
+        from google.cloud.logging.resource import Resource
 
         PAYLOAD = 'PAYLOAD'
         IID = 'IID'
@@ -84,13 +86,16 @@ def test_ctor_explicit(self):
             'requestUrl': URI,
             'status': STATUS,
         }
+        resource = Resource(type='global', labels={})
+
         logger = _Logger(self.LOGGER_NAME, self.PROJECT)
         entry = self._make_one(PAYLOAD, logger,
                                insert_id=IID,
                                timestamp=TIMESTAMP,
                                labels=LABELS,
                                severity=SEVERITY,
-                               http_request=REQUEST)
+                               http_request=REQUEST,
+                               resource=resource)
         self.assertEqual(entry.payload, PAYLOAD)
         self.assertIs(entry.logger, logger)
         self.assertEqual(entry.insert_id, IID)
@@ -100,6 +105,7 @@ def test_ctor_explicit(self):
         self.assertEqual(entry.http_request['requestMethod'], METHOD)
         self.assertEqual(entry.http_request['requestUrl'], URI)
         self.assertEqual(entry.http_request['status'], STATUS)
+        self.assertEqual(entry.resource, resource)
 
     def test_from_api_repr_missing_data_no_loggers(self):
         client = _Client(self.PROJECT)
@@ -124,6 +130,7 @@ def test_from_api_repr_missing_data_no_loggers(self):
     def test_from_api_repr_w_loggers_no_logger_match(self):
         from datetime import datetime
         from google.cloud._helpers import UTC
+        from google.cloud.logging.resource import Resource
 
         klass = self._get_target_class()
         client = _Client(self.PROJECT)
@@ -136,6 +143,16 @@ def test_from_api_repr_w_loggers_no_logger_match(self):
         LABELS = {'foo': 'bar', 'baz': 'qux'}
         METHOD = 'POST'
         URI = 'https://api.example.com/endpoint'
+        RESOURCE = Resource(
+            type='gae_app',
+            labels={
+                'type': 'gae_app',
+                'labels': {
+                    'module_id': 'default',
+                    'version': 'test',
+                }
+            }
+        )
         STATUS = '500'
         API_REPR = {
             'dummyPayload': PAYLOAD,
@@ -149,6 +166,7 @@ def test_from_api_repr_w_loggers_no_logger_match(self):
                 'requestUrl': URI,
                 'status': STATUS,
             },
+            'resource': RESOURCE._to_dict(),
         }
         loggers = {}
         entry = klass.from_api_repr(API_REPR, client, loggers=loggers)
@@ -165,6 +183,7 @@ def test_from_api_repr_w_loggers_no_logger_match(self):
         self.assertIs(logger.client, client)
         self.assertEqual(logger.name, self.LOGGER_NAME)
         self.assertEqual(loggers, {LOG_NAME: logger})
+        self.assertEqual(entry.resource, RESOURCE)
 
     def test_from_api_repr_w_loggers_w_logger_match(self):
         from datetime import datetime
diff --git a/logging/tests/unit/test_logger.py b/logging/tests/unit/test_logger.py
--- a/logging/tests/unit/test_logger.py
+++ b/logging/tests/unit/test_logger.py
@@ -96,6 +96,7 @@ def test_log_text_w_str_implicit_client(self):
             'textPayload': TEXT,
             'resource': {
                 'type': 'global',
+                'labels': {},
             },
         }]
         client = _Client(self.PROJECT)
@@ -116,6 +117,7 @@ def test_log_text_w_default_labels(self):
             'textPayload': TEXT,
             'resource': {
                 'type': 'global',
+                'labels': {},
             },
             'labels': DEFAULT_LABELS,
         }]
@@ -141,6 +143,7 @@ def test_log_text_w_timestamp(self):
             'timestamp': '2016-12-31T00:01:02.999999Z',
             'resource': {
                 'type': 'global',
+                'labels': {},
             },
         }]
         client = _Client(self.PROJECT)
@@ -172,6 +175,7 @@ def test_log_text_w_unicode_explicit_client_labels_severity_httpreq(self):
             'textPayload': TEXT,
             'resource': {
                 'type': 'global',
+                'labels': {},
             },
             'labels': LABELS,
             'insertId': IID,
@@ -198,6 +202,7 @@ def test_log_struct_w_implicit_client(self):
             'jsonPayload': STRUCT,
             'resource': {
                 'type': 'global',
+                'labels': {},
             },
         }]
         client = _Client(self.PROJECT)
@@ -218,6 +223,7 @@ def test_log_struct_w_default_labels(self):
             'jsonPayload': STRUCT,
             'resource': {
                 'type': 'global',
+                'labels': {},
             },
             'labels': DEFAULT_LABELS,
         }]
@@ -251,6 +257,7 @@ def test_log_struct_w_explicit_client_labels_severity_httpreq(self):
             'jsonPayload': STRUCT,
             'resource': {
                 'type': 'global',
+                'labels': {},
             },
             'labels': LABELS,
             'insertId': IID,
@@ -282,6 +289,7 @@ def test_log_struct_w_timestamp(self):
             'timestamp': '2016-12-31T00:01:02.999999Z',
             'resource': {
                 'type': 'global',
+                'labels': {},
             },
         }]
         client = _Client(self.PROJECT)
@@ -305,6 +313,7 @@ def test_log_proto_w_implicit_client(self):
             'protoPayload': json.loads(MessageToJson(message)),
             'resource': {
                 'type': 'global',
+                'labels': {},
             },
         }]
         client = _Client(self.PROJECT)
@@ -329,6 +338,7 @@ def test_log_proto_w_default_labels(self):
             'protoPayload': json.loads(MessageToJson(message)),
             'resource': {
                 'type': 'global',
+                'labels': {},
             },
             'labels': DEFAULT_LABELS,
         }]
@@ -367,6 +377,7 @@ def test_log_proto_w_explicit_client_labels_severity_httpreq(self):
             'protoPayload': json.loads(MessageToJson(message)),
             'resource': {
                 'type': 'global',
+                'labels': {},
             },
             'labels': LABELS,
             'insertId': IID,
@@ -402,6 +413,7 @@ def test_log_proto_w_timestamp(self):
             'timestamp': '2016-12-31T00:01:02.999999Z',
             'resource': {
                 'type': 'global',
+                'labels': {},
             },
         }]
         client = _Client(self.PROJECT)
@@ -530,16 +542,19 @@ def test_ctor_defaults(self):
         self.assertEqual(len(batch.entries), 0)
 
     def test_log_text_defaults(self):
+        from google.cloud.logging.logger import _GLOBAL_RESOURCE
         TEXT = 'This is the entry text'
         client = _Client(project=self.PROJECT, connection=_make_credentials())
         logger = _Logger()
         batch = self._make_one(logger, client=client)
         batch.log_text(TEXT)
         self.assertEqual(batch.entries,
-                         [('text', TEXT, None, None, None, None, None)])
+                         [('text', TEXT, None, None, None, None, None,
+                           _GLOBAL_RESOURCE)])
 
     def test_log_text_explicit(self):
         import datetime
+        from google.cloud.logging.resource import Resource
 
         TEXT = 'This is the entry text'
         LABELS = {'foo': 'bar', 'baz': 'qux'}
@@ -554,16 +569,26 @@ def test_log_text_explicit(self):
             'status': STATUS,
         }
         TIMESTAMP = datetime.datetime(2016, 12, 31, 0, 1, 2, 999999)
+        RESOURCE = Resource(
+            type='gae_app',
+            labels={
+                'module_id': 'default',
+                'version_id': 'test',
+        })
+
         client = _Client(project=self.PROJECT, connection=_make_credentials())
         logger = _Logger()
         batch = self._make_one(logger, client=client)
         batch.log_text(TEXT, labels=LABELS, insert_id=IID, severity=SEVERITY,
-                       http_request=REQUEST, timestamp=TIMESTAMP)
+                       http_request=REQUEST, timestamp=TIMESTAMP,
+                       resource=RESOURCE)
         self.assertEqual(
             batch.entries,
-            [('text', TEXT, LABELS, IID, SEVERITY, REQUEST, TIMESTAMP)])
+            [('text', TEXT, LABELS, IID, SEVERITY, REQUEST, TIMESTAMP,
+              RESOURCE)])
 
     def test_log_struct_defaults(self):
+        from google.cloud.logging.logger import _GLOBAL_RESOURCE
         STRUCT = {'message': 'Message text', 'weather': 'partly cloudy'}
         client = _Client(project=self.PROJECT, connection=_make_credentials())
         logger = _Logger()
@@ -571,10 +596,12 @@ def test_log_struct_defaults(self):
         batch.log_struct(STRUCT)
         self.assertEqual(
             batch.entries,
-            [('struct', STRUCT, None, None, None, None, None)])
+            [('struct', STRUCT, None, None, None, None, None,
+              _GLOBAL_RESOURCE)])
 
     def test_log_struct_explicit(self):
         import datetime
+        from google.cloud.logging.resource import Resource
 
         STRUCT = {'message': 'Message text', 'weather': 'partly cloudy'}
         LABELS = {'foo': 'bar', 'baz': 'qux'}
@@ -589,17 +616,27 @@ def test_log_struct_explicit(self):
             'status': STATUS,
         }
         TIMESTAMP = datetime.datetime(2016, 12, 31, 0, 1, 2, 999999)
+        RESOURCE = Resource(
+            type='gae_app',
+            labels={
+                'module_id': 'default',
+                'version_id': 'test',
+            }
+        )
+
         client = _Client(project=self.PROJECT, connection=_make_credentials())
         logger = _Logger()
         batch = self._make_one(logger, client=client)
         batch.log_struct(STRUCT, labels=LABELS, insert_id=IID,
                          severity=SEVERITY, http_request=REQUEST,
-                         timestamp=TIMESTAMP)
+                         timestamp=TIMESTAMP, resource=RESOURCE)
         self.assertEqual(
             batch.entries,
-            [('struct', STRUCT, LABELS, IID, SEVERITY, REQUEST, TIMESTAMP)])
+            [('struct', STRUCT, LABELS, IID, SEVERITY, REQUEST, TIMESTAMP ,
+              RESOURCE)])
 
     def test_log_proto_defaults(self):
+        from google.cloud.logging.logger import _GLOBAL_RESOURCE
         from google.protobuf.struct_pb2 import Struct
         from google.protobuf.struct_pb2 import Value
 
@@ -609,10 +646,12 @@ def test_log_proto_defaults(self):
         batch = self._make_one(logger, client=client)
         batch.log_proto(message)
         self.assertEqual(batch.entries,
-                         [('proto', message, None, None, None, None, None)])
+                         [('proto', message, None, None, None, None, None,
+                           _GLOBAL_RESOURCE)])
 
     def test_log_proto_explicit(self):
         import datetime
+        from google.cloud.logging.resource import Resource
         from google.protobuf.struct_pb2 import Struct
         from google.protobuf.struct_pb2 import Value
 
@@ -629,24 +668,61 @@ def test_log_proto_explicit(self):
             'status': STATUS,
         }
         TIMESTAMP = datetime.datetime(2016, 12, 31, 0, 1, 2, 999999)
+        RESOURCE = Resource(
+            type='gae_app',
+            labels={
+                'module_id': 'default',
+                'version_id': 'test',
+            }
+        )
         client = _Client(project=self.PROJECT, connection=_make_credentials())
         logger = _Logger()
         batch = self._make_one(logger, client=client)
         batch.log_proto(message, labels=LABELS, insert_id=IID,
                         severity=SEVERITY, http_request=REQUEST,
-                        timestamp=TIMESTAMP)
+                        timestamp=TIMESTAMP, resource=RESOURCE)
         self.assertEqual(
             batch.entries,
-            [('proto', message, LABELS, IID, SEVERITY, REQUEST, TIMESTAMP)])
+            [('proto', message, LABELS, IID, SEVERITY, REQUEST, TIMESTAMP,
+              RESOURCE)])
 
     def test_commit_w_invalid_entry_type(self):
         logger = _Logger()
         client = _Client(project=self.PROJECT, connection=_make_credentials())
         batch = self._make_one(logger, client)
-        batch.entries.append(('bogus', 'BOGUS', None, None, None, None, None))
+        batch.entries.append(('bogus', 'BOGUS', None, None, None, None, None,
+                              None))
         with self.assertRaises(ValueError):
             batch.commit()
 
+    def test_commit_w_resource_specified(self):
+        from google.cloud.logging.logger import _GLOBAL_RESOURCE
+        from google.cloud.logging.resource import Resource
+
+        logger = _Logger()
+        client = _Client(project=self.PROJECT, connection=_make_credentials())
+        api = client.logging_api = _DummyLoggingAPI()
+        RESOURCE = Resource(
+            type='gae_app',
+            labels={
+                'module_id': 'default',
+                'version_id': 'test',
+            }
+        )
+
+        batch = self._make_one(logger, client, resource=RESOURCE)
+        MESSAGE = 'This is the entry text'
+        ENTRIES = [
+            {'textPayload': MESSAGE},
+            {'textPayload': MESSAGE, 'resource': _GLOBAL_RESOURCE._to_dict()},
+        ]
+        batch.log_text(MESSAGE, resource=None)
+        batch.log_text(MESSAGE)
+        batch.commit()
+        self.assertEqual(api._write_entries_called_with,
+                         (ENTRIES, logger.full_name,
+                          RESOURCE._to_dict(), None))
+
     def test_commit_w_bound_client(self):
         import json
         import datetime
@@ -654,6 +730,8 @@ def test_commit_w_bound_client(self):
         from google.protobuf.struct_pb2 import Struct
         from google.protobuf.struct_pb2 import Value
         from google.cloud._helpers import _datetime_to_rfc3339
+        from google.cloud.logging.logger import _GLOBAL_RESOURCE
+
 
         TEXT = 'This is the entry text'
         STRUCT = {'message': TEXT, 'weather': 'partly cloudy'}
@@ -664,17 +742,17 @@ def test_commit_w_bound_client(self):
         TIMESTAMP1 = datetime.datetime(2016, 12, 31, 0, 0, 1, 999999)
         TIMESTAMP2 = datetime.datetime(2016, 12, 31, 0, 0, 2, 999999)
         TIMESTAMP3 = datetime.datetime(2016, 12, 31, 0, 0, 3, 999999)
-        RESOURCE = {
-            'type': 'global',
-        }
         ENTRIES = [
             {'textPayload': TEXT, 'insertId': IID1,
-             'timestamp': _datetime_to_rfc3339(TIMESTAMP1)},
+             'timestamp': _datetime_to_rfc3339(TIMESTAMP1),
+             'resource': _GLOBAL_RESOURCE._to_dict()},
             {'jsonPayload': STRUCT, 'insertId': IID2,
-             'timestamp': _datetime_to_rfc3339(TIMESTAMP2)},
+             'timestamp': _datetime_to_rfc3339(TIMESTAMP2),
+             'resource': _GLOBAL_RESOURCE._to_dict()},
             {'protoPayload': json.loads(MessageToJson(message)),
              'insertId': IID3,
-             'timestamp': _datetime_to_rfc3339(TIMESTAMP3)},
+             'timestamp': _datetime_to_rfc3339(TIMESTAMP3),
+             'resource': _GLOBAL_RESOURCE._to_dict()},
         ]
         client = _Client(project=self.PROJECT)
         api = client.logging_api = _DummyLoggingAPI()
@@ -688,7 +766,7 @@ def test_commit_w_bound_client(self):
 
         self.assertEqual(list(batch.entries), [])
         self.assertEqual(api._write_entries_called_with,
-                         (ENTRIES, logger.full_name, RESOURCE, None))
+                         (ENTRIES, logger.full_name, None, None))
 
     def test_commit_w_alternate_client(self):
         import json
@@ -696,6 +774,7 @@ def test_commit_w_alternate_client(self):
         from google.protobuf.struct_pb2 import Struct
         from google.protobuf.struct_pb2 import Value
         from google.cloud.logging.logger import Logger
+        from google.cloud.logging.logger import _GLOBAL_RESOURCE
 
         TEXT = 'This is the entry text'
         STRUCT = {'message': TEXT, 'weather': 'partly cloudy'}
@@ -718,12 +797,14 @@ def test_commit_w_alternate_client(self):
         client2 = _Client(project=self.PROJECT)
         api = client2.logging_api = _DummyLoggingAPI()
         logger = Logger('logger_name', client1, labels=DEFAULT_LABELS)
-        RESOURCE = {'type': 'global'}
         ENTRIES = [
-            {'textPayload': TEXT, 'labels': LABELS},
-            {'jsonPayload': STRUCT, 'severity': SEVERITY},
+            {'textPayload': TEXT, 'labels': LABELS, 'resource':
+                _GLOBAL_RESOURCE._to_dict()},
+            {'jsonPayload': STRUCT, 'severity': SEVERITY,
+             'resource': _GLOBAL_RESOURCE._to_dict()},
             {'protoPayload': json.loads(MessageToJson(message)),
-             'httpRequest': REQUEST},
+             'httpRequest': REQUEST,
+             'resource': _GLOBAL_RESOURCE._to_dict()},
         ]
         batch = self._make_one(logger, client=client1)
 
@@ -734,7 +815,7 @@ def test_commit_w_alternate_client(self):
 
         self.assertEqual(list(batch.entries), [])
         self.assertEqual(api._write_entries_called_with,
-                         (ENTRIES, logger.full_name, RESOURCE, DEFAULT_LABELS))
+                         (ENTRIES, logger.full_name, None, DEFAULT_LABELS))
 
     def test_context_mgr_success(self):
         import json
@@ -742,6 +823,8 @@ def test_context_mgr_success(self):
         from google.protobuf.struct_pb2 import Struct
         from google.protobuf.struct_pb2 import Value
         from google.cloud.logging.logger import Logger
+        from google.cloud.logging.logger import _GLOBAL_RESOURCE
+
 
         TEXT = 'This is the entry text'
         STRUCT = {'message': TEXT, 'weather': 'partly cloudy'}
@@ -760,13 +843,13 @@ def test_context_mgr_success(self):
         client = _Client(project=self.PROJECT)
         api = client.logging_api = _DummyLoggingAPI()
         logger = Logger('logger_name', client, labels=DEFAULT_LABELS)
-        RESOURCE = {
-            'type': 'global',
-        }
         ENTRIES = [
-            {'textPayload': TEXT, 'httpRequest': REQUEST},
-            {'jsonPayload': STRUCT, 'labels': LABELS},
+            {'textPayload': TEXT, 'httpRequest': REQUEST,
+             'resource': _GLOBAL_RESOURCE._to_dict()},
+            {'jsonPayload': STRUCT, 'labels': LABELS,
+             'resource': _GLOBAL_RESOURCE._to_dict()},
             {'protoPayload': json.loads(MessageToJson(message)),
+             'resource': _GLOBAL_RESOURCE._to_dict(),
              'severity': SEVERITY},
         ]
         batch = self._make_one(logger, client=client)
@@ -778,12 +861,13 @@ def test_context_mgr_success(self):
 
         self.assertEqual(list(batch.entries), [])
         self.assertEqual(api._write_entries_called_with,
-                         (ENTRIES, logger.full_name, RESOURCE, DEFAULT_LABELS))
+                         (ENTRIES, logger.full_name, None, DEFAULT_LABELS))
 
     def test_context_mgr_failure(self):
         import datetime
         from google.protobuf.struct_pb2 import Struct
         from google.protobuf.struct_pb2 import Value
+        from google.cloud.logging.logger import _GLOBAL_RESOURCE
 
         TEXT = 'This is the entry text'
         STRUCT = {'message': TEXT, 'weather': 'partly cloudy'}
@@ -804,9 +888,12 @@ def test_context_mgr_failure(self):
         api = client.logging_api = _DummyLoggingAPI()
         logger = _Logger()
         UNSENT = [
-            ('text', TEXT, None, IID, None, None, TIMESTAMP),
-            ('struct', STRUCT, None, None, SEVERITY, None, None),
-            ('proto', message, LABELS, None, None, REQUEST, None),
+            ('text', TEXT, None, IID, None, None, TIMESTAMP,
+             _GLOBAL_RESOURCE),
+            ('struct', STRUCT, None, None, SEVERITY, None, None,
+             _GLOBAL_RESOURCE),
+            ('proto', message, LABELS, None, None, REQUEST, None,
+             _GLOBAL_RESOURCE),
         ]
         batch = self._make_one(logger, client=client)