Repository: googleapis/google-cloud-python

Issue:
Where are the allowed log entry protobuf types documented?
The [`LogEntry` API docs](https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/LogEntry#FIELDS.proto_payload) say:

```
The log entry payload, represented as a protocol buffer. You can only use protoPayload values that belong to a set of approved types.

An object containing fields of an arbitrary type. An additional field "@type" contains a URI identifying the type. Example: { "id": 1234, "@type": "types.example.com/standard/id" }.
```

Where are the allowed protobuf types documented, and the corresponding URIs?



Discussion:
The [discovery docs](https://www.googleapis.com/discovery/v1/apis/logging/v2beta1/rest) don't provide any more information about the allowed types.

:+1: 

Absent any way to type-check the inputs, I'm thinking we just punt on allowing users to create entries with `protoPayload` set:  we will still parse those created via other services (#1578).

Yeah punt until we hear from real users. @jgeewax WDYT?

That's one option.

Another is that we can do zero client-side checking, send the protobuf as it is. If it's an invalid type it'll get rejected, right ? That might be the better way to go as different versions of the API might allow new types...

OK, I guess we just use `google.protobuf.json_format.MessageToJson` to render whatever message is passed, and then re-parse it using `json.loads`.

I will also add a helper which parses/merges the JSON `protoPayload` into a user-supplied protobuf messge object using `google.protobuf.json_format.Parse`, after dumping the value via `json.dumps`.

I wish `google.protobuf.json_format._AnyMessageToJsonObject` [wasn't private](https://github.com/google/protobuf/blob/9240acd179708c3a2ab79508af6fd68efa6f883a/python/google/protobuf/json_format.py#L207) -- it looks like exactly what I need.

https://github.com/google/protobuf/issues/1351


Resolving patch:
diff --git a/gcloud/logging/entries.py b/gcloud/logging/entries.py
--- a/gcloud/logging/entries.py
+++ b/gcloud/logging/entries.py
@@ -14,6 +14,10 @@
 
 """Log entries within the Google Cloud Logging API."""
 
+import json
+
+from google.protobuf.json_format import Parse
+
 from gcloud._helpers import _rfc3339_nanos_to_datetime
 from gcloud.logging._helpers import logger_name_from_path
 
@@ -100,3 +104,13 @@ class ProtobufEntry(_BaseEntry):
     https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/LogEntry
     """
     _PAYLOAD_KEY = 'protoPayload'
+
+    def parse_message(self, message):
+        """Parse payload into a protobuf message.
+
+        Mutates the passed-in ``message`` in place.
+
+        :type message: Protobuf message
+        :param message: the message to be logged
+        """
+        Parse(json.dumps(self.payload), message)
diff --git a/gcloud/logging/logger.py b/gcloud/logging/logger.py
--- a/gcloud/logging/logger.py
+++ b/gcloud/logging/logger.py
@@ -14,6 +14,10 @@
 
 """Define API Loggers."""
 
+import json
+
+from google.protobuf.json_format import MessageToJson
+
 
 class Logger(object):
     """Loggers represent named targets for log entries.
@@ -89,7 +93,7 @@ def log_text(self, text, client=None):
             method='POST', path='/entries:write', data=data)
 
     def log_struct(self, info, client=None):
-        """API call:  log a text message via a POST request
+        """API call:  log a structured message via a POST request
 
         See:
         https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/entries/write
@@ -115,6 +119,35 @@ def log_struct(self, info, client=None):
         client.connection.api_request(
             method='POST', path='/entries:write', data=data)
 
+    def log_proto(self, message, client=None):
+        """API call:  log a protobuf message via a POST request
+
+        See:
+        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/entries/write
+
+        :type message: Protobuf message
+        :param message: the message to be logged
+
+        :type client: :class:`gcloud.logging.client.Client` or ``NoneType``
+        :param client: the client to use.  If not passed, falls back to the
+                       ``client`` stored on the current logger.
+        """
+        client = self._require_client(client)
+        as_json_str = MessageToJson(message)
+        as_json = json.loads(as_json_str)
+
+        data = {
+            'entries': [{
+                'logName': self.full_name,
+                'protoPayload': as_json,
+                'resource': {
+                    'type': 'global',
+                },
+            }],
+        }
+        client.connection.api_request(
+            method='POST', path='/entries:write', data=data)
+
     def delete(self, client=None):
         """API call:  delete all entries in a logger via a DELETE request
 


Test patch:
diff --git a/gcloud/logging/test_entries.py b/gcloud/logging/test_entries.py
--- a/gcloud/logging/test_entries.py
+++ b/gcloud/logging/test_entries.py
@@ -122,6 +122,31 @@ def test_from_api_repr_w_loggers_w_logger_match(self):
         self.assertTrue(entry.logger is LOGGER)
 
 
+class TestProtobufEntry(unittest2.TestCase):
+
+    PROJECT = 'PROJECT'
+    LOGGER_NAME = 'LOGGER_NAME'
+
+    def _getTargetClass(self):
+        from gcloud.logging.entries import ProtobufEntry
+        return ProtobufEntry
+
+    def _makeOne(self, *args, **kw):
+        return self._getTargetClass()(*args, **kw)
+
+    def test_parse_message(self):
+        import json
+        from google.protobuf.json_format import MessageToJson
+        from google.protobuf.struct_pb2 import Struct, Value
+        LOGGER = object()
+        message = Struct(fields={'foo': Value(bool_value=False)})
+        with_true = Struct(fields={'foo': Value(bool_value=True)})
+        PAYLOAD = json.loads(MessageToJson(with_true))
+        entry = self._makeOne(PAYLOAD, LOGGER)
+        entry.parse_message(message)
+        self.assertTrue(message.fields['foo'])
+
+
 def _datetime_to_rfc3339_w_nanos(value):
     from gcloud._helpers import _RFC3339_NO_FRACTION
     no_fraction = value.strftime(_RFC3339_NO_FRACTION)
diff --git a/gcloud/logging/test_logger.py b/gcloud/logging/test_logger.py
--- a/gcloud/logging/test_logger.py
+++ b/gcloud/logging/test_logger.py
@@ -127,6 +127,57 @@ def test_log_struct_w_explicit_client(self):
         self.assertEqual(req['path'], '/entries:write')
         self.assertEqual(req['data'], SENT)
 
+    def test_log_proto_w_implicit_client(self):
+        import json
+        from google.protobuf.json_format import MessageToJson
+        from google.protobuf.struct_pb2 import Struct, Value
+        message = Struct(fields={'foo': Value(bool_value=True)})
+        conn = _Connection({})
+        client = _Client(self.PROJECT, conn)
+        logger = self._makeOne(self.LOGGER_NAME, client=client)
+        logger.log_proto(message)
+        self.assertEqual(len(conn._requested), 1)
+        req = conn._requested[0]
+        SENT = {
+            'entries': [{
+                'logName': 'projects/%s/logs/%s' % (
+                    self.PROJECT, self.LOGGER_NAME),
+                'protoPayload': json.loads(MessageToJson(message)),
+                'resource': {
+                    'type': 'global',
+                },
+            }],
+        }
+        self.assertEqual(req['method'], 'POST')
+        self.assertEqual(req['path'], '/entries:write')
+        self.assertEqual(req['data'], SENT)
+
+    def test_log_proto_w_explicit_client(self):
+        import json
+        from google.protobuf.json_format import MessageToJson
+        from google.protobuf.struct_pb2 import Struct, Value
+        message = Struct(fields={'foo': Value(bool_value=True)})
+        conn = _Connection({})
+        client1 = _Client(self.PROJECT, object())
+        client2 = _Client(self.PROJECT, conn)
+        logger = self._makeOne(self.LOGGER_NAME, client=client1)
+        logger.log_proto(message, client=client2)
+        self.assertEqual(len(conn._requested), 1)
+        req = conn._requested[0]
+        SENT = {
+            'entries': [{
+                'logName': 'projects/%s/logs/%s' % (
+                    self.PROJECT, self.LOGGER_NAME),
+                'protoPayload': json.loads(MessageToJson(message)),
+                'resource': {
+                    'type': 'global',
+                },
+            }],
+        }
+        self.assertEqual(req['method'], 'POST')
+        self.assertEqual(req['path'], '/entries:write')
+        self.assertEqual(req['data'], SENT)
+
     def test_delete_w_bound_client(self):
         PATH = 'projects/%s/logs/%s' % (self.PROJECT, self.LOGGER_NAME)
         conn = _Connection({})