170 lines
5.3 KiB
Python
170 lines
5.3 KiB
Python
from ddtrace.vendor import wrapt
|
|
from ddtrace.vendor.wrapt import wrap_function_wrapper as _w
|
|
|
|
import molten
|
|
|
|
from ... import Pin, config
|
|
from ...compat import urlencode
|
|
from ...constants import ANALYTICS_SAMPLE_RATE_KEY
|
|
from ...ext import SpanTypes, http
|
|
from ...propagation.http import HTTPPropagator
|
|
from ...utils.formats import asbool, get_env
|
|
from ...utils.importlib import func_name
|
|
from ...utils.wrappers import unwrap as _u
|
|
from .wrappers import WrapperComponent, WrapperRenderer, WrapperMiddleware, WrapperRouter, MOLTEN_ROUTE
|
|
|
|
MOLTEN_VERSION = tuple(map(int, molten.__version__.split()[0].split('.')))
|
|
|
|
# Configure default configuration
|
|
config._add('molten', dict(
|
|
service_name=get_env('molten', 'service_name', 'molten'),
|
|
app='molten',
|
|
distributed_tracing=asbool(get_env('molten', 'distributed_tracing', True)),
|
|
))
|
|
|
|
|
|
def patch():
|
|
"""Patch the instrumented methods
|
|
"""
|
|
if getattr(molten, '_datadog_patch', False):
|
|
return
|
|
setattr(molten, '_datadog_patch', True)
|
|
|
|
pin = Pin(
|
|
service=config.molten['service_name'],
|
|
app=config.molten['app']
|
|
)
|
|
|
|
# add pin to module since many classes use __slots__
|
|
pin.onto(molten)
|
|
|
|
_w(molten.BaseApp, '__init__', patch_app_init)
|
|
_w(molten.App, '__call__', patch_app_call)
|
|
|
|
|
|
def unpatch():
|
|
"""Remove instrumentation
|
|
"""
|
|
if getattr(molten, '_datadog_patch', False):
|
|
setattr(molten, '_datadog_patch', False)
|
|
|
|
# remove pin
|
|
pin = Pin.get_from(molten)
|
|
if pin:
|
|
pin.remove_from(molten)
|
|
|
|
_u(molten.BaseApp, '__init__')
|
|
_u(molten.App, '__call__')
|
|
_u(molten.Router, 'add_route')
|
|
|
|
|
|
def patch_app_call(wrapped, instance, args, kwargs):
|
|
"""Patch wsgi interface for app
|
|
"""
|
|
pin = Pin.get_from(molten)
|
|
|
|
if not pin or not pin.enabled():
|
|
return wrapped(*args, **kwargs)
|
|
|
|
# DEV: This is safe because this is the args for a WSGI handler
|
|
# https://www.python.org/dev/peps/pep-3333/
|
|
environ, start_response = args
|
|
|
|
request = molten.http.Request.from_environ(environ)
|
|
resource = func_name(wrapped)
|
|
|
|
# Configure distributed tracing
|
|
if config.molten.get('distributed_tracing', True):
|
|
propagator = HTTPPropagator()
|
|
# request.headers is type Iterable[Tuple[str, str]]
|
|
context = propagator.extract(dict(request.headers))
|
|
# Only need to activate the new context if something was propagated
|
|
if context.trace_id:
|
|
pin.tracer.context_provider.activate(context)
|
|
|
|
with pin.tracer.trace('molten.request', service=pin.service, resource=resource, span_type=SpanTypes.WEB) as span:
|
|
# set analytics sample rate with global config enabled
|
|
span.set_tag(
|
|
ANALYTICS_SAMPLE_RATE_KEY,
|
|
config.molten.get_analytics_sample_rate(use_global_config=True)
|
|
)
|
|
|
|
@wrapt.function_wrapper
|
|
def _w_start_response(wrapped, instance, args, kwargs):
|
|
""" Patch respond handling to set metadata """
|
|
|
|
pin = Pin.get_from(molten)
|
|
if not pin or not pin.enabled():
|
|
return wrapped(*args, **kwargs)
|
|
|
|
status, headers, exc_info = args
|
|
code, _, _ = status.partition(' ')
|
|
|
|
try:
|
|
code = int(code)
|
|
except ValueError:
|
|
pass
|
|
|
|
if not span.get_tag(MOLTEN_ROUTE):
|
|
# if route never resolve, update root resource
|
|
span.resource = u'{} {}'.format(request.method, code)
|
|
|
|
span.set_tag(http.STATUS_CODE, code)
|
|
|
|
# mark 5xx spans as error
|
|
if 500 <= code < 600:
|
|
span.error = 1
|
|
|
|
return wrapped(*args, **kwargs)
|
|
|
|
# patching for extracting response code
|
|
start_response = _w_start_response(start_response)
|
|
|
|
span.set_tag(http.METHOD, request.method)
|
|
span.set_tag(http.URL, '%s://%s:%s%s' % (
|
|
request.scheme, request.host, request.port, request.path,
|
|
))
|
|
if config.molten.trace_query_string:
|
|
span.set_tag(http.QUERY_STRING, urlencode(dict(request.params)))
|
|
span.set_tag('molten.version', molten.__version__)
|
|
return wrapped(environ, start_response, **kwargs)
|
|
|
|
|
|
def patch_app_init(wrapped, instance, args, kwargs):
|
|
"""Patch app initialization of middleware, components and renderers
|
|
"""
|
|
# allow instance to be initialized before wrapping them
|
|
wrapped(*args, **kwargs)
|
|
|
|
# add Pin to instance
|
|
pin = Pin.get_from(molten)
|
|
|
|
if not pin or not pin.enabled():
|
|
return
|
|
|
|
# Wrappers here allow us to trace objects without altering class or instance
|
|
# attributes, which presents a problem when classes in molten use
|
|
# ``__slots__``
|
|
|
|
instance.router = WrapperRouter(instance.router)
|
|
|
|
# wrap middleware functions/callables
|
|
instance.middleware = [
|
|
WrapperMiddleware(mw)
|
|
for mw in instance.middleware
|
|
]
|
|
|
|
# wrap components objects within injector
|
|
# NOTE: the app instance also contains a list of components but it does not
|
|
# appear to be used for anything passing along to the dependency injector
|
|
instance.injector.components = [
|
|
WrapperComponent(c)
|
|
for c in instance.injector.components
|
|
]
|
|
|
|
# but renderers objects
|
|
instance.renderers = [
|
|
WrapperRenderer(r)
|
|
for r in instance.renderers
|
|
]
|