From dfd815abc83fdb301613fc2254be7b5fcd546984 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 28 Mar 2016 13:27:36 -0400 Subject: [PATCH 1/7] Track labels set on an entry by the backend. --- gcloud/logging/entries.py | 10 ++++++++-- gcloud/logging/test_entries.py | 11 ++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/gcloud/logging/entries.py b/gcloud/logging/entries.py index d94d7d984a1a..ca1cf62f5db4 100644 --- a/gcloud/logging/entries.py +++ b/gcloud/logging/entries.py @@ -37,12 +37,17 @@ class _BaseEntry(object): :type timestamp: :class:`datetime.datetime`, or :class:`NoneType` :param timestamp: (optional) timestamp for the entry + + :type labels: dict or :class:`NoneType` + :param labels: (optional) mapping of labels for the entry """ - def __init__(self, payload, logger, insert_id=None, timestamp=None): + def __init__(self, payload, logger, + insert_id=None, timestamp=None, labels=None): self.payload = payload self.logger = logger self.insert_id = insert_id self.timestamp = timestamp + self.labels = labels @classmethod def from_api_repr(cls, resource, client, loggers=None): @@ -76,7 +81,8 @@ def from_api_repr(cls, resource, client, loggers=None): timestamp = resource.get('timestamp') if timestamp is not None: timestamp = _rfc3339_nanos_to_datetime(timestamp) - return cls(payload, logger, insert_id, timestamp) + labels = resource.get('labels') + return cls(payload, logger, insert_id, timestamp, labels) class TextEntry(_BaseEntry): diff --git a/gcloud/logging/test_entries.py b/gcloud/logging/test_entries.py index 4505c7655ff6..2da275d71ea0 100644 --- a/gcloud/logging/test_entries.py +++ b/gcloud/logging/test_entries.py @@ -39,18 +39,21 @@ def test_ctor_defaults(self): self.assertTrue(entry.logger is logger) self.assertTrue(entry.insert_id is None) self.assertTrue(entry.timestamp is None) + self.assertTrue(entry.labels is None) def test_ctor_explicit(self): import datetime PAYLOAD = 'PAYLOAD' IID = 'IID' TIMESTAMP = datetime.datetime.now() + LABELS = {'foo': 'bar', 'baz': 'qux'} logger = _Logger(self.LOGGER_NAME, self.PROJECT) - entry = self._makeOne(PAYLOAD, logger, IID, TIMESTAMP) + entry = self._makeOne(PAYLOAD, logger, IID, TIMESTAMP, LABELS) self.assertEqual(entry.payload, PAYLOAD) self.assertTrue(entry.logger is logger) self.assertEqual(entry.insert_id, IID) self.assertEqual(entry.timestamp, TIMESTAMP) + self.assertEqual(entry.labels, LABELS) def test_from_api_repr_missing_data_no_loggers(self): client = _Client(self.PROJECT) @@ -79,11 +82,13 @@ def test_from_api_repr_w_loggers_no_logger_match(self): NOW = datetime.utcnow().replace(tzinfo=UTC) TIMESTAMP = _datetime_to_rfc3339_w_nanos(NOW) LOG_NAME = 'projects/%s/logs/%s' % (self.PROJECT, self.LOGGER_NAME) + LABELS = {'foo': 'bar', 'baz': 'qux'} API_REPR = { 'dummyPayload': PAYLOAD, 'logName': LOG_NAME, 'insertId': IID, 'timestamp': TIMESTAMP, + 'labels': LABELS, } loggers = {} klass = self._getTargetClass() @@ -91,6 +96,7 @@ def test_from_api_repr_w_loggers_no_logger_match(self): self.assertEqual(entry.payload, PAYLOAD) self.assertEqual(entry.insert_id, IID) self.assertEqual(entry.timestamp, NOW) + self.assertEqual(entry.labels, LABELS) logger = entry.logger self.assertTrue(isinstance(logger, _Logger)) self.assertTrue(logger.client is client) @@ -106,11 +112,13 @@ def test_from_api_repr_w_loggers_w_logger_match(self): NOW = datetime.utcnow().replace(tzinfo=UTC) TIMESTAMP = _datetime_to_rfc3339_w_nanos(NOW) LOG_NAME = 'projects/%s/logs/%s' % (self.PROJECT, self.LOGGER_NAME) + LABELS = {'foo': 'bar', 'baz': 'qux'} API_REPR = { 'dummyPayload': PAYLOAD, 'logName': LOG_NAME, 'insertId': IID, 'timestamp': TIMESTAMP, + 'labels': LABELS, } LOGGER = object() loggers = {LOG_NAME: LOGGER} @@ -119,6 +127,7 @@ def test_from_api_repr_w_loggers_w_logger_match(self): self.assertEqual(entry.payload, PAYLOAD) self.assertEqual(entry.insert_id, IID) self.assertEqual(entry.timestamp, NOW) + self.assertEqual(entry.labels, LABELS) self.assertTrue(entry.logger is LOGGER) From 2c4c5b331589866b62c93f91b014e92f333d8537 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 28 Mar 2016 13:30:55 -0400 Subject: [PATCH 2/7] Allow setting default labels on a logger instance. --- gcloud/logging/logger.py | 7 ++++++- gcloud/logging/test_logger.py | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/gcloud/logging/logger.py b/gcloud/logging/logger.py index 59c77d49f5e9..30521a9711a0 100644 --- a/gcloud/logging/logger.py +++ b/gcloud/logging/logger.py @@ -31,10 +31,15 @@ class Logger(object): :type client: :class:`gcloud.logging.client.Client` :param client: A client which holds credentials and project configuration for the logger (which requires a project). + + :type labels: dict or :class:`NoneType` + :param labels: (optional) mapping of default labels for entries written + via this logger. """ - def __init__(self, name, client): + def __init__(self, name, client, labels=None): self.name = name self._client = client + self.labels = labels @property def client(self): diff --git a/gcloud/logging/test_logger.py b/gcloud/logging/test_logger.py index a155ce693fa9..bfed47cb8ea1 100644 --- a/gcloud/logging/test_logger.py +++ b/gcloud/logging/test_logger.py @@ -27,7 +27,7 @@ def _getTargetClass(self): def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) - def test_ctor(self): + def test_ctor_defaults(self): conn = _Connection() client = _Client(self.PROJECT, conn) logger = self._makeOne(self.LOGGER_NAME, client=client) @@ -36,6 +36,19 @@ def test_ctor(self): self.assertEqual(logger.project, self.PROJECT) self.assertEqual(logger.full_name, 'projects/%s/logs/%s' % (self.PROJECT, self.LOGGER_NAME)) + self.assertEqual(logger.labels, None) + + def test_ctor_explicit(self): + LABELS = {'foo': 'bar', 'baz': 'qux'} + conn = _Connection() + client = _Client(self.PROJECT, conn) + logger = self._makeOne(self.LOGGER_NAME, client=client, labels=LABELS) + self.assertEqual(logger.name, self.LOGGER_NAME) + self.assertTrue(logger.client is client) + self.assertEqual(logger.project, self.PROJECT) + self.assertEqual(logger.full_name, 'projects/%s/logs/%s' + % (self.PROJECT, self.LOGGER_NAME)) + self.assertEqual(logger.labels, LABELS) def test_batch_w_bound_client(self): from gcloud.logging.logger import Batch From 64e2f1cf9094871dce158dc61476210540213f7c Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 28 Mar 2016 13:49:13 -0400 Subject: [PATCH 3/7] Add support for logging entries with labels. Labels can be passed in explicitly to the 'log_text', 'log_struct', or 'log_proto' methods. If not passed, any default values configured on the logger instance will be used. See: #1566. --- gcloud/logging/logger.py | 46 ++++++++++++++- gcloud/logging/test_logger.py | 108 +++++++++++++++++++++++++++++++--- 2 files changed, 142 insertions(+), 12 deletions(-) diff --git a/gcloud/logging/logger.py b/gcloud/logging/logger.py index 30521a9711a0..2d76c699567c 100644 --- a/gcloud/logging/logger.py +++ b/gcloud/logging/logger.py @@ -83,7 +83,23 @@ def batch(self, client=None): client = self._require_client(client) return Batch(self, client) - def log_text(self, text, client=None): + def _get_labels(self, labels): + """Return effective labels. + + Helper for :meth:`log_text`, :meth:`log_struct`, and :meth:`log_proto`. + + :type labels: dict or :class:`NoneType` + :param labels: labels passed in to calling method. + + :rtype: dict or :class:`NoneType`. + :returns: the passed-in labels, if not none, else any default labels + configured on the logger instance. + """ + if labels is not None: + return labels + return self.labels + + def log_text(self, text, client=None, labels=None): """API call: log a text message via a POST request See: @@ -95,6 +111,9 @@ def log_text(self, text, client=None): :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. + + :type labels: dict or :class:`NoneType` + :param labels: (optional) mapping of labels for the entry. """ client = self._require_client(client) @@ -107,10 +126,15 @@ def log_text(self, text, client=None): }, }], } + + labels = self._get_labels(labels) + if labels is not None: + data['entries'][0]['labels'] = labels + client.connection.api_request( method='POST', path='/entries:write', data=data) - def log_struct(self, info, client=None): + def log_struct(self, info, client=None, labels=None): """API call: log a structured message via a POST request See: @@ -122,6 +146,9 @@ def log_struct(self, info, client=None): :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. + + :type labels: dict or :class:`NoneType` + :param labels: (optional) mapping of labels for the entry. """ client = self._require_client(client) @@ -134,10 +161,15 @@ def log_struct(self, info, client=None): }, }], } + + labels = self._get_labels(labels) + if labels is not None: + data['entries'][0]['labels'] = labels + client.connection.api_request( method='POST', path='/entries:write', data=data) - def log_proto(self, message, client=None): + def log_proto(self, message, client=None, labels=None): """API call: log a protobuf message via a POST request See: @@ -149,6 +181,9 @@ def log_proto(self, message, client=None): :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. + + :type labels: dict or :class:`NoneType` + :param labels: (optional) mapping of labels for the entry. """ client = self._require_client(client) as_json_str = MessageToJson(message) @@ -163,6 +198,11 @@ def log_proto(self, message, client=None): }, }], } + + labels = self._get_labels(labels) + if labels is not None: + data['entries'][0]['labels'] = labels + client.connection.api_request( method='POST', path='/entries:write', data=data) diff --git a/gcloud/logging/test_logger.py b/gcloud/logging/test_logger.py index bfed47cb8ea1..7849c9d43013 100644 --- a/gcloud/logging/test_logger.py +++ b/gcloud/logging/test_logger.py @@ -94,13 +94,41 @@ def test_log_text_w_str_implicit_client(self): self.assertEqual(req['path'], '/entries:write') self.assertEqual(req['data'], SENT) - def test_log_text_w_unicode_explicit_client(self): + def test_log_text_w_default_labels(self): + TEXT = 'TEXT' + DEFAULT_LABELS = {'foo': 'spam'} + conn = _Connection({}) + client = _Client(self.PROJECT, conn) + logger = self._makeOne(self.LOGGER_NAME, client=client, + labels=DEFAULT_LABELS) + logger.log_text(TEXT) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + SENT = { + 'entries': [{ + 'logName': 'projects/%s/logs/%s' % ( + self.PROJECT, self.LOGGER_NAME), + 'textPayload': TEXT, + 'resource': { + 'type': 'global', + }, + 'labels': DEFAULT_LABELS, + }], + } + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/entries:write') + self.assertEqual(req['data'], SENT) + + def test_log_text_w_unicode_explicit_client_and_labels(self): TEXT = u'TEXT' + DEFAULT_LABELS = {'foo': 'spam'} + LABELS = {'foo': 'bar', 'baz': 'qux'} conn = _Connection({}) client1 = _Client(self.PROJECT, object()) client2 = _Client(self.PROJECT, conn) - logger = self._makeOne(self.LOGGER_NAME, client=client1) - logger.log_text(TEXT, client=client2) + logger = self._makeOne(self.LOGGER_NAME, client=client1, + labels=DEFAULT_LABELS) + logger.log_text(TEXT, client=client2, labels=LABELS) self.assertEqual(len(conn._requested), 1) req = conn._requested[0] SENT = { @@ -111,6 +139,7 @@ def test_log_text_w_unicode_explicit_client(self): 'resource': { 'type': 'global', }, + 'labels': LABELS, }], } self.assertEqual(req['method'], 'POST') @@ -139,13 +168,41 @@ def test_log_struct_w_implicit_client(self): self.assertEqual(req['path'], '/entries:write') self.assertEqual(req['data'], SENT) - def test_log_struct_w_explicit_client(self): + def test_log_struct_w_default_labels(self): STRUCT = {'message': 'MESSAGE', 'weather': 'cloudy'} + DEFAULT_LABELS = {'foo': 'spam'} + conn = _Connection({}) + client = _Client(self.PROJECT, conn) + logger = self._makeOne(self.LOGGER_NAME, client=client, + labels=DEFAULT_LABELS) + logger.log_struct(STRUCT) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + SENT = { + 'entries': [{ + 'logName': 'projects/%s/logs/%s' % ( + self.PROJECT, self.LOGGER_NAME), + 'jsonPayload': STRUCT, + 'resource': { + 'type': 'global', + }, + 'labels': DEFAULT_LABELS, + }], + } + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/entries:write') + self.assertEqual(req['data'], SENT) + + def test_log_struct_w_explicit_client_and_labels(self): + STRUCT = {'message': 'MESSAGE', 'weather': 'cloudy'} + DEFAULT_LABELS = {'foo': 'spam'} + LABELS = {'foo': 'bar', 'baz': 'qux'} conn = _Connection({}) client1 = _Client(self.PROJECT, object()) client2 = _Client(self.PROJECT, conn) - logger = self._makeOne(self.LOGGER_NAME, client=client1) - logger.log_struct(STRUCT, client=client2) + logger = self._makeOne(self.LOGGER_NAME, client=client1, + labels=DEFAULT_LABELS) + logger.log_struct(STRUCT, client=client2, labels=LABELS) self.assertEqual(len(conn._requested), 1) req = conn._requested[0] SENT = { @@ -156,6 +213,7 @@ def test_log_struct_w_explicit_client(self): 'resource': { 'type': 'global', }, + 'labels': LABELS, }], } self.assertEqual(req['method'], 'POST') @@ -187,16 +245,47 @@ def test_log_proto_w_implicit_client(self): self.assertEqual(req['path'], '/entries:write') self.assertEqual(req['data'], SENT) - def test_log_proto_w_explicit_client(self): + def test_log_proto_w_default_labels(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)}) + DEFAULT_LABELS = {'foo': 'spam'} + conn = _Connection({}) + client = _Client(self.PROJECT, conn) + logger = self._makeOne(self.LOGGER_NAME, client=client, + labels=DEFAULT_LABELS) + 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', + }, + 'labels': DEFAULT_LABELS, + }], + } + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/entries:write') + self.assertEqual(req['data'], SENT) + + def test_log_proto_w_explicit_client_and_labels(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)}) + DEFAULT_LABELS = {'foo': 'spam'} + LABELS = {'foo': 'bar', 'baz': 'qux'} 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) + logger = self._makeOne(self.LOGGER_NAME, client=client1, + labels=DEFAULT_LABELS) + logger.log_proto(message, client=client2, labels=LABELS) self.assertEqual(len(conn._requested), 1) req = conn._requested[0] SENT = { @@ -207,6 +296,7 @@ def test_log_proto_w_explicit_client(self): 'resource': { 'type': 'global', }, + 'labels': LABELS, }], } self.assertEqual(req['method'], 'POST') From 53df5fca4c68f59eb412a2e5890a0a4cf9bd8307 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 28 Mar 2016 14:27:23 -0400 Subject: [PATCH 4/7] Add 'Logger.path' property. It was needed earlier for batch support, but was masked by the attribute on the mock '_Logger' used by the batch tests. --- gcloud/logging/logger.py | 8 ++++++-- gcloud/logging/test_logger.py | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/gcloud/logging/logger.py b/gcloud/logging/logger.py index 2d76c699567c..4477df260572 100644 --- a/gcloud/logging/logger.py +++ b/gcloud/logging/logger.py @@ -56,6 +56,11 @@ def full_name(self): """Fully-qualified name used in logging APIs""" return 'projects/%s/logs/%s' % (self.project, self.name) + @property + def path(self): + """URI path for use in logging APIs""" + return '/%s' % (self.full_name,) + def _require_client(self, client): """Check client or verify over-ride. @@ -217,8 +222,7 @@ def delete(self, client=None): ``client`` stored on the current logger. """ client = self._require_client(client) - client.connection.api_request( - method='DELETE', path='/%s' % self.full_name) + client.connection.api_request(method='DELETE', path=self.path) def list_entries(self, projects=None, filter_=None, order_by=None, page_size=None, page_token=None): diff --git a/gcloud/logging/test_logger.py b/gcloud/logging/test_logger.py index 7849c9d43013..9a5a698c27ea 100644 --- a/gcloud/logging/test_logger.py +++ b/gcloud/logging/test_logger.py @@ -36,6 +36,8 @@ def test_ctor_defaults(self): self.assertEqual(logger.project, self.PROJECT) self.assertEqual(logger.full_name, 'projects/%s/logs/%s' % (self.PROJECT, self.LOGGER_NAME)) + self.assertEqual(logger.path, '/projects/%s/logs/%s' + % (self.PROJECT, self.LOGGER_NAME)) self.assertEqual(logger.labels, None) def test_ctor_explicit(self): @@ -48,6 +50,8 @@ def test_ctor_explicit(self): self.assertEqual(logger.project, self.PROJECT) self.assertEqual(logger.full_name, 'projects/%s/logs/%s' % (self.PROJECT, self.LOGGER_NAME)) + self.assertEqual(logger.path, '/projects/%s/logs/%s' + % (self.PROJECT, self.LOGGER_NAME)) self.assertEqual(logger.labels, LABELS) def test_batch_w_bound_client(self): From 7aa9629c42aa00dd5794d4e2d6b0260b7d0f858e Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 28 Mar 2016 14:27:55 -0400 Subject: [PATCH 5/7] Add label support to the newly-added batch feature. --- gcloud/logging/logger.py | 29 +++++++++---- gcloud/logging/test_logger.py | 79 ++++++++++++++++++++++++++++------- 2 files changed, 86 insertions(+), 22 deletions(-) diff --git a/gcloud/logging/logger.py b/gcloud/logging/logger.py index 4477df260572..5f356b996e0c 100644 --- a/gcloud/logging/logger.py +++ b/gcloud/logging/logger.py @@ -291,29 +291,38 @@ def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: self.commit() - def log_text(self, text): + def log_text(self, text, labels=None): """Add a text entry to be logged during :meth:`commit`. :type text: string :param text: the text entry + + :type labels: dict or :class:`NoneType` + :param labels: (optional) mapping of labels for the entry. """ - self.entries.append(('text', text)) + self.entries.append(('text', text, labels)) - def log_struct(self, info): + def log_struct(self, info, labels=None): """Add a struct entry to be logged during :meth:`commit`. :type info: dict :param info: the struct entry + + :type labels: dict or :class:`NoneType` + :param labels: (optional) mapping of labels for the entry. """ - self.entries.append(('struct', info)) + self.entries.append(('struct', info, labels)) - def log_proto(self, message): + def log_proto(self, message, labels=None): """Add a protobuf entry to be logged during :meth:`commit`. :type message: protobuf message :param message: the protobuf entry + + :type labels: dict or :class:`NoneType` + :param labels: (optional) mapping of labels for the entry. """ - self.entries.append(('proto', message)) + self.entries.append(('proto', message, labels)) def commit(self, client=None): """Send saved log entries as a single API call. @@ -324,12 +333,16 @@ def commit(self, client=None): """ if client is None: client = self.client + data = { 'logName': self.logger.path, 'resource': {'type': 'global'}, } + if self.logger.labels is not None: + data['labels'] = self.logger.labels + entries = data['entries'] = [] - for entry_type, entry in self.entries: + for entry_type, entry, labels in self.entries: if entry_type == 'text': info = {'textPayload': entry} elif entry_type == 'struct': @@ -340,6 +353,8 @@ def commit(self, client=None): info = {'protoPayload': as_json} else: raise ValueError('Unknown entry type: %s' % (entry_type,)) + if labels is not None: + info['labels'] = labels entries.append(info) client.connection.api_request( diff --git a/gcloud/logging/test_logger.py b/gcloud/logging/test_logger.py index 9a5a698c27ea..9149fabfddc4 100644 --- a/gcloud/logging/test_logger.py +++ b/gcloud/logging/test_logger.py @@ -394,7 +394,7 @@ def test_ctor_defaults(self): self.assertTrue(batch.client is CLIENT) self.assertEqual(len(batch.entries), 0) - def test_log_text(self): + def test_log_text_defaults(self): TEXT = 'This is the entry text' connection = _Connection() CLIENT = _Client(project=self.PROJECT, connection=connection) @@ -402,9 +402,20 @@ def test_log_text(self): batch = self._makeOne(logger, client=CLIENT) batch.log_text(TEXT) self.assertEqual(len(connection._requested), 0) - self.assertEqual(batch.entries, [('text', TEXT)]) + self.assertEqual(batch.entries, [('text', TEXT, None)]) - def test_log_struct(self): + def test_log_text_explicit(self): + TEXT = 'This is the entry text' + LABELS = {'foo': 'bar', 'baz': 'qux'} + connection = _Connection() + CLIENT = _Client(project=self.PROJECT, connection=connection) + logger = _Logger() + batch = self._makeOne(logger, client=CLIENT) + batch.log_text(TEXT, labels=LABELS) + self.assertEqual(len(connection._requested), 0) + self.assertEqual(batch.entries, [('text', TEXT, LABELS)]) + + def test_log_struct_defaults(self): STRUCT = {'message': 'Message text', 'weather': 'partly cloudy'} connection = _Connection() CLIENT = _Client(project=self.PROJECT, connection=connection) @@ -412,9 +423,20 @@ def test_log_struct(self): batch = self._makeOne(logger, client=CLIENT) batch.log_struct(STRUCT) self.assertEqual(len(connection._requested), 0) - self.assertEqual(batch.entries, [('struct', STRUCT)]) + self.assertEqual(batch.entries, [('struct', STRUCT, None)]) - def test_log_proto(self): + def test_log_struct_explicit(self): + STRUCT = {'message': 'Message text', 'weather': 'partly cloudy'} + LABELS = {'foo': 'bar', 'baz': 'qux'} + connection = _Connection() + CLIENT = _Client(project=self.PROJECT, connection=connection) + logger = _Logger() + batch = self._makeOne(logger, client=CLIENT) + batch.log_struct(STRUCT, labels=LABELS) + self.assertEqual(len(connection._requested), 0) + self.assertEqual(batch.entries, [('struct', STRUCT, LABELS)]) + + def test_log_proto_defaults(self): from google.protobuf.struct_pb2 import Struct, Value message = Struct(fields={'foo': Value(bool_value=True)}) connection = _Connection() @@ -423,14 +445,26 @@ def test_log_proto(self): batch = self._makeOne(logger, client=CLIENT) batch.log_proto(message) self.assertEqual(len(connection._requested), 0) - self.assertEqual(batch.entries, [('proto', message)]) + self.assertEqual(batch.entries, [('proto', message, None)]) + + def test_log_proto_explicit(self): + from google.protobuf.struct_pb2 import Struct, Value + message = Struct(fields={'foo': Value(bool_value=True)}) + LABELS = {'foo': 'bar', 'baz': 'qux'} + connection = _Connection() + CLIENT = _Client(project=self.PROJECT, connection=connection) + logger = _Logger() + batch = self._makeOne(logger, client=CLIENT) + batch.log_proto(message, labels=LABELS) + self.assertEqual(len(connection._requested), 0) + self.assertEqual(batch.entries, [('proto', message, LABELS)]) def test_commit_w_invalid_entry_type(self): logger = _Logger() conn = _Connection() CLIENT = _Client(project=self.PROJECT, connection=conn) batch = self._makeOne(logger, CLIENT) - batch.entries.append(('bogus', 'BOGUS')) + batch.entries.append(('bogus', 'BOGUS', None)) with self.assertRaises(ValueError): batch.commit() @@ -471,25 +505,29 @@ def test_commit_w_alternate_client(self): import json from google.protobuf.json_format import MessageToJson from google.protobuf.struct_pb2 import Struct, Value + from gcloud.logging.logger import Logger TEXT = 'This is the entry text' STRUCT = {'message': TEXT, 'weather': 'partly cloudy'} message = Struct(fields={'foo': Value(bool_value=True)}) + DEFAULT_LABELS = {'foo': 'spam'} + LABELS = {'foo': 'bar', 'baz': 'qux'} conn1 = _Connection() conn2 = _Connection({}) CLIENT1 = _Client(project=self.PROJECT, connection=conn1) CLIENT2 = _Client(project=self.PROJECT, connection=conn2) - logger = _Logger() + logger = Logger('logger_name', CLIENT1, labels=DEFAULT_LABELS) SENT = { 'logName': logger.path, 'resource': {'type': 'global'}, + 'labels': DEFAULT_LABELS, 'entries': [ - {'textPayload': TEXT}, + {'textPayload': TEXT, 'labels': LABELS}, {'structPayload': STRUCT}, {'protoPayload': json.loads(MessageToJson(message))}, ], } batch = self._makeOne(logger, client=CLIENT1) - batch.log_text(TEXT) + batch.log_text(TEXT, labels=LABELS) batch.log_struct(STRUCT) batch.log_proto(message) batch.commit(client=CLIENT2) @@ -505,20 +543,24 @@ def test_context_mgr_success(self): import json from google.protobuf.json_format import MessageToJson from google.protobuf.struct_pb2 import Struct, Value + from gcloud.logging.logger import Logger TEXT = 'This is the entry text' STRUCT = {'message': TEXT, 'weather': 'partly cloudy'} message = Struct(fields={'foo': Value(bool_value=True)}) + DEFAULT_LABELS = {'foo': 'spam'} + LABELS = {'foo': 'bar', 'baz': 'qux'} conn = _Connection({}) CLIENT = _Client(project=self.PROJECT, connection=conn) - logger = _Logger() + logger = Logger('logger_name', CLIENT, labels=DEFAULT_LABELS) SENT = { 'logName': logger.path, 'resource': { 'type': 'global', }, + 'labels': DEFAULT_LABELS, 'entries': [ {'textPayload': TEXT}, - {'structPayload': STRUCT}, + {'structPayload': STRUCT, 'labels': LABELS}, {'protoPayload': json.loads(MessageToJson(message))}, ], } @@ -526,7 +568,7 @@ def test_context_mgr_success(self): with batch as other: other.log_text(TEXT) - other.log_struct(STRUCT) + other.log_struct(STRUCT, labels=LABELS) other.log_proto(message) self.assertEqual(list(batch.entries), []) @@ -540,18 +582,23 @@ def test_context_mgr_failure(self): from google.protobuf.struct_pb2 import Struct, Value TEXT = 'This is the entry text' STRUCT = {'message': TEXT, 'weather': 'partly cloudy'} + LABELS = {'foo': 'bar', 'baz': 'qux'} message = Struct(fields={'foo': Value(bool_value=True)}) conn = _Connection({}) CLIENT = _Client(project=self.PROJECT, connection=conn) logger = _Logger() - UNSENT = [('text', TEXT), ('struct', STRUCT), ('proto', message)] + UNSENT = [ + ('text', TEXT, None), + ('struct', STRUCT, None), + ('proto', message, LABELS), + ] batch = self._makeOne(logger, client=CLIENT) try: with batch as other: other.log_text(TEXT) other.log_struct(STRUCT) - other.log_proto(message) + other.log_proto(message, labels=LABELS) raise _Bugout() except _Bugout: pass @@ -562,6 +609,8 @@ def test_context_mgr_failure(self): class _Logger(object): + labels = None + def __init__(self, name="NAME", project="PROJECT"): self.path = '/projects/%s/logs/%s' % (project, name) From 43a09369919ba4f366c468e18a3a96d03e2d17b4 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 28 Mar 2016 14:45:11 -0400 Subject: [PATCH 6/7] Fix incorrect 'structPayload' entry in 'Batch.commit'. --- gcloud/logging/logger.py | 2 +- gcloud/logging/test_logger.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gcloud/logging/logger.py b/gcloud/logging/logger.py index 5f356b996e0c..fea76e862d3e 100644 --- a/gcloud/logging/logger.py +++ b/gcloud/logging/logger.py @@ -346,7 +346,7 @@ def commit(self, client=None): if entry_type == 'text': info = {'textPayload': entry} elif entry_type == 'struct': - info = {'structPayload': entry} + info = {'jsonPayload': entry} elif entry_type == 'proto': as_json_str = MessageToJson(entry) as_json = json.loads(as_json_str) diff --git a/gcloud/logging/test_logger.py b/gcloud/logging/test_logger.py index 9149fabfddc4..ad698de504f9 100644 --- a/gcloud/logging/test_logger.py +++ b/gcloud/logging/test_logger.py @@ -485,7 +485,7 @@ def test_commit_w_bound_client(self): }, 'entries': [ {'textPayload': TEXT}, - {'structPayload': STRUCT}, + {'jsonPayload': STRUCT}, {'protoPayload': json.loads(MessageToJson(message))}, ], } @@ -522,7 +522,7 @@ def test_commit_w_alternate_client(self): 'labels': DEFAULT_LABELS, 'entries': [ {'textPayload': TEXT, 'labels': LABELS}, - {'structPayload': STRUCT}, + {'jsonPayload': STRUCT}, {'protoPayload': json.loads(MessageToJson(message))}, ], } @@ -560,7 +560,7 @@ def test_context_mgr_success(self): 'labels': DEFAULT_LABELS, 'entries': [ {'textPayload': TEXT}, - {'structPayload': STRUCT, 'labels': LABELS}, + {'jsonPayload': STRUCT, 'labels': LABELS}, {'protoPayload': json.loads(MessageToJson(message))}, ], } From 5cbeaca0514b55442e7ff32789d0b511e9ffe21d Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 28 Mar 2016 14:46:13 -0400 Subject: [PATCH 7/7] Factor out construction of log entry resource into a helper. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1668#discussion_r57606003 --- gcloud/logging/logger.py | 94 ++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 51 deletions(-) diff --git a/gcloud/logging/logger.py b/gcloud/logging/logger.py index fea76e862d3e..f9dc41ff6459 100644 --- a/gcloud/logging/logger.py +++ b/gcloud/logging/logger.py @@ -88,21 +88,49 @@ def batch(self, client=None): client = self._require_client(client) return Batch(self, client) - def _get_labels(self, labels): - """Return effective labels. + def _make_entry_resource(self, text=None, info=None, message=None, + labels=None): + """Return a log entry resource of the appropriate type. Helper for :meth:`log_text`, :meth:`log_struct`, and :meth:`log_proto`. + Only one of ``text``, ``info``, or ``message`` should be passed. + + :type text: string or :class:`NoneType` + :param text: text payload + + :type info: dict or :class:`NoneType` + :param info: struct payload + + :type message: Protobuf message or :class:`NoneType` + :param message: protobuf payload + :type labels: dict or :class:`NoneType` :param labels: labels passed in to calling method. - - :rtype: dict or :class:`NoneType`. - :returns: the passed-in labels, if not none, else any default labels - configured on the logger instance. """ + resource = { + 'logName': self.full_name, + 'resource': {'type': 'global'}, + } + + if text is not None: + resource['textPayload'] = text + + if info is not None: + resource['jsonPayload'] = info + + if message is not None: + as_json_str = MessageToJson(message) + as_json = json.loads(as_json_str) + resource['protoPayload'] = as_json + + if labels is None: + labels = self.labels + if labels is not None: - return labels - return self.labels + resource['labels'] = labels + + return resource def log_text(self, text, client=None, labels=None): """API call: log a text message via a POST request @@ -121,20 +149,9 @@ def log_text(self, text, client=None, labels=None): :param labels: (optional) mapping of labels for the entry. """ client = self._require_client(client) + entry_resource = self._make_entry_resource(text=text, labels=labels) - data = { - 'entries': [{ - 'logName': self.full_name, - 'textPayload': text, - 'resource': { - 'type': 'global', - }, - }], - } - - labels = self._get_labels(labels) - if labels is not None: - data['entries'][0]['labels'] = labels + data = {'entries': [entry_resource]} client.connection.api_request( method='POST', path='/entries:write', data=data) @@ -156,20 +173,8 @@ def log_struct(self, info, client=None, labels=None): :param labels: (optional) mapping of labels for the entry. """ client = self._require_client(client) - - data = { - 'entries': [{ - 'logName': self.full_name, - 'jsonPayload': info, - 'resource': { - 'type': 'global', - }, - }], - } - - labels = self._get_labels(labels) - if labels is not None: - data['entries'][0]['labels'] = labels + entry_resource = self._make_entry_resource(info=info, labels=labels) + data = {'entries': [entry_resource]} client.connection.api_request( method='POST', path='/entries:write', data=data) @@ -191,22 +196,9 @@ def log_proto(self, message, client=None, labels=None): :param labels: (optional) mapping of labels for the entry. """ 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', - }, - }], - } - - labels = self._get_labels(labels) - if labels is not None: - data['entries'][0]['labels'] = labels + entry_resource = self._make_entry_resource( + message=message, labels=labels) + data = {'entries': [entry_resource]} client.connection.api_request( method='POST', path='/entries:write', data=data)