diff --git a/logging/google/cloud/logging/logger.py b/logging/google/cloud/logging/logger.py index 842481af42da..d5a5b201dca0 100644 --- a/logging/google/cloud/logging/logger.py +++ b/logging/google/cloud/logging/logger.py @@ -17,6 +17,7 @@ import json from google.protobuf.json_format import MessageToJson +from google.cloud._helpers import _datetime_to_rfc3339 class Logger(object): @@ -92,7 +93,7 @@ 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): + http_request=None, timestamp=None): """Return a log entry resource of the appropriate type. Helper for :meth:`log_text`, :meth:`log_struct`, and :meth:`log_proto`. @@ -121,6 +122,9 @@ def _make_entry_resource(self, text=None, info=None, message=None, :param http_request: (optional) info about HTTP request associated with the entry + :type timestamp: :class:`datetime.datetime` + :param timestamp: (optional) timestamp of event being logged. + :rtype: dict :returns: The JSON resource created. """ @@ -155,10 +159,13 @@ def _make_entry_resource(self, text=None, info=None, message=None, if http_request is not None: resource['httpRequest'] = http_request + if timestamp is not None: + resource['timestamp'] = _datetime_to_rfc3339(timestamp) + return resource def log_text(self, text, client=None, labels=None, insert_id=None, - severity=None, http_request=None): + severity=None, http_request=None, timestamp=None): """API call: log a text message via a POST request See: @@ -184,15 +191,18 @@ def log_text(self, text, client=None, labels=None, insert_id=None, :type http_request: dict :param http_request: (optional) info about HTTP request associated with the entry + + :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) + http_request=http_request, timestamp=timestamp) client.logging_api.write_entries([entry_resource]) def log_struct(self, info, client=None, labels=None, insert_id=None, - severity=None, http_request=None): + severity=None, http_request=None, timestamp=None): """API call: log a structured message via a POST request See: @@ -218,15 +228,18 @@ def log_struct(self, info, client=None, labels=None, insert_id=None, :type http_request: dict :param http_request: (optional) info about HTTP request associated with the entry. + + :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) + http_request=http_request, timestamp=timestamp) client.logging_api.write_entries([entry_resource]) def log_proto(self, message, client=None, labels=None, insert_id=None, - severity=None, http_request=None): + severity=None, http_request=None, timestamp=None): """API call: log a protobuf message via a POST request See: @@ -252,11 +265,14 @@ def log_proto(self, message, client=None, labels=None, insert_id=None, :type http_request: dict :param http_request: (optional) info about HTTP request associated with the entry. + + :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) + severity=severity, http_request=http_request, timestamp=timestamp) client.logging_api.write_entries([entry_resource]) def delete(self, client=None): @@ -340,7 +356,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): + http_request=None, timestamp=None): """Add a text entry to be logged during :meth:`commit`. :type text: str @@ -358,12 +374,16 @@ def log_text(self, text, labels=None, insert_id=None, severity=None, :type http_request: dict :param http_request: (optional) info about HTTP request associated with the entry. + + :type timestamp: :class:`datetime.datetime` + :param timestamp: (optional) timestamp of event being logged. """ self.entries.append( - ('text', text, labels, insert_id, severity, http_request)) + ('text', text, labels, insert_id, severity, http_request, + timestamp)) def log_struct(self, info, labels=None, insert_id=None, severity=None, - http_request=None): + http_request=None, timestamp=None): """Add a struct entry to be logged during :meth:`commit`. :type info: dict @@ -381,12 +401,16 @@ def log_struct(self, info, labels=None, insert_id=None, severity=None, :type http_request: dict :param http_request: (optional) info about HTTP request associated with the entry. + + :type timestamp: :class:`datetime.datetime` + :param timestamp: (optional) timestamp of event being logged. """ self.entries.append( - ('struct', info, labels, insert_id, severity, http_request)) + ('struct', info, labels, insert_id, severity, http_request, + timestamp)) def log_proto(self, message, labels=None, insert_id=None, severity=None, - http_request=None): + http_request=None, timestamp=None): """Add a protobuf entry to be logged during :meth:`commit`. :type message: protobuf message @@ -404,9 +428,13 @@ def log_proto(self, message, labels=None, insert_id=None, severity=None, :type http_request: dict :param http_request: (optional) info about HTTP request associated with the entry. + + :type timestamp: :class:`datetime.datetime` + :param timestamp: (optional) timestamp of event being logged. """ self.entries.append( - ('proto', message, labels, insert_id, severity, http_request)) + ('proto', message, labels, insert_id, severity, http_request, + timestamp)) def commit(self, client=None): """Send saved log entries as a single API call. @@ -427,7 +455,8 @@ def commit(self, client=None): kwargs['labels'] = self.logger.labels entries = [] - for entry_type, entry, labels, iid, severity, http_req in self.entries: + for (entry_type, entry, labels, iid, severity, http_req, + timestamp) in self.entries: if entry_type == 'text': info = {'textPayload': entry} elif entry_type == 'struct': @@ -446,6 +475,8 @@ def commit(self, client=None): info['severity'] = severity if http_req is not None: info['httpRequest'] = http_req + if timestamp is not None: + info['timestamp'] = timestamp entries.append(info) client.logging_api.write_entries(entries, **kwargs) diff --git a/logging/unit_tests/test_logger.py b/logging/unit_tests/test_logger.py index 15e7e7146b80..48edaf0ed5a4 100644 --- a/logging/unit_tests/test_logger.py +++ b/logging/unit_tests/test_logger.py @@ -125,6 +125,29 @@ def test_log_text_w_default_labels(self): self.assertEqual(api._write_entries_called_with, (ENTRIES, None, None, None)) + def test_log_text_w_timestamp(self): + import datetime + + TEXT = 'TEXT' + TIMESTAMP = datetime.datetime(2016, 12, 31, 0, 1, 2, 999999) + ENTRIES = [{ + 'logName': 'projects/%s/logs/%s' % ( + self.PROJECT, self.LOGGER_NAME), + 'textPayload': TEXT, + 'timestamp': '2016-12-31T00:01:02.999999Z', + 'resource': { + 'type': 'global', + }, + }] + client = _Client(self.PROJECT) + api = client.logging_api = _DummyLoggingAPI() + logger = self._make_one(self.LOGGER_NAME, client=client) + + logger.log_text(TEXT, timestamp=TIMESTAMP) + + self.assertEqual(api._write_entries_called_with, + (ENTRIES, None, None, None)) + def test_log_text_w_unicode_explicit_client_labels_severity_httpreq(self): TEXT = u'TEXT' DEFAULT_LABELS = {'foo': 'spam'} @@ -243,6 +266,28 @@ def test_log_struct_w_explicit_client_labels_severity_httpreq(self): self.assertEqual(api._write_entries_called_with, (ENTRIES, None, None, None)) + def test_log_struct_w_timestamp(self): + import datetime + STRUCT = {'message': 'MESSAGE', 'weather': 'cloudy'} + TIMESTAMP = datetime.datetime(2016, 12, 31, 0, 1, 2, 999999) + ENTRIES = [{ + 'logName': 'projects/%s/logs/%s' % ( + self.PROJECT, self.LOGGER_NAME), + 'jsonPayload': STRUCT, + 'timestamp': '2016-12-31T00:01:02.999999Z', + 'resource': { + 'type': 'global', + }, + }] + client = _Client(self.PROJECT) + api = client.logging_api = _DummyLoggingAPI() + logger = self._make_one(self.LOGGER_NAME, client=client) + + logger.log_struct(STRUCT, timestamp=TIMESTAMP) + + self.assertEqual(api._write_entries_called_with, + (ENTRIES, None, None, None)) + def test_log_proto_w_implicit_client(self): import json from google.protobuf.json_format import MessageToJson @@ -332,6 +377,31 @@ def test_log_proto_w_explicit_client_labels_severity_httpreq(self): self.assertEqual(api._write_entries_called_with, (ENTRIES, None, None, None)) + def test_log_proto_w_timestamp(self): + import json + import datetime + from google.protobuf.json_format import MessageToJson + from google.protobuf.struct_pb2 import Struct, Value + message = Struct(fields={'foo': Value(bool_value=True)}) + TIMESTAMP = datetime.datetime(2016, 12, 31, 0, 1, 2, 999999) + ENTRIES = [{ + 'logName': 'projects/%s/logs/%s' % ( + self.PROJECT, self.LOGGER_NAME), + 'protoPayload': json.loads(MessageToJson(message)), + 'timestamp': '2016-12-31T00:01:02.999999Z', + 'resource': { + 'type': 'global', + }, + }] + client = _Client(self.PROJECT) + api = client.logging_api = _DummyLoggingAPI() + logger = self._make_one(self.LOGGER_NAME, client=client) + + logger.log_proto(message, timestamp=TIMESTAMP) + + self.assertEqual(api._write_entries_called_with, + (ENTRIES, None, None, None)) + def test_delete_w_bound_client(self): client = _Client(project=self.PROJECT) api = client.logging_api = _DummyLoggingAPI() @@ -454,9 +524,10 @@ def test_log_text_defaults(self): batch = self._make_one(logger, client=client) batch.log_text(TEXT) self.assertEqual(batch.entries, - [('text', TEXT, None, None, None, None)]) + [('text', TEXT, None, None, None, None, None)]) def test_log_text_explicit(self): + import datetime TEXT = 'This is the entry text' LABELS = {'foo': 'bar', 'baz': 'qux'} IID = 'IID' @@ -469,13 +540,15 @@ def test_log_text_explicit(self): 'requestUrl': URI, 'status': STATUS, } + TIMESTAMP = datetime.datetime(2016, 12, 31, 0, 1, 2, 999999) 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) - self.assertEqual(batch.entries, - [('text', TEXT, LABELS, IID, SEVERITY, REQUEST)]) + http_request=REQUEST, timestamp=TIMESTAMP) + self.assertEqual( + batch.entries, + [('text', TEXT, LABELS, IID, SEVERITY, REQUEST, TIMESTAMP)]) def test_log_struct_defaults(self): STRUCT = {'message': 'Message text', 'weather': 'partly cloudy'} @@ -483,10 +556,12 @@ def test_log_struct_defaults(self): logger = _Logger() batch = self._make_one(logger, client=client) batch.log_struct(STRUCT) - self.assertEqual(batch.entries, - [('struct', STRUCT, None, None, None, None)]) + self.assertEqual( + batch.entries, + [('struct', STRUCT, None, None, None, None, None)]) def test_log_struct_explicit(self): + import datetime STRUCT = {'message': 'Message text', 'weather': 'partly cloudy'} LABELS = {'foo': 'bar', 'baz': 'qux'} IID = 'IID' @@ -499,13 +574,16 @@ def test_log_struct_explicit(self): 'requestUrl': URI, 'status': STATUS, } + TIMESTAMP = datetime.datetime(2016, 12, 31, 0, 1, 2, 999999) 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) - self.assertEqual(batch.entries, - [('struct', STRUCT, LABELS, IID, SEVERITY, REQUEST)]) + severity=SEVERITY, http_request=REQUEST, + timestamp=TIMESTAMP) + self.assertEqual( + batch.entries, + [('struct', STRUCT, LABELS, IID, SEVERITY, REQUEST, TIMESTAMP)]) def test_log_proto_defaults(self): from google.protobuf.struct_pb2 import Struct, Value @@ -515,9 +593,10 @@ 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)]) + [('proto', message, None, None, None, None, None)]) def test_log_proto_explicit(self): + import datetime from google.protobuf.struct_pb2 import Struct, Value message = Struct(fields={'foo': Value(bool_value=True)}) LABELS = {'foo': 'bar', 'baz': 'qux'} @@ -531,24 +610,28 @@ def test_log_proto_explicit(self): 'requestUrl': URI, 'status': STATUS, } + TIMESTAMP = datetime.datetime(2016, 12, 31, 0, 1, 2, 999999) 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) - self.assertEqual(batch.entries, - [('proto', message, LABELS, IID, SEVERITY, REQUEST)]) + severity=SEVERITY, http_request=REQUEST, + timestamp=TIMESTAMP) + self.assertEqual( + batch.entries, + [('proto', message, LABELS, IID, SEVERITY, REQUEST, TIMESTAMP)]) 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)) + batch.entries.append(('bogus', 'BOGUS', None, None, None, None, None)) with self.assertRaises(ValueError): batch.commit() def test_commit_w_bound_client(self): import json + import datetime from google.protobuf.json_format import MessageToJson from google.protobuf.struct_pb2 import Struct, Value TEXT = 'This is the entry text' @@ -557,23 +640,26 @@ def test_commit_w_bound_client(self): IID1 = 'IID1' IID2 = 'IID2' IID3 = 'IID3' + 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}, - {'jsonPayload': STRUCT, 'insertId': IID2}, + {'textPayload': TEXT, 'insertId': IID1, 'timestamp': TIMESTAMP1}, + {'jsonPayload': STRUCT, 'insertId': IID2, 'timestamp': TIMESTAMP2}, {'protoPayload': json.loads(MessageToJson(message)), - 'insertId': IID3}, + 'insertId': IID3, 'timestamp': TIMESTAMP3}, ] client = _Client(project=self.PROJECT) api = client.logging_api = _DummyLoggingAPI() logger = _Logger() batch = self._make_one(logger, client=client) - batch.log_text(TEXT, insert_id=IID1) - batch.log_struct(STRUCT, insert_id=IID2) - batch.log_proto(message, insert_id=IID3) + batch.log_text(TEXT, insert_id=IID1, timestamp=TIMESTAMP1) + batch.log_struct(STRUCT, insert_id=IID2, timestamp=TIMESTAMP2) + batch.log_proto(message, insert_id=IID3, timestamp=TIMESTAMP3) batch.commit() self.assertEqual(list(batch.entries), []) @@ -667,6 +753,7 @@ def test_context_mgr_success(self): (ENTRIES, logger.full_name, RESOURCE, DEFAULT_LABELS)) def test_context_mgr_failure(self): + import datetime from google.protobuf.struct_pb2 import Struct, Value TEXT = 'This is the entry text' STRUCT = {'message': TEXT, 'weather': 'partly cloudy'} @@ -681,20 +768,21 @@ def test_context_mgr_failure(self): 'requestUrl': URI, 'status': STATUS, } + TIMESTAMP = datetime.datetime(2016, 12, 31, 0, 1, 2, 999999) message = Struct(fields={'foo': Value(bool_value=True)}) client = _Client(project=self.PROJECT) api = client.logging_api = _DummyLoggingAPI() logger = _Logger() UNSENT = [ - ('text', TEXT, None, IID, None, None), - ('struct', STRUCT, None, None, SEVERITY, None), - ('proto', message, LABELS, None, None, REQUEST), + ('text', TEXT, None, IID, None, None, TIMESTAMP), + ('struct', STRUCT, None, None, SEVERITY, None, None), + ('proto', message, LABELS, None, None, REQUEST, None), ] batch = self._make_one(logger, client=client) try: with batch as other: - other.log_text(TEXT, insert_id=IID) + other.log_text(TEXT, insert_id=IID, timestamp=TIMESTAMP) other.log_struct(STRUCT, severity=SEVERITY) other.log_proto(message, labels=LABELS, http_request=REQUEST) raise _Bugout()