Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
5 changes: 5 additions & 0 deletions ddtrace/contrib/__init__.py
Original file line number Diff line number Diff line change
@@ -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__)
109 changes: 109 additions & 0 deletions ddtrace/contrib/django/__init__.py
Original file line number Diff line number Diff line change
@@ -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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Been a long time since I used django (around 1.4), but I think you need to check for class views.

https://github.com/getsentry/sentry/blob/master/src/sentry/middleware/stats.py#L33

Copy link

@masci masci Jun 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why Sentry does that, within process_view the as_view method of CBVs is already called [0][1], so view_func is a function object in any case and func_name should work. See [2] for a similar use case.

[0] https://docs.djangoproject.com/en/1.9/topics/http/middleware/#process-view
[1] https://github.com/django/django/blob/1.9/django/views/generic/base.py#L78
[2] https://github.com/django-debug-toolbar/django-debug-toolbar/blob/master/debug_toolbar/middleware.py#L70

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DataDog/trace-examples@60e6d66

i tried it, i think it works.

hello.views.IndexView


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


86 changes: 86 additions & 0 deletions ddtrace/contrib/django/db.py
Original file line number Diff line number Diff line change
@@ -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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this method is only invoked with vendor as self._vendor = getattr(conn, 'vendor', 'db') so this branch is never hit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm just worried it might be missing at some point. want to be defensive.

return "db" # should this ever happen?
elif vendor == "sqlite":
return "sqlite3" # for consitency with the sqlite3 integration
else:
return vendor
44 changes: 44 additions & 0 deletions ddtrace/contrib/django/templates.py
Original file line number Diff line number Diff line change
@@ -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__

3 changes: 2 additions & 1 deletion ddtrace/ext/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions ddtrace/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
'blinker',
'elasticsearch',
'psycopg2',
'django'
]

setup(
Expand Down