Skip to content

Commit 1916cca

Browse files
Bill Prindhermes
authored andcommitted
Add Async Background Thread transport
Refactors handlers into separate package Adds background threaded transport Adds fix to Batch commit to properly set log name
1 parent 3067a5a commit 1916cca

20 files changed

+786
-59
lines changed

docs/index.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@
107107
logging-metric
108108
logging-sink
109109
logging-handlers
110+
logging-transports-sync
111+
logging-transports-thread
112+
logging-transports-base
110113

111114
.. toctree::
112115
:maxdepth: 0

docs/logging-handlers.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Python Logging Module Handler
22
==============================
33

4-
.. automodule:: gcloud.logging.handlers
4+
.. automodule:: gcloud.logging.handlers.handlers
55
:members:
66
:show-inheritance:

docs/logging-transports-base.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Python Logging Handler Sync Transport
2+
======================================
3+
4+
.. automodule:: gcloud.logging.handlers.transports.base
5+
:members:
6+
:show-inheritance:

docs/logging-transports-sync.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Python Logging Handler Sync Transport
2+
======================================
3+
4+
.. automodule:: gcloud.logging.handlers.transports.sync
5+
:members:
6+
:show-inheritance:

docs/logging-transports-thread.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Python Logging Handler Threaded Transport
2+
=========================================
3+
4+
5+
.. automodule:: gcloud.logging.handlers.transports.background_thread
6+
:members:
7+
:show-inheritance:

docs/logging-usage.rst

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -402,12 +402,21 @@ Logging client.
402402
>>> cloud_logger = logging.getLogger('cloudLogger')
403403
>>> cloud_logger.setLevel(logging.INFO) # defaults to WARN
404404
>>> cloud_logger.addHandler(handler)
405-
>>> cloud_logger.error('bad news') # API call
405+
>>> cloud_logger.error('bad news')
406406

407407
.. note::
408408

409-
This handler currently only supports a synchronous API call, which means each logging statement
410-
that uses this handler will require an API call.
409+
This handler by default uses an asynchronous transport that sends log entries on a background
410+
thread. However, the API call will still be made in the same process. For other transport
411+
options, see the transports section.
412+
413+
All logs will go to a single custom log, which defaults to "python". The name of the Python
414+
logger will be included in the structured log entry under the "python_logger" field. You can
415+
change it by providing a name to the handler:
416+
417+
.. doctest::
418+
419+
>>> handler = CloudLoggingHandler(client, name="mycustomlog")
411420

412421
It is also possible to attach the handler to the root Python logger, so that for example a plain
413422
`logging.warn` call would be sent to Cloud Logging, as well as any other loggers created. However,
@@ -424,4 +433,24 @@ this automatically:
424433
>>> handler = CloudLoggingHandler(client)
425434
>>> logging.getLogger().setLevel(logging.INFO) # defaults to WARN
426435
>>> setup_logging(handler)
427-
>>> logging.error('bad news') # API call
436+
>>> logging.error('bad news')
437+
438+
You can also exclude certain loggers:
439+
440+
.. doctest::
441+
442+
>>> setup_logging(handler, excluded_loggers=('werkzeug',)))
443+
444+
445+
446+
Python logging handler transports
447+
==================================
448+
449+
The Python logging handler can use different transports. The default is
450+
:class:`gcloud.logging.handlers.BackgroundThreadTransport`.
451+
452+
1. :class:`gcloud.logging.handlers.BackgroundThreadTransport` this is the default. It writes
453+
entries on a background :class:`python.threading.Thread`.
454+
455+
1. :class:`gcloud.logging.handlers.SyncTransport` this handler does a direct API call on each
456+
logging statement to write the entry.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2016 Google Inc. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Python :mod:`logging` handlers for Google Cloud Logging."""
16+
17+
from gcloud.logging.handlers.handlers import CloudLoggingHandler
18+
from gcloud.logging.handlers.handlers import setup_logging
Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,22 @@
1616

1717
import logging
1818

19+
from gcloud.logging.handlers.transports import BackgroundThreadTransport
20+
21+
1922
EXCLUDE_LOGGER_DEFAULTS = (
2023
'gcloud',
21-
'oauth2client.client'
24+
'oauth2client'
2225
)
2326

27+
DEFAULT_LOGGER_NAME = 'python'
28+
2429

25-
class CloudLoggingHandler(logging.StreamHandler, object):
26-
"""Python standard logging handler to log messages to the Google Cloud
27-
Logging API.
30+
class CloudLoggingHandler(logging.StreamHandler):
31+
"""Python standard ``logging`` handler.
2832
29-
This handler can be used to route Python standard logging messages to
30-
Google Cloud logging.
33+
This handler can be used to route Python standard logging messages
34+
directly to the Google Cloud Logging API.
3135
3236
Note that this handler currently only supports a synchronous API call,
3337
which means each logging statement that uses this handler will require
@@ -37,6 +41,18 @@ class CloudLoggingHandler(logging.StreamHandler, object):
3741
:param client: the authenticated gcloud logging client for this handler
3842
to use
3943
44+
:type name: str
45+
:param name: the name of the custom log in Stackdriver Logging. Defaults
46+
to 'python'. The name of the Python logger will be represented
47+
in the ``python_logger`` field.
48+
49+
:type transport: type
50+
:param transport: Class for creating new transport objects. It should
51+
extend from the base :class:`.Transport` type and
52+
implement :meth`.Transport.send`. Defaults to
53+
:class:`.BackgroundThreadTransport`. The other
54+
option is :class:`.SyncTransport`.
55+
4056
Example:
4157
4258
.. doctest::
@@ -51,30 +67,37 @@ class CloudLoggingHandler(logging.StreamHandler, object):
5167
cloud_logger.setLevel(logging.INFO)
5268
cloud_logger.addHandler(handler)
5369
54-
cloud.logger.error("bad news") # API call
70+
cloud.logger.error('bad news') # API call
5571
5672
"""
5773

58-
def __init__(self, client):
74+
def __init__(self, client,
75+
name=DEFAULT_LOGGER_NAME,
76+
transport=BackgroundThreadTransport):
5977
super(CloudLoggingHandler, self).__init__()
78+
self.name = name
6079
self.client = client
80+
self.transport = transport(client, name)
6181

6282
def emit(self, record):
63-
"""
64-
Overrides the default emit behavior of StreamHandler.
83+
"""Actually log the specified logging record.
84+
85+
Overrides the default emit behavior of ``StreamHandler``.
6586
6687
See: https://docs.python.org/2/library/logging.html#handler-objects
88+
89+
:type record: :class:`logging.LogRecord`
90+
:param record: The record to be logged.
6791
"""
6892
message = super(CloudLoggingHandler, self).format(record)
69-
logger = self.client.logger(record.name)
70-
logger.log_struct({"message": message},
71-
severity=record.levelname)
93+
self.transport.send(record, message)
7294

7395

7496
def setup_logging(handler, excluded_loggers=EXCLUDE_LOGGER_DEFAULTS):
75-
"""Helper function to attach the CloudLoggingAPI handler to the Python
76-
root logger, while excluding loggers this library itself uses to avoid
77-
infinite recursion
97+
"""Attach the ``CloudLogging`` handler to the Python root logger
98+
99+
Excludes loggers that this library itself uses to avoid
100+
infinite recursion.
78101
79102
:type handler: :class:`logging.handler`
80103
:param handler: the handler to attach to the global handler
@@ -90,14 +113,14 @@ def setup_logging(handler, excluded_loggers=EXCLUDE_LOGGER_DEFAULTS):
90113
91114
import logging
92115
import gcloud.logging
93-
from gcloud.logging.handlers import CloudLoggingAPIHandler
116+
from gcloud.logging.handlers import CloudLoggingHandler
94117
95118
client = gcloud.logging.Client()
96119
handler = CloudLoggingHandler(client)
97-
setup_logging(handler)
120+
gcloud.logging.setup_logging(handler)
98121
logging.getLogger().setLevel(logging.DEBUG)
99122
100-
logging.error("bad news") # API call
123+
logging.error('bad news') # API call
101124
102125
"""
103126
all_excluded_loggers = set(excluded_loggers + EXCLUDE_LOGGER_DEFAULTS)
Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#!/usr/bin/env python
21
# Copyright 2016 Google Inc. All Rights Reserved.
32
#
43
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,32 +21,32 @@ class TestCloudLoggingHandler(unittest.TestCase):
2221
PROJECT = 'PROJECT'
2322

2423
def _getTargetClass(self):
25-
from gcloud.logging.handlers import CloudLoggingHandler
24+
from gcloud.logging.handlers.handlers import CloudLoggingHandler
2625
return CloudLoggingHandler
2726

2827
def _makeOne(self, *args, **kw):
2928
return self._getTargetClass()(*args, **kw)
3029

3130
def test_ctor(self):
3231
client = _Client(self.PROJECT)
33-
handler = self._makeOne(client)
32+
handler = self._makeOne(client, transport=_Transport)
3433
self.assertEqual(handler.client, client)
3534

3635
def test_emit(self):
3736
client = _Client(self.PROJECT)
38-
handler = self._makeOne(client)
37+
handler = self._makeOne(client, transport=_Transport)
3938
LOGNAME = 'loggername'
4039
MESSAGE = 'hello world'
4140
record = _Record(LOGNAME, logging.INFO, MESSAGE)
4241
handler.emit(record)
43-
self.assertEqual(client.logger(LOGNAME).log_struct_called_with,
44-
({'message': MESSAGE}, logging.INFO))
42+
43+
self.assertEqual(handler.transport.send_called_with, (record, MESSAGE))
4544

4645

4746
class TestSetupLogging(unittest.TestCase):
4847

4948
def _callFUT(self, handler, excludes=None):
50-
from gcloud.logging.handlers import setup_logging
49+
from gcloud.logging.handlers.handlers import setup_logging
5150
if excludes:
5251
return setup_logging(handler, excluded_loggers=excludes)
5352
else:
@@ -94,20 +93,10 @@ def release(self):
9493
pass # pragma: NO COVER
9594

9695

97-
class _Logger(object):
98-
99-
def log_struct(self, message, severity=None):
100-
self.log_struct_called_with = (message, severity)
101-
102-
10396
class _Client(object):
10497

10598
def __init__(self, project):
10699
self.project = project
107-
self.logger_ = _Logger()
108-
109-
def logger(self, _): # pylint: disable=unused-argument
110-
return self.logger_
111100

112101

113102
class _Record(object):
@@ -122,3 +111,12 @@ def __init__(self, name, level, message):
122111

123112
def getMessage(self):
124113
return self.message
114+
115+
116+
class _Transport(object):
117+
118+
def __init__(self, client, name):
119+
pass
120+
121+
def send(self, record, message):
122+
self.send_called_with = (record, message)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright 2016 Google Inc. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Transport classes for Python logging integration.
16+
17+
Currently two options are provided, a synchronous transport that makes
18+
an API call for each log statement, and an asynchronous handler that
19+
sends the API using a :class:`~gcloud.logging.logger.Batch` object in
20+
the background.
21+
"""
22+
23+
from gcloud.logging.handlers.transports.base import Transport
24+
from gcloud.logging.handlers.transports.sync import SyncTransport
25+
from gcloud.logging.handlers.transports.background_thread import (
26+
BackgroundThreadTransport)

0 commit comments

Comments
 (0)