Skip to content
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

Send trace context with logs from web applications #3448

Merged
merged 49 commits into from
Jun 2, 2017
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
9a20be9
Send trace context with logs from web applications
liyanhui1228 May 22, 2017
cf283c4
Fix style
liyanhui1228 May 22, 2017
583d076
Improved code for web framework detection
liyanhui1228 May 23, 2017
5be368e
Fix year
liyanhui1228 May 23, 2017
28e3a6c
Drop module level variables
liyanhui1228 May 23, 2017
cdc96b4
Address Jon's comments and add some unit tests (not complete yet)
liyanhui1228 May 25, 2017
fb83ab6
Fix stuff
liyanhui1228 May 25, 2017
9a20c4e
Add unit test for flask and some refactor
liyanhui1228 May 25, 2017
f8b7e65
Add unit test for middleware
liyanhui1228 May 26, 2017
65efdcd
Add unit test for django
liyanhui1228 May 26, 2017
f78cc2e
Fix style
liyanhui1228 May 26, 2017
33b92dd
Address jon's comments
liyanhui1228 May 26, 2017
5cc798f
Address jon's comments
liyanhui1228 May 30, 2017
6ef3f49
Address all comments
liyanhui1228 Jun 2, 2017
14c2545
fix style
liyanhui1228 Jun 2, 2017
199ada0
Send trace context with logs from web applications
liyanhui1228 May 22, 2017
425c8ef
Fix style
liyanhui1228 May 22, 2017
1047fd5
Improved code for web framework detection
liyanhui1228 May 23, 2017
46012eb
Fix year
liyanhui1228 May 23, 2017
43148bf
Drop module level variables
liyanhui1228 May 23, 2017
de02a1d
Address Jon's comments and add some unit tests (not complete yet)
liyanhui1228 May 25, 2017
ffb57c4
Fix stuff
liyanhui1228 May 25, 2017
8fd8f31
Add unit test for flask and some refactor
liyanhui1228 May 25, 2017
4480027
Add unit test for middleware
liyanhui1228 May 26, 2017
ad00bdb
Add unit test for django
liyanhui1228 May 26, 2017
8ed81ad
Fix style
liyanhui1228 May 26, 2017
a6b25bb
Address jon's comments
liyanhui1228 May 26, 2017
6123764
Address jon's comments
liyanhui1228 May 30, 2017
3caccc7
Address all comments
liyanhui1228 Jun 2, 2017
4589a06
fix style
liyanhui1228 Jun 2, 2017
2889cfc
Tweaks for style / coverage / lint.
dhermes Jun 2, 2017
53738d7
Merge branch 'yanhuil/trace_id' of https://github.com/GoogleCloudPlat…
liyanhui1228 Jun 2, 2017
1957077
Send trace context with logs from web applications
liyanhui1228 May 22, 2017
bf683f7
Fix style
liyanhui1228 May 22, 2017
51cdb39
Improved code for web framework detection
liyanhui1228 May 23, 2017
2479d91
Fix year
liyanhui1228 May 23, 2017
6c04196
Drop module level variables
liyanhui1228 May 23, 2017
f70c84e
Address Jon's comments and add some unit tests (not complete yet)
liyanhui1228 May 25, 2017
6fc4b21
Fix stuff
liyanhui1228 May 25, 2017
2106bfc
Add unit test for flask and some refactor
liyanhui1228 May 25, 2017
c66fc31
Add unit test for middleware
liyanhui1228 May 26, 2017
035c4a7
Add unit test for django
liyanhui1228 May 26, 2017
bd8c1b4
Fix style
liyanhui1228 May 26, 2017
51bf193
Address jon's comments
liyanhui1228 May 26, 2017
6b90d1e
Address jon's comments
liyanhui1228 May 30, 2017
0c6c4f6
Address all comments
liyanhui1228 Jun 2, 2017
fe8a3d5
fix style
liyanhui1228 Jun 2, 2017
4dff904
Tweaks for style / coverage / lint.
dhermes Jun 2, 2017
9bac365
Merge branch 'yanhuil/trace_id' of https://github.com/GoogleCloudPlat…
liyanhui1228 Jun 2, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions logging/google/cloud/logging/handlers/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@
import math
import json

try:
import flask
except ImportError: # pragma: NO COVER
flask = None

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

from google.cloud.logging.handlers.middleware.request import (
_get_django_request)

_FLASK_TRACE_HEADER = 'X_CLOUD_TRACE_CONTEXT'
_DJANGO_TRACE_HEADER = 'HTTP_X_CLOUD_TRACE_CONTEXT'


def format_stackdriver_json(record, message):
"""Helper to format a LogRecord in in Stackdriver fluentd format.
Expand All @@ -37,3 +48,58 @@ def format_stackdriver_json(record, message):
}

return json.dumps(payload)


def get_trace_id_from_flask():
"""Get trace_id from flask request headers.

:rtype: str
:return: Trace_id in HTTP request headers.
"""
if flask is None or not flask.request:
return None

header = flask.request.headers.get(_FLASK_TRACE_HEADER)

if header is None:
return None

trace_id = header.split('/')[0]

This comment was marked as spam.

This comment was marked as spam.


return trace_id


def get_trace_id_from_django():
"""Get trace_id from django request headers.

:rtype: str
:return: Trace_id in HTTP request headers.
"""
request = _get_django_request()

if request is None:
return None

header = request.META.get(_DJANGO_TRACE_HEADER)
if header is None:
return None

trace_id = header.split('/')[0]

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.


return trace_id


def get_trace_id():
"""Helper to get trace_id from web application request header.

:rtype: str
:returns: Trace_id in HTTP request headers.
"""
checkers = (get_trace_id_from_django, get_trace_id_from_flask)

for checker in checkers:
trace_id = checker()
if trace_id is not None:
return trace_id

return None
23 changes: 22 additions & 1 deletion logging/google/cloud/logging/handlers/app_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import os

from google.cloud.logging.handlers._helpers import get_trace_id
from google.cloud.logging.handlers.handlers import CloudLoggingHandler
from google.cloud.logging.handlers.transports import BackgroundThreadTransport
from google.cloud.logging.resource import Resource
Expand All @@ -30,6 +31,8 @@
_GAE_SERVICE_ENV = 'GAE_SERVICE'
_GAE_VERSION_ENV = 'GAE_VERSION'

_TRACE_ID_LABEL = 'appengine.googleapis.com/trace_id'


class AppEngineHandler(CloudLoggingHandler):
"""A logging handler that sends App Engine-formatted logs to Stackdriver.
Expand All @@ -50,7 +53,8 @@ def __init__(self, client,
client,
name=_DEFAULT_GAE_LOGGER_NAME,
transport=transport,
resource=self.get_gae_resource())
resource=self.get_gae_resource(),
labels=self.get_gae_labels())

def get_gae_resource(self):
"""Return the GAE resource using the environment variables.
Expand All @@ -67,3 +71,20 @@ def get_gae_resource(self):
},
)
return gae_resource

def get_gae_labels(self):
"""Return the labels for GAE app.

If the trace ID can be detected, it will be included as a label.
Currently, no other labels are included.

:rtype: dict
:returns: Labels for GAE app.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

"""
gae_labels = {}

trace_id = get_trace_id()
if trace_id is not None:
gae_labels[_TRACE_ID_LABEL] = trace_id

return gae_labels
13 changes: 11 additions & 2 deletions logging/google/cloud/logging/handlers/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ class CloudLoggingHandler(logging.StreamHandler):
:param resource: (Optional) Monitored resource of the entry, defaults
to the global resource type.

:type labels: dict
:param labels: (Optional) Mapping of labels for the entry.

Example:

.. code-block:: python
Expand All @@ -79,12 +82,14 @@ class CloudLoggingHandler(logging.StreamHandler):
def __init__(self, client,
name=DEFAULT_LOGGER_NAME,
transport=BackgroundThreadTransport,
resource=_GLOBAL_RESOURCE):
resource=_GLOBAL_RESOURCE,
labels=None):
super(CloudLoggingHandler, self).__init__()
self.name = name
self.client = client
self.transport = transport(client, name)
self.resource = resource
self.labels = labels

def emit(self, record):
"""Actually log the specified logging record.
Expand All @@ -97,7 +102,11 @@ def emit(self, record):
:param record: The record to be logged.
"""
message = super(CloudLoggingHandler, self).format(record)
self.transport.send(record, message, resource=self.resource)
self.transport.send(
record,
message,
resource=self.resource,
labels=self.labels)


def setup_logging(handler, excluded_loggers=EXCLUDED_LOGGER_DEFAULTS,
Expand Down
17 changes: 17 additions & 0 deletions logging/google/cloud/logging/handlers/middleware/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2017 Google Inc. All Rights Reserved.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

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

from google.cloud.logging.handlers.middleware.request import RequestMiddleware

__all__ = ['RequestMiddleware']
45 changes: 45 additions & 0 deletions logging/google/cloud/logging/handlers/middleware/request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright 2017 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.

"""Django middleware helper to capture a request.

The request is stored on a thread-local so that it can be
inspected by other helpers.
"""

import threading


_thread_locals = threading.local()


def _get_django_request():
"""Get Django request from thread local.

:rtype: str
:returns: Django request.
"""
return getattr(_thread_locals, 'request', None)


class RequestMiddleware(object):
"""Saves the request in thread local"""

def process_request(self, request):
"""Called on each request, before Django decides which view to execute.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.


:type request: :class:`~django.http.request.HttpRequest`
:param request: Django http request.
"""
_thread_locals.request = request

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def _main_thread_terminated(self):
else:
print('Failed to send %d pending logs.' % (self._queue.qsize(),))

def enqueue(self, record, message, resource=None):
def enqueue(self, record, message, resource=None, labels=None):
"""Queues a log entry to be written by the background thread.

:type record: :class:`logging.LogRecord`
Expand All @@ -215,6 +215,9 @@ def enqueue(self, record, message, resource=None):

:type resource: :class:`~google.cloud.logging.resource.Resource`
:param resource: (Optional) Monitored resource of the entry

:type labels: dict
:param labels: (Optional) Mapping of labels for the entry.
"""
self._queue.put_nowait({
'info': {
Expand All @@ -223,6 +226,7 @@ def enqueue(self, record, message, resource=None):
},
'severity': record.levelname,
'resource': resource,
'labels': labels,
})

def flush(self):
Expand Down Expand Up @@ -257,7 +261,7 @@ def __init__(self, client, name, grace_period=_DEFAULT_GRACE_PERIOD,
self.worker = _Worker(logger)
self.worker.start()

def send(self, record, message, resource=None):
def send(self, record, message, resource=None, labels=None):
"""Overrides Transport.send().

:type record: :class:`logging.LogRecord`
Expand All @@ -269,8 +273,11 @@ def send(self, record, message, resource=None):

:type resource: :class:`~google.cloud.logging.resource.Resource`
:param resource: (Optional) Monitored resource of the entry.

:type labels: dict
:param labels: (Optional) Mapping of labels for the entry.
"""
self.worker.enqueue(record, message, resource=resource)
self.worker.enqueue(record, message, resource=resource, labels=labels)

def flush(self):
"""Submit any pending log records."""
Expand Down
5 changes: 4 additions & 1 deletion logging/google/cloud/logging/handlers/transports/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Transport(object):
client and name object, and must override :meth:`send`.
"""

def send(self, record, message, resource=None):
def send(self, record, message, resource=None, labels=None):
"""Transport send to be implemented by subclasses.
:type record: :class:`logging.LogRecord`
Expand All @@ -34,6 +34,9 @@ def send(self, record, message, resource=None):
:type resource: :class:`~google.cloud.logging.resource.Resource`
:param resource: (Optional) Monitored resource of the entry.
:type labels: dict
:param labels: (Optional) Mapping of labels for the entry.
"""
raise NotImplementedError

Expand Down
11 changes: 9 additions & 2 deletions logging/google/cloud/logging/handlers/transports/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class SyncTransport(Transport):
def __init__(self, client, name):
self.logger = client.logger(name)

def send(self, record, message, resource=None):
def send(self, record, message, resource=None, labels=None):
"""Overrides transport.send().

:type record: :class:`logging.LogRecord`
Expand All @@ -38,8 +38,15 @@ def send(self, record, message, resource=None):
:type message: str
:param message: The message from the ``LogRecord`` after being
formatted by the associated log formatters.

:type resource: :class:`~google.cloud.logging.resource.Resource`
:param resource: (Optional) Monitored resource of the entry.

:type labels: dict
:param labels: (Optional) Mapping of labels for the entry.
"""
info = {'message': message, 'python_logger': record.name}
self.logger.log_struct(info,
severity=record.levelname,
resource=resource)
resource=resource,
labels=labels)
4 changes: 3 additions & 1 deletion logging/nox.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ def unit_tests(session, python_version):
session.interpreter = 'python{}'.format(python_version)

# Install all test dependencies, then install this package in-place.
session.install('mock', 'pytest', 'pytest-cov', *LOCAL_DEPS)
session.install(
'mock', 'pytest', 'pytest-cov',
'flask', 'django', *LOCAL_DEPS)
session.install('-e', '.')

# Run py.test against the unit tests.
Expand Down
Loading