Skip to content

Commit 800353f

Browse files
liyanhui1228landrito
authored andcommitted
Send trace context with logs from web applications (googleapis#3448)
1 parent ac68480 commit 800353f

File tree

15 files changed

+499
-27
lines changed

15 files changed

+499
-27
lines changed

logging/google/cloud/logging/handlers/_helpers.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@
1717
import math
1818
import json
1919

20+
try:
21+
import flask
22+
except ImportError: # pragma: NO COVER
23+
flask = None
24+
25+
from google.cloud.logging.handlers.middleware.request import (
26+
_get_django_request)
27+
28+
_FLASK_TRACE_HEADER = 'X_CLOUD_TRACE_CONTEXT'
29+
_DJANGO_TRACE_HEADER = 'HTTP_X_CLOUD_TRACE_CONTEXT'
30+
2031

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

3950
return json.dumps(payload)
51+
52+
53+
def get_trace_id_from_flask():
54+
"""Get trace_id from flask request headers.
55+
56+
:rtype: str
57+
:return: Trace_id in HTTP request headers.
58+
"""
59+
if flask is None or not flask.request:
60+
return None
61+
62+
header = flask.request.headers.get(_FLASK_TRACE_HEADER)
63+
64+
if header is None:
65+
return None
66+
67+
trace_id = header.split('/', 1)[0]
68+
69+
return trace_id
70+
71+
72+
def get_trace_id_from_django():
73+
"""Get trace_id from django request headers.
74+
75+
:rtype: str
76+
:return: Trace_id in HTTP request headers.
77+
"""
78+
request = _get_django_request()
79+
80+
if request is None:
81+
return None
82+
83+
header = request.META.get(_DJANGO_TRACE_HEADER)
84+
if header is None:
85+
return None
86+
87+
trace_id = header.split('/', 1)[0]
88+
89+
return trace_id
90+
91+
92+
def get_trace_id():
93+
"""Helper to get trace_id from web application request header.
94+
95+
:rtype: str
96+
:returns: Trace_id in HTTP request headers.
97+
"""
98+
checkers = (get_trace_id_from_django, get_trace_id_from_flask)
99+
100+
for checker in checkers:
101+
trace_id = checker()
102+
if trace_id is not None:
103+
return trace_id
104+
105+
return None

logging/google/cloud/logging/handlers/app_engine.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import os
2222

23+
from google.cloud.logging.handlers._helpers import get_trace_id
2324
from google.cloud.logging.handlers.handlers import CloudLoggingHandler
2425
from google.cloud.logging.handlers.transports import BackgroundThreadTransport
2526
from google.cloud.logging.resource import Resource
@@ -30,6 +31,8 @@
3031
_GAE_SERVICE_ENV = 'GAE_SERVICE'
3132
_GAE_VERSION_ENV = 'GAE_VERSION'
3233

34+
_TRACE_ID_LABEL = 'appengine.googleapis.com/trace_id'
35+
3336

3437
class AppEngineHandler(CloudLoggingHandler):
3538
"""A logging handler that sends App Engine-formatted logs to Stackdriver.
@@ -50,7 +53,8 @@ def __init__(self, client,
5053
client,
5154
name=_DEFAULT_GAE_LOGGER_NAME,
5255
transport=transport,
53-
resource=self.get_gae_resource())
56+
resource=self.get_gae_resource(),
57+
labels=self.get_gae_labels())
5458

5559
def get_gae_resource(self):
5660
"""Return the GAE resource using the environment variables.
@@ -67,3 +71,20 @@ def get_gae_resource(self):
6771
},
6872
)
6973
return gae_resource
74+
75+
def get_gae_labels(self):
76+
"""Return the labels for GAE app.
77+
78+
If the trace ID can be detected, it will be included as a label.
79+
Currently, no other labels are included.
80+
81+
:rtype: dict
82+
:returns: Labels for GAE app.
83+
"""
84+
gae_labels = {}
85+
86+
trace_id = get_trace_id()
87+
if trace_id is not None:
88+
gae_labels[_TRACE_ID_LABEL] = trace_id
89+
90+
return gae_labels

logging/google/cloud/logging/handlers/handlers.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ class CloudLoggingHandler(logging.StreamHandler):
5757
:param resource: (Optional) Monitored resource of the entry, defaults
5858
to the global resource type.
5959
60+
:type labels: dict
61+
:param labels: (Optional) Mapping of labels for the entry.
62+
6063
Example:
6164
6265
.. code-block:: python
@@ -79,12 +82,14 @@ class CloudLoggingHandler(logging.StreamHandler):
7982
def __init__(self, client,
8083
name=DEFAULT_LOGGER_NAME,
8184
transport=BackgroundThreadTransport,
82-
resource=_GLOBAL_RESOURCE):
85+
resource=_GLOBAL_RESOURCE,
86+
labels=None):
8387
super(CloudLoggingHandler, self).__init__()
8488
self.name = name
8589
self.client = client
8690
self.transport = transport(client, name)
8791
self.resource = resource
92+
self.labels = labels
8893

8994
def emit(self, record):
9095
"""Actually log the specified logging record.
@@ -97,7 +102,11 @@ def emit(self, record):
97102
:param record: The record to be logged.
98103
"""
99104
message = super(CloudLoggingHandler, self).format(record)
100-
self.transport.send(record, message, resource=self.resource)
105+
self.transport.send(
106+
record,
107+
message,
108+
resource=self.resource,
109+
labels=self.labels)
101110

102111

103112
def setup_logging(handler, excluded_loggers=EXCLUDED_LOGGER_DEFAULTS,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2017 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+
from google.cloud.logging.handlers.middleware.request import RequestMiddleware
16+
17+
__all__ = ['RequestMiddleware']
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright 2017 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+
"""Django middleware helper to capture a request.
16+
17+
The request is stored on a thread-local so that it can be
18+
inspected by other helpers.
19+
"""
20+
21+
import threading
22+
23+
24+
_thread_locals = threading.local()
25+
26+
27+
def _get_django_request():
28+
"""Get Django request from thread local.
29+
30+
:rtype: str
31+
:returns: Django request.
32+
"""
33+
return getattr(_thread_locals, 'request', None)
34+
35+
36+
class RequestMiddleware(object):
37+
"""Saves the request in thread local"""
38+
39+
def process_request(self, request):
40+
"""Called on each request, before Django decides which view to execute.
41+
42+
:type request: :class:`~django.http.request.HttpRequest`
43+
:param request: Django http request.
44+
"""
45+
_thread_locals.request = request

logging/google/cloud/logging/handlers/transports/background_thread.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def _main_thread_terminated(self):
203203
else:
204204
print('Failed to send %d pending logs.' % (self._queue.qsize(),))
205205

206-
def enqueue(self, record, message, resource=None):
206+
def enqueue(self, record, message, resource=None, labels=None):
207207
"""Queues a log entry to be written by the background thread.
208208
209209
:type record: :class:`logging.LogRecord`
@@ -215,6 +215,9 @@ def enqueue(self, record, message, resource=None):
215215
216216
:type resource: :class:`~google.cloud.logging.resource.Resource`
217217
:param resource: (Optional) Monitored resource of the entry
218+
219+
:type labels: dict
220+
:param labels: (Optional) Mapping of labels for the entry.
218221
"""
219222
self._queue.put_nowait({
220223
'info': {
@@ -223,6 +226,7 @@ def enqueue(self, record, message, resource=None):
223226
},
224227
'severity': record.levelname,
225228
'resource': resource,
229+
'labels': labels,
226230
})
227231

228232
def flush(self):
@@ -257,7 +261,7 @@ def __init__(self, client, name, grace_period=_DEFAULT_GRACE_PERIOD,
257261
self.worker = _Worker(logger)
258262
self.worker.start()
259263

260-
def send(self, record, message, resource=None):
264+
def send(self, record, message, resource=None, labels=None):
261265
"""Overrides Transport.send().
262266
263267
:type record: :class:`logging.LogRecord`
@@ -269,8 +273,11 @@ def send(self, record, message, resource=None):
269273
270274
:type resource: :class:`~google.cloud.logging.resource.Resource`
271275
:param resource: (Optional) Monitored resource of the entry.
276+
277+
:type labels: dict
278+
:param labels: (Optional) Mapping of labels for the entry.
272279
"""
273-
self.worker.enqueue(record, message, resource=resource)
280+
self.worker.enqueue(record, message, resource=resource, labels=labels)
274281

275282
def flush(self):
276283
"""Submit any pending log records."""

logging/google/cloud/logging/handlers/transports/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Transport(object):
2222
client and name object, and must override :meth:`send`.
2323
"""
2424

25-
def send(self, record, message, resource=None):
25+
def send(self, record, message, resource=None, labels=None):
2626
"""Transport send to be implemented by subclasses.
2727
2828
:type record: :class:`logging.LogRecord`
@@ -34,6 +34,9 @@ def send(self, record, message, resource=None):
3434
3535
:type resource: :class:`~google.cloud.logging.resource.Resource`
3636
:param resource: (Optional) Monitored resource of the entry.
37+
38+
:type labels: dict
39+
:param labels: (Optional) Mapping of labels for the entry.
3740
"""
3841
raise NotImplementedError
3942

logging/google/cloud/logging/handlers/transports/sync.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class SyncTransport(Transport):
2929
def __init__(self, client, name):
3030
self.logger = client.logger(name)
3131

32-
def send(self, record, message, resource=None):
32+
def send(self, record, message, resource=None, labels=None):
3333
"""Overrides transport.send().
3434
3535
:type record: :class:`logging.LogRecord`
@@ -38,8 +38,15 @@ def send(self, record, message, resource=None):
3838
:type message: str
3939
:param message: The message from the ``LogRecord`` after being
4040
formatted by the associated log formatters.
41+
42+
:type resource: :class:`~google.cloud.logging.resource.Resource`
43+
:param resource: (Optional) Monitored resource of the entry.
44+
45+
:type labels: dict
46+
:param labels: (Optional) Mapping of labels for the entry.
4147
"""
4248
info = {'message': message, 'python_logger': record.name}
4349
self.logger.log_struct(info,
4450
severity=record.levelname,
45-
resource=resource)
51+
resource=resource,
52+
labels=labels)

logging/nox.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ def unit_tests(session, python_version):
3131
session.interpreter = 'python{}'.format(python_version)
3232

3333
# Install all test dependencies, then install this package in-place.
34-
session.install('mock', 'pytest', 'pytest-cov', *LOCAL_DEPS)
34+
session.install(
35+
'mock', 'pytest', 'pytest-cov',
36+
'flask', 'django', *LOCAL_DEPS)
3537
session.install('-e', '.')
3638

3739
# Run py.test against the unit tests.

0 commit comments

Comments
 (0)