diff --git a/ddtrace/_events.py b/ddtrace/_events.py new file mode 100644 index 00000000000..16bd0059899 --- /dev/null +++ b/ddtrace/_events.py @@ -0,0 +1,104 @@ +from typing import Callable +from typing import Mapping +from typing import Optional +from typing import Type +from typing import Union + +import attr + +from ddtrace import Span +from ddtrace import _hooks +from ddtrace import config +from ddtrace.internal import compat + + +_HOOKS = _hooks.Hooks() + + +@attr.s(frozen=True) +class IntegrationEvent(object): + """ + An IntegrationEvent is emitted by an integration (e.g. the flask framework integration) + and is linked to a span. + """ + span = attr.ib(type=Span) + integration = attr.ib(type=str) + + def emit(self): + # type: () -> None + """Alias for emitting this event.""" + emit(self) + + @classmethod + def register(cls, func=None): + # type: (Optional[Callable]) -> Optional[Callable] + """Alias for registering a listener for this event type.""" + return register(cls, func=func) + + @classmethod + def deregister(cls, func): + # type: (Callable) -> None + """Alias for deregistering a listener for this event type.""" + deregister(cls, func) + + if config._raise: + @span.validator # type: ignore + def check_span(self, attribute, value): + assert isinstance(value, Span) + + @integration.validator # type: ignore + def check_integration(self, attribute, value): + assert value in config._config + + +@attr.s(frozen=True) +class WebRequest(IntegrationEvent): + """ + The WebRequest event is emitted by web framework integrations before the WebResponse event. + """ + method = attr.ib(type=str) + url = attr.ib(type=str) + headers = attr.ib(type=Mapping[str, str], factory=dict) + query = attr.ib(type=Optional[str], default=None) + + if config._raise: + @method.validator # type: ignore + def check_method(self, attribute, value): + assert value in ("HEAD", "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "PROPFIND", "TRACE", "CONNECT") + + @url.validator # type: ignore + def check_url(self, attribute, value): + compat.parse.urlparse(value) + + +@attr.s(frozen=True) +class WebResponse(IntegrationEvent): + """ + The WebResponse event is emitted by web frameworks after the WebRequest event. + """ + status_code = attr.ib(type=Union[int, str]) + status_msg = attr.ib(type=Optional[str], default=None) + headers = attr.ib(type=Mapping[str, str], factory=dict) + + if config._raise: + @status_code.validator # type: ignore + def check_status_code(self, attribute, value): + int(value) + + +def emit(event): + # type: (IntegrationEvent) -> None + """Notify registered listeners about an event.""" + _HOOKS.emit(event.__class__, event) + + +def register(event_type, func=None): + # type: (Type[IntegrationEvent], Optional[Callable]) -> Optional[Callable] + """Register a function for a specific event type.""" + return _HOOKS.register(event_type, func) + + +def deregister(event_type, func): + # type: (Type[IntegrationEvent], Callable) -> None + """Deregister a function for an event type.""" + _HOOKS.deregister(event_type, func) diff --git a/ddtrace/_hooks.py b/ddtrace/_hooks.py index 3283fe4df77..350c167504c 100644 --- a/ddtrace/_hooks.py +++ b/ddtrace/_hooks.py @@ -129,5 +129,9 @@ def emit( for func in self._hooks.get(hook, ()): try: func(*args, **kwargs) - except Exception: + except Exception as e: + from ddtrace import config + + if config._raise: + raise e log.error("Failed to run hook %s function %s", hook, func, exc_info=True) diff --git a/ddtrace/contrib/aiohttp/middlewares.py b/ddtrace/contrib/aiohttp/middlewares.py index 7cb42f4e840..5c26bb79b8f 100644 --- a/ddtrace/contrib/aiohttp/middlewares.py +++ b/ddtrace/contrib/aiohttp/middlewares.py @@ -1,3 +1,4 @@ +from ddtrace import _events from ddtrace import config from .. import trace_utils @@ -57,6 +58,16 @@ async def attach_context(request): request[REQUEST_CONTEXT_KEY] = request_span.context request[REQUEST_SPAN_KEY] = request_span request[REQUEST_CONFIG_KEY] = app[CONFIG_KEY] + + _events.WebRequest( + span=request_span, + method=request.method, + url=str(request.url), # DEV: request.url is a yarl's URL object + headers=request.headers, + query=request.query_string, + integration=config.aiohttp.integration_name, + ).emit() + try: response = await handler(request) return response @@ -98,20 +109,17 @@ async def on_prepare(request, response): # DEV: aiohttp is special case maintains separate configuration from config api trace_query_string = request[REQUEST_CONFIG_KEY].get("trace_query_string") - if trace_query_string is None: - trace_query_string = config.http.trace_query_string - if trace_query_string: + if trace_query_string is True: request_span.set_tag(http.QUERY_STRING, request.query_string) + elif trace_query_string is False: + request_span.set_tag(http.QUERY_STRING, None) - trace_utils.set_http_meta( - request_span, - config.aiohttp, - method=request.method, - url=str(request.url), # DEV: request.url is a yarl's URL object + _events.WebResponse( + span=request_span, status_code=response.status, - request_headers=request.headers, - response_headers=response.headers, - ) + headers=response.headers, + integration=config.aiohttp.integration_name, + ).emit() request_span.finish() diff --git a/ddtrace/contrib/asgi/middleware.py b/ddtrace/contrib/asgi/middleware.py index d86aa8b81bc..67e49f65f60 100644 --- a/ddtrace/contrib/asgi/middleware.py +++ b/ddtrace/contrib/asgi/middleware.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING import ddtrace +from ddtrace import _events from ddtrace import config from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY from ddtrace.ext import SpanTypes @@ -138,34 +139,37 @@ async def __call__(self, scope, receive, send): else: url = None - if self.integration_config.trace_query_string: - query_string = scope.get("query_string") - if len(query_string) > 0: - query_string = bytes_to_str(query_string) - else: - query_string = None + query_string = scope.get("query_string") + if len(query_string) > 0: + query_string = bytes_to_str(query_string) - trace_utils.set_http_meta( - span, self.integration_config, method=method, url=url, query=query_string, request_headers=headers - ) + _events.WebRequest( + span=span, + method=method, + url=url, + headers=headers, + query=query_string, + integration=self.integration_config.integration_name, + ).emit() tags = _extract_versions_from_scope(scope, self.integration_config) span.set_tags(tags) async def wrapped_send(message): - if span and message.get("type") == "http.response.start" and "status" in message: - status_code = message["status"] - else: - status_code = None - - if "headers" in message: - response_headers = message["headers"] - else: - response_headers = None - - trace_utils.set_http_meta( - span, self.integration_config, status_code=status_code, response_headers=response_headers - ) + status = message.get("status") + response_headers = message.get("headers") + if ( + span + and message.get("type") == "http.response.start" + and status is not None + and response_headers is not None + ): + _events.WebResponse( + span=span, + status_code=status, + headers=response_headers, + integration=self.integration_config.integration_name, + ).emit() return await send(message) diff --git a/ddtrace/contrib/bottle/trace.py b/ddtrace/contrib/bottle/trace.py index e0b5950a616..8ce5dd8297c 100644 --- a/ddtrace/contrib/bottle/trace.py +++ b/ddtrace/contrib/bottle/trace.py @@ -4,6 +4,7 @@ from bottle import response import ddtrace +from ddtrace import _events from ddtrace import config from .. import trace_utils @@ -52,6 +53,18 @@ def wrapped(*args, **kwargs): # set analytics sample rate with global config enabled s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.bottle.get_analytics_sample_rate(use_global_config=True)) + method = request.method + url = request.urlparts._replace(query="").geturl() + + _events.WebRequest( + span=s, + method=method, + url=url, + headers=request.headers, + query=request.query_string, + integration=config.bottle.integration_name, + ).emit() + code = None result = None try: @@ -79,17 +92,11 @@ def wrapped(*args, **kwargs): # will be default response_code = response.status_code - method = request.method - url = request.urlparts._replace(query="").geturl() - trace_utils.set_http_meta( - s, - config.bottle, - method=method, - url=url, + _events.WebResponse( + span=s, status_code=response_code, - query=request.query_string, - request_headers=request.headers, - response_headers=response.headers, - ) + headers=response.headers, + integration=config.bottle.integration_name, + ).emit() return wrapped diff --git a/ddtrace/contrib/cherrypy/middleware.py b/ddtrace/contrib/cherrypy/middleware.py index e306fdfdb45..4f5d32bdd7b 100644 --- a/ddtrace/contrib/cherrypy/middleware.py +++ b/ddtrace/contrib/cherrypy/middleware.py @@ -10,6 +10,7 @@ from cherrypy.lib.httputil import valid_status # project +from ddtrace import _events from ddtrace import config from .. import trace_utils @@ -72,12 +73,22 @@ def _on_start_resource(self): self._tracer, int_config=config.cherrypy, request_headers=cherrypy.request.headers ) - cherrypy.request._datadog_span = self._tracer.trace( + cherrypy.request._datadog_span = span = self._tracer.trace( SPAN_NAME, service=trace_utils.int_service(None, config.cherrypy, default="cherrypy"), span_type=SpanTypes.WEB, ) + url = compat.to_unicode(cherrypy.request.base + cherrypy.request.path_info) + + _events.WebRequest( + span=span, + method=cherrypy.request.method, + url=url, + headers=cherrypy.request.headers, + integration=config.cherrypy.integration_name, + ).emit() + def _after_error_response(self): span = getattr(cherrypy.request, "_datadog_span", None) @@ -118,18 +129,14 @@ def _close_span(self, span): resource = "{} {}".format(cherrypy.request.method, cherrypy.request.path_info) span.resource = compat.to_unicode(resource) - url = compat.to_unicode(cherrypy.request.base + cherrypy.request.path_info) status_code, _, _ = valid_status(cherrypy.response.status) - trace_utils.set_http_meta( - span, - config.cherrypy, - method=cherrypy.request.method, - url=url, + _events.WebResponse( + span=span, status_code=status_code, - request_headers=cherrypy.request.headers, - response_headers=cherrypy.response.headers, - ) + headers=cherrypy.response.headers, + integration=config.cherrypy.integration_name, + ).emit() span.finish() diff --git a/ddtrace/contrib/django/utils.py b/ddtrace/contrib/django/utils.py index 91b1c9f7f5e..b90a9ecb6ca 100644 --- a/ddtrace/contrib/django/utils.py +++ b/ddtrace/contrib/django/utils.py @@ -1,6 +1,7 @@ from django.utils.functional import SimpleLazyObject import six +from ddtrace import _events from ddtrace import config from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY from ddtrace.constants import SPAN_MEASURED_KEY @@ -196,6 +197,28 @@ def _before_request_tags(pin, span, request): span._set_str_tag("django.request.class", func_name(request)) + url = get_request_uri(request) + + if DJANGO22: + request_headers = request.headers + else: + request_headers = {} + for header, value in request.META.items(): + name = from_wsgi_header(header) + if name: + request_headers[name] = value + + qs = request.META.get("QUERY_STRING", None) + + _events.WebRequest( + span=span, + method=request.method, + url=url, + query=qs, + headers=request_headers, + integration=config.django.integration_name, + ).emit() + def _after_request_tags(pin, span, request, response): # Response can be None in the event that the request failed @@ -252,29 +275,11 @@ def _after_request_tags(pin, span, request, response): set_tag_array(span, "django.response.template", template_names) - url = get_request_uri(request) - - if DJANGO22: - request_headers = request.headers - else: - request_headers = {} - for header, value in request.META.items(): - name = from_wsgi_header(header) - if name: - request_headers[name] = value - # DEV: Resolve the view and resource name at the end of the request in case # urlconf changes at any point during the request _set_resolver_tags(pin, span, request) response_headers = dict(response.items()) if response else {} - trace_utils.set_http_meta( - span, - config.django, - method=request.method, - url=url, - status_code=status, - query=request.META.get("QUERY_STRING", None), - request_headers=request_headers, - response_headers=response_headers, - ) + _events.WebResponse( + span=span, status_code=status, headers=response_headers, integration=config.django.integration_name + ).emit() diff --git a/ddtrace/contrib/falcon/middleware.py b/ddtrace/contrib/falcon/middleware.py index 9c453923765..c4c56244046 100644 --- a/ddtrace/contrib/falcon/middleware.py +++ b/ddtrace/contrib/falcon/middleware.py @@ -1,10 +1,10 @@ import sys +from ddtrace import _events from ddtrace import config +from ddtrace.contrib import trace_utils from ddtrace.ext import SpanTypes -from ddtrace.ext import http as httpx -from .. import trace_utils from ...constants import ANALYTICS_SAMPLE_RATE_KEY from ...constants import SPAN_MEASURED_KEY from ...internal.compat import iteritems @@ -33,9 +33,14 @@ def process_request(self, req, resp): # set analytics sample rate with global config enabled span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.falcon.get_analytics_sample_rate(use_global_config=True)) - trace_utils.set_http_meta( - span, config.falcon, method=req.method, url=req.url, query=req.query_string, request_headers=req.headers - ) + _events.WebRequest( + span=span, + method=req.method, + url=req.url, + query=req.query_string or None, + headers=req.headers, + integration=config.falcon.integration_name, + ).emit() def process_resource(self, req, resp, resource, params): span = self.tracer.current_span() @@ -58,7 +63,9 @@ def process_response(self, req, resp, resource, req_succeeded=None): if resource is None: status = "404" span.resource = "%s 404" % req.method - span.set_tag(httpx.STATUS_CODE, status) + _events.WebResponse( + span=span, status_code=status, headers=resp._headers, integration=config.falcon.integration_name + ).emit() span.finish() return @@ -74,12 +81,14 @@ def process_response(self, req, resp, resource, req_succeeded=None): # if get an Exception (404 is still an exception) status = _detect_and_set_status_error(err_type, span) - trace_utils.set_http_meta(span, config.falcon, status_code=status, response_headers=resp._headers) - # Emit span hook for this response # DEV: Emit before closing so they can overwrite `span.resource` if they want config.falcon.hooks.emit("request", span, req, resp) + _events.WebResponse( + span=span, status_code=status, headers=resp._headers, integration=config.falcon.integration_name + ).emit() + # Close the span span.finish() diff --git a/ddtrace/contrib/flask/patch.py b/ddtrace/contrib/flask/patch.py index a510a8567b9..8b42c03118f 100644 --- a/ddtrace/contrib/flask/patch.py +++ b/ddtrace/contrib/flask/patch.py @@ -2,6 +2,7 @@ import werkzeug from ddtrace import Pin +from ddtrace import _events from ddtrace import config from ddtrace.vendor.wrapt import wrap_function_wrapper as _w @@ -272,7 +273,9 @@ def traced_start_response(status_code, headers): if not span.get_tag(FLASK_ENDPOINT) and not span.get_tag(FLASK_URL_RULE): span.resource = u" ".join((request.method, code)) - trace_utils.set_http_meta(span, config.flask, status_code=code, response_headers=headers) + _events.WebResponse( + span=span, status_code=code, headers=headers, integration=config.flask.integration_name + ).emit() return func(status_code, headers) return traced_start_response @@ -318,15 +321,14 @@ def traced_wsgi_app(pin, wrapped, instance, args, kwargs): start_response = _wrap_start_response(start_response, span, request) # DEV: We set response status code in `_wrap_start_response` - # DEV: Use `request.base_url` and not `request.url` to keep from leaking any query string parameters - trace_utils.set_http_meta( - span, - config.flask, + _events.WebRequest( + span=span, method=request.method, - url=request.base_url, + url=request.url, + headers=request.headers, query=request.query_string, - request_headers=request.headers, - ) + integration=config.flask.integration_name, + ).emit() return wrapped(environ, start_response) diff --git a/ddtrace/contrib/molten/patch.py b/ddtrace/contrib/molten/patch.py index 6b7b5fe3344..3bd05917b9e 100644 --- a/ddtrace/contrib/molten/patch.py +++ b/ddtrace/contrib/molten/patch.py @@ -1,5 +1,6 @@ import molten +from ddtrace import _events from ddtrace.vendor import wrapt from ddtrace.vendor.wrapt import wrap_function_wrapper as _w @@ -114,7 +115,9 @@ def _w_start_response(wrapped, instance, args, kwargs): # if route never resolve, update root resource span.resource = u"{} {}".format(request.method, code) - trace_utils.set_http_meta(span, config.molten, status_code=code) + _events.WebResponse( + span=span, status_code=code, headers=headers, integration=config.molten.integration_name + ).emit() return wrapped(*args, **kwargs) @@ -128,9 +131,15 @@ def _w_start_response(wrapped, instance, args, kwargs): request.path, ) query = urlencode(dict(request.params)) - trace_utils.set_http_meta( - span, config.molten, method=request.method, url=url, query=query, request_headers=request.headers - ) + + _events.WebRequest( + span=span, + method=request.method, + url=url, + query=query, + headers=request.headers, + integration=config.molten.integration_name, + ).emit() span.set_tag("molten.version", molten.__version__) return wrapped(environ, start_response, **kwargs) diff --git a/ddtrace/contrib/pylons/middleware.py b/ddtrace/contrib/pylons/middleware.py index 1b2224b069b..54a8aaa7b37 100644 --- a/ddtrace/contrib/pylons/middleware.py +++ b/ddtrace/contrib/pylons/middleware.py @@ -3,6 +3,7 @@ from pylons import config from webob import Request +from ddtrace import _events from ddtrace import config as ddconfig from .. import trace_utils @@ -57,7 +58,14 @@ def __call__(self, environ, start_response): # set analytics sample rate with global config enabled span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, ddconfig.pylons.get_analytics_sample_rate(use_global_config=True)) - trace_utils.set_http_meta(span, ddconfig.pylons, request_headers=request.headers) + _events.WebRequest( + span=span, + method=request.method, + url=request.url, + query=request.query_string, + headers=request.headers, + integration=ddconfig.pylons.integration_name, + ).emit() if not span.sampled: return self.app(environ, start_response) @@ -70,9 +78,12 @@ def _start_response(status, *args, **kwargs): else: response_headers = kwargs.get("response_headers", {}) http_code = int(status.split()[0]) - trace_utils.set_http_meta( - span, ddconfig.pylons, status_code=http_code, response_headers=response_headers - ) + _events.WebResponse( + span=span, + status_code=http_code, + headers=response_headers, + integration=ddconfig.pylons.integration_name, + ).emit() return start_response(status, *args, **kwargs) try: @@ -87,7 +98,13 @@ def _start_response(status, *args, **kwargs): int(code) except (TypeError, ValueError): code = 500 - trace_utils.set_http_meta(span, ddconfig.pylons, status_code=code) + + _events.WebResponse( + span=span, + status_code=code, + headers={}, + integration=ddconfig.pylons.integration_name, + ).emit() # re-raise the original exception with its original traceback reraise(typ, val, tb=tb) @@ -105,19 +122,6 @@ def _start_response(status, *args, **kwargs): if span.resource == span.name: span.resource = "%s.%s" % (controller, action) - url = "%s://%s:%s%s" % ( - environ.get("wsgi.url_scheme"), - environ.get("SERVER_NAME"), - environ.get("SERVER_PORT"), - environ.get("PATH_INFO"), - ) - trace_utils.set_http_meta( - span, - ddconfig.pylons, - method=environ.get("REQUEST_METHOD"), - url=url, - query=environ.get("QUERY_STRING"), - ) if controller: span._set_str_tag("pylons.route.controller", controller) if action: diff --git a/ddtrace/contrib/pyramid/trace.py b/ddtrace/contrib/pyramid/trace.py index ffc81fae180..e3a59133184 100644 --- a/ddtrace/contrib/pyramid/trace.py +++ b/ddtrace/contrib/pyramid/trace.py @@ -4,6 +4,7 @@ # project import ddtrace +from ddtrace import _events from ddtrace import config from ddtrace.vendor import wrapt @@ -78,6 +79,16 @@ def trace_tween(request): span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, settings.get(SETTINGS_ANALYTICS_SAMPLE_RATE, True)) setattr(request, DD_SPAN, span) # used to find the tracer in templates + + _events.WebRequest( + span=span, + method=request.method, + url=request.path_url, + headers=request.headers, + query=request.query_string, + integration=config.pyramid.integration_name, + ).emit() + response = None status = None try: @@ -105,16 +116,13 @@ def trace_tween(request): else: response_headers = None - trace_utils.set_http_meta( - span, - config.pyramid, - method=request.method, - url=request.path_url, + _events.WebResponse( + span=span, status_code=status, - query=request.query_string, - request_headers=request.headers, - response_headers=response_headers, - ) + headers=response_headers, + integration=config.pyramid.integration_name, + ).emit() + return response return trace_tween diff --git a/ddtrace/contrib/sanic/patch.py b/ddtrace/contrib/sanic/patch.py index 41f1bfa2bfd..8d1518b4163 100644 --- a/ddtrace/contrib/sanic/patch.py +++ b/ddtrace/contrib/sanic/patch.py @@ -3,6 +3,7 @@ import sanic import ddtrace +from ddtrace import _events from ddtrace import config from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY from ddtrace.ext import SpanTypes @@ -30,7 +31,9 @@ def update_span(span, response): # invalid response causes ServerError exception which must be handled status_code = 500 response_headers = None - trace_utils.set_http_meta(span, config.sanic, status_code=status_code, response_headers=response_headers) + _events.WebResponse( + span=span, status_code=status_code, headers=response_headers, integration=config.sanic.integration_name + ).emit() def _wrap_response_callback(span, callback): @@ -165,9 +168,15 @@ def unwrap(request, write_callback=None, stream_callback=None, **kwargs): query_string = request.query_string if isinstance(query_string, bytes): query_string = query_string.decode() - trace_utils.set_http_meta( - span, config.sanic, method=method, url=url, query=query_string, request_headers=headers - ) + + _events.WebRequest( + span=span, + method=method, + url=url, + query=query_string, + headers=headers, + integration=config.sanic.integration_name, + ).emit() if write_callback is not None: new_kwargs["write_callback"] = _wrap_response_callback(span, write_callback) diff --git a/ddtrace/contrib/tornado/handlers.py b/ddtrace/contrib/tornado/handlers.py index 612a21997c8..922eb17f46a 100644 --- a/ddtrace/contrib/tornado/handlers.py +++ b/ddtrace/contrib/tornado/handlers.py @@ -1,12 +1,12 @@ from tornado.web import HTTPError +from ddtrace import _events from ddtrace import config from .. import trace_utils from ...constants import ANALYTICS_SAMPLE_RATE_KEY from ...constants import SPAN_MEASURED_KEY from ...ext import SpanTypes -from ...ext import http from .constants import CONFIG_KEY from .constants import REQUEST_SPAN_KEY from .stack_context import TracerStackContext @@ -44,6 +44,15 @@ def execute(func, handler, args, kwargs): setattr(handler.request, REQUEST_SPAN_KEY, request_span) + _events.WebRequest( + span=request_span, + method=handler.request.method, + url=handler.request.full_url(), + headers=handler.request.headers, + query=handler.request.query, + integration=config.tornado.integration_name, + ).emit() + return func(*args, **kwargs) @@ -61,11 +70,12 @@ def on_finish(func, handler, args, kwargs): # 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.full_url().rsplit("?", 1)[0]) - if config.tornado.trace_query_string: - request_span.set_tag(http.QUERY_STRING, request.query) + _events.WebResponse( + span=request_span, + status_code=handler.get_status(), + headers={}, + integration=config.tornado.integration_name, + ).emit() request_span.finish() return func(*args, **kwargs) diff --git a/ddtrace/contrib/trace_utils.py b/ddtrace/contrib/trace_utils.py index 080528c25c4..2200927cd95 100644 --- a/ddtrace/contrib/trace_utils.py +++ b/ddtrace/contrib/trace_utils.py @@ -14,6 +14,8 @@ from ddtrace import Pin from ddtrace import config +from ddtrace._events import WebRequest +from ddtrace._events import WebResponse from ddtrace.ext import http from ddtrace.internal.logger import get_logger from ddtrace.propagation.http import HTTPPropagator @@ -320,3 +322,28 @@ def set_flattened_tags( for prefix, value in items: for tag, v in _flatten(value, sep, prefix, exclude_policy): span.set_tag(tag, processor(v) if processor is not None else v) + + +@WebRequest.register() +def web_request_handler(event): + # type: (WebRequest) -> None + set_http_meta( + event.span, + getattr(config, event.integration), + method=event.method, + url=event.url, + request_headers=event.headers, + query=event.query, + ) + + +@WebResponse.register() +def web_response_handler(event): + # type: (WebResponse) -> None + set_http_meta( + event.span, + getattr(config, event.integration), + status_code=event.status_code, + status_msg=event.status_msg, + response_headers=event.headers, + ) diff --git a/ddtrace/contrib/wsgi/wsgi.py b/ddtrace/contrib/wsgi/wsgi.py index f3edd0a657e..01d419ab1a8 100644 --- a/ddtrace/contrib/wsgi/wsgi.py +++ b/ddtrace/contrib/wsgi/wsgi.py @@ -17,6 +17,7 @@ from six.moves.urllib.parse import quote import ddtrace +from ddtrace import _events from ddtrace import config from ddtrace.ext import SpanTypes from ddtrace.internal.logger import get_logger @@ -99,8 +100,13 @@ def intercept_start_response(status, response_headers, exc_info=None): span = self.tracer.current_root_span() if span is not None: status_code, status_msg = status.split(" ", 1) - span.set_tag("http.status_msg", status_msg) - trace_utils.set_http_meta(span, config.wsgi, status_code=status_code, response_headers=response_headers) + _events.WebResponse( + span=span, + status_code=status_code, + status_msg=status_msg, + headers=response_headers, + integration=config.wsgi.integration_name, + ).emit() with self.tracer.trace( "wsgi.start_response", service=trace_utils.int_service(None, config.wsgi), @@ -136,9 +142,15 @@ def intercept_start_response(status, response_headers, exc_info=None): method = environ.get("REQUEST_METHOD") query_string = environ.get("QUERY_STRING") request_headers = get_request_headers(environ) - trace_utils.set_http_meta( - span, config.wsgi, method=method, url=url, query=query_string, request_headers=request_headers - ) + + _events.WebRequest( + span=span, + method=method, + url=url, + query=query_string, + headers=request_headers, + integration=config.wsgi.integration_name, + ).emit() if self.span_modifier: self.span_modifier(span, environ) diff --git a/tests/contrib/pylons/test_pylons.py b/tests/contrib/pylons/test_pylons.py index 7ef6b720265..80023966e8b 100644 --- a/tests/contrib/pylons/test_pylons.py +++ b/tests/contrib/pylons/test_pylons.py @@ -57,7 +57,7 @@ def test_controller_exception(self): assert span.service == "web" assert span.resource == "root.raise_exception" assert span.error == 0 - assert span.get_tag(http.URL) == "http://localhost:80/raise_exception" + assert span.get_tag(http.URL) == "http://localhost/raise_exception" assert_span_http_status_code(span, 200) assert http.QUERY_STRING not in span.meta assert span.get_tag(errors.ERROR_MSG) is None @@ -90,7 +90,7 @@ def test_mw_exc_success(self): assert span.service == "web" assert span.resource == "None.None" assert span.error == 0 - assert span.get_tag(http.URL) == "http://localhost:80/" + assert span.get_tag(http.URL) == "http://localhost/" assert_span_http_status_code(span, 200) assert span.get_tag(errors.ERROR_MSG) is None assert span.get_tag(errors.ERROR_TYPE) is None @@ -120,7 +120,7 @@ def test_middleware_exception(self): assert span.service == "web" assert span.resource == "None.None" assert span.error == 1 - assert span.get_tag(http.URL) == "http://localhost:80/" + assert span.get_tag(http.URL) == "http://localhost/" assert_span_http_status_code(span, 500) assert span.get_tag(errors.ERROR_MSG) == "Middleware exception" assert span.get_tag(errors.ERROR_TYPE) == "exceptions.Exception" @@ -144,7 +144,7 @@ def test_exc_success(self): assert span.service == "web" assert span.resource == "root.raise_exception" assert span.error == 0 - assert span.get_tag(http.URL) == "http://localhost:80/raise_exception" + assert span.get_tag(http.URL) == "http://localhost/raise_exception" assert_span_http_status_code(span, 200) assert span.get_tag(errors.ERROR_MSG) is None assert span.get_tag(errors.ERROR_TYPE) is None @@ -168,7 +168,7 @@ def test_exc_client_failure(self): assert span.service == "web" assert span.resource == "root.raise_exception" assert span.error == 0 - assert span.get_tag(http.URL) == "http://localhost:80/raise_exception" + assert span.get_tag(http.URL) == "http://localhost/raise_exception" assert_span_http_status_code(span, 404) assert span.get_tag(errors.ERROR_MSG) is None assert span.get_tag(errors.ERROR_TYPE) is None @@ -318,7 +318,7 @@ def test_failure_500(self): assert span.error == 1 assert_span_http_status_code(span, 500) assert span.get_tag("error.msg") == "Ouch!" - assert span.get_tag(http.URL) == "http://localhost:80/raise_exception" + assert span.get_tag(http.URL) == "http://localhost/raise_exception" assert "Exception: Ouch!" in span.get_tag("error.stack") def test_failure_500_with_wrong_code(self): @@ -334,7 +334,7 @@ def test_failure_500_with_wrong_code(self): assert span.resource == "root.raise_wrong_code" assert span.error == 1 assert_span_http_status_code(span, 500) - assert span.meta.get(http.URL) == "http://localhost:80/raise_wrong_code" + assert span.meta.get(http.URL) == "http://localhost/raise_wrong_code" assert span.get_tag("error.msg") == "Ouch!" assert "Exception: Ouch!" in span.get_tag("error.stack") @@ -351,7 +351,7 @@ def test_failure_500_with_custom_code(self): assert span.resource == "root.raise_custom_code" assert span.error == 1 assert_span_http_status_code(span, 512) - assert span.meta.get(http.URL) == "http://localhost:80/raise_custom_code" + assert span.meta.get(http.URL) == "http://localhost/raise_custom_code" assert span.get_tag("error.msg") == "Ouch!" assert "Exception: Ouch!" in span.get_tag("error.stack") @@ -368,7 +368,7 @@ def test_failure_500_with_code_method(self): assert span.resource == "root.raise_code_method" assert span.error == 1 assert_span_http_status_code(span, 500) - assert span.meta.get(http.URL) == "http://localhost:80/raise_code_method" + assert span.meta.get(http.URL) == "http://localhost/raise_code_method" assert span.get_tag("error.msg") == "Ouch!" def test_distributed_tracing_default(self): @@ -453,7 +453,7 @@ def test_success_200_ot(self): assert dd_span.service == "web" assert dd_span.resource == "root.index" assert_span_http_status_code(dd_span, 200) - assert dd_span.meta.get(http.URL) == "http://localhost:80/" + assert dd_span.meta.get(http.URL) == "http://localhost/" assert dd_span.error == 0 def test_request_headers(self): diff --git a/tests/contrib/tornado/test_tornado_web.py b/tests/contrib/tornado/test_tornado_web.py index 6e80a487959..8203cd281bb 100644 --- a/tests/contrib/tornado/test_tornado_web.py +++ b/tests/contrib/tornado/test_tornado_web.py @@ -41,10 +41,11 @@ def test_success_handler(self, query_string=""): assert "tests.contrib.tornado.web.app.SuccessHandler" == request_span.resource assert "GET" == request_span.get_tag("http.method") assert_span_http_status_code(request_span, 200) - assert self.get_url("/success/") == request_span.get_tag(http.URL) if config.tornado.trace_query_string: + assert self.get_url("/success/" + fqs) == request_span.get_tag(http.URL) assert query_string == request_span.get_tag(http.QUERY_STRING) else: + assert self.get_url("/success/") == request_span.get_tag(http.URL) assert http.QUERY_STRING not in request_span.meta assert 0 == request_span.error diff --git a/tests/tracer/test_global_config.py b/tests/tracer/test_global_config.py index ce82a3f5470..6c0a9992367 100644 --- a/tests/tracer/test_global_config.py +++ b/tests/tracer/test_global_config.py @@ -8,6 +8,7 @@ from ..utils import DummyTracer from ..utils import override_env +from ..utils import override_global_config class GlobalConfigTestCase(TestCase): @@ -162,8 +163,11 @@ def on_web_request(span, request): assert "web.request" not in span.meta # Emit the span + with pytest.raises(TypeError): + self.config.web.hooks.emit("request", span, "request", response="response") # DEV: This also asserts that no exception was raised - self.config.web.hooks.emit("request", span, "request", response="response") + with override_global_config(dict(_raise=False)): + self.config.web.hooks.emit("request", span, "request", response="response") # Assert we did not update the span assert "web.request" not in span.meta @@ -215,8 +219,12 @@ def test_settings_hook_failure(self): span = self.tracer.start_span("web.request") # Emit the span + with pytest.raises(Exception): + self.config.web.hooks.emit("request", span) # DEV: This is the test, to ensure no exceptions are raised - self.config.web.hooks.emit("request", span) + with override_global_config(dict(_raise=False)): + self.config.web.hooks.emit("request", span) + on_web_request.assert_called() def test_settings_no_hook(self): @@ -244,8 +252,11 @@ def on_web_request(span): span.set_tag("web.request", "/") # Emit the span + with pytest.raises(AttributeError): + self.config.web.hooks.emit("request", None) # DEV: This is the test, to ensure no exceptions are raised - self.config.web.hooks.emit("request", None) + with override_global_config(dict(_raise=False)): + self.config.web.hooks.emit("request", None) def test_dd_version(self): c = Config()