Skip to content

Commit

Permalink
Merge pull request #2151 from dhermes/fix-2102
Browse files Browse the repository at this point in the history
Merging logging-stdlib-handler-feature branch back into master
  • Loading branch information
dhermes committed Aug 22, 2016
2 parents 20eea35 + c9c761d commit 0bf3b68
Show file tree
Hide file tree
Showing 21 changed files with 1,048 additions and 10 deletions.
3 changes: 2 additions & 1 deletion CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ Running System Tests
can be downloaded directly from the developer's console by clicking
"Generate new JSON key". See private key
`docs <https://cloud.google.com/storage/docs/authentication#generating-a-private-key>`__
for more details.
for more details. In order for Logging system tests to work, the Service Account
will also have to be made a project Owner. This can be changed under "IAM & Admin".

- Examples of these can be found in ``system_tests/local_test_setup.sample``. We
recommend copying this to ``system_tests/local_test_setup``, editing the
Expand Down
4 changes: 4 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@
logging-entries
logging-metric
logging-sink
logging-handlers
logging-transports-sync
logging-transports-thread
logging-transports-base

.. toctree::
:maxdepth: 0
Expand Down
6 changes: 6 additions & 0 deletions docs/logging-handlers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Python Logging Module Handler
==============================

.. automodule:: gcloud.logging.handlers.handlers
:members:
:show-inheritance:
6 changes: 6 additions & 0 deletions docs/logging-transports-base.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Python Logging Handler Sync Transport
======================================

.. automodule:: gcloud.logging.handlers.transports.base
:members:
:show-inheritance:
6 changes: 6 additions & 0 deletions docs/logging-transports-sync.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Python Logging Handler Sync Transport
======================================

.. automodule:: gcloud.logging.handlers.transports.sync
:members:
:show-inheritance:
7 changes: 7 additions & 0 deletions docs/logging-transports-thread.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Python Logging Handler Threaded Transport
=========================================


.. automodule:: gcloud.logging.handlers.transports.background_thread
:members:
:show-inheritance:
71 changes: 71 additions & 0 deletions docs/logging-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,74 @@ Delete a sink:
>>> sink.delete() # API call
>>> sink.exists() # API call
False

Integration with Python logging module
---------------------------------------------


It's possible to tie the Python :mod:`logging` module directly into Google Cloud Logging. To use it,
create a :class:`CloudLoggingHandler <gcloud.logging.CloudLoggingHandler>` instance from your
Logging client.

.. doctest::

>>> import logging
>>> import gcloud.logging # Don't conflict with standard logging
>>> from gcloud.logging.handlers import CloudLoggingHandler
>>> client = gcloud.logging.Client()
>>> handler = CloudLoggingHandler(client)
>>> cloud_logger = logging.getLogger('cloudLogger')
>>> cloud_logger.setLevel(logging.INFO) # defaults to WARN
>>> cloud_logger.addHandler(handler)
>>> cloud_logger.error('bad news')

.. note::

This handler by default uses an asynchronous transport that sends log entries on a background
thread. However, the API call will still be made in the same process. For other transport
options, see the transports section.

All logs will go to a single custom log, which defaults to "python". The name of the Python
logger will be included in the structured log entry under the "python_logger" field. You can
change it by providing a name to the handler:

.. doctest::

>>> handler = CloudLoggingHandler(client, name="mycustomlog")

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, as well as any other loggers created. 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:

.. doctest::

>>> import logging
>>> import gcloud.logging # Don't conflict with standard logging
>>> from gcloud.logging.handlers import CloudLoggingHandler, setup_logging
>>> client = gcloud.logging.Client()
>>> handler = CloudLoggingHandler(client)
>>> logging.getLogger().setLevel(logging.INFO) # defaults to WARN
>>> setup_logging(handler)
>>> logging.error('bad news')

You can also exclude certain loggers:

.. doctest::

>>> setup_logging(handler, excluded_loggers=('werkzeug',)))



Python logging handler transports
==================================

The Python logging handler can use different transports. The default is
:class:`gcloud.logging.handlers.BackgroundThreadTransport`.

1. :class:`gcloud.logging.handlers.BackgroundThreadTransport` this is the default. It writes
entries on a background :class:`python.threading.Thread`.

1. :class:`gcloud.logging.handlers.SyncTransport` this handler does a direct API call on each
logging statement to write the entry.
18 changes: 18 additions & 0 deletions gcloud/logging/handlers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Python :mod:`logging` handlers for Google Cloud Logging."""

from gcloud.logging.handlers.handlers import CloudLoggingHandler
from gcloud.logging.handlers.handlers import setup_logging
133 changes: 133 additions & 0 deletions gcloud/logging/handlers/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Python :mod:`logging` handlers for Google Cloud Logging."""

import logging

from gcloud.logging.handlers.transports import BackgroundThreadTransport


EXCLUDE_LOGGER_DEFAULTS = (
'gcloud',
'oauth2client'
)

DEFAULT_LOGGER_NAME = 'python'


class CloudLoggingHandler(logging.StreamHandler):
"""Python standard ``logging`` handler.
This handler can be used to route Python standard logging messages
directly to the Google Cloud Logging API.
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.
:type client: :class:`gcloud.logging.client`
:param client: the authenticated gcloud logging client for this handler
to use
:type name: str
:param name: the name of the custom log in Stackdriver Logging. Defaults
to 'python'. The name of the Python logger will be represented
in the ``python_logger`` field.
:type transport: type
:param transport: Class for creating new transport objects. It should
extend from the base :class:`.Transport` type and
implement :meth`.Transport.send`. Defaults to
:class:`.BackgroundThreadTransport`. The other
option is :class:`.SyncTransport`.
Example:
.. doctest::
import gcloud.logging
from gcloud.logging.handlers import CloudLoggingHandler
client = gcloud.logging.Client()
handler = CloudLoggingHandler(client)
cloud_logger = logging.getLogger('cloudLogger')
cloud_logger.setLevel(logging.INFO)
cloud_logger.addHandler(handler)
cloud.logger.error('bad news') # API call
"""

def __init__(self, client,
name=DEFAULT_LOGGER_NAME,
transport=BackgroundThreadTransport):
super(CloudLoggingHandler, self).__init__()
self.name = name
self.client = client
self.transport = transport(client, name)

def emit(self, record):
"""Actually log the specified logging record.
Overrides the default emit behavior of ``StreamHandler``.
See: https://docs.python.org/2/library/logging.html#handler-objects
:type record: :class:`logging.LogRecord`
:param record: The record to be logged.
"""
message = super(CloudLoggingHandler, self).format(record)
self.transport.send(record, message)


def setup_logging(handler, excluded_loggers=EXCLUDE_LOGGER_DEFAULTS):
"""Attach the ``CloudLogging`` handler to the Python root logger
Excludes loggers that this library itself uses to avoid
infinite recursion.
:type handler: :class:`logging.handler`
:param handler: the handler to attach to the global handler
:type excluded_loggers: tuple
:param excluded_loggers: The loggers to not attach the handler to. This
will always include the loggers in the path of
the logging client itself.
Example:
.. doctest::
import logging
import gcloud.logging
from gcloud.logging.handlers import CloudLoggingHandler
client = gcloud.logging.Client()
handler = CloudLoggingHandler(client)
gcloud.logging.setup_logging(handler)
logging.getLogger().setLevel(logging.DEBUG)
logging.error('bad news') # API call
"""
all_excluded_loggers = set(excluded_loggers + EXCLUDE_LOGGER_DEFAULTS)
logger = logging.getLogger()
logger.addHandler(handler)
logger.addHandler(logging.StreamHandler())
for logger_name in all_excluded_loggers:
logger = logging.getLogger(logger_name)
logger.propagate = False
logger.addHandler(logging.StreamHandler())
Loading

0 comments on commit 0bf3b68

Please sign in to comment.