Skip to content

Commit

Permalink
Merge pull request #1668 from tseaver/logging-support_entry_labels
Browse files Browse the repository at this point in the history
Support entry labels
  • Loading branch information
tseaver committed Mar 28, 2016
2 parents fad7f4c + 5cbeaca commit f1b9261
Show file tree
Hide file tree
Showing 4 changed files with 300 additions and 73 deletions.
10 changes: 8 additions & 2 deletions gcloud/logging/entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
142 changes: 99 additions & 43 deletions gcloud/logging/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -51,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.
Expand Down Expand Up @@ -78,7 +88,51 @@ def batch(self, client=None):
client = self._require_client(client)
return Batch(self, client)

def log_text(self, text, client=None):
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.
"""
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:
resource['labels'] = labels

return resource

def log_text(self, text, client=None, labels=None):
"""API call: log a text message via a POST request
See:
Expand All @@ -90,22 +144,19 @@ 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)
entry_resource = self._make_entry_resource(text=text, labels=labels)

data = {'entries': [entry_resource]}

data = {
'entries': [{
'logName': self.full_name,
'textPayload': text,
'resource': {
'type': 'global',
},
}],
}
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:
Expand All @@ -117,22 +168,18 @@ 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)
entry_resource = self._make_entry_resource(info=info, labels=labels)
data = {'entries': [entry_resource]}

data = {
'entries': [{
'logName': self.full_name,
'jsonPayload': info,
'resource': {
'type': 'global',
},
}],
}
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:
Expand All @@ -144,20 +191,15 @@ 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)
as_json = json.loads(as_json_str)
entry_resource = self._make_entry_resource(
message=message, labels=labels)
data = {'entries': [entry_resource]}

data = {
'entries': [{
'logName': self.full_name,
'protoPayload': as_json,
'resource': {
'type': 'global',
},
}],
}
client.connection.api_request(
method='POST', path='/entries:write', data=data)

Expand All @@ -172,8 +214,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):
Expand Down Expand Up @@ -242,29 +283,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.
Expand All @@ -275,22 +325,28 @@ 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':
info = {'structPayload': entry}
info = {'jsonPayload': entry}
elif entry_type == 'proto':
as_json_str = MessageToJson(entry)
as_json = json.loads(as_json_str)
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(
Expand Down
11 changes: 10 additions & 1 deletion gcloud/logging/test_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -79,18 +82,21 @@ 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()
entry = klass.from_api_repr(API_REPR, client, loggers=loggers)
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)
Expand All @@ -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}
Expand All @@ -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)


Expand Down
Loading

0 comments on commit f1b9261

Please sign in to comment.