Skip to content

Commit

Permalink
Merge pull request googleapis#2668 from waprin/newhandlers
Browse files Browse the repository at this point in the history
Add GAE and GKE fluentd Handlers
  • Loading branch information
dhermes authored Nov 22, 2016
2 parents 0f2b39a + e2658f0 commit a787604
Show file tree
Hide file tree
Showing 18 changed files with 625 additions and 96 deletions.
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@
logging-sink
logging-stdlib-usage
logging-handlers
logging-handlers-app-engine
logging-handlers-container-engine
logging-transports-sync
logging-transports-thread
logging-transports-base
Expand Down
6 changes: 6 additions & 0 deletions docs/logging-handlers-app-engine.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Google App Engine flexible Log Handler
======================================

.. automodule:: google.cloud.logging.handlers.app_engine
:members:
:show-inheritance:
6 changes: 6 additions & 0 deletions docs/logging-handlers-container-engine.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Google Container Engine Log Handler
===================================

.. automodule:: google.cloud.logging.handlers.container_engine
:members:
:show-inheritance:
105 changes: 105 additions & 0 deletions docs/logging-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,108 @@ Delete a sink:
:start-after: [START sink_delete]
:end-before: [END sink_delete]
:dedent: 4

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

It's possible to tie the Python :mod:`logging` module directly into Google
Stackdriver Logging. There are different handler options to accomplish this.
To automatically pick the default for your current environment, use
:meth:`~google.cloud.logging.client.Client.get_default_handler`.

.. literalinclude:: logging_snippets.py
:start-after: [START create_default_handler]
:end-before: [END create_default_handler]
:dedent: 4

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 Stackdriver Logging,
as well as any other loggers created. A helper method
:meth:`~google.cloud.logging.client.Client.setup_logging` is provided
to configure this automatically.

.. literalinclude:: logging_snippets.py
:start-after: [START setup_logging]
:end-before: [END setup_logging]
:dedent: 4

.. note::

To reduce cost and quota usage, do not enable Stackdriver logging
handlers while testing locally.

You can also exclude certain loggers:

.. literalinclude:: logging_snippets.py
:start-after: [START setup_logging_excludes]
:end-before: [END setup_logging_excludes]
:dedent: 4

Cloud Logging Handler
=====================

If you prefer not to use
:meth:`~google.cloud.logging.client.Client.get_default_handler`, you can
directly create a
:class:`~google.cloud.logging.handlers.handlers.CloudLoggingHandler` instance
which will write directly to the API.

.. literalinclude:: logging_snippets.py
:start-after: [START create_cloud_handler]
:end-before: [END create_cloud_handler]
:dedent: 4

.. 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:

.. literalinclude:: logging_snippets.py
:start-after: [START create_named_handler]
:end-before: [END create_named_handler]
:dedent: 4

fluentd logging handlers
========================

Besides :class:`~google.cloud.logging.handlers.handlers.CloudLoggingHandler`,
which writes directly to the API, two other handlers are provided.
:class:`~google.cloud.logging.handlers.app_engine.AppEngineHandler`, which is
recommended when running on the Google App Engine Flexible vanilla runtimes
(i.e. your app.yaml contains ``runtime: python``), and
:class:`~google.cloud.logging.handlers.container_engine.ContainerEngineHandler`
, which is recommended when running on `Google Container Engine`_ with the
Stackdriver Logging plugin enabled.

:meth:`~google.cloud.logging.client.Client.get_default_handler` and
:meth:`~google.cloud.logging.client.Client.setup_logging` will attempt to use
the environment to automatically detect whether the code is running in
these platforms and use the appropriate handler.

In both cases, the fluentd agent is configured to automatically parse log files
in an expected format and forward them to Stackdriver logging. The handlers
provided help set the correct metadata such as log level so that logs can be
filtered accordingly.

Cloud Logging Handler transports
=================================

The :class:`~google.cloud.logging.handlers.handlers.CloudLoggingHandler`
logging handler can use different transports. The default is
:class:`~google.cloud.logging.handlers.BackgroundThreadTransport`.

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

1. :class:`~google.cloud.logging.handlers.SyncTransport` this handler does a
direct API call on each logging statement to write the entry.


.. _Google Container Engine: https://cloud.google.com/container-engine/
38 changes: 38 additions & 0 deletions docs/logging_snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,44 @@ def sink_pubsub(client, to_delete):
to_delete.pop(0)


@snippet
def logging_handler(client):
# [START create_default_handler]
import logging
handler = client.get_default_handler()
cloud_logger = logging.getLogger('cloudLogger')
cloud_logger.setLevel(logging.INFO)
cloud_logger.addHandler(handler)
cloud_logger.error('bad news')
# [END create_default_handler]

# [START create_cloud_handler]
from google.cloud.logging.handlers import CloudLoggingHandler
handler = CloudLoggingHandler(client)
cloud_logger = logging.getLogger('cloudLogger')
cloud_logger.setLevel(logging.INFO)
cloud_logger.addHandler(handler)
cloud_logger.error('bad news')
# [END create_cloud_handler]

# [START create_named_handler]
handler = CloudLoggingHandler(client, name='mycustomlog')
# [END create_named_handler]


@snippet
def setup_logging(client):
import logging
# [START setup_logging]
client.setup_logging(log_level=logging.INFO)
# [END setup_logging]

# [START setup_logging_excludes]
client.setup_logging(log_level=logging.INFO,
excluded_loggers=('werkzeug',))
# [END setup_logging_excludes]


def _line_no(func):
return func.__code__.co_firstlineno

Expand Down
53 changes: 53 additions & 0 deletions logging/google/cloud/logging/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

"""Client for interacting with the Google Stackdriver Logging API."""

import logging
import os

try:
Expand All @@ -34,6 +35,12 @@
from google.cloud.logging._http import _LoggingAPI as JSONLoggingAPI
from google.cloud.logging._http import _MetricsAPI as JSONMetricsAPI
from google.cloud.logging._http import _SinksAPI as JSONSinksAPI
from google.cloud.logging.handlers import CloudLoggingHandler
from google.cloud.logging.handlers import AppEngineHandler
from google.cloud.logging.handlers import ContainerEngineHandler
from google.cloud.logging.handlers import setup_logging
from google.cloud.logging.handlers.handlers import EXCLUDED_LOGGER_DEFAULTS

from google.cloud.logging.logger import Logger
from google.cloud.logging.metric import Metric
from google.cloud.logging.sink import Sink
Expand All @@ -42,6 +49,15 @@
_DISABLE_GAX = os.getenv(DISABLE_GRPC, False)
_USE_GAX = _HAVE_GAX and not _DISABLE_GAX

_APPENGINE_FLEXIBLE_ENV_VM = 'GAE_APPENGINE_HOSTNAME'
"""Environment variable set in App Engine when vm:true is set."""

_APPENGINE_FLEXIBLE_ENV_FLEX = 'GAE_INSTANCE'
"""Environment variable set in App Engine when env:flex is set."""

_CONTAINER_ENGINE_ENV = 'KUBERNETES_SERVICE'
"""Environment variable set in a Google Container Engine environment."""


class Client(JSONClient):
"""Client to bundle configuration needed for API requests.
Expand Down Expand Up @@ -264,3 +280,40 @@ def list_metrics(self, page_size=None, page_token=None):
"""
return self.metrics_api.list_metrics(
self.project, page_size, page_token)

def get_default_handler(self):
"""Return the default logging handler based on the local environment.
:rtype: :class:`logging.Handler`
:returns: The default log handler based on the environment
"""
if (_APPENGINE_FLEXIBLE_ENV_VM in os.environ or
_APPENGINE_FLEXIBLE_ENV_FLEX in os.environ):
return AppEngineHandler()
elif _CONTAINER_ENGINE_ENV in os.environ:
return ContainerEngineHandler()
else:
return CloudLoggingHandler(self)

def setup_logging(self, log_level=logging.INFO,
excluded_loggers=EXCLUDED_LOGGER_DEFAULTS):
"""Attach default Stackdriver logging handler to the root logger.
This method uses the default log handler, obtained by
:meth:`~get_default_handler`, and attaches it to the root Python
logger, so that a call such as ``logging.warn``, as well as all child
loggers, will report to Stackdriver logging.
:type log_level: int
:param log_level: (Optional) Python logging log level. Defaults to
:const:`logging.INFO`.
:type excluded_loggers: tuple
:param excluded_loggers: (Optional) The loggers to not attach the
handler to. This will always include the
loggers in the path of the logging client
itself.
"""
handler = self.get_default_handler()
setup_logging(handler, log_level=log_level,
excluded_loggers=excluded_loggers)
3 changes: 3 additions & 0 deletions logging/google/cloud/logging/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@

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

from google.cloud.logging.handlers.app_engine import AppEngineHandler
from google.cloud.logging.handlers.container_engine import (
ContainerEngineHandler)
from google.cloud.logging.handlers.handlers import CloudLoggingHandler
from google.cloud.logging.handlers.handlers import setup_logging
39 changes: 39 additions & 0 deletions logging/google/cloud/logging/handlers/_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 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.

"""Helper functions for logging handlers."""

import math
import json


def format_stackdriver_json(record, message):
"""Helper to format a LogRecord in in Stackdriver fluentd format.
:rtype: str
:returns: JSON str to be written to the log file.
"""
subsecond, second = math.modf(record.created)

payload = {
'message': message,
'timestamp': {
'seconds': int(second),
'nanos': int(subsecond * 1e9),
},
'thread': record.thread,
'severity': record.levelname,
}

return json.dumps(payload)
73 changes: 73 additions & 0 deletions logging/google/cloud/logging/handlers/app_engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 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.

"""Logging handler for App Engine Flexible
Logs to the well-known file that the fluentd sidecar container on App Engine
Flexible is configured to read from and send to Stackdriver Logging.
See the fluentd configuration here:
https://github.com/GoogleCloudPlatform/appengine-sidecars-docker/tree/master/fluentd_logger
"""

# This file is largely copied from:
# https://github.com/GoogleCloudPlatform/python-compat-runtime/blob/master
# /appengine-vmruntime/vmruntime/cloud_logging.py

import logging.handlers
import os

from google.cloud.logging.handlers._helpers import format_stackdriver_json

_LOG_PATH_TEMPLATE = '/var/log/app_engine/app.{pid}.json'
_MAX_LOG_BYTES = 128 * 1024 * 1024
_LOG_FILE_COUNT = 3


class AppEngineHandler(logging.handlers.RotatingFileHandler):
"""A handler that writes to the App Engine fluentd Stackdriver log file.
Writes to the file that the fluentd agent on App Engine Flexible is
configured to discover logs and send them to Stackdriver Logging.
Log entries are wrapped in JSON and with appropriate metadata. The
process of converting the user's formatted logs into a JSON payload for
Stackdriver Logging consumption is implemented as part of the handler
itself, and not as a formatting step, so as not to interfere with
user-defined logging formats.
"""

def __init__(self):
"""Construct the handler
Large log entries will get mangled if multiple workers write to the
same file simultaneously, so we'll use the worker's PID to pick a log
filename.
"""
self.filename = _LOG_PATH_TEMPLATE.format(pid=os.getpid())
super(AppEngineHandler, self).__init__(self.filename,
maxBytes=_MAX_LOG_BYTES,
backupCount=_LOG_FILE_COUNT)

def format(self, record):
"""Format the specified record into the expected JSON structure.
:type record: :class:`~logging.LogRecord`
:param record: the log record
:rtype: str
:returns: JSON str to be written to the log file
"""
message = super(AppEngineHandler, self).format(record)
return format_stackdriver_json(record, message)
Loading

0 comments on commit a787604

Please sign in to comment.