diff --git a/Rakefile b/Rakefile index 6abfbe6824c..b198360fe1b 100644 --- a/Rakefile +++ b/Rakefile @@ -24,6 +24,11 @@ task :upload do sh "s3cmd put ddtrace-*.whl s3://pypi.datadoghq.com/" end +task :dev do + sh "pip uninstall ddtrace" + sh "pip install -e ." +end + task :ci => [:clean, :test, :build] task :release => [:ci, :upload] diff --git a/ddtrace/contrib/__init__.py b/ddtrace/contrib/__init__.py index e69de29bb2d..ad455755aad 100644 --- a/ddtrace/contrib/__init__.py +++ b/ddtrace/contrib/__init__.py @@ -0,0 +1,5 @@ + + +def func_name(f): + """ Return a human readable version of the function's name. """ + return "%s.%s" % (f.__module__, f.__name__) diff --git a/ddtrace/contrib/django/__init__.py b/ddtrace/contrib/django/__init__.py new file mode 100644 index 00000000000..2550bb8e40f --- /dev/null +++ b/ddtrace/contrib/django/__init__.py @@ -0,0 +1,109 @@ + + +import logging +from types import MethodType + + +# project +from ... import tracer +from ...ext import http, errors +from ...contrib import func_name +from .templates import patch_template +from .db import patch_db + +# 3p +from django.apps import apps + + +log = logging.getLogger(__name__) + + +class TraceMiddleware(object): + + def __init__(self): + # override if necessary (can't initialize though) + self.tracer = tracer + self.service = "django" + + try: + patch_template(self.tracer) + except Exception: + log.exception("error patching template class") + + def process_request(self, request): + try: + patch_db(self.tracer) # ensure that connections are always patched. + + span = self.tracer.trace( + "django.request", + service=self.service, + resource="unknown", # will be filled by process view + span_type=http.TYPE) + + span.set_tag(http.METHOD, request.method) + span.set_tag(http.URL, request.path) + _set_req_span(request, span) + except Exception: + log.exception("error tracing request") + + def process_view(self, request, view_func, *args, **kwargs): + span = _get_req_span(request) + if span: + span.resource = func_name(view_func) + + def process_response(self, request, response): + try: + span = _get_req_span(request) + if span: + span.set_tag(http.STATUS_CODE, response.status_code) + + if apps.is_installed("django.contrib.auth"): + span = _set_auth_tags(span, request) + + span.finish() + + except Exception: + log.exception("error tracing request") + finally: + return response + + def process_exception(self, request, exception): + try: + span = _get_req_span(request) + if span: + span.set_tag(http.STATUS_CODE, '500') + span.set_traceback() # will set the exception info + except Exception: + log.exception("error processing exception") + + + +def _get_req_span(request): + """ Return the datadog span from the given request. """ + return getattr(request, '_datadog_request_span', None) + +def _set_req_span(request, span): + """ Set the datadog span on the given request. """ + return setattr(request, '_datadog_request_span', span) + + +def _set_auth_tags(span, request): + """ Patch any available auth tags from the request onto the span. """ + user = getattr(request, 'user', None) + if not user: + return + + if hasattr(user, 'is_authenticated'): + span.set_tag('django.user.is_authenticated', user.is_authenticated()) + + uid = getattr(user, 'pk', None) + if uid: + span.set_tag('django.user.id', uid) + + uname = getattr(user, 'username', None) + if uname: + span.set_tag('django.user.name', uname) + + return span + + diff --git a/ddtrace/contrib/django/db.py b/ddtrace/contrib/django/db.py new file mode 100644 index 00000000000..60f4880ac36 --- /dev/null +++ b/ddtrace/contrib/django/db.py @@ -0,0 +1,86 @@ + +import logging + +from django.db import connections + +# project +from ...ext import sql as sqlx + + +log = logging.getLogger(__name__) + + +def patch_db(tracer): + for c in connections.all(): + patch_conn(tracer, c) + +def patch_conn(tracer, conn): + attr = '_datadog_original_cursor' + if hasattr(conn, attr): + log.debug("already patched") + return + + conn._datadog_original_cursor = conn.cursor + def cursor(): + return TracedCursor(tracer, conn, conn._datadog_original_cursor()) + conn.cursor = cursor + + +class TracedCursor(object): + + def __init__(self, tracer, conn, cursor): + self.tracer = tracer + self.conn = conn + self.cursor = cursor + + self._vendor = getattr(conn, 'vendor', 'db') # e.g sqlite, postgres + self._alias = getattr(conn, 'alias', 'default') # e.g. default, users + + prefix = _vendor_to_prefix(self._vendor) + self._name = "%s.%s" % (prefix, "query") # e.g sqlite3.query + self._service = "%s%s" % (self._alias or prefix, "db") # e.g. defaultdb or postgresdb + + def _trace(self, func, sql, params): + with self.tracer.trace(self._name, resource=sql, service=self._service, span_type=sqlx.TYPE) as span: + span.set_tag(sqlx.QUERY, sql) + span.set_tag("django.db.vendor", self._vendor) + span.set_tag("django.db.alias", self._alias) + try: + return func(sql, params) + finally: + rows = self.cursor.cursor.rowcount + if rows and 0 <= rows: + span.set_tag(sqlx.ROWS, self.cursor.cursor.rowcount) + + def callproc(self, procname, params=None): + return self._trace(self.cursor.callproc, procname, params) + + def execute(self, sql, params=None): + return self._trace(self.cursor.execute, sql, params) + + def executemany(self, sql, param_list): + return self._trace(self.cursor.executemany, sql, param_list) + + def close(self): + return self.cursor.close() + + def __getattr__(self, attr): + return getattr(self.cursor, attr) + + def __iter__(self): + return iter(self.cursor) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + +def _vendor_to_prefix(vendor): + if not vendor: + return "db" # should this ever happen? + elif vendor == "sqlite": + return "sqlite3" # for consitency with the sqlite3 integration + else: + return vendor diff --git a/ddtrace/contrib/django/templates.py b/ddtrace/contrib/django/templates.py new file mode 100644 index 00000000000..aeeea4eba56 --- /dev/null +++ b/ddtrace/contrib/django/templates.py @@ -0,0 +1,44 @@ +""" +code to measure django template rendering. +""" + + +# stdlib +import logging + +# project +from ...ext import http, errors + +# 3p +from django.template import Template + + +log = logging.getLogger(__name__) + + +def patch_template(tracer): + """ will patch django's template rendering function to include timing + and trace information. + """ + + # FIXME[matt] we're patching the template class here. ideally we'd only + # patch so we can use multiple tracers at once, but i suspect this is fine + # in practice. + attr = '_datadog_original_render' + if getattr(Template, attr, None): + log.debug("already patched") + return + + setattr(Template, attr, Template.render) + + class TracedTemplate(object): + + def render(self, context): + with tracer.trace('django.template', span_type=http.TEMPLATE) as span: + try: + return Template._datadog_original_render(self, context) + finally: + span.set_tag('django.template_name', context.template_name or 'unknown') + + Template.render = TracedTemplate.render.__func__ + diff --git a/ddtrace/ext/sql.py b/ddtrace/ext/sql.py index e1e8adbcb70..36d2d07b08c 100644 --- a/ddtrace/ext/sql.py +++ b/ddtrace/ext/sql.py @@ -3,4 +3,5 @@ TYPE = "sql" # tags -QUERY = "sql.query" +QUERY = "sql.query" # the query text +ROWS = "sql.rows" # number of rows returned by a query diff --git a/ddtrace/span.py b/ddtrace/span.py index 0da43e25341..81d1d38c8ac 100644 --- a/ddtrace/span.py +++ b/ddtrace/span.py @@ -162,8 +162,10 @@ def pprint(self): ("end", "" if not self.duration else self.start + self.duration), ("duration", self.duration), ("error", self.error), + ("tags", "") ] + lines.extend((" ", "%s:%s" % kv) for kv in self.meta.items()) return "\n".join("%10s %s" % l for l in lines) def __enter__(self): diff --git a/setup.py b/setup.py index fffe6e20aa1..164dceeed1d 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,7 @@ 'blinker', 'elasticsearch', 'psycopg2', + 'django' ] setup(