Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
beab429
[tornado] use StackContext and a custom ContextManager to keep track …
Mar 1, 2017
e7bde03
[tornado] use a TracerStackContext to collect tracing information dur…
Feb 28, 2017
1275d41
[tornado] minor on flake8
Mar 9, 2017
c8a516a
[tornado] run_with_trace_context uses TracerStackContext()
Mar 10, 2017
64c3e2e
[tornado] provide a trace_app() function that wraps Tornado web handlers
Mar 10, 2017
88f8fe6
[tornado] add some tests; default ErrorHandler and tracer.wrap() are …
Mar 10, 2017
f887de4
[tornado] minor on flake
Mar 10, 2017
354ac2c
[tornado] use a custom TracerErrorHandler to trace 404 errors
Mar 12, 2017
8308d79
[tornado] handle default handler class tracing when defined
Mar 12, 2017
a43311d
[tornado] tracing tests for RedirectHandler and StaticHandler
Mar 12, 2017
07cce06
[tornado] allow methods to untrace users application
Mar 12, 2017
4d735d3
[tornado] test concurrency with multiple threads
Mar 12, 2017
a31dcb5
[tornado] wrap_executor to handle tracer.wrap() decorators
Mar 13, 2017
8b69a71
[ci] fix tests execution
Mar 13, 2017
bddcbda
[tornado] update TracerStackContext documentation
Mar 13, 2017
c078def
[tornado] remove previous TraceMiddleware implementation
Mar 13, 2017
b38b362
[tornado] add documentation to use the trace_app
Mar 13, 2017
d14d129
[tornado] using the Handler class name instead of the URL as a resource
Mar 13, 2017
8e73355
[tornado] prepend the handler module name in the span.resource
Mar 13, 2017
6c92a43
[docs] improve Tornado docs by removing misleading usage with the Con…
Apr 3, 2017
a960cfe
[tornado] rename settings in constants
Apr 3, 2017
c352db7
[tornado] wrap_executor uses the right signature with kwargs
Apr 3, 2017
032618a
[tornado] fix inconsistent AsyncHTTPClient cache in tests
Apr 3, 2017
236be71
[tornado] major refactoring: patch `tornado` instead of the single app
Apr 4, 2017
0ffa549
[tornado] provide TornadoTestCase for easy testing
Apr 4, 2017
229946b
[tornado] handle run_on_executor decorator when executed in a differe…
Apr 5, 2017
ba8ffee
[tornado] tracer.wrap() handle Future objects whatever is the underly…
Apr 5, 2017
aacf34c
[tornado] test parenting when using executors
Apr 5, 2017
13bf2a5
[tornado] better comments
Apr 5, 2017
8d37112
[tornado] handle run_on_executor safety
Apr 5, 2017
0eaeff8
[tornado] handle run_on_executor with arguments
Apr 5, 2017
815b1c5
[tornado] add tracer configuration from Tornado settings
Apr 6, 2017
f2e0330
[tornado] add docs according to latest API
Apr 6, 2017
abb2c79
[tornado] minor on flake
Apr 6, 2017
72a6fc4
[tornado] add support for Template generation tracing
Apr 6, 2017
770a86c
[tornado] template tracing supports partials and exceptions
Apr 6, 2017
6832be8
[tornado] HTTPError exception with status code 500 is handled
Apr 6, 2017
3d08853
[tornado] reload module when patch/unpatch are applied
Apr 10, 2017
0bae27f
[tornado] run_on_executor decorator passes the context to Tornado int…
Apr 10, 2017
a7a5e08
[tornado] use try-except in test application when decorator kwarg is …
Apr 10, 2017
7e4353d
[tornado] update docs
Apr 10, 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
90 changes: 90 additions & 0 deletions ddtrace/contrib/tornado/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""
The Tornado integration traces all ``RequestHandler`` defined in a Tornado web application.
Auto instrumentation is available using the ``patch`` function as follows::

from ddtrace import tracer, patch
patch(tornado=True)

import tornado.web
import tornado.gen
import tornado.ioloop

# create your handlers
class MainHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self):
self.write("Hello, world")

# create your application
app = tornado.web.Application([
(r'/', MainHandler),
])

# and run it as usual
app.listen(8888)
tornado.ioloop.IOLoop.current().start()

When any type of ``RequestHandler`` is hit, a request root span is automatically created and if you want
to trace more parts of your application, you can use both the ``Tracer.wrap()`` decorator and
the ``Tracer.trace()`` method like usual::

class MainHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self):
yield self.notify()
yield self.blocking_method()
with tracer.trace('tornado.before_write') as span:
# trace more work in the handler

@tracer.wrap('tornado.executor_handler')
@tornado.concurrent.run_on_executor
def blocking_method(self):
# do something expensive

@tracer.wrap('tornado.notify', service='tornado-notification')
@tornado.gen.coroutine
def notify(self):
# do something

Tornado settings can be used to change some tracing configuration, like::

settings = {
'datadog_trace': {
'default_service': 'my-tornado-app',
'tags': {'env': 'production'},
},
}

app = tornado.web.Application([
(r'/', MainHandler),
], **settings)

The available settings are:
* `default_service` (default: `tornado-web`): set the service name used by the tracer. Usually
this configuration must be updated with a meaningful name.
* `tags` (default: `{}`): set global tags that should be applied to all spans.
* `enabled` (default: `true`): define if the tracer is enabled or not. If set to `false`, the
code is still instrumented but no spans are sent to the APM agent.
* `agent_hostname` (default: `localhost`): define the hostname of the APM agent.
* `agent_port` (default: `8126`): define the port of the APM agent.
"""
from ..util import require_modules


required_modules = ['tornado']

with require_modules(required_modules) as missing_modules:
if not missing_modules:
from .patch import patch, unpatch
from .stack_context import run_with_trace_context, TracerStackContext

# alias for API compatibility
context_provider = TracerStackContext.current_context

__all__ = [
'patch',
'unpatch',
'context_provider',
'run_with_trace_context',
'TracerStackContext',
]
60 changes: 60 additions & 0 deletions ddtrace/contrib/tornado/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import ddtrace

from tornado import template

from . import decorators
from .constants import CONFIG_KEY
from .stack_context import TracerStackContext

from ...ext import AppTypes


def tracer_config(__init__, app, args, kwargs):
"""
Wrap Tornado web application so that we can configure services info and
tracing settings after the initialization.
"""
# call the Application constructor
__init__(*args, **kwargs)

# default settings
settings = {
'tracer': ddtrace.tracer,
'default_service': 'tornado-web',
}

# update defaults with users settings
user_settings = app.settings.get(CONFIG_KEY)
if user_settings:
settings.update(user_settings)

app.settings[CONFIG_KEY] = settings
tracer = settings['tracer']
service = settings['default_service']

# the tracer must use the right Context propagation and wrap executor;
# this action is done twice because the patch() method uses the
# global tracer while here we can have a different instance (even if
# this is not usual).
tracer.configure(
context_provider=TracerStackContext.current_context,
wrap_executor=decorators.wrap_executor,
enabled=settings.get('enabled', None),
hostname=settings.get('agent_hostname', None),
port=settings.get('agent_port', None),
)

# set global tags if any
tags = settings.get('tags', None)
if tags:
tracer.set_tags(tags)

# configure the current service
tracer.set_service_info(
service=service,
app='tornado',
app_type=AppTypes.web,
)

# configure the PIN object for template rendering
ddtrace.Pin(app='tornado', service=service, app_type='web', tracer=tracer).onto(template)
9 changes: 9 additions & 0 deletions ddtrace/contrib/tornado/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
This module defines Tornado settings that are shared between
integration modules.
"""
CONFIG_KEY = 'datadog_trace'
REQUEST_CONTEXT_KEY = 'datadog_context'
REQUEST_SPAN_KEY = '__datadog_request_span'
FUTURE_SPAN_KEY = '__datadog_future_span'
PARENT_SPAN_KEY = '__datadog_parent_span'
131 changes: 131 additions & 0 deletions ddtrace/contrib/tornado/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import sys
import ddtrace

from functools import wraps

from .constants import FUTURE_SPAN_KEY, PARENT_SPAN_KEY
from .stack_context import TracerStackContext


def _finish_span(future):
"""
Finish the span if it's attached to the given ``Future`` object.
This method is a Tornado callback used to close a decorated function
executed as a coroutine or as a synchronous function in another thread.
"""
span = getattr(future, FUTURE_SPAN_KEY, None)

if span:
if callable(getattr(future, 'exc_info', None)):
# retrieve the exception from the coroutine object
exc_info = future.exc_info()
if exc_info:
span.set_exc_info(*exc_info)
elif callable(getattr(future, 'exception', None)):
# retrieve the exception from the Future object
# that is executed in a different Thread
if future.exception():
span.set_exc_info(*sys.exc_info())

span.finish()


def _run_on_executor(run_on_executor, _, params, kw_params):
"""
Wrap the `run_on_executor` function so that when a function is executed
in a different thread, we pass the current parent Span to the intermediate
function that will execute the original call. The original function
is then executed within a `TracerStackContext` so that `tracer.trace()`
can be used as usual, both with empty or existing `Context`.
"""
def pass_context_decorator(fn):
"""
Decorator that is used to wrap the original `run_on_executor_decorator`
so that we can pass the current active context before the `executor.submit`
is called. In this case we get the `parent_span` reference and we pass
that reference to `fn` reference. Because in the outer wrapper we replace
the original call with our `traced_wrapper`, we're sure that the `parent_span`
is passed to our intermediate function and not to the user function.
"""
@wraps(fn)
def wrapper(*args, **kwargs):
# from the current context, retrive the active span
current_ctx = ddtrace.tracer.get_call_context()
parent_span = getattr(current_ctx, '_current_span', None)

# pass the current parent span in the Future call so that
# it can be retrieved later
kwargs.update({PARENT_SPAN_KEY: parent_span})
return fn(*args, **kwargs)
return wrapper

# we expect exceptions here if the `run_on_executor` is called with
# wrong arguments; in that case we should not do anything because
# the exception must not be handled here
decorator = run_on_executor(*params, **kw_params)

# `run_on_executor` can be called with arguments; in this case we
# return an inner decorator that holds the real function that should be
# called
if decorator.__module__ == 'tornado.concurrent':
def run_on_executor_decorator(deco_fn):
def inner_traced_wrapper(*args, **kwargs):
# retrieve the parent span from the function kwargs
parent_span = kwargs.pop(PARENT_SPAN_KEY, None)
return run_executor_stack_context(deco_fn, args, kwargs, parent_span)
return pass_context_decorator(decorator(inner_traced_wrapper))

return run_on_executor_decorator

# return our wrapper function that executes an intermediate function to
# trace the real execution in a different thread
def traced_wrapper(*args, **kwargs):
# retrieve the parent span from the function kwargs
parent_span = kwargs.pop(PARENT_SPAN_KEY, None)
return run_executor_stack_context(params[0], args, kwargs, parent_span)

return pass_context_decorator(run_on_executor(traced_wrapper))


def run_executor_stack_context(fn, args, kwargs, parent_span):
"""
This intermediate function is always executed in a newly created thread. Here
using a `TracerStackContext` is legit because this function doesn't interfere
with the main thread loop. `StackContext` states are thread-local and retrieving
the context here will always bring to an empty `Context`.
"""
with TracerStackContext():
ctx = ddtrace.tracer.get_call_context()
ctx._current_span = parent_span
return fn(*args, **kwargs)


def wrap_executor(tracer, fn, args, kwargs, span_name, service=None, resource=None, span_type=None):
"""
Wrap executor function used to change the default behavior of
``Tracer.wrap()`` method. A decorated Tornado function can be
a regular function or a coroutine; if a coroutine is decorated, a
span is attached to the returned ``Future`` and a callback is set
so that it will close the span when the ``Future`` is done.
"""
span = tracer.trace(span_name, service=service, resource=resource, span_type=span_type)

# catch standard exceptions raised in synchronous executions
try:
future = fn(*args, **kwargs)

# duck-typing: if it has `add_done_callback` it's a Future
# object whatever is the underlying implementation
if callable(getattr(future, 'add_done_callback', None)):
setattr(future, FUTURE_SPAN_KEY, span)
future.add_done_callback(_finish_span)
else:
# we don't have a future so the `future` variable
# holds the result of the function
span.finish()
except Exception:
span.set_traceback()
span.finish()
raise

return future
83 changes: 83 additions & 0 deletions ddtrace/contrib/tornado/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from tornado.web import HTTPError

from .constants import CONFIG_KEY, REQUEST_CONTEXT_KEY, REQUEST_SPAN_KEY
from .stack_context import TracerStackContext
from ...ext import http


def execute(func, handler, args, kwargs):
"""
Wrap the handler execute method so that the entire request is within the same
``TracerStackContext``. This simplifies users code when the automatic ``Context``
retrieval is used via ``Tracer.trace()`` method.
"""
# retrieve tracing settings
settings = handler.settings[CONFIG_KEY]
tracer = settings['tracer']
service = settings['default_service']

with TracerStackContext():
# attach the context to the request
setattr(handler.request, REQUEST_CONTEXT_KEY, tracer.get_call_context())

# store the request span in the request so that it can be used later
request_span = tracer.trace(
'tornado.request',
service=service,
span_type=http.TYPE
)
setattr(handler.request, REQUEST_SPAN_KEY, request_span)

return func(*args, **kwargs)


def on_finish(func, handler, args, kwargs):
"""
Wrap the ``RequestHandler.on_finish`` method. This is the last executed method
after the response has been sent, and it's used to retrieve and close the
current request span (if available).
"""
request = handler.request
request_span = getattr(request, REQUEST_SPAN_KEY, None)
if request_span:
# use the class name as a resource; if an handler is not available, the
# default handler class will be used so we don't pollute the resource
# space here
klass = handler.__class__
request_span.resource = '{}.{}'.format(klass.__module__, klass.__name__)
request_span.set_tag('http.method', request.method)
request_span.set_tag('http.status_code', handler.get_status())
request_span.set_tag('http.url', request.uri)
request_span.finish()

return func(*args, **kwargs)


def log_exception(func, handler, args, kwargs):
"""
Wrap the ``RequestHandler.log_exception``. This method is called when an
Exception is not handled in the user code. In this case, we save the exception
in the current active span. If the Tornado ``Finish`` exception is raised, this wrapper
will not be called because ``Finish`` is not an exception.
"""
# safe-guard: expected arguments -> log_exception(self, typ, value, tb)
value = args[1] if len(args) == 3 else None
if not value:
return func(*args, **kwargs)

# retrieve the current span
tracer = handler.settings[CONFIG_KEY]['tracer']
current_span = tracer.current_span()

if isinstance(value, HTTPError):
# Tornado uses HTTPError exceptions to stop and return a status code that
# is not a 2xx. In this case we want to check the status code to be sure that
# only 5xx are traced as errors, while any other HTTPError exception is handled as
# usual.
if 500 <= value.status_code <= 599:
current_span.set_exc_info(*args)
else:
# any other uncaught exception should be reported as error
current_span.set_exc_info(*args)

return func(*args, **kwargs)
Loading