Skip to content

Commit 0bbf805

Browse files
author
Bill Prin
committed
Add Logging Handler
1 parent 22bf022 commit 0bbf805

File tree

5 files changed

+263
-0
lines changed

5 files changed

+263
-0
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
logging-entries
110110
logging-metric
111111
logging-sink
112+
logging-handlers
112113

113114
.. toctree::
114115
:maxdepth: 0

docs/logging-handlers.rst

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

docs/logging-usage.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,32 @@ Delete a sink:
314314
>>> sink.delete() # API call
315315
>>> sink.exists() # API call
316316
False
317+
318+
Python Logging Integration:
319+
320+
It's possible to tie the Python `logging` module directly into Google Cloud Logging. To use it, create a :class:`CloudLoggingAPIHandler <gcloud.logging.CloudLoggingAPIHandler` instance from your Logging client.
321+
322+
.. doctest::
323+
>>> import logging
324+
>>> import gcloud.logging # Don't conflict with standard logging
325+
>>> from gcloud.logging.handlers import CloudLoggingAPIHandler
326+
>>> client = logging.Client()
327+
>>> handler = CloudLoggingAPIHandler(client)
328+
>>> cloud_logger = logging.getLogger('cloudLogger')
329+
>>> cloud_logger.setLevel(logging.INFO) # defaults to WARN
330+
>>> cloud_logger.addHandler(handler)
331+
>>> cloud_logger.error('bad news') # API call
332+
333+
Note that this handler currently only supports a synchronous API call, which means each logging statement that uses this handler will require an API call.
334+
335+
It is also possible to attach the handler to the root Python logger, so that for example a plain `logging.warn` call would be sent to Cloud Logging. However, you must avoid infinite recursion from the logging calls the client itself makes. A helper method :meth:`setup_logging <gcloud.logging.handlers.setup_logging` is provided to configure this automatically:
336+
337+
.. doctest::
338+
>>> import logging
339+
>>> import gcloud.logging # Don't conflict with standard logging
340+
>>> from gcloud.logging.handlers import CloudLoggingAPIHandler, setup_logging
341+
>>> client = gcloud.logging.Client()
342+
>>> handler = CloudLoggingAPIHandler(client)
343+
>>> logging.getLogger().setLevel(logging.INFO) # defaults to WARN
344+
>>> setup_logging(handler)
345+
>>> logging.error('bad news') # API call

gcloud/logging/handlers.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
"""Google Cloud Logging API Logging handlers."""
16+
17+
import logging
18+
19+
EXCLUDE_LOGGER_DEFAULTS = (
20+
'gcloud',
21+
'oauth2client.client'
22+
)
23+
24+
25+
class CloudLoggingAPIHandler(logging.StreamHandler):
26+
"""Python standard logging handler to log messages to the Google Cloud
27+
Logging API.
28+
29+
This handler can be used to route Python standard logging messages to
30+
Google Cloud logging.
31+
32+
Note that this handler currently only supports a synchronous API call,
33+
which means each logging statement that uses this handler will require
34+
an API call.
35+
36+
:type client: :class:`gcloud.logging.client`
37+
:param client: the authenticated gcloud logging client for this handler
38+
to use
39+
40+
Example:
41+
import gcloud.logging
42+
from gcloud.logging.handlers import CloudLoggingAPIHandler
43+
44+
client = gcloud.logging.Client()
45+
handler = CloudLoggingAPIHandler(client)
46+
47+
cloud_logger = logging.getLogger('cloudLogger')
48+
cloud_logger.setLevel(logging.INFO)
49+
cloud_logger.addHandler(handler)
50+
51+
cloud.logger.error("bad news") # API call
52+
"""
53+
54+
def __init__(self, client):
55+
logging.StreamHandler.__init__(self)
56+
self.client = client
57+
58+
def emit(self, record):
59+
"""
60+
Overrides the default emit behavior of StreamHandler.
61+
62+
See: https://docs.python.org/2/library/logging.html#handler-objects
63+
"""
64+
message = logging.StreamHandler.format(self, record)
65+
logger = self.client.logger(record.name)
66+
logger.log_struct({"message": message},
67+
severity=record.levelname)
68+
69+
70+
def setup_logging(handler, excluded_loggers=EXCLUDE_LOGGER_DEFAULTS):
71+
"""Helper function to attach the CloudLoggingAPI handler to the Python
72+
root logger, while excluding loggers this library itself uses to avoid
73+
infinite recursion
74+
75+
:type handler: :class:`logging.handler`
76+
:param handler: the handler to attach to the global handler
77+
78+
:type excluded_loggers: list
79+
:param excluded_loggers: the loggers to not attach the handler to. At a
80+
minimum this should include loggers that the
81+
handlers itself will call to avoid infinite
82+
recursion.
83+
84+
Example:
85+
import logging
86+
import gcloud.logging
87+
from gcloud.logging.handlers import CloudLoggingAPIHandler
88+
89+
client = gcloud.logging.Client()
90+
handler = CloudLoggingAPIHandler(client)
91+
setup_logging(handler)
92+
logging.getLogger().setLevel(logging.DEBUG)
93+
94+
logging.error("bad news") # API call
95+
"""
96+
logger = logging.getLogger()
97+
logger.addHandler(handler)
98+
logger.addHandler(logging.StreamHandler())
99+
for logger_name in excluded_loggers:
100+
logger = logging.getLogger(logger_name)
101+
logger.propagate = False
102+
logger.addHandler(logging.StreamHandler())

gcloud/logging/test_handlers.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#!/usr/bin/env python
2+
# Copyright 2016 Google Inc. All Rights Reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import logging
17+
18+
import unittest2
19+
20+
21+
class TestHandler(unittest2.TestCase):
22+
23+
PROJECT = 'PROJECT'
24+
25+
def _getTargetClass(self):
26+
from gcloud.logging.handlers import CloudLoggingAPIHandler
27+
return CloudLoggingAPIHandler
28+
29+
def _makeOne(self, *args, **kw):
30+
return self._getTargetClass()(*args, **kw)
31+
32+
def test_ctor(self):
33+
client = _Client(self.PROJECT)
34+
handler = self._makeOne(client)
35+
self.assertEqual(handler.client, client)
36+
37+
def test_emit(self):
38+
client = _Client(self.PROJECT)
39+
handler = self._makeOne(client)
40+
LOGNAME = 'loggername'
41+
MESSAGE = 'hello world'
42+
record = _Record(LOGNAME, logging.INFO, MESSAGE)
43+
handler.emit(record)
44+
self.assertEqual(client.logger(LOGNAME).log_struct_called_with,
45+
({'message': MESSAGE}, logging.INFO))
46+
47+
48+
class TestSetupLogging(unittest2.TestCase):
49+
50+
def _callFUT(self, handler, excludes=None):
51+
from gcloud.logging.handlers import setup_logging
52+
if excludes:
53+
return setup_logging(handler, excluded_loggers=excludes)
54+
else:
55+
return setup_logging(handler)
56+
57+
def test_setup_logging(self):
58+
handler = _Handler(logging.INFO)
59+
self._callFUT(handler)
60+
61+
root_handlers = logging.getLogger().handlers
62+
self.assertIn(handler, root_handlers)
63+
64+
def test_setup_logging_excludes(self):
65+
INCLUDED_LOGGER_NAME = 'includeme'
66+
EXCLUDED_LOGGER_NAME = 'excludeme'
67+
68+
handler = _Handler(logging.INFO)
69+
self._callFUT(handler, [EXCLUDED_LOGGER_NAME])
70+
71+
included_logger = logging.getLogger(INCLUDED_LOGGER_NAME)
72+
self.assertTrue(included_logger.propagate)
73+
74+
excluded_logger = logging.getLogger(EXCLUDED_LOGGER_NAME)
75+
self.assertNotIn(handler, excluded_logger.handlers)
76+
self.assertFalse(excluded_logger.propagate)
77+
78+
def tearDown(self):
79+
# cleanup handlers
80+
root_handlers = logging.getLogger().handlers
81+
for handler in root_handlers:
82+
logging.getLogger().removeHandler(handler)
83+
84+
85+
class _Handler(object):
86+
87+
def __init__(self, level):
88+
self.level = level
89+
90+
def acquire(self):
91+
pass # pragma: NO COVER
92+
93+
def release(self):
94+
pass # pragma: NO COVER
95+
96+
97+
class _Logger(object):
98+
99+
def log_struct(self, message, severity=None):
100+
self.log_struct_called_with = (message, severity)
101+
102+
103+
class _Client(object):
104+
105+
def __init__(self, project):
106+
self.project = project
107+
self.logger_ = _Logger()
108+
109+
def logger(self, _): # pylint: disable=unused-argument
110+
return self.logger_
111+
112+
113+
class _Record(object):
114+
115+
def __init__(self, name, level, message):
116+
self.name = name
117+
self.levelname = level
118+
self.message = message
119+
self.exc_info = None
120+
self.exc_text = None
121+
self.stack_info = None
122+
123+
def getMessage(self):
124+
return self.message

0 commit comments

Comments
 (0)