Skip to content

Commit 930342a

Browse files
authored
Merge pull request #1942 from waprin/master
Add Async Background Thread transport
2 parents 17251f4 + 4456c70 commit 930342a

20 files changed

+772
-52
lines changed

docs/index.rst

+3
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@
111111
logging-metric
112112
logging-sink
113113
logging-handlers
114+
logging-transports-sync
115+
logging-transports-thread
116+
logging-transports-base
114117

115118
.. toctree::
116119
:maxdepth: 0

docs/logging-handlers.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Python Logging Module Handler
22
==============================
33

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

docs/logging-transports-base.rst

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

docs/logging-transports-sync.rst

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

docs/logging-transports-thread.rst

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

docs/logging-usage.rst

+34-4
Original file line numberDiff line numberDiff line change
@@ -396,12 +396,21 @@ Logging client.
396396
>>> cloud_logger = logging.getLogger('cloudLogger')
397397
>>> cloud_logger.setLevel(logging.INFO) # defaults to WARN
398398
>>> cloud_logger.addHandler(handler)
399-
>>> cloud_logger.error('bad news') # API call
399+
>>> cloud_logger.error('bad news')
400400

401401
.. note::
402402

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

406415
It is also possible to attach the handler to the root Python logger, so that for example a plain
407416
`logging.warn` call would be sent to Cloud Logging, as well as any other loggers created. However,
@@ -418,4 +427,25 @@ this automatically:
418427
>>> handler = CloudLoggingHandler(client)
419428
>>> logging.getLogger().setLevel(logging.INFO) # defaults to WARN
420429
>>> setup_logging(handler)
421-
>>> logging.error('bad news') # API call
430+
>>> logging.error('bad news')
431+
432+
You can also exclude certain loggers:
433+
434+
.. doctest::
435+
436+
>>> setup_logging(handler, excluded_loggers=('werkzeug',)))
437+
438+
439+
440+
Python logging handler transports
441+
==================================
442+
443+
The Python logging handler can use different transports. The default is
444+
:class:`gcloud.logging.handlers.BackgroundThreadTransport`.
445+
446+
1. :class:`gcloud.logging.handlers.BackgroundThreadTransport` this is the default. It writes
447+
entries on a background :class:`python.threading.Thread`.
448+
449+
1. :class:`gcloud.logging.handlers.SyncTransport` this handler does a direct API call on each
450+
logging statement to write the entry.
451+

gcloud/logging/handlers/__init__.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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, setup_logging

gcloud/logging/handlers.py renamed to gcloud/logging/handlers/handlers.py

+25-8
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616

1717
import logging
1818

19+
from gcloud.logging.handlers.transports import BackgroundThreadTransport
20+
1921
EXCLUDE_LOGGER_DEFAULTS = (
2022
'gcloud',
21-
'oauth2client.client'
23+
'oauth2client'
2224
)
2325

26+
DEFAULT_LOGGER_NAME = "python"
27+
2428

2529
class CloudLoggingHandler(logging.StreamHandler, object):
2630
"""Python standard logging handler to log messages to the Google Cloud
@@ -36,6 +40,17 @@ class CloudLoggingHandler(logging.StreamHandler, object):
3640
:type client: :class:`gcloud.logging.client`
3741
:param client: the authenticated gcloud logging client for this handler
3842
to use
43+
:type name: str
44+
:param name: the name of the custom log in Stackdriver Logging. Defaults
45+
to "python". The name of the Python logger will be represented
46+
in the "python_logger" field.
47+
48+
:type transport: :class:`gcloud.logging.handlers.transports.Transport`
49+
:param transport: the class object to instantiate. It should extend from
50+
the base Transport type and implement
51+
:meth`gcloud.logging.handlers.transports.base.Transport.send`
52+
Defaults to BackgroundThreadTransport. The other
53+
option is SyncTransport.
3954
4055
Example:
4156
@@ -55,9 +70,13 @@ class CloudLoggingHandler(logging.StreamHandler, object):
5570
5671
"""
5772

58-
def __init__(self, client):
73+
def __init__(self, client,
74+
name=DEFAULT_LOGGER_NAME,
75+
transport=BackgroundThreadTransport):
5976
super(CloudLoggingHandler, self).__init__()
77+
self.name = name
6078
self.client = client
79+
self.transport = transport(client, name)
6180

6281
def emit(self, record):
6382
"""
@@ -66,13 +85,11 @@ def emit(self, record):
6685
See: https://docs.python.org/2/library/logging.html#handler-objects
6786
"""
6887
message = super(CloudLoggingHandler, self).format(record)
69-
logger = self.client.logger(record.name)
70-
logger.log_struct({"message": message},
71-
severity=record.levelname)
88+
self.transport.send(record, message)
7289

7390

7491
def setup_logging(handler, excluded_loggers=EXCLUDE_LOGGER_DEFAULTS):
75-
"""Helper function to attach the CloudLoggingAPI handler to the Python
92+
"""Helper function to attach the CloudLogging handler to the Python
7693
root logger, while excluding loggers this library itself uses to avoid
7794
infinite recursion
7895
@@ -90,11 +107,11 @@ def setup_logging(handler, excluded_loggers=EXCLUDE_LOGGER_DEFAULTS):
90107
91108
import logging
92109
import gcloud.logging
93-
from gcloud.logging.handlers import CloudLoggingAPIHandler
110+
from gcloud.logging.handlers import CloudLoggingHandler
94111
95112
client = gcloud.logging.Client()
96113
handler = CloudLoggingHandler(client)
97-
setup_logging(handler)
114+
gcloud.logging.setup_logging(handler)
98115
logging.getLogger().setLevel(logging.DEBUG)
99116
100117
logging.error("bad news") # API call

gcloud/logging/test_handlers.py renamed to gcloud/logging/handlers/test_handlers.py

+15-17
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");
@@ -23,32 +22,32 @@ class TestCloudLoggingHandler(unittest2.TestCase):
2322
PROJECT = 'PROJECT'
2423

2524
def _getTargetClass(self):
26-
from gcloud.logging.handlers import CloudLoggingHandler
25+
from gcloud.logging.handlers.handlers import CloudLoggingHandler
2726
return CloudLoggingHandler
2827

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

3231
def test_ctor(self):
3332
client = _Client(self.PROJECT)
34-
handler = self._makeOne(client)
33+
handler = self._makeOne(client, transport=_Transport)
3534
self.assertEqual(handler.client, client)
3635

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

4746

4847
class TestSetupLogging(unittest2.TestCase):
4948

5049
def _callFUT(self, handler, excludes=None):
51-
from gcloud.logging.handlers import setup_logging
50+
from gcloud.logging.handlers.handlers import setup_logging
5251
if excludes:
5352
return setup_logging(handler, excluded_loggers=excludes)
5453
else:
@@ -95,20 +94,10 @@ def release(self):
9594
pass # pragma: NO COVER
9695

9796

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

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

113102

114103
class _Record(object):
@@ -123,3 +112,12 @@ def __init__(self, name, level, message):
123112

124113
def getMessage(self):
125114
return self.message
115+
116+
117+
class _Transport(object):
118+
119+
def __init__(self, client, name):
120+
pass
121+
122+
def send(self, record, message):
123+
self.send_called_with = (record, message)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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.Batch` object in the background.
20+
"""
21+
22+
from gcloud.logging.handlers.transports.base import Transport
23+
from gcloud.logging.handlers.transports.sync import SyncTransport
24+
from gcloud.logging.handlers.transports.background_thread import (
25+
BackgroundThreadTransport)

0 commit comments

Comments
 (0)