-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add GAE and GKE fluentd Handlers #2668
Conversation
@dhermes /cc @jonparrott |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty awesome stuff!
@@ -0,0 +1,6 @@ | |||
Google App Engine Flexible Log Handler | |||
======================================= |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
@@ -0,0 +1,6 @@ | |||
Google Container Engine Log Handler | |||
======================================= |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
Logging client. | ||
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, | ||
use :meth:``google.cloud.logging.client.get_default_handler`. |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
>>> cloud_logger = logging.getLogger('cloudLogger') | ||
>>> cloud_logger.setLevel(logging.INFO) # defaults to WARN | ||
>>> cloud_logger.addHandler(handler) | ||
>>> cloud_logger.error('bad news') | ||
|
||
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, |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
method :meth:`setup_logging <google.cloud.logging.handlers.setup_logging>` is provided to configure | ||
this automatically: | ||
CloudLoggingHandler | ||
======================= |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
@@ -15,4 +15,6 @@ | |||
"""Python :mod:`logging` handlers for Google Cloud Logging.""" | |||
|
|||
from google.cloud.logging.handlers.handlers import CloudLoggingHandler | |||
from google.cloud.logging.handlers.GAEHandler import GAEHandler |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
: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. | ||
:param excluded_loggers: (Optional)The loggers to not attach the handler |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
to. This will always include the loggers in the | ||
path of the logging client itself. | ||
|
||
:type log_level: :mod:`logging` log level |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
@@ -184,7 +184,7 @@ def batch(self): | |||
|
|||
class _Client(object): | |||
|
|||
def __init__(self, project): | |||
def __init__(self, project, _=None, __=None): |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
@@ -584,6 +637,9 @@ def create_scoped(self, scope): | |||
self._scopes = scope | |||
return self | |||
|
|||
def authorize(self, http): | |||
pass |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
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. | ||
|
||
This file is largely copied from: |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
import math | ||
import os | ||
|
||
LOG_PATH_TEMPLATE = '/var/log/app_engine/app.{pid}.json' |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
LOG_FILE_COUNT = 3 | ||
|
||
|
||
class GAEHandler(logging.handlers.RotatingFileHandler): |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
|
||
|
||
class GAEHandler(logging.handlers.RotatingFileHandler): | ||
"""A handler that writes to the GAE fluentd Stackdriver logging file. |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
class GAEHandler(logging.handlers.RotatingFileHandler): | ||
"""A handler that writes to the GAE fluentd Stackdriver logging file. | ||
|
||
Writes to the file that the fluentd agent on App Engine Flexbile is |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
target=self._run, | ||
name='google.cloud.logging.handlers.transport.Worker') | ||
thread_name = 'google.cloud.logging.handlers.transport.Worker' | ||
self._thread = threading.Thread(target=self._run, |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
LOGNAME = 'loggername' | ||
MESSAGE = 'hello world' | ||
record = _Record(LOGNAME, logging.INFO, MESSAGE) | ||
expected_payload = {'message': MESSAGE, |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
self.assertEqual(payload, json.dumps(expected_payload)) | ||
|
||
|
||
class _Record(object): |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
import logging | ||
import json | ||
handler = self._makeOne() | ||
LOGNAME = 'loggername' |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
@@ -184,7 +184,7 @@ def batch(self): | |||
|
|||
class _Client(object): | |||
|
|||
def __init__(self, project): | |||
def __init__(self, project, _=None, __=None): |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
@@ -110,6 +110,8 @@ | |||
logging-metric | |||
logging-sink | |||
logging-handlers | |||
logging-handlers-GAEHandler | |||
logging-handlers-GKEHandler |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
Google App Engine Flexible Log Handler | ||
======================================= | ||
|
||
.. automodule:: google.cloud.logging.handlers.GAEHandler |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
Google Container Engine Log Handler | ||
======================================= | ||
|
||
.. automodule:: google.cloud.logging.handlers.GKEHandler |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
create a :class:`CloudLoggingHandler <google.cloud.logging.CloudLoggingHandler>` instance from your | ||
Logging client. | ||
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, |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
Logging client. | ||
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, | ||
use :meth:``google.cloud.logging.client.get_default_handler`. |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
|
||
client = self._makeOne(self.PROJECT, credentials=_Credentials()) | ||
|
||
with _Monkey(os, getenv=lambda var: var == 'GAE_APPENGINE_HOSTNAME'): |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
|
||
self.assertIsInstance(handler, GKEHandler) | ||
|
||
with _Monkey(os, getenv=lambda _: None): |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
|
||
self.assertIsInstance(handler, GAEHandler) | ||
|
||
with _Monkey(os, getenv=lambda var: var == 'KUBERNETES_SERVICE'): |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
def test_setup_logging(self): | ||
from google.cloud._testing import _Monkey | ||
import google.cloud.logging.client as MUT | ||
client = self._makeOne( |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
self.called = False | ||
|
||
def __call__(self, _, log_level, | ||
excluded_loggers=None): # pylintdisable=unused-variable |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
Think I addressed all comments, let me make sure Travis is passing and then I'll do another once-over before I request another review. |
@@ -390,11 +391,12 @@ Delete a sink: | |||
False | |||
|
|||
Integration with Python logging module | |||
--------------------------------------------- | |||
--------------------------------------- |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
@@ -596,7 +596,7 @@ def test_get_default_handler_container_engine(self): | |||
client = self._makeOne(project=self.PROJECT, | |||
credentials=_Credentials(), | |||
use_gax=False) | |||
os.environ[_CONTAINER_ENGINE_ENV] = True | |||
os.environ[_CONTAINER_ENGINE_ENV] = "True" |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
(i.e. your app.yaml contains ``runtime: python``), and | ||
:class:`~google.cloud.handlers.container_engine.ContainerEngineHandler`, | ||
which is | ||
recommended when running on `Google Container Engine` with the Stackdriver |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
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 |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
""" | ||
|
||
# This file is largely copied from: | ||
# https://github.com/GoogleCloudPlatform/python-compat-runtime/blob/master/appengine-vmruntime/vmruntime/cloud_logging.py |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
>>> setup_logging(handler, excluded_loggers=('werkzeug',))) | ||
>>> handler = CloudLoggingHandler(client, name="mycustomlog") | ||
|
||
fluentd logging handlers |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
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 |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
method :meth:`setup_logging <google.cloud.logging.handlers.setup_logging>` is provided to configure | ||
this automatically: | ||
If you prefer not to use :meth:`google.cloud.logging.client | ||
.get_default_handler`, you can directly create a |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
'message': message, | ||
'timestamp': { | ||
'seconds': 5, | ||
'nanos': int(.03 * 1e9) |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
'timestamp': {'seconds': 5, | ||
'nanos': int(.03 * 1e9)}, | ||
'thread': record.thread, | ||
'severity': record.levelname} |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
'nanos': int(subsecond * 1e9) | ||
}, | ||
'thread': record.thread, | ||
'severity': record.levelname |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
'message': message, | ||
'timestamp': { | ||
'seconds': int(second), | ||
'nanos': int(subsecond * 1e9) |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
payload = { | ||
'message': message, | ||
'timestamp': {'seconds': int(second), | ||
'nanos': int(subsecond * 1e9)}, |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
c755a71
to
c132584
Compare
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 Flexbile is |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
itself, and not as a formatting step, so as not to interfere with | ||
user-defined logging formats. | ||
|
||
Logging calls can also alternatively 'trace_id' in as a field in the |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
"""Logging handler for Google Container Engine (GKE). | ||
|
||
Formats log messages in a JSON format, so that Kubernetes clusters with the | ||
fluentd google cloud plugin installed can format their log messages so that |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
|
||
|
||
class ContainerEngineHandler(logging.StreamHandler): | ||
"""Handler to formats log messages in good fluentd GKE format. |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
@@ -15,27 +15,21 @@ | |||
"""Python :mod:`logging` handlers for Google Cloud Logging.""" | |||
|
|||
import logging | |||
|
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
class TestContainerEngineHandler(unittest.TestCase): | ||
PROJECT = 'PROJECT' | ||
|
||
def _getTargetClass(self): |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
'message': message, | ||
'timestamp': { | ||
'seconds': 5, | ||
'nanos': int(.03 * 1e9) |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
'message': MESSAGE, | ||
'python_logger': PYTHON_LOGGER_NAME | ||
'message': message, | ||
'python_logger': python_logger_name |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
'message': MESSAGE, | ||
'python_logger': PYTHON_LOGGER_NAME | ||
'message': message, | ||
'python_logger': python_logger_name |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
@@ -584,6 +637,9 @@ def create_scoped(self, scope): | |||
self._scopes = scope | |||
return self | |||
|
|||
def authorize(self, http): | |||
pass |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
@@ -562,6 +629,9 @@ def create_scoped(self, scope): | |||
self._scopes = scope | |||
return self | |||
|
|||
def authorize(self, _): |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
@@ -12,6 +12,7 @@ | |||
# See the License for the specific language governing permissions and | |||
# limitations under the License. | |||
|
|||
import mock |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
use_gax=False) | ||
handler = client.get_default_handler() | ||
self.assertIsInstance(handler, CloudLoggingHandler) | ||
self.assertTrue(credentials.authorized, http_mock) |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
from google.cloud.logging.handlers import CloudLoggingHandler | ||
|
||
http_mock = mock.Mock() |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
from google.cloud._testing import _Monkey | ||
import google.cloud.logging.client as MUT | ||
|
||
http_mock = mock.Mock() |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
|
||
def test_setup_logging(self): | ||
@mock.patch('copy.deepcopy') | ||
def test_setup_logging(self, deepcopy): |
This comment was marked as spam.
This comment was marked as spam.
Sorry, something went wrong.
@waprin Pretty please squash (either in the GitHub UI or in your local copy). @jonparrott Do you want to do a final pass? |
On GAE and GKE with the plugin installed, there is a fluentd plugin that collects logs from files. However without the right formatting, metadata like log_level is lost. Furthermore, the fluentd agents are configured to set the correct resources types, which could be done in the main handler as well, but it’s easier to rely on the fluentd configurations. This adds two new handlers and some helper functions to detect when they should be used.
Add GAE and GKE fluentd Handlers
On GAE and GKE with the plugin installed, there is a fluentd plugin that collects logs from files. However without the right formatting, metadata like log_level is lost. Furthermore, the fluentd agents are configured to set the correct resources types, which could be done in the main handler as well, but it’s easier to rely on the fluentd configurations.
This adds two new handlers and some helper functions to detect when they should be used.